Current section

7 Versions

Jump to

Compare versions

9 files changed
+334 additions
-126 deletions
  @@ -1,24 +1,51 @@
1 - # Mtproto
1 + ### [MTProto](https://core.telegram.org/mtproto) transport for Elixir [![Hex.pm](https://img.shields.io/hexpm/v/mtproto.svg)](https://hex.pm/packages/mtproto) [![Travis](https://img.shields.io/travis/ccsteam/mtproto.svg)](https://travis-ci.org/ccsteam/mtproto) [![Coverage Status](https://coveralls.io/repos/github/ccsteam/mtproto/badge.svg?branch=master)](https://coveralls.io/github/ccsteam/mtproto?branch=master)
2 2
3 - **TODO: Add description**
3 + ---
4 +
5 + MTProto (protocol) transport implementation in Elixir, acts like gen_tcp and others, supports most service commands.
6 +
7 + ---
8 +
9 + ### TODO
10 +
11 + * Tests, more testing;
12 + * Add checks for numbers in DH algorithm;
13 + * Checks for salts and nonce hashes during authorization;
14 + * Data Center migration;
15 + * Fix handling msg_seqno (?).
16 +
17 + ---
4 18
5 19 ## Installation
6 20
7 - If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
21 + 1. Add `mtproto` to your list of dependencies in `mix.exs`:
8 22
9 - 1. Add `mtproto` to your list of dependencies in `mix.exs`:
23 + ```elixir
24 + def deps do
25 + [{:mtproto, "~> 57.0.0-alpha"}]
26 + end
27 + ```
10 28
11 - ```elixir
12 - def deps do
13 - [{:mtproto, "~> 0.1.0"}]
14 - end
15 - ```
29 + 2. Ensure `mtproto` is started before your application:
16 30
17 - 2. Ensure `mtproto` is started before your application:
31 + ```elixir
32 + def application do
33 + [applications: [:mtproto]]
34 + end
35 + ```
18 36
19 - ```elixir
20 - def application do
21 - [applications: [:mtproto]]
22 - end
23 - ```
37 + ---
24 38
39 + ### Usage
40 +
41 + ...
42 +
43 + ---
44 +
45 + ### Contributing
46 +
47 + 1. Fork it
48 + 2. Create your feature branch (`git checkout -b my-new-feature`)
49 + 3. Commit your changes (`git commit -am 'add some feature'`)
50 + 4. Push to the branch (`git push origin my-new-feature`)
51 + 5. Create new Pull Request
  @@ -1,11 +1,12 @@
1 1 {<<"app">>,<<"mtproto">>}.
2 2 {<<"build_tools">>,[<<"mix">>]}.
3 - {<<"description">>,<<"TL language parser for Elixir">>}.
3 + {<<"description">>,<<"MTProto transport for Elixir">>}.
4 4 {<<"elixir">>,<<"~> 1.3">>}.
5 5 {<<"files">>,
6 6 [<<"lib/mtproto.ex">>,<<"lib/mtproto/auth.ex">>,<<"lib/mtproto/crypto.ex">>,
7 - <<"lib/mtproto/math.ex">>,<<"lib/mtproto/packet.ex">>,
8 - <<"priv/server_public.key">>,<<"mix.exs">>,<<"README.md">>]}.
7 + <<"lib/mtproto/dc.ex">>,<<"lib/mtproto/math.ex">>,
8 + <<"lib/mtproto/packet.ex">>,<<"priv/server_public.key">>,<<"mix.exs">>,
9 + <<"README.md">>]}.
9 10 {<<"licenses">>,[<<"MIT">>]}.
10 11 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/ccsteam/mtproto">>}]}.
11 12 {<<"maintainers">>,[<<"Yuri Artemev">>,<<"Alexander Malaev">>]}.
  @@ -18,5 +19,5 @@
18 19 [{<<"app">>,<<"tl">>},
19 20 {<<"name">>,<<"tl">>},
20 21 {<<"optional">>,false},
21 - {<<"requirement">>,<<"~> 57.0.0-beta">>}]]}.
22 - {<<"version">>,<<"0.1.0-alpha">>}.
22 + {<<"requirement">>,<<"~> 57.1.0-rc">>}]]}.
23 + {<<"version">>,<<"57.0.0-alpha">>}.
  @@ -3,10 +3,10 @@ defmodule MTProto do
