steamrb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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