%%% @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(<>, 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(<>) -> #{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(<>, 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(<>, Acc) -> trim(T, [H | Acc]).