3 3
4 4 require Logger
5 5
6 - alias MTProto.{Auth, Crypto, Packet}
6 + alias MTProto.{Auth, Crypto, DC, Packet}
7 7
8 - def start_link(notifier_pid) do
9 - Connection.start_link(__MODULE__, [notifier: notifier_pid])
8 + def start_link(opts) do
9 + Connection.start_link(__MODULE__, opts)
10 10 end
11 11
12 12 def notifier_process(client, notifier_pid) do
  @@ -25,11 +25,11 @@ defmodule MTProto do
25 25 Connection.call(client, {:send, request})
26 26 end
27 27
28 - def dump_state(client) do
29 - Connection.call(client, :dump_state)
28 + def close(client) do
29 + Connection.call(client, :close)
30 30 end
31 31
32 - # ...
32 + # gen_server callbacks
33 33
34 34 defmodule State do
35 35 @moduledoc """
  @@ -54,7 +54,8 @@ defmodule MTProto do
54 54 :msg_seqno - used for storing message sequence number, for using in MTProto packets;
55 55 :msg_ids - list of message IDs, using for server acks;
56 56 :dc_options - list of telegram datacenters;
57 - :dc - current datacenter ID.
57 + :dc - current datacenter ID;
58 + :reconnect - reconnect state, see MTProto.DC for details.
58 59 """
59 60 defstruct [:notifier, :socket,
60 61 :packet_buffer,
  @@ -62,7 +63,8 @@ defmodule MTProto do
62 63 :auth_state, :auth_params,
63 64 :auth_key, :auth_key_hash, :server_salt,
64 65 :msg_seqno, :msg_ids,
65 - :dc_options, :dc]
66 + :dc_options, :dc,
67 + :reconnect]
66 68 end
67 69
68 70 defmodule AuthParams do
  @@ -70,37 +72,44 @@ defmodule MTProto do
70 72 end
71 73
72 74 def init(opts) do
75 + notifier = opts[:notifier]
76 + session_id = Keyword.get(opts, :session_id, Crypto.make_session_id)
77 + msg_seqno = Keyword.get(opts, :msg_seqno, 0)
78 +
73 79 {:connect, :init,
74 - %State{notifier: opts[:notifier], packet_buffer: <<>>,
75 - msg_seqno: 0, msg_ids: []}}
80 + %State{auth_state: :connected, notifier: opts[:notifier], packet_buffer: <<>>,
81 + session_id: session_id, msg_seqno: msg_seqno, msg_ids: []}}
76 82 end
77 83
78 - def connect(_, %{socket: nil,} = state) do
84 + def connect(_, %{socket: nil} = state) do
79 85 {host, port} = choose_server(state)
80 86 Logger.debug("connect to #{inspect host}, #{inspect port}")
81 87
82 88 case :gen_tcp.connect(host, port, [:binary, active: false], 5000) do
83 89 {:ok, socket} ->
84 - Logger.debug("connected")
85 90 send(self, :after_connect)
86 - send(state.notifier, :connected)
87 - session_id = Crypto.make_session_id()
88 - {:ok, %{state|socket: socket, session_id: session_id, auth_state: :connected}}
91 + send_to_notifier(state, {:connected, host, port})
92 + {:ok, %{state|socket: socket}}
89 93 {:error, _} ->
90 94 {:backoff, 1000, state}
91 95 end
92 96 end
93 97
98 + def disconnect({:close, _from}, %{socket: socket} = state) do
99 + :ok = :gen_tcp.close(socket)
100 + {:stop, :normal, state}
101 + end
102 + def disconnect({:reconnect, reason}, %{socket: socket} = state) do
103 + Logger.debug("need to reconnect #{inspect reason}")
104 + :ok = :gen_tcp.close(socket)
105 + {:connect, :reconnect, %{state|socket: nil}}
106 + end
94 107 def disconnect(info, %{socket: socket} = state) do
95 108 Logger.debug("disconnected #{inspect info}")
96 109 :ok = :gen_tcp.close(socket)
97 - :error_logger.format("Connection error: ~p~n", [info])
98 - {:connect, :reconnect, %{state | socket: nil}}
110 + {:connect, :reconnect, %{state|socket: nil}}
99 111 end
100 112
101 - def handle_call(:dump_state, _, state) do
102 - {:reply, state, state}
103 - end
104 113 def handle_call(:authorize, _, state) do
105 114 # init MTProto authorization
106 115 state = Auth.init(self(), state)
  @@ -144,17 +153,32 @@ defmodule MTProto do
