mtproto 0.0.5 → 0.0.6
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 +4 -4
- data/.env.example +5 -0
- data/Rakefile +13 -0
- data/lib/mtproto/auth_key_generator.rb +20 -7
- data/lib/mtproto/client.rb +80 -60
- data/lib/mtproto/connection.rb +103 -0
- data/lib/mtproto/crypto/rsa_key.rb +9 -15
- data/lib/mtproto/errors.rb +33 -0
- data/lib/mtproto/tl/code_settings.rb +25 -0
- data/lib/mtproto/tl/config.rb +4 -2
- data/lib/mtproto/tl/gzip_packed.rb +1 -1
- data/lib/mtproto/tl/message.rb +6 -0
- data/lib/mtproto/tl/sent_code.rb +128 -0
- data/lib/mtproto/transport/tcp_connection.rb +1 -1
- data/lib/mtproto/version.rb +1 -1
- data/lib/mtproto.rb +4 -0
- metadata +6 -3
- data/ext/aes_ige/aes_ige.bundle +0 -0
- data/ext/factorization/factorization.bundle +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e7f507f0d6cf42ac479bf224933efca12e7519cd67d7e9d8796ceba14f517960
|
|
4
|
+
data.tar.gz: 79c5137adfc60752fecb5367c6b90a08e7648f8fc523a95382fe7e755888c439
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48de4193ba146630f831f690527cefcc0577c1bf56ab32452092e68dd75fa5bf86b294b8578e88e4daa26766c39ef78aa60c1a132e38975d6b01f0c21c508f29
|
|
7
|
+
data.tar.gz: 46d2ad65c9c1f59ea5f75789fb27008620117ab63877dad711add23cb6d477ad9cea5e5dffb724cc4d1d128a67dd9b75f8db9eddaf8e30a16005c377cb1a7542
|
data/.env.example
ADDED
data/Rakefile
CHANGED
|
@@ -14,7 +14,20 @@ end
|
|
|
14
14
|
|
|
15
15
|
RSpec::Core::RakeTask.new(:spec) do |t|
|
|
16
16
|
t.rspec_opts = '--require spec_helper'
|
|
17
|
+
t.pattern = 'spec/**/*_spec.rb'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
RSpec::Core::RakeTask.new('spec:unit') do |t|
|
|
21
|
+
t.rspec_opts = '--require spec_helper'
|
|
22
|
+
t.pattern = 'spec/lib/**/*_spec.rb'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
RSpec::Core::RakeTask.new('spec:integration') do |t|
|
|
26
|
+
t.rspec_opts = '--require spec_helper'
|
|
27
|
+
t.pattern = 'spec/integration/**/*_spec.rb'
|
|
17
28
|
end
|
|
18
29
|
|
|
19
30
|
task spec: :compile
|
|
31
|
+
task 'spec:unit': :compile
|
|
32
|
+
task 'spec:integration': :compile
|
|
20
33
|
task default: :spec
|
|
@@ -5,10 +5,17 @@ require 'digest'
|
|
|
5
5
|
|
|
6
6
|
module MTProto
|
|
7
7
|
class AuthKeyGenerator
|
|
8
|
-
attr_reader :connection, :auth_key, :server_salt, :time_offset
|
|
8
|
+
attr_reader :connection, :auth_key, :server_salt, :time_offset, :timeout
|
|
9
|
+
|
|
10
|
+
def initialize(connection, public_key, dc_number = nil, test_mode: false, timeout: 10)
|
|
11
|
+
raise ArgumentError, "public_key is required" if public_key.nil? || public_key.empty?
|
|
12
|
+
raise ArgumentError, "dc_number must be positive. Use test_mode: true for test DCs" if dc_number && dc_number < 0
|
|
9
13
|
|
|
10
|
-
def initialize(connection)
|
|
11
14
|
@connection = connection
|
|
15
|
+
@public_key = public_key
|
|
16
|
+
@dc_number = dc_number
|
|
17
|
+
@test_mode = test_mode
|
|
18
|
+
@timeout = timeout
|
|
12
19
|
@auth_key = nil
|
|
13
20
|
@server_salt = nil
|
|
14
21
|
@time_offset = 0
|
|
@@ -76,7 +83,7 @@ module MTProto
|
|
|
76
83
|
message = TL::Message.req_pq_multi(nonce)
|
|
77
84
|
@connection.send(message.serialize)
|
|
78
85
|
|
|
79
|
-
response_data = @connection.recv(timeout:
|
|
86
|
+
response_data = @connection.recv(timeout: @timeout)
|
|
80
87
|
response_message = TL::Message.deserialize(response_data)
|
|
81
88
|
res_pq = response_message.parse_res_pq
|
|
82
89
|
|
|
@@ -86,13 +93,19 @@ module MTProto
|
|
|
86
93
|
end
|
|
87
94
|
|
|
88
95
|
def find_server_key(fingerprints)
|
|
89
|
-
server_key = Crypto::RSAKey.find_by_fingerprint(fingerprints)
|
|
96
|
+
server_key = Crypto::RSAKey.find_by_fingerprint(fingerprints, @public_key)
|
|
90
97
|
raise 'No matching RSA key found!' unless server_key
|
|
91
98
|
|
|
92
99
|
server_key
|
|
93
100
|
end
|
|
94
101
|
|
|
95
102
|
def encrypt_pq_inner_data(res_pq, p, q, server_key, new_nonce)
|
|
103
|
+
raise ArgumentError, "dc_number is required for auth key generation" if @dc_number.nil?
|
|
104
|
+
|
|
105
|
+
# For test DCs, add 10000 to the DC number per MTProto spec
|
|
106
|
+
# For production DCs, use the DC number as-is
|
|
107
|
+
dc_value = @test_mode ? (@dc_number + 10000) : @dc_number
|
|
108
|
+
|
|
96
109
|
inner_data = TL::PQInnerData.new(
|
|
97
110
|
pq: Crypto::Factorization.bytes_to_integer(res_pq[:pq]),
|
|
98
111
|
p: p,
|
|
@@ -100,7 +113,7 @@ module MTProto
|
|
|
100
113
|
nonce: res_pq[:nonce],
|
|
101
114
|
server_nonce: res_pq[:server_nonce],
|
|
102
115
|
new_nonce: new_nonce,
|
|
103
|
-
dc:
|
|
116
|
+
dc: dc_value
|
|
104
117
|
)
|
|
105
118
|
|
|
106
119
|
Crypto::RSA_PAD.encrypt(inner_data.serialize, server_key)
|
|
@@ -118,7 +131,7 @@ module MTProto
|
|
|
118
131
|
|
|
119
132
|
@connection.send(message.serialize)
|
|
120
133
|
|
|
121
|
-
response_data = @connection.recv(timeout:
|
|
134
|
+
response_data = @connection.recv(timeout: @timeout)
|
|
122
135
|
response_message = TL::Message.deserialize(response_data)
|
|
123
136
|
server_dh_params = response_message.parse_server_DH_params_ok
|
|
124
137
|
|
|
@@ -186,7 +199,7 @@ module MTProto
|
|
|
186
199
|
|
|
187
200
|
@connection.send(message.serialize)
|
|
188
201
|
|
|
189
|
-
response_data = @connection.recv(timeout:
|
|
202
|
+
response_data = @connection.recv(timeout: @timeout)
|
|
190
203
|
response_message = TL::Message.deserialize(response_data)
|
|
191
204
|
response_message.parse_dh_gen_response
|
|
192
205
|
end
|
data/lib/mtproto/client.rb
CHANGED
|
@@ -8,36 +8,45 @@ require_relative 'tl/message'
|
|
|
8
8
|
|
|
9
9
|
module MTProto
|
|
10
10
|
class Client
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
2 => ['149.154.167.51', 443],
|
|
14
|
-
3 => ['149.154.175.100', 443],
|
|
15
|
-
4 => ['149.154.167.91', 443],
|
|
16
|
-
5 => ['91.108.56.130', 443]
|
|
17
|
-
}.freeze
|
|
11
|
+
attr_reader :connection, :server_key, :auth_key, :server_salt, :time_offset, :session, :timeout
|
|
12
|
+
attr_accessor :api_id, :api_hash, :device_model, :system_version, :app_version, :system_lang_code, :lang_pack, :lang_code
|
|
18
13
|
|
|
19
|
-
|
|
14
|
+
CONSTRUCTOR_MSGS_ACK = 0x62d6b459
|
|
20
15
|
|
|
21
|
-
def initialize(
|
|
22
|
-
|
|
23
|
-
raise ArgumentError, "
|
|
16
|
+
def initialize(api_id: nil, api_hash: nil, host:, port: 443, public_key: nil, dc_number: nil, test_mode: false, timeout: 10)
|
|
17
|
+
raise ArgumentError, "host is required" if host.nil? || host.empty?
|
|
18
|
+
raise ArgumentError, "dc_number must be positive. Use test_mode: true for test DCs" if dc_number && dc_number < 0
|
|
24
19
|
|
|
25
20
|
codec = Transport::AbridgedPacketCodec.new
|
|
26
21
|
@connection = Transport::TCPConnection.new(host, port, codec)
|
|
22
|
+
@public_key = public_key
|
|
23
|
+
@dc_number = dc_number
|
|
24
|
+
@test_mode = test_mode
|
|
25
|
+
@timeout = timeout
|
|
27
26
|
@server_key = nil
|
|
28
27
|
@auth_key = nil
|
|
29
28
|
@server_salt = nil
|
|
30
29
|
@time_offset = 0
|
|
31
30
|
@session = nil
|
|
32
31
|
@connection_initialized = false
|
|
32
|
+
|
|
33
|
+
# Client configuration defaults
|
|
34
|
+
@api_id = api_id || 0
|
|
35
|
+
@api_hash = api_hash || ''
|
|
36
|
+
@device_model = 'Ruby MTProto'
|
|
37
|
+
@system_version = RUBY_DESCRIPTION
|
|
38
|
+
@app_version = '0.1.0'
|
|
39
|
+
@system_lang_code = 'en'
|
|
40
|
+
@lang_pack = ''
|
|
41
|
+
@lang_code = 'en'
|
|
33
42
|
end
|
|
34
43
|
|
|
35
|
-
def connect
|
|
36
|
-
@connection.connect
|
|
44
|
+
def connect!
|
|
45
|
+
@connection.connect!
|
|
37
46
|
end
|
|
38
47
|
|
|
39
|
-
def disconnect
|
|
40
|
-
@connection.close
|
|
48
|
+
def disconnect!
|
|
49
|
+
@connection.close if @connection
|
|
41
50
|
end
|
|
42
51
|
|
|
43
52
|
def req_pq_multi
|
|
@@ -48,7 +57,7 @@ module MTProto
|
|
|
48
57
|
|
|
49
58
|
@connection.send(payload)
|
|
50
59
|
|
|
51
|
-
response_data = @connection.recv(timeout:
|
|
60
|
+
response_data = @connection.recv(timeout: @timeout)
|
|
52
61
|
response_message = TL::Message.deserialize(response_data)
|
|
53
62
|
|
|
54
63
|
res_pq = response_message.parse_res_pq
|
|
@@ -59,7 +68,9 @@ module MTProto
|
|
|
59
68
|
end
|
|
60
69
|
|
|
61
70
|
def make_auth_key
|
|
62
|
-
|
|
71
|
+
raise ArgumentError, "public_key is required for auth key generation" if @public_key.nil? || @public_key.empty?
|
|
72
|
+
|
|
73
|
+
generator = AuthKeyGenerator.new(@connection, @public_key, @dc_number, test_mode: @test_mode, timeout: @timeout)
|
|
63
74
|
result = generator.generate
|
|
64
75
|
|
|
65
76
|
@auth_key = generator.auth_key
|
|
@@ -94,7 +105,7 @@ module MTProto
|
|
|
94
105
|
|
|
95
106
|
@connection.send(encrypted_msg.serialize)
|
|
96
107
|
|
|
97
|
-
response_data = @connection.recv(timeout:
|
|
108
|
+
response_data = @connection.recv(timeout: @timeout)
|
|
98
109
|
|
|
99
110
|
decrypted = EncryptedMessage.decrypt(
|
|
100
111
|
auth_key: @auth_key,
|
|
@@ -105,16 +116,12 @@ module MTProto
|
|
|
105
116
|
response_body = decrypted[:body]
|
|
106
117
|
|
|
107
118
|
constructor = response_body[0, 4].unpack1('L<')
|
|
108
|
-
puts " [RPC] First response constructor: 0x#{constructor.to_s(16)} (#{response_body.bytesize} bytes)"
|
|
109
119
|
|
|
110
120
|
if constructor == TL::NewSessionCreated::CONSTRUCTOR
|
|
111
|
-
puts " [RPC] Got new_session_created, waiting for actual response..."
|
|
112
121
|
session_info = TL::NewSessionCreated.deserialize(response_body)
|
|
113
122
|
@server_salt = session_info.server_salt
|
|
114
|
-
puts " [RPC] Updated server_salt to: 0x#{@server_salt.to_s(16)}"
|
|
115
123
|
|
|
116
|
-
response_data = @connection.recv(timeout:
|
|
117
|
-
puts " [RPC] Second response received (#{response_data.bytesize} bytes encrypted)"
|
|
124
|
+
response_data = @connection.recv(timeout: @timeout)
|
|
118
125
|
decrypted = EncryptedMessage.decrypt(
|
|
119
126
|
auth_key: @auth_key,
|
|
120
127
|
encrypted_message_data: response_data,
|
|
@@ -122,37 +129,32 @@ module MTProto
|
|
|
122
129
|
)
|
|
123
130
|
response_body = decrypted[:body]
|
|
124
131
|
constructor = response_body[0, 4].unpack1('L<')
|
|
125
|
-
puts " [RPC] Second response constructor: 0x#{constructor.to_s(16)} (#{response_body.bytesize} bytes)"
|
|
126
132
|
end
|
|
127
133
|
|
|
128
134
|
if constructor == TL::Message::CONSTRUCTOR_MSG_CONTAINER
|
|
129
|
-
puts " [RPC] Response is a container, unpacking..."
|
|
130
135
|
container = TL::MsgContainer.deserialize(response_body)
|
|
131
|
-
puts " [RPC] Container has #{container.messages.size} messages"
|
|
132
|
-
|
|
133
|
-
container.messages.each_with_index do |msg, i|
|
|
134
|
-
msg_constructor = msg[:body][0, 4].unpack1('L<')
|
|
135
|
-
puts " [RPC] Message #{i}: constructor=0x#{msg_constructor.to_s(16)}, size=#{msg[:body].bytesize}"
|
|
136
|
-
end
|
|
137
136
|
|
|
138
137
|
rpc_result = container.messages.find do |msg|
|
|
139
138
|
msg[:body][0, 4].unpack1('L<') == 0xf35c6d01
|
|
140
139
|
end
|
|
141
140
|
|
|
142
|
-
if rpc_result
|
|
143
|
-
puts " [RPC] Found rpc_result in container, extracting..."
|
|
144
|
-
return rpc_result[:body][12..]
|
|
145
|
-
end
|
|
141
|
+
return rpc_result[:body][12..] if rpc_result
|
|
146
142
|
|
|
147
143
|
new_session = container.messages.find { |msg| msg[:body][0, 4].unpack1('L<') == TL::NewSessionCreated::CONSTRUCTOR }
|
|
148
144
|
if new_session
|
|
149
|
-
puts " [RPC] Container has new_session_created, updating salt and waiting for RPC result..."
|
|
150
145
|
session_info = TL::NewSessionCreated.deserialize(new_session[:body])
|
|
151
146
|
@server_salt = session_info.server_salt
|
|
152
|
-
puts " [RPC] Updated server_salt to: 0x#{@server_salt.to_s(16)}"
|
|
153
147
|
|
|
154
|
-
|
|
155
|
-
|
|
148
|
+
other_messages = container.messages.reject do |msg|
|
|
149
|
+
constructor = msg[:body][0, 4].unpack1('L<')
|
|
150
|
+
constructor == TL::NewSessionCreated::CONSTRUCTOR || constructor == CONSTRUCTOR_MSGS_ACK
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
if !other_messages.empty?
|
|
154
|
+
return other_messages.first[:body]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
response_data = @connection.recv(timeout: @timeout)
|
|
156
158
|
decrypted = EncryptedMessage.decrypt(
|
|
157
159
|
auth_key: @auth_key,
|
|
158
160
|
encrypted_message_data: response_data,
|
|
@@ -160,15 +162,10 @@ module MTProto
|
|
|
160
162
|
)
|
|
161
163
|
response_body = decrypted[:body]
|
|
162
164
|
constructor = response_body[0, 4].unpack1('L<')
|
|
163
|
-
puts " [RPC] Next response constructor: 0x#{constructor.to_s(16)} (#{response_body.bytesize} bytes)"
|
|
164
165
|
|
|
165
|
-
if constructor == 0xf35c6d01
|
|
166
|
-
puts " [RPC] Next response is rpc_result, extracting payload..."
|
|
167
|
-
return response_body[12..]
|
|
168
|
-
end
|
|
166
|
+
return response_body[12..] if constructor == 0xf35c6d01
|
|
169
167
|
|
|
170
168
|
if constructor == TL::Message::CONSTRUCTOR_MSG_CONTAINER
|
|
171
|
-
puts " [RPC] Next response is also a container, looking for rpc_result..."
|
|
172
169
|
container = TL::MsgContainer.deserialize(response_body)
|
|
173
170
|
rpc_result = container.messages.find { |msg| msg[:body][0, 4].unpack1('L<') == 0xf35c6d01 }
|
|
174
171
|
return rpc_result[:body][12..] if rpc_result
|
|
@@ -177,16 +174,11 @@ module MTProto
|
|
|
177
174
|
return response_body
|
|
178
175
|
end
|
|
179
176
|
|
|
180
|
-
puts " [RPC] No rpc_result found, returning first message body"
|
|
181
177
|
return container.messages.first[:body]
|
|
182
178
|
end
|
|
183
179
|
|
|
184
|
-
if constructor == 0xf35c6d01
|
|
185
|
-
puts " [RPC] Response is rpc_result, extracting payload..."
|
|
186
|
-
return response_body[12..]
|
|
187
|
-
end
|
|
180
|
+
return response_body[12..] if constructor == 0xf35c6d01
|
|
188
181
|
|
|
189
|
-
puts " [RPC] Returning raw response body (constructor: 0x#{constructor.to_s(16)})"
|
|
190
182
|
response_body
|
|
191
183
|
end
|
|
192
184
|
|
|
@@ -197,7 +189,7 @@ module MTProto
|
|
|
197
189
|
body
|
|
198
190
|
end
|
|
199
191
|
|
|
200
|
-
def init_connection(api_id
|
|
192
|
+
def init_connection(api_id:, device_model:, system_version:, app_version:, system_lang_code:, lang_pack:, lang_code:, query:)
|
|
201
193
|
body = TL::Serializer.serialize_int(0xc1cd5ea9)
|
|
202
194
|
flags = 0
|
|
203
195
|
body += TL::Serializer.serialize_int(flags)
|
|
@@ -206,7 +198,7 @@ module MTProto
|
|
|
206
198
|
body += TL::Serializer.serialize_string(system_version)
|
|
207
199
|
body += TL::Serializer.serialize_string(app_version)
|
|
208
200
|
body += TL::Serializer.serialize_string(system_lang_code)
|
|
209
|
-
body += TL::Serializer.serialize_string(
|
|
201
|
+
body += TL::Serializer.serialize_string(lang_pack)
|
|
210
202
|
body += TL::Serializer.serialize_string(lang_code)
|
|
211
203
|
body += query
|
|
212
204
|
body
|
|
@@ -217,13 +209,41 @@ module MTProto
|
|
|
217
209
|
|
|
218
210
|
unless @connection_initialized
|
|
219
211
|
query = init_connection(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
212
|
+
api_id: @api_id,
|
|
213
|
+
device_model: @device_model,
|
|
214
|
+
system_version: @system_version,
|
|
215
|
+
app_version: @app_version,
|
|
216
|
+
system_lang_code: @system_lang_code,
|
|
217
|
+
lang_pack: @lang_pack,
|
|
218
|
+
lang_code: @lang_code,
|
|
219
|
+
query: query
|
|
220
|
+
)
|
|
221
|
+
query = invoke_with_layer(214, query)
|
|
222
|
+
@connection_initialized = true
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
rpc_call(query)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def auth_send_code(phone_number, code_settings: {})
|
|
229
|
+
raise ArgumentError, 'phone_number is required' if phone_number.nil? || phone_number.empty?
|
|
230
|
+
|
|
231
|
+
query = [0xa677244f].pack('L<')
|
|
232
|
+
query += TL::Serializer.serialize_string(phone_number)
|
|
233
|
+
query += TL::Serializer.serialize_int(@api_id)
|
|
234
|
+
query += TL::Serializer.serialize_string(@api_hash)
|
|
235
|
+
query += TL::CodeSettings.serialize(code_settings)
|
|
236
|
+
|
|
237
|
+
unless @connection_initialized
|
|
238
|
+
query = init_connection(
|
|
239
|
+
api_id: @api_id,
|
|
240
|
+
device_model: @device_model,
|
|
241
|
+
system_version: @system_version,
|
|
242
|
+
app_version: @app_version,
|
|
243
|
+
system_lang_code: @system_lang_code,
|
|
244
|
+
lang_pack: @lang_pack,
|
|
245
|
+
lang_code: @lang_code,
|
|
246
|
+
query: query
|
|
227
247
|
)
|
|
228
248
|
query = invoke_with_layer(214, query)
|
|
229
249
|
@connection_initialized = true
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
class Connection
|
|
5
|
+
attr_reader :client
|
|
6
|
+
|
|
7
|
+
def initialize(api_id: nil, api_hash: nil, host:, port: 443, public_key:, dc_number:, test_mode: false, timeout: 10)
|
|
8
|
+
@client = Client.new(
|
|
9
|
+
api_id: api_id,
|
|
10
|
+
api_hash: api_hash,
|
|
11
|
+
host: host,
|
|
12
|
+
port: port,
|
|
13
|
+
public_key: public_key,
|
|
14
|
+
dc_number: dc_number,
|
|
15
|
+
test_mode: test_mode,
|
|
16
|
+
timeout: timeout
|
|
17
|
+
)
|
|
18
|
+
@connected = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def connect!
|
|
22
|
+
return if @connected
|
|
23
|
+
|
|
24
|
+
@client.connect!
|
|
25
|
+
@client.make_auth_key
|
|
26
|
+
@connected = true
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def disconnect!
|
|
30
|
+
@client.disconnect! if @connected
|
|
31
|
+
@connected = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ping(ping_id = nil)
|
|
35
|
+
raise NotConnectedError unless @connected
|
|
36
|
+
|
|
37
|
+
ping_id ||= rand(2**63)
|
|
38
|
+
body = TL::Message.ping(ping_id)
|
|
39
|
+
|
|
40
|
+
response_body = @client.rpc_call(body)
|
|
41
|
+
message = TL::Message.new(body: response_body)
|
|
42
|
+
pong = message.parse_pong
|
|
43
|
+
|
|
44
|
+
raise PingMismatchError unless pong[:ping_id] == ping_id
|
|
45
|
+
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def get_config
|
|
50
|
+
raise NotConnectedError unless @connected
|
|
51
|
+
|
|
52
|
+
response = @client.help_get_config
|
|
53
|
+
|
|
54
|
+
# Handle gzip compression
|
|
55
|
+
constructor = response[0, 4].unpack1('L<')
|
|
56
|
+
if constructor == TL::GzipPacked::CONSTRUCTOR
|
|
57
|
+
response = TL::GzipPacked.unpack(response)
|
|
58
|
+
constructor = response[0, 4].unpack1('L<')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Handle RPC errors
|
|
62
|
+
if constructor == TL::RpcError::CONSTRUCTOR
|
|
63
|
+
error = TL::RpcError.deserialize(response)
|
|
64
|
+
raise MTProto::RpcError.new(error.error_code, error.error_message)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Parse and return config
|
|
68
|
+
if constructor == TL::Config::CONSTRUCTOR || constructor == TL::Config::CONSTRUCTOR_ALT
|
|
69
|
+
TL::Config.deserialize(response)
|
|
70
|
+
else
|
|
71
|
+
raise UnexpectedConstructorError.new(constructor)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def send_code(phone_number, code_settings: {})
|
|
76
|
+
raise NotConnectedError unless @connected
|
|
77
|
+
raise ArgumentError, 'phone_number is required' if phone_number.nil? || phone_number.empty?
|
|
78
|
+
|
|
79
|
+
response = @client.auth_send_code(phone_number, code_settings: code_settings)
|
|
80
|
+
|
|
81
|
+
# Handle gzip compression
|
|
82
|
+
constructor = response[0, 4].unpack1('L<')
|
|
83
|
+
if constructor == TL::GzipPacked::CONSTRUCTOR
|
|
84
|
+
response = TL::GzipPacked.unpack(response)
|
|
85
|
+
constructor = response[0, 4].unpack1('L<')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Handle RPC errors
|
|
89
|
+
if constructor == TL::RpcError::CONSTRUCTOR
|
|
90
|
+
error = TL::RpcError.deserialize(response)
|
|
91
|
+
raise MTProto::RpcError.new(error.error_code, error.error_message)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Parse and return sent code
|
|
95
|
+
if constructor == TL::SentCode::CONSTRUCTOR
|
|
96
|
+
sent_code = TL::SentCode.deserialize(response)
|
|
97
|
+
sent_code.to_h
|
|
98
|
+
else
|
|
99
|
+
raise UnexpectedConstructorError.new(constructor)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -6,30 +6,24 @@ require 'digest'
|
|
|
6
6
|
module MTProto
|
|
7
7
|
module Crypto
|
|
8
8
|
class RSAKey
|
|
9
|
-
TELEGRAM_KEY = <<~PEM
|
|
10
|
-
-----BEGIN RSA PUBLIC KEY-----
|
|
11
|
-
MIIBCgKCAQEA6LszBcC1LGzyr992NzE0ieY+BSaOW622Aa9Bd4ZHLl+TuFQ4lo4g
|
|
12
|
-
5nKaMBwK/BIb9xUfg0Q29/2mgIR6Zr9krM7HjuIcCzFvDtr+L0GQjae9H0pRB2OO
|
|
13
|
-
62cECs5HKhT5DZ98K33vmWiLowc621dQuwKWSQKjWf50XYFw42h21P2KXUGyp2y/
|
|
14
|
-
+aEyZ+uVgLLQbRA1dEjSDZ2iGRy12Mk5gpYc397aYp438fsJoHIgJ2lgMv5h7WY9
|
|
15
|
-
t6N/byY9Nw9p21Og3AoXSL2q/2IJ1WRUhebgAdGVMlV1fkuOQoEzR7EdpqtQD9Cs
|
|
16
|
-
5+bfo3Nhmcyvk5ftB0WkJ9z6bNZ7yxrP8wIDAQAB
|
|
17
|
-
-----END RSA PUBLIC KEY-----
|
|
18
|
-
PEM
|
|
19
|
-
|
|
20
9
|
attr_reader :key, :fingerprint
|
|
21
10
|
|
|
22
11
|
def initialize(pem_string)
|
|
12
|
+
raise ArgumentError, "pem_string is required" if pem_string.nil? || pem_string.empty?
|
|
13
|
+
|
|
23
14
|
@key = OpenSSL::PKey::RSA.new(pem_string)
|
|
24
15
|
@fingerprint = calculate_fingerprint
|
|
25
16
|
end
|
|
26
17
|
|
|
27
|
-
def self.
|
|
28
|
-
|
|
18
|
+
def self.from_pem(pem_string)
|
|
19
|
+
new(pem_string)
|
|
29
20
|
end
|
|
30
21
|
|
|
31
|
-
def self.find_by_fingerprint(fingerprints)
|
|
32
|
-
|
|
22
|
+
def self.find_by_fingerprint(fingerprints, public_key)
|
|
23
|
+
raise ArgumentError, "public_key is required" if public_key.nil? || public_key.empty?
|
|
24
|
+
|
|
25
|
+
key = new(public_key)
|
|
26
|
+
key if fingerprints.include?(key.fingerprint)
|
|
33
27
|
end
|
|
34
28
|
|
|
35
29
|
private
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class NotConnectedError < Error
|
|
7
|
+
def initialize(msg = 'Not connected. Call connect first.')
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class PingMismatchError < Error
|
|
13
|
+
def initialize(msg = 'Ping ID mismatch')
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class RpcError < Error
|
|
19
|
+
attr_reader :error_code, :error_message
|
|
20
|
+
|
|
21
|
+
def initialize(error_code, error_message)
|
|
22
|
+
@error_code = error_code
|
|
23
|
+
@error_message = error_message
|
|
24
|
+
super("RPC Error #{error_code}: #{error_message}")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class UnexpectedConstructorError < Error
|
|
29
|
+
def initialize(constructor)
|
|
30
|
+
super("Unexpected constructor: 0x#{constructor.to_s(16)}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
module TL
|
|
5
|
+
class CodeSettings
|
|
6
|
+
CONSTRUCTOR = 0xad253d78
|
|
7
|
+
|
|
8
|
+
def self.serialize(settings = {})
|
|
9
|
+
body = Serializer.serialize_int(CONSTRUCTOR)
|
|
10
|
+
|
|
11
|
+
flags = 0
|
|
12
|
+
flags |= (1 << 0) if settings[:allow_flashcall]
|
|
13
|
+
flags |= (1 << 1) if settings[:current_number]
|
|
14
|
+
flags |= (1 << 4) if settings[:allow_app_hash]
|
|
15
|
+
flags |= (1 << 5) if settings[:allow_missed_call]
|
|
16
|
+
flags |= (1 << 7) if settings[:allow_firebase]
|
|
17
|
+
flags |= (1 << 9) if settings[:unknown_number]
|
|
18
|
+
|
|
19
|
+
body += Serializer.serialize_int(flags)
|
|
20
|
+
|
|
21
|
+
body
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/mtproto/tl/config.rb
CHANGED
|
@@ -10,6 +10,7 @@ module MTProto
|
|
|
10
10
|
|
|
11
11
|
def self.deserialize(data)
|
|
12
12
|
offset = 4
|
|
13
|
+
|
|
13
14
|
flags = data[offset, 4].unpack1('L<')
|
|
14
15
|
offset += 4
|
|
15
16
|
|
|
@@ -28,7 +29,7 @@ module MTProto
|
|
|
28
29
|
dc_options_constructor = data[offset, 4].unpack1('L<')
|
|
29
30
|
offset += 4
|
|
30
31
|
|
|
31
|
-
raise "Expected vector constructor" unless dc_options_constructor == 0x1cb5c415
|
|
32
|
+
raise "Expected vector constructor 0x1cb5c415, got 0x#{dc_options_constructor.to_s(16)}" unless dc_options_constructor == 0x1cb5c415
|
|
32
33
|
|
|
33
34
|
dc_options_count = data[offset, 4].unpack1('L<')
|
|
34
35
|
offset += 4
|
|
@@ -65,10 +66,11 @@ module MTProto
|
|
|
65
66
|
|
|
66
67
|
def self.deserialize_from(data)
|
|
67
68
|
offset = 0
|
|
69
|
+
|
|
68
70
|
constructor = data[offset, 4].unpack1('L<')
|
|
69
71
|
offset += 4
|
|
70
72
|
|
|
71
|
-
raise "Expected dcOption constructor" unless constructor == 0x18b7a10d
|
|
73
|
+
raise "Expected dcOption constructor 0x18b7a10d, got 0x#{constructor.to_s(16)}" unless constructor == 0x18b7a10d
|
|
72
74
|
|
|
73
75
|
flags = data[offset, 4].unpack1('L<')
|
|
74
76
|
offset += 4
|
|
@@ -34,7 +34,7 @@ module MTProto
|
|
|
34
34
|
compressed_data = data[offset, length]
|
|
35
35
|
raise "Not enough data: expected #{length}, got #{compressed_data&.bytesize}" if compressed_data.nil? || compressed_data.bytesize < length
|
|
36
36
|
|
|
37
|
-
Zlib::GzipReader.new(StringIO.new(compressed_data)).read
|
|
37
|
+
Zlib::GzipReader.new(StringIO.new(compressed_data)).read.force_encoding(Encoding::BINARY)
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
end
|
data/lib/mtproto/tl/message.rb
CHANGED
|
@@ -74,6 +74,12 @@ module MTProto
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
def self.deserialize(data)
|
|
77
|
+
if data.bytesize < 20
|
|
78
|
+
raise(ArgumentError,
|
|
79
|
+
"Invalid MTProto message: expected at least 20 bytes, got #{data.bytesize} bytes (hex: #{data.unpack1('H*')})",
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
77
83
|
auth_key_id = data[0, 8].unpack1('Q<')
|
|
78
84
|
msg_id = data[8, 8].unpack1('Q<')
|
|
79
85
|
body_length = data[16, 4].unpack1('L<')
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MTProto
|
|
4
|
+
module TL
|
|
5
|
+
class SentCode
|
|
6
|
+
CONSTRUCTOR = 0x5e002502
|
|
7
|
+
|
|
8
|
+
attr_reader :flags, :type, :phone_code_hash, :next_type, :timeout
|
|
9
|
+
|
|
10
|
+
def self.deserialize(data)
|
|
11
|
+
offset = 4
|
|
12
|
+
|
|
13
|
+
flags = data[offset, 4].unpack1('L<')
|
|
14
|
+
offset += 4
|
|
15
|
+
|
|
16
|
+
type, type_bytes_read = SentCodeType.deserialize_from(data[offset..])
|
|
17
|
+
offset += type_bytes_read
|
|
18
|
+
|
|
19
|
+
phone_code_hash_length = data[offset].ord
|
|
20
|
+
offset += 1
|
|
21
|
+
|
|
22
|
+
phone_code_hash = data[offset, phone_code_hash_length]
|
|
23
|
+
offset += phone_code_hash_length
|
|
24
|
+
|
|
25
|
+
padding = (4 - ((phone_code_hash_length + 1) % 4)) % 4
|
|
26
|
+
offset += padding
|
|
27
|
+
|
|
28
|
+
next_type = nil
|
|
29
|
+
if (flags & (1 << 1)) != 0
|
|
30
|
+
next_type, next_type_bytes_read = CodeType.deserialize_from(data[offset..])
|
|
31
|
+
offset += next_type_bytes_read
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
timeout = nil
|
|
35
|
+
if (flags & (1 << 2)) != 0
|
|
36
|
+
timeout = data[offset, 4].unpack1('L<')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
new(
|
|
40
|
+
flags: flags,
|
|
41
|
+
type: type,
|
|
42
|
+
phone_code_hash: phone_code_hash,
|
|
43
|
+
next_type: next_type,
|
|
44
|
+
timeout: timeout
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def initialize(flags:, type:, phone_code_hash:, next_type: nil, timeout: nil)
|
|
49
|
+
@flags = flags
|
|
50
|
+
@type = type
|
|
51
|
+
@phone_code_hash = phone_code_hash
|
|
52
|
+
@next_type = next_type
|
|
53
|
+
@timeout = timeout
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_h
|
|
57
|
+
{
|
|
58
|
+
phone_code_hash: @phone_code_hash,
|
|
59
|
+
type: @type,
|
|
60
|
+
next_type: @next_type,
|
|
61
|
+
timeout: @timeout
|
|
62
|
+
}.compact
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
module SentCodeType
|
|
67
|
+
def self.deserialize_from(data)
|
|
68
|
+
constructor = data[0, 4].unpack1('L<')
|
|
69
|
+
offset = 4
|
|
70
|
+
|
|
71
|
+
case constructor
|
|
72
|
+
when 0x3dbb5986 # auth.sentCodeTypeApp
|
|
73
|
+
length = data[offset, 4].unpack1('L<')
|
|
74
|
+
offset += 4
|
|
75
|
+
[{ _: :sent_code_type_app, length: length }, offset]
|
|
76
|
+
when 0xc000bba2 # auth.sentCodeTypeSms
|
|
77
|
+
length = data[offset, 4].unpack1('L<')
|
|
78
|
+
offset += 4
|
|
79
|
+
[{ _: :sent_code_type_sms, length: length }, offset]
|
|
80
|
+
when 0x5353e5a7 # auth.sentCodeTypeCall
|
|
81
|
+
length = data[offset, 4].unpack1('L<')
|
|
82
|
+
offset += 4
|
|
83
|
+
[{ _: :sent_code_type_call, length: length }, offset]
|
|
84
|
+
when 0xab03c6d9 # auth.sentCodeTypeFlashCall
|
|
85
|
+
pattern_length = data[offset].ord
|
|
86
|
+
offset += 1
|
|
87
|
+
pattern = data[offset, pattern_length]
|
|
88
|
+
offset += pattern_length
|
|
89
|
+
padding = (4 - ((pattern_length + 1) % 4)) % 4
|
|
90
|
+
offset += padding
|
|
91
|
+
[{ _: :sent_code_type_flash_call, pattern: pattern }, offset]
|
|
92
|
+
when 0x82006484 # auth.sentCodeTypeMissedCall
|
|
93
|
+
prefix_length = data[offset].ord
|
|
94
|
+
offset += 1
|
|
95
|
+
prefix = data[offset, prefix_length]
|
|
96
|
+
offset += prefix_length
|
|
97
|
+
padding = (4 - ((prefix_length + 1) % 4)) % 4
|
|
98
|
+
offset += padding
|
|
99
|
+
length = data[offset, 4].unpack1('L<')
|
|
100
|
+
offset += 4
|
|
101
|
+
[{ _: :sent_code_type_missed_call, prefix: prefix, length: length }, offset]
|
|
102
|
+
else
|
|
103
|
+
raise "Unknown SentCodeType constructor: 0x#{constructor.to_s(16)}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
module CodeType
|
|
109
|
+
def self.deserialize_from(data)
|
|
110
|
+
constructor = data[0, 4].unpack1('L<')
|
|
111
|
+
offset = 4
|
|
112
|
+
|
|
113
|
+
case constructor
|
|
114
|
+
when 0x72a3158c # codeTypeSms
|
|
115
|
+
[{ _: :code_type_sms }, offset]
|
|
116
|
+
when 0x741cd3e3 # codeTypeCall
|
|
117
|
+
[{ _: :code_type_call }, offset]
|
|
118
|
+
when 0x226ccefb # codeTypeFlashCall
|
|
119
|
+
[{ _: :code_type_flash_call }, offset]
|
|
120
|
+
when 0xd61ad6ee # codeTypeMissedCall
|
|
121
|
+
[{ _: :code_type_missed_call }, offset]
|
|
122
|
+
else
|
|
123
|
+
raise "Unknown CodeType constructor: 0x#{constructor.to_s(16)}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
data/lib/mtproto/version.rb
CHANGED
data/lib/mtproto.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'mtproto/version'
|
|
4
|
+
require_relative 'mtproto/errors'
|
|
4
5
|
require_relative 'mtproto/transport/abridged_packet_codec'
|
|
5
6
|
require_relative 'mtproto/transport/tcp_connection'
|
|
6
7
|
require_relative 'mtproto/tl/message'
|
|
@@ -14,6 +15,8 @@ require_relative 'mtproto/tl/new_session_created'
|
|
|
14
15
|
require_relative 'mtproto/tl/rpc_error'
|
|
15
16
|
require_relative 'mtproto/tl/gzip_packed'
|
|
16
17
|
require_relative 'mtproto/tl/config'
|
|
18
|
+
require_relative 'mtproto/tl/code_settings'
|
|
19
|
+
require_relative 'mtproto/tl/sent_code'
|
|
17
20
|
require_relative 'mtproto/crypto/rsa_key'
|
|
18
21
|
require_relative 'mtproto/crypto/factorization'
|
|
19
22
|
require_relative 'mtproto/crypto/aes_ige'
|
|
@@ -26,6 +29,7 @@ require_relative 'mtproto/auth_key_generator'
|
|
|
26
29
|
require_relative 'mtproto/session'
|
|
27
30
|
require_relative 'mtproto/encrypted_message'
|
|
28
31
|
require_relative 'mtproto/client'
|
|
32
|
+
require_relative 'mtproto/connection'
|
|
29
33
|
|
|
30
34
|
module MTProto
|
|
31
35
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mtproto
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Artem Levenkov
|
|
@@ -17,19 +17,19 @@ executables: []
|
|
|
17
17
|
extensions: []
|
|
18
18
|
extra_rdoc_files: []
|
|
19
19
|
files:
|
|
20
|
+
- ".env.example"
|
|
20
21
|
- ".ruby-version"
|
|
21
22
|
- Rakefile
|
|
22
23
|
- ext/aes_ige/Makefile
|
|
23
|
-
- ext/aes_ige/aes_ige.bundle
|
|
24
24
|
- ext/aes_ige/aes_ige.c
|
|
25
25
|
- ext/aes_ige/extconf.rb
|
|
26
26
|
- ext/factorization/Makefile
|
|
27
27
|
- ext/factorization/extconf.rb
|
|
28
|
-
- ext/factorization/factorization.bundle
|
|
29
28
|
- ext/factorization/factorization.c
|
|
30
29
|
- lib/mtproto.rb
|
|
31
30
|
- lib/mtproto/auth_key_generator.rb
|
|
32
31
|
- lib/mtproto/client.rb
|
|
32
|
+
- lib/mtproto/connection.rb
|
|
33
33
|
- lib/mtproto/crypto/aes_ige.rb
|
|
34
34
|
- lib/mtproto/crypto/auth_key_helper.rb
|
|
35
35
|
- lib/mtproto/crypto/dh_key_exchange.rb
|
|
@@ -39,9 +39,11 @@ files:
|
|
|
39
39
|
- lib/mtproto/crypto/rsa_key.rb
|
|
40
40
|
- lib/mtproto/crypto/rsa_pad.rb
|
|
41
41
|
- lib/mtproto/encrypted_message.rb
|
|
42
|
+
- lib/mtproto/errors.rb
|
|
42
43
|
- lib/mtproto/session.rb
|
|
43
44
|
- lib/mtproto/tl/bad_msg_notification.rb
|
|
44
45
|
- lib/mtproto/tl/client_dh_inner_data.rb
|
|
46
|
+
- lib/mtproto/tl/code_settings.rb
|
|
45
47
|
- lib/mtproto/tl/config.rb
|
|
46
48
|
- lib/mtproto/tl/gzip_packed.rb
|
|
47
49
|
- lib/mtproto/tl/message.rb
|
|
@@ -49,6 +51,7 @@ files:
|
|
|
49
51
|
- lib/mtproto/tl/new_session_created.rb
|
|
50
52
|
- lib/mtproto/tl/p_q_inner_data.rb
|
|
51
53
|
- lib/mtproto/tl/rpc_error.rb
|
|
54
|
+
- lib/mtproto/tl/sent_code.rb
|
|
52
55
|
- lib/mtproto/tl/serializer.rb
|
|
53
56
|
- lib/mtproto/tl/server_dh_inner_data.rb
|
|
54
57
|
- lib/mtproto/transport/abridged_packet_codec.rb
|
data/ext/aes_ige/aes_ige.bundle
DELETED
|
Binary file
|
|
Binary file
|