Packages

Erlang library for decoding and encoding Telegram MTProxy URLs

Current section

Files

Jump to
mtproxy_url src mtproxy_url.erl
Raw

src/mtproxy_url.erl

%%% @author Konstantin Rusalov
%%% @copyright (c) 2026 Konstantin Rusalov
-module(mtproxy_url).
-export([encode/1, encode/3, decode/1]).
-moduledoc """
MTProxy URL decoding and encoding
""".
-type ip4() :: {0..255, 0..255, 0..255, 0..255}.
-type ip6() :: {0..65535, 0..65535, 0..65535, 0..65535, 0..65535, 0..65535, 0..65535, 0..65535}.
-type ip_port() :: 0..65535.
-type server() ::
#{type := ip4, value := ip4()}
| #{type := ip6, value := ip6()}
| #{type := host, value := binary()}
| binary().
-type secret() ::
#{type := fake_tls, value := binary(), sni := binary()}
| #{type := padding, value := binary()}
| #{type := normal, value := binary()}
| binary().
-type url() :: binary().
-doc """
Encode URL
## Examples:
```erlang
Param = #{port => 443,server =>#{type => ip4,value => {1,1,1,1}}, secret => #{type => fake_tls, value => <<1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16>>, sni => <<"google.com">>}},
{ok,Result}=mtproxy_url:encode(Param).
```
""".
-spec encode(#{server := server(), port := ip_port(), secret := secret()}) -> {ok, binary()}.
encode(#{server := Server1, port := Port1, secret := Secret1}) ->
encode(Server1, Port1, Secret1).
-spec encode(server(), ip_port(), secret()) -> {ok, binary()}.
encode(Server1, Port1, Secret1) ->
Server =
case Server1 of
#{type := host, value := Host} -> Host;
#{type := TypeIp, value := Ip} when TypeIp =:= ip4; TypeIp =:= ip6 -> inet:ntoa(Ip);
Server1 when is_binary(Server1) -> Server1
end,
Secret =
case Secret1 of
#{type := fake_tls, value := Val, sni := Sni} ->
binary:encode_hex(<<16#ee, Val/binary, Sni/binary>>, lowercase);
#{type := padding, value := Val} ->
binary:encode_hex(<<16#dd, Val/binary>>, lowercase);
#{type := _, value := Val} ->
binary:encode_hex(Val, lowercase);
Secret1 when is_binary(Secret1) -> Secret1
end,
{ok,
list_to_binary(
io_lib:format("tg://proxy?server=~s&port=~p&secret=~s", [Server, Port1, Secret])
)}.
-doc """
Decode URL
## Examples:
```erlang
Param = <<"tg://proxy?server=1.1.1.1&port=443&secret=ee0102030405060708090a0b0c0d0e0f10676f6f676c652e636f6d">>,
{ok,Result}=mtproxy_url:decode(Param).
```
""".
-spec decode(url()) ->
{ok, #{server := server(), port := ip_port(), secret := secret()}}
| {error, term()}
| {term(), term()}.
decode(Url1) when is_binary(Url1) ->
try
Url = trim(Url1),
Param = url_param(Url),
case binary_to_integer(proplists:get_value(port, Param)) of
Port when Port > 0 ->
Server = server_decode(proplists:get_value(server, Param)),
Secret = proplists:get_value(secret, Param),
{ok, #{
server => Server,
port => Port,
secret => secret_decode(Secret)
}};
_ ->
{error, port}
end
catch
E:M -> {E, M}
end.
-spec server_decode(binary()) -> server().
server_decode(Bin) ->
server_decode_acc(Bin, []).
server_decode_acc(<<>>, Acc) ->
H = list_to_binary(lists:reverse(Acc)),
case inet:parse_address(binary_to_list(H)) of
{error, einval} ->
#{type => host, value => H};
{ok, {_, _, _, _} = Ip} ->
#{type => ip4, value => Ip};
{ok, _ = Ip} ->
#{type => ip6, value => Ip}
end;
server_decode_acc(<<$.>>, Acc) ->
server_decode_acc(<<>>, Acc);
server_decode_acc(<<H:8, T/binary>>, Acc) ->
case H of
H when H >= $A andalso H =< $Z ->
server_decode_acc(T, [H + 32 | Acc]);
H ->
server_decode_acc(T, [H | Acc])
end.
-spec secret_decode(binary()) -> secret().
secret_decode(Bin) ->
Secret =
try binary:decode_hex(Bin) of
Hex ->
case size(Hex) of
S when S < 16 ->
try base64_decode(Bin) of
X -> X
catch
_:_ -> Hex
end;
_S ->
Hex
end
catch
_:_ -> base64_decode(Bin)
end,
secret_parse(Secret).
secret_parse(<<16#ee, Rand:16/binary, Bin/binary>>) ->
#{type => fake_tls, value => Rand, sni => Bin};
secret_parse(<<16#dd, Rand:16/binary>>) ->
#{type => padding, value => Rand};
secret_parse(<<Rand:16/binary>>) ->
#{type => normal, value => Rand}.
-spec base64_decode(binary()) -> binary().
base64_decode(Bin) ->
Bin1 = base64_normalize(Bin, 0, []),
base64:decode(Bin1).
base64_normalize(<<>>, I, Acc) ->
Padding =
case I rem 4 of
2 -> <<"==">>;
3 -> <<"=">>;
_ -> <<>>
end,
list_to_binary(lists:reverse([Padding | Acc]));
base64_normalize(<<$-, T/binary>>, I, Acc) ->
base64_normalize(T, I + 1, [$+ | Acc]);
base64_normalize(<<$_, T/binary>>, I, Acc) ->
base64_normalize(T, I + 1, [$/ | Acc]);
base64_normalize(<<H:8, T/binary>>, I, Acc) ->
base64_normalize(T, I + 1, [H | Acc]).
url_query(<<$?, T/binary>>) -> T;
url_query(<<_:1/binary, T/binary>>) -> url_query(T);
url_query(<<>>) -> <<>>.
-spec url_param(binary()) -> [{atom(), binary()}].
url_param(Url) ->
Query = url_query(Url),
[
begin
case binary:split(X, [<<$=>>]) of
[V, E] -> {param_to_atom(V), E};
_ -> {undef, undef}
end
end
|| X <- binary:split(Query, [<<$&>>], [global]), X /= <<>>
].
param_to_atom(<<"server">>) -> server;
param_to_atom(<<"port">>) -> port;
param_to_atom(<<"secret">>) -> secret;
param_to_atom(_) -> undef.
-spec trim(binary()) -> binary().
trim(Bin) ->
trim(Bin, []).
trim(<<>>, Acc) -> list_to_binary(lists:reverse(Acc));
trim(<<$\r, T/binary>>, Acc) -> trim(T, Acc);
trim(<<$\n, T/binary>>, Acc) -> trim(T, Acc);
trim(<<$\s, T/binary>>, Acc) -> trim(T, Acc);
trim(<<H:8, T/binary>>, Acc) -> trim(T, [H | Acc]).