144 153
145 154 Logger.debug("connection ok!")
146 155
147 - {:noreply, state}
156 + # TODO migrate to another DC
157 + # if we tried to reconnect
158 + # state =
159 + # case state.reconnect do
160 + # {:dc, dc_id} ->
161 + # # auth_key used from %TL.Auth.ExportedAuthorization{id: user_id, bytes: auth_key}
162 + # %TL.Auth.ImportAuthorization{id: user_id, bytes: auth_key}
163 + # _ ->
164 + # state
165 + # end
166 +
167 + if state.auth_key && state.auth_key_hash && state.server_salt do
168 + send(self, :authorized)
169 + end
170 +
171 + {:noreply, %{state|reconnect: nil}}
172 + end
173 + def handle_info({:reconnect, {:change_dc, dc_id} = reason}, state) do
174 + Logger.debug("migrate to DC##{dc_id}")
175 + {:disconnect, {:reconnect, reason}, %{state|reconnect: {:dc, dc_id}}}
148 176 end
149 177 def handle_info({:reconnect, reason}, state) do
150 178 Logger.debug("reconnect #{inspect reason}")
151 - {:disconnect, {:reconnect, reason}, state}
179 + {:disconnect, {:reconnect, reason}, %{state|reconnect: :random}}
152 180 end
153 181 def handle_info(:authorized, %{socket: socket} = state) do
154 - # notify about authorization result
155 - send(state.notifier,
156 - {:authorized, state.auth_key, state.auth_key_hash, state.server_salt})
157 -
158 182 # init connection
159 183 case send_rpc_request(socket, init_connection_request(), state) do
160 184 {:ok, state} -> {:noreply, state}
  @@ -176,11 +200,8 @@ defmodule MTProto do
176 200 {:noreply, %{state|packet_buffer: incomplete_packet}}
177 201 {:ok, packet, rest} ->
178 202 state = %{state|packet_buffer: rest}
179 -
180 - # IO.puts " |-| packet: #{inspect packet, limit: 50_000}"
181 203 decode_result = Packet.decode_packet(packet, state)
182 204
183 - # IO.puts " |-| decoded: #{inspect decode_result}"
184 205 case decode_result do
185 206 {:error, reason} ->
186 207 {:stop, {:error, reason}, state}
  @@ -203,22 +224,41 @@ defmodule MTProto do
203 224 {:noreply, state}
204 225 end
205 226
227 + def terminate(_reason, state) do
228 + # :gen_tcp.close(state.socket)
229 + end
230 +
206 231 defp handle_packet(state, packet) do
207 - IO.puts "\n\n ---- handle_packet #{inspect packet, limit: 100_000}\n"
232 + Logger.debug "handle_packet #{inspect packet, limit: 100_000}"
208 233 case packet do
209 234 %TL.MTProto.Msg.Container{messages: messages} ->
210 - # calls `handle_packet#(state, %TL.MTProto.Message{...})` on each message
211 235 Enum.reduce(messages, state, fn(message, state) ->
212 236 handle_packet(state, message)
213 237 end)
214 - %TL.MTProto.Message{msg_id: msg_id, body: body} ->
215 - handle_packet(state, body)
238 + %TL.MTProto.Message{seqno: seqno, msg_id: msg_id, body: body} ->
239 + state = handle_packet(state, body)
240 + %{state|msg_seqno: state.msg_seqno + 2}
216 241 %TL.MTProto.Msgs.Ack{msg_ids: msg_ids} ->
217 242 %{state|msg_ids: state.msg_ids -- msg_ids}
218 243 %TL.MTProto.New.Session.Created{server_salt: server_salt} ->
219 - %{state|server_salt: <<server_salt :: little-size(64)>>}
244 + # convert to binary
245 + server_salt = <<server_salt :: little-size(64)>>
246 + # notify about authorization result, because this packet means
247 + # that connection is initialized after authorization
248 + send_to_notifier(state,
249 + {:authorized, state.auth_key, state.auth_key_hash, server_salt})
250 + # update state
251 + %{state|server_salt: server_salt}
252 + # TODO migrate to another DC
253 + # %TL.MTProto.Rpc.Error{error_code: 303, error_message: <<"NETWORK_MIGRATE_", dc_id :: binary>> = message} ->
254 + # send_to_notifier(state, {:error, 303, message})
255 + # dc_id = String.to_integer(dc_id)
256 + # request = %TL.Auth.ExportAuthorization{dc_id: dc_id}
257 + # send_rpc_request(state.socket, request, state)
258 + # # send(self, {:reconnect, {:change_dc, dc_id}})
259 + # %{state|reconnect: {:dc, dc_id}}
220 260 %TL.MTProto.Rpc.Error{error_code: code, error_message: message} ->
221 - send(state.notifier, {:error, code, message})
261 + send_to_notifier(state, {:error, code, message})
222 262 # TODO do we need to reconnect when server responds with error?
223 263 # send(self, {:reconnect, :change_dc})
224 264 state
  @@ -226,17 +266,29 @@ defmodule MTProto do
