steamrb 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 99caa4cd2e21acd6cf5b3d53ce3079a6ca6a7a15
4
+ data.tar.gz: 3ccd1cc12a21b735f67d42d511b94a3c6a9079e2
5
+ SHA512:
6
+ metadata.gz: 2593c4948cd249f4464e3baabd18252ee7ba1b616cf81955cf5dddfa5303e63b65db3f3ff3ecc0ebb9c8472c50689a568aeb4fbba99cdf89e7b85aa2dd7753b8
7
+ data.tar.gz: 532adb1b7b9e99a5faff2e845d2a6f689b2684b0b88f181d69c77b65378dcac16e2b2704820309045b2a6a4b49e02f6cc9c0e48b1e5aceae41816e41bcac1a9d
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ .DS_Store
2
+ Gemfile.lock
3
+ doc/
4
+ .yardoc/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format progress
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,4 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.3
3
+ Exclude:
4
+ - lib/steam/protocol/language/*.rb
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.13.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # Steam
2
+
3
+ Ruby Client for Steam.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'steam'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install steam
20
+
21
+ ## Usage
22
+
23
+ To use you may subscribe a listener class to the Steam Client object in order
24
+ to react to events it fires.
25
+
26
+ ```ruby
27
+ require 'steam'
28
+
29
+ class MyClient < Steam::Client
30
+ def initialize(username, password)
31
+ super
32
+ @username = username
33
+ @password = password
34
+ @token = nil
35
+
36
+ file = SentryFile.new
37
+ @sha = file.read
38
+ end
39
+
40
+ def ready
41
+ steam_user.login(@username, @password, @token, @sha)
42
+ end
43
+
44
+ def on_logon(msg)
45
+ case msg.body.eresult
46
+ when EResult::OK
47
+ # logged in
48
+ when EResult::ACCOUNT_LOGON_DENIED
49
+ puts 'Input token'
50
+ @token = STDIN.gets.chomp
51
+ restart
52
+ sleep 5
53
+ else
54
+ raise "Error: #{msg.body.eresult}"
55
+ end
56
+ end
57
+ end
58
+
59
+ bot = MyClient.new
60
+ bot.start
61
+ ```
62
+
63
+ You may add game specific APIs by including them as a gem. CSGO plugin is provided by `fastpeek/steam-csgo`.
64
+
65
+ ```ruby
66
+
67
+ class MyClient < Steam::Client
68
+ def initialize(username, password)
69
+ super
70
+ @username = username
71
+ @password = password
72
+ @token = nil
73
+
74
+ file = SentryFile.new
75
+ @sha = file.read
76
+ end
77
+
78
+ def ready
79
+ steam_user.login(@username, @password, @token, @sha)
80
+ end
81
+
82
+ def csgo_match_info(match_info)
83
+ # do something with match_info
84
+ end
85
+
86
+ def csgo_ready
87
+ csgo.request_match_info(3_176_070_719_880_560_711,
88
+ 3_176_076_024_165_171_409,
89
+ 39_898)
90
+ end
91
+
92
+ def on_logon(msg)
93
+ case msg.body.eresult
94
+ when EResult::OK
95
+ csgo.start
96
+ when EResult::ACCOUNT_LOGON_DENIED
97
+ puts 'Input token'
98
+ @token = STDIN.gets.chomp
99
+ restart
100
+ else
101
+ raise "Error: #{msg.body.eresult}"
102
+ end
103
+ end
104
+ end
105
+
106
+ bot = MyClient.new
107
+ bot.plugin(:csgo)
108
+ bot.start
109
+ ```
110
+
111
+ ## License
112
+
113
+ MIT
114
+
115
+ ## References
116
+ [SteamKit](https://github.com/SteamRE/SteamKit/)
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ require 'rspec/core/rake_task'
3
+ require 'rubocop/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new('spec:units') do |task|
6
+ task.rspec_opts = '--tag ~integration'
7
+ end
8
+
9
+ RSpec::Core::RakeTask.new('spec:integration') do |task|
10
+ task.rspec_opts = '--tag integration'
11
+ end
12
+
13
+ RuboCop::RakeTask.new do |task|
14
+ task.options = ['lib/']
15
+ end
16
+
17
+ desc 'Run all specs and linter'
18
+ task 'spec:all' => ['spec:units', 'spec:integration'] do
19
+ end
20
+
21
+ task default: ['spec:units']
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'steam'
5
+
6
+ require 'irb'
7
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/ext/string.rb ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Core string extensions
4
+ class String
5
+ # Camelize a string
6
+ #
7
+ # @example Camelize a string
8
+ # "hello_world".camelize # => HelloWorld
9
+ def camelize
10
+ split('_').collect(&:capitalize).join
11
+ end
12
+ alias classify camelize
13
+
14
+ # Underscore a String. This method will also downcase the string
15
+ #
16
+ # @example Underscore a string
17
+ # "HelloWorld" # => "hello_world"
18
+ def underscore
19
+ gsub(/::/, '/')
20
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
21
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
22
+ .tr('-', '_')
23
+ .downcase
24
+ end
25
+
26
+ # Look up a constant via a string.
27
+ #
28
+ # @example Using constantize
29
+ # "Object".constantize # => Object
30
+ # rubocop:disable LineLength
31
+ def constantize
32
+ names = split('::')
33
+ names.shift if names.empty? || names.first.empty?
34
+ constant = Object
35
+ names.each do |name|
36
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
37
+ end
38
+ constant
39
+ end
40
+
41
+ alias snakecase underscore
42
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+ require 'forwardable'
3
+
4
+ module Steam
5
+ # Reads bytes from a given IO object.
6
+ class ByteReader
7
+ extend Forwardable
8
+
9
+ attr_reader :io
10
+
11
+ # Create a ByteReader object
12
+ #
13
+ # @param io [:read] an io object
14
+ def initialize(io)
15
+ @io = io
16
+ end
17
+
18
+ # Reads an unsigned 64 bit integer from the stream
19
+ #
20
+ # @return [Integer] The 64 bit integer
21
+ def read_int64
22
+ io.read(8).unpack('C*').each_with_index.reduce(0) do |sum, (byte, index)|
23
+ sum + byte * (256**index)
24
+ end
25
+ end
26
+ alias int64 read_int64
27
+
28
+ # Reads an unsigned short from the stream
29
+ #
30
+ # @return [Integer] The short
31
+ def read_short
32
+ io.read(2).unpack('S*').first
33
+ end
34
+ alias short read_short
35
+
36
+ # Reads a string of a given length from the stream
37
+ #
38
+ # @return [String] The read string
39
+ def string(len)
40
+ io.read(len)
41
+ end
42
+
43
+ # Reads a single bytes from the stream
44
+ #
45
+ # @return [Integer] The byte
46
+ def byte
47
+ io.read(1).ord
48
+ end
49
+
50
+ # Reads an unsigned 32 bit integer from the stream
51
+ #
52
+ # @return [Integer] The 32 bit integer
53
+ def unsigned_int32
54
+ io.read(4).unpack('<I*').first
55
+ end
56
+
57
+ # Reads an signed 32 bit integer from the stream
58
+ #
59
+ # @return [Integer] The 32 bit integer
60
+ def signed_int32
61
+ io.read(4).unpack('<i*').first
62
+ end
63
+
64
+ # Reads an signed 16 bit integer from the stream
65
+ #
66
+ # @return [Integer] The 16 bit integer
67
+ def signed_int16
68
+ io.read(2).unpack('<s*').first
69
+ end
70
+
71
+ def_delegator :@io, :read, :read
72
+ def_delegator :@io, :readbyte, :readbyte
73
+ def_delegator :@io, :eof?, :eof?
74
+ def_delegator :@io, :tell, :tell
75
+ def_delegator :@io, :lineno, :lineno
76
+ end
77
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+ require 'forwardable'
3
+
4
+ module Steam
5
+ # Writes bytes to a given io object
6
+ class ByteWriter
7
+ extend Forwardable
8
+
9
+ attr_reader :io
10
+
11
+ # Wrap an Int64 allowing the lo and hi
12
+ # 32 bits to be extracted
13
+ class Int64Type
14
+ # Create an Int64Type instance
15
+ #
16
+ # @param value [Integer] the 64 bit int
17
+ def initialize(value)
18
+ @value = value
19
+ end
20
+
21
+ # The low 32 bits
22
+ #
23
+ # @return [Integer]
24
+ def lo
25
+ @value & 0xFFFFFFFF
26
+ end
27
+
28
+ # The high 32 bits
29
+ #
30
+ # @return [Integer]
31
+ def hi
32
+ (@value >> 32) & 0xFFFFFFFF
33
+ end
34
+
35
+ # An array of the low and high bits
36
+ #
37
+ # @return [Integer]
38
+ def int32s
39
+ [lo, hi]
40
+ end
41
+ end
42
+
43
+ def initialize
44
+ @io = StringIO.new
45
+ @io.set_encoding('BINARY')
46
+ end
47
+
48
+ # Rewind the stream
49
+ def rewind
50
+ @io.rewind
51
+ end
52
+
53
+ # Writes an unsigned 32 bit integer from the stream
54
+ def write_unsigned_int32(value)
55
+ @io.write([value].pack('<I'))
56
+ end
57
+
58
+ # Writes an signed long from the stream
59
+ def write_signed_long(value)
60
+ @io.write([value].pack('<l_'))
61
+ end
62
+
63
+ # Writes a 32 bit intger to the stream
64
+ def write_int32(value)
65
+ @io.write([value].pack('<l'))
66
+ end
67
+
68
+ # Writes a 64 bit intger to the stream
69
+ def write_int64(value)
70
+ int64 = Int64Type.new(value)
71
+
72
+ int64.int32s.each do |int|
73
+ write_int32(int)
74
+ end
75
+ end
76
+
77
+ # Writes a single byte to the stream
78
+ def write_byte(byte)
79
+ @io.write([byte].pack('C'))
80
+ end
81
+
82
+ # Writes the given bytes to the stream
83
+ def write(bytes)
84
+ @io.write(bytes)
85
+ end
86
+ alias write_string write
87
+
88
+ # The byte representation of this writer object
89
+ #
90
+ # @return [String] byte representation of this writer object
91
+ def string
92
+ @io.rewind
93
+ @io.string
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+ module Steam
3
+ # Represents a Client on the Steam Network. The Client creates a connection
4
+ # to the Steam networks, and send and receives Message objects to that
5
+ # connection.
6
+ #
7
+ # The Client holds a collection of handlers that react to received packets,
8
+ # if they care about the packet data.
9
+ class Client
10
+ # The internal connection to Steam
11
+ attr_reader :connection
12
+
13
+ # The Handler for Steam user related events (changing name, login, etc)
14
+ attr_reader :steam_user
15
+
16
+ # The Handler for Steam app related events (play tokens, etc)
17
+ attr_reader :steam_apps
18
+
19
+ # The Handler for the game coordinator events (playing game, etc)
20
+ attr_reader :game_coordinator
21
+
22
+ # A list of tokens requires for the game coordinator to launch games
23
+ attr_reader :connect_tokens
24
+
25
+ # The SteamId for the current user, nil if not logged in
26
+ attr_reader :steam_id
27
+
28
+ # The session id for the current user, nilt if not logged in
29
+ attr_reader :session_id
30
+
31
+ # The plugin engine
32
+ attr_reader :plugins
33
+
34
+ def initialize(connection = nil)
35
+ connection ||= Networking::Connection.new(ServerList.new.to_a.sample)
36
+ @connect_tokens = []
37
+ @steam_user = nil
38
+ @steam_apps = nil
39
+ @game_coordinator = nil
40
+ @steam_id = default_steam_id
41
+ @connection = connection
42
+ @plugins = Plugins.new(self)
43
+ init_handlers
44
+ end
45
+
46
+ # Called by SteamUser handler when the logon message is to be handled.
47
+ # This must be implemented by the subclass
48
+ def on_logon(_msg)
49
+ raise NotImplementedError
50
+ end
51
+
52
+ # Called by SteamUser handler when the user is fully logged in. This must
53
+ # be implemented by the subclass.
54
+ def ready
55
+ raise NotImplementedError
56
+ end
57
+
58
+ # Registers a plugin with the Client via the plugin name
59
+ #
60
+ # @param plugin [Symbol,String] the plugin name
61
+ def plugin(plugin)
62
+ @plugins.plugin(plugin.to_sym)
63
+ end
64
+
65
+ # Stop and start the Client
66
+ def restart
67
+ stop
68
+ start
69
+ end
70
+
71
+ # Disconnect the Client
72
+ def stop
73
+ steam_user.logoff if @steam_id && @session_id
74
+ connection.disconnect
75
+ @steam_id = default_steam_id
76
+ @session_id = nil
77
+ end
78
+
79
+ # Create a new Connection object, and start listening and handling
80
+ # received packets.
81
+ #
82
+ # @return nil
83
+ def start
84
+ @plugins.load
85
+
86
+ connection.open
87
+ connection.each_packet do |packet|
88
+ handle_net_msg(packet)
89
+ end
90
+ end
91
+
92
+ # Sends a Message to the underlying Steam connection. If we have an active
93
+ # session, the session id and steam id is automatically associated with
94
+ # the request
95
+ #
96
+ # @param msg [Protocol::Message] the Message to send to Steam
97
+ def send_msg(msg)
98
+ Steam.logger.debug("Sending #{msg.body.class.name} to Steam")
99
+
100
+ if msg.proto?
101
+ msg.session_id = session_id
102
+ msg.steam_id = steam_id.to_i
103
+ end
104
+
105
+ connection.send_msg(msg).nonzero?
106
+ end
107
+
108
+ # Creates a new session for the Client
109
+ #
110
+ # @param steam_id [String]
111
+ # @param session_id [String]
112
+ def create_session(steam_id, session_id)
113
+ @steam_id = CommunityId.new(steam_id)
114
+ @session_id = session_id
115
+ end
116
+
117
+ # Update the Client's internal connection token list
118
+ #
119
+ # @param tokens [Array<String>] the new list of tokens
120
+ # @param max [Integer] the amount of tokens to keep
121
+ def update_connect_tokens(tokens, max)
122
+ tokens.each { |t| @connect_tokens.insert(0, t) }
123
+ @connect_tokens = @connect_tokens.take(max)
124
+ end
125
+
126
+ # Sets the Client's connection session key. Used for encrypting
127
+ # and decrypting packets
128
+ #
129
+ # @param key [String] the key
130
+ def session_key=(key)
131
+ connection.session_key = key
132
+ end
133
+
134
+ # Starts the background heartbeat thread.
135
+ #
136
+ # @param tick [Integer] the time to wait between
137
+ # heartbeat messages
138
+ def start_heartbeat(tick)
139
+ Thread.new do |t|
140
+ t.abort_on_exception = true
141
+ until connection.disconnected?
142
+ send_msg(heartbeat_message)
143
+ sleep(tick)
144
+ end
145
+ end
146
+ end
147
+
148
+ # Passes each GC message to any plugins.
149
+ #
150
+ # @param msg [GcProtobufMessage] The message to handle
151
+ def gc_message(msg)
152
+ @plugins.loaded_plugins.each do |plugin|
153
+ send(plugin).gc_message(msg)
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ # Instantiates the various handlers, and adds them to the handler
160
+ # collection
161
+ def init_handlers
162
+ @auth = Handler::Auth.new(self)
163
+ @steam_user = Handler::SteamUser.new(self)
164
+ @steam_apps = Handler::SteamApps.new(self)
165
+ @game_coordinator = Handler::GameCoordinator.new(self)
166
+
167
+ @handlers = Handler::Collection.new
168
+ @handlers.add(@auth, @steam_user, @steam_apps, @game_coordinator)
169
+ end
170
+
171
+ # Handles an incoming packet, if the pack is a multi packet (ie: it contains
172
+ # more than one packet) each packet is extracted and passed back to this
173
+ # method.
174
+ #
175
+ # @param packet [Packet] the packet to handle
176
+ def handle_net_msg(packet)
177
+ return handle_multi(packet) if packet.proto? && packet.multi?
178
+ @handlers.handle(packet)
179
+ end
180
+
181
+ # Handles a packet containing one or more other Packets. Sometimes
182
+ # the Packet data will be Gziped, this method handles that case as well.
183
+ #
184
+ # @note this method does not ensure the packet is a multi packet, the
185
+ # calling method should handle this
186
+ # @param packet [Networking::Packet] the packet to handle
187
+ def handle_multi(packet)
188
+ msg = packet.as_message(Steamclient::CMsgMulti.new)
189
+
190
+ body = StringIO.new(msg.body.message_body)
191
+ payload = if msg.body&.size_unzipped&.nonzero?
192
+ Zlib::GzipReader.new(body)
193
+ else
194
+ body
195
+ end
196
+
197
+ handle_packets_from_multi_payload(payload)
198
+ end
199
+
200
+ # Extracts one or more packet from a given payload IO object.
201
+ #
202
+ # @param payload [#read] The payload io object
203
+ def handle_packets_from_multi_payload(payload)
204
+ reader = ByteReader.new(payload)
205
+
206
+ until reader.eof?
207
+ packet_length = reader.unsigned_int32
208
+ handle_net_msg(Networking::Packet.new(reader.read(packet_length)))
209
+ end
210
+ end
211
+
212
+ # The heartbeat message that we send once the user is connected
213
+ #
214
+ # @return [ProtobufMessage]
215
+ def heartbeat_message
216
+ ProtobufMessage.new(MsgHdrProtoBuf.new,
217
+ Steamclient::CMsgClientHeartBeat.new,
218
+ EMsg::CLIENT_HEART_BEAT)
219
+ end
220
+
221
+ # The default SteamID to be sent with each message before a user
222
+ # has been authed
223
+ # @return [Steam2Id]
224
+ def default_steam_id
225
+ Steam2Id.new(EUniverse::PUBLIC, 0, 0)
226
+ end
227
+ end
228
+ end