226 266 state = handle_packet(state, result)
227 267 # FIXME do we need to remove msg_id from current state in this place?
228 268 %{state|msg_ids: state.msg_ids -- [msg_id]}
269 + %TL.MTProto.Bad.Server.Salt{new_server_salt: server_salt} ->
270 + # convert to binary
271 + server_salt = <<server_salt :: little-size(64)>>
272 + # reconnect to use new server_salt
273 + send(self, {:reconnect, :server_salt_changed})
274 + # notify handler
275 + send_to_notifier(state, {:config, :server_salt, server_salt})
276 + %{state|server_salt: server_salt}
277 + %TL.MTProto.Bad.Msg.Notification{error_code: code} ->
278 + send_to_notifier(state, {:error, code, "bad_msg_id"})
279 + state
229 280 %TL.MTProto.Gzip.Packed{packed_data: packed_data} ->
230 281 {:ok, data} = TL.Serializer.decode(:zlib.gunzip(packed_data))
231 282 handle_packet(state, data)
232 283 # stores this_dc and dc list, changes when server fails
233 284 # or returns Rpc.Error, or accidentally disconnected
234 - %TL.Config{dc_options: dc_options, this_dc: dc} ->
235 - Logger.debug("set dc options, current: #{inspect dc}, list: #{inspect dc_options}")
285 + %TL.Config{dc_options: dc_options, this_dc: dc} = config ->
286 + # notify config
287 + send_to_notifier(state, {:config, :server_config, config})
236 288 %{state|dc_options: dc_options, dc: dc}
237 289 result ->
238 290 # IO.puts " --- handle_packet result: #{inspect result}"
239 - send(state.notifier, {:result, result})
291 + send_to_notifier(state, {:result, result})
240 292 state
241 293 end
242 294 end
  @@ -263,9 +315,15 @@ defmodule MTProto do
263 315 query: %TL.Help.GetConfig{}}}
264 316 end
265 317
266 - def send_rpc_request(socket, request, state) do
318 + defp send_to_notifier(state, message) do
319 + send(state.notifier, {:tl, message})
320 + end
321 +
322 + defp send_rpc_request(socket, request, state) do
267 323 {packet, state} = Packet.encode(request, state)
268 324
325 + send_to_notifier(state, {:msg_seqno, state.msg_seqno})
326 +
269 327 case :gen_tcp.send(socket, packet) do
270 328 :ok ->
271 329 {:ok, state}
  @@ -278,25 +336,8 @@ defmodule MTProto do
278 336 :ok = :inet.setopts(socket, active: :once)
279 337 end
280 338
281 - defp choose_server(%{dc: dc, dc_options: dc_options} = state) when length(dc_options) > 0 do
282 - acceptable_dcs = Enum.filter(dc_options, fn(dc_opt) ->
283 - dc_opt.ipv6 == false and dc_opt.id != dc
284 - end)
285 -
286 - new_dc = Enum.random(acceptable_dcs)
287 -
288 - {String.to_charlist(new_dc.ip_address), new_dc.port}
289 - end
290 - defp choose_server(_state) do
291 - {telegram_host(), telegram_port()}
292 - end
293 -
294 - defp telegram_host do
295 - config(:host)
296 - end
297 -
298 - defp telegram_port do
299 - config(:port)
339 + defp choose_server(state) do
340 + DC.choose(state.reconnect, state.dc, state.dc_options)
300 341 end
301 342
302 343 defp config(key, default \\ nil) do
  @@ -11,8 +11,7 @@ defmodule MTProto.Auth do
11 11 # make req_pq#60469778
12 12 req_pq = TL.MTProto.encode(%TL.MTProto.Req.Pq{nonce: nonce})
13 13
14 - # make auth
15 - # :ok = :gen_tcp.send(socket, Packet.encode_bare(req_pq, Math.make_message_id()))
14 + # send req_pq
16 15 send_bare_packet(client, req_pq)
17 16
18 17 # create auth_params
  @@ -131,13 +130,7 @@ defmodule MTProto.Auth do
131 130 {:ok, %TL.MTProto.Dh.Gen.Ok{} = dh_gen} ->
132 131 # check nonce_hash1
133 132 if state.auth_params.nonce_hash1 == dh_gen.new_nonce_hash1 do
134 - IO.puts " -- authorized"
135 - IO.puts " --- auth_key #{inspect state.auth_key, limit: 10240}"
136 - IO.puts " --- auth_key_hash #{inspect state.auth_key_hash, limit: 10240}"
137 - IO.puts " --- server_salt #{inspect state.server_salt, limit: 10240}"
138 -
139 133 send(client, :authorized)
140 -
141 134 {:ok, %State{state|auth_state: :encrypted, auth_params: nil}}
142 135 else
143 136 {:error, :mismatched_nonce_hash1, state}
  @@ -72,23 +72,53 @@ defmodule MTProto.Crypto do
72 72 encrypt_aes_ige256(tmp_aes_key, tmp_aes_iv, data_with_hash)
73 73 end
74 74
75 + @doc """
76 + auth_key_hash is computed := 64 lower-order bits of SHA1(auth_key).
77 +
78 + The server checks whether there already is another key with the same
79 + `auth_key_hash` and responds in one of the following ways.
80 + """
75 81 def auth_key_hash(auth_key) do
76 - sha = sha1(auth_key)
77 - :binary.part(sha, {byte_size(sha), -8})
82 + substr(sha1(auth_key), 12, 8)
78 83 end
79 84
85 + @doc """
86 + `new_nonce_hash1`, `new_nonce_hash2`, and `new_nonce_hash3` are obtained
87 + as the 128 lower-order bits of SHA1 of the byte string derived from the
88 + new_nonce string by adding a single byte with the value of 1, 2, or 3,
89 + and followed by another 8 bytes with auth_key_aux_hash.
90 +
91 + Different values are required to prevent an intruder from changing server
92 + response `dh_gen_ok` into `dh_gen_retry`.
93 + """
80 94 def make_nonce_hash1(new_nonce, auth_key) do
81 - auth_key_hash = :binary.part(sha1(auth_key), 0, 8)
95 + auth_key_hash = substr(sha1(auth_key), 0, 8)
82 96 nonce =
83 97 <<new_nonce :: binary-size(32), 1 :: size(8),
84 98 auth_key_hash :: binary-size(8)>>
85 99
86 - :binary.part(sha1(nonce), 4, 16)
100 + substr(sha1(nonce), 4, 16)
87 101 end
88 102
103 + @doc """
104 + server_salt := substr(new_nonce, 0, 8) XOR substr(server_nonce, 0, 8)
105 +
106 + https://core.telegram.org/mtproto/auth_key#dh-key-exchange-complete (9)
107 + """
89 108 def make_server_salt(new_nonce, server_nonce) do
90 - server_salt = :binary.part(new_nonce, 0, 8)
91 - Math.binary_bxor(server_salt, :binary.part(server_nonce, 0, 8))
109 + Math.binary_bxor(substr(new_nonce, 0, 8), substr(server_nonce, 0, 8))
110 + end
111 +
112 + def sha1(data) do
113 + :crypto.hash(:sha, data)
114 + end
115 +
116 + def encrypt_aes_ige256(aes_key, aes_iv, plain) do
117 + :crypto.block_encrypt(:aes_ige256, aes_key, aes_iv, plain)
118 + end
119 +
120 + def decrypt_aes_ige256(aes_key, aes_iv, encrypted) do
121 + :crypto.block_decrypt(:aes_ige256, aes_key, aes_iv, encrypted)
92 122 end
93 123
94 124 ### internal functions
  @@ -96,18 +126,6 @@ defmodule MTProto.Crypto do
96 126 defp action_value(:decode), do: 8
97 127 defp action_value(:encode), do: 0
98 128
99 - defp sha1(data) do
100 - :crypto.hash(:sha, data)
101 - end
102 -
103 - defp encrypt_aes_ige256(aes_key, aes_iv, plain) do
104 - :crypto.block_encrypt(:aes_ige256, aes_key, aes_iv, plain)
105 - end
106 -
107 - defp decrypt_aes_ige256(aes_key, aes_iv, encrypted) do
108 - :crypto.block_decrypt(:aes_ige256, aes_key, aes_iv, encrypted)
109 - end
110 -
111 129 defp substr(bin, start, length) do
112 130 :binary.part(bin, start, length)
113 131 end
Loading more files…