<commit_before>%% @author <NAME> <<EMAIL>>
%% @doc Handle communication with trackers
%% <p>This module handles all communication with a tracker. It will
%% periodically announce to the tracker for an update. Eventual errors
%% and new Peers are fed back into the Peer Manager process so they
%% can be processed to completion.</p>
%% <p>For UDP tracking, we delegate the work to the UDP tracker
%% system.</p>
%% @end
-module(etorrent_tracker_communication).

-behaviour(gen_server).
-include("log.hrl").
-include("types.hrl").

-ifdef(TEST).
-include_lib("eqc/include/eqc.hrl").
-include_lib("eunit/include/eunit.hrl").
-endif.

%% API
-export([start_link/5, completed/1]).
-ignore_xref([{'start_link', 5}]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).

-record(state, {queued_message = none,
                %% The hard timer is the time we *must* wait on the tracker.
                %% soft timer may be overridden if we want to change state.
                soft_timer = none,
                hard_timer = none,
                url = [[]] :: [tier()],
                info_hash = none,
                peer_id = none,
                trackerid = none,
                control_pid = none,
                torrent_id = none}).

-define(DEFAULT_CONNECTION_TIMEOUT_INTERVAL, 1800).
-define(DEFAULT_CONNECTION_TIMEOUT_MIN_INTERVAL, 60).
-define(DEFAULT_TRACKER_OVERLOAD_INTERVAL, 300).
-define(DEFAULT_REQUEST_TIMEOUT, 240).
%%====================================================================

%% @doc Start the server
%% <p>Start the server. We are given a large amount of
%% information. The `ControlPid' refers to the Pid of the controller
%% process. The `UrlTiers' are a list of lists of string() parameters,
%% each a URL. Next comes the `Infohash' as a binary(), the `PeerId'
%% parameter and finally the `TorrentId': the identifier of the torrent.</p> 
%% @end
%% @todo What module, precisely do the control pid come from?
-spec start_link(pid(), [tier()], binary(), integer(), integer()) ->
    ignore | {ok, pid()} | {error, term()}.
start_link(ControlPid, UrlTiers, InfoHash, PeerId, TorrentId) ->
    gen_server:start_link(?MODULE,
                          [ControlPid,
                            UrlTiers, InfoHash, PeerId, TorrentId],
                          []).

%% @doc Prod the tracker and tell it we completed to torrent
%% @end
-spec completed(pid()) -> ok.
completed(Pid) ->
    gen_server:cast(Pid, completed).

%%====================================================================

%% @private
init([ControlPid, UrlTiers, InfoHash, PeerId, TorrentId]) ->
    process_flag(trap_exit, true),
    random:seed(now()),
    HardRef = erlang:send_after(0, self(), hard_timeout),
    SoftRef = erlang:send_after(timer:seconds(?DEFAULT_CONNECTION_TIMEOUT_INTERVAL),
			       self(),
			       soft_timeout),
    Url = swap_urls(shuffle_tiers(UrlTiers)),
    {ok, #state{control_pid = ControlPid,
                torrent_id = TorrentId,
                url = Url,
                info_hash = InfoHash,
                peer_id = PeerId,

                soft_timer = SoftRef,
                hard_timer = HardRef,

                queued_message = started}}.

%% @private
handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State}.

%% @private
handle_cast(completed, S) ->
    NS = contact_tracker(completed, S),
    {noreply, NS};
handle_cast(Msg, #state { hard_timer = none } = S) ->
    NS = contact_tracker(Msg, S),
    {noreply, NS};
handle_cast(Msg, S) ->
    ?ERR([unknown_msg, Msg]),
    {noreply, S}.

%% @private
handle_info(hard_timeout,
    #state { queued_message = none } = S) ->
    %% There is nothing to do with the hard_timer, just ignore this
    {noreply, S#state { hard_timer = none }};
handle_info(hard_timeout, S) ->
    NS = contact_tracker(S#state.queued_message, S),
    {noreply, NS#state { queued_message = none}};
handle_info(soft_timeout, S) ->
    %% Soft timeout
    NS = contact_tracker(S),
    {noreply, NS};
handle_info({Ref, _M}, State) when is_reference(Ref) ->
    %% Quench late messages arriving
    {noreply, State};
handle_info(_Info, State) ->
    {noreply, State}.

%% @private
terminate(Reason, S) when Reason =:= shutdown; Reason =:= normal ->
    _NS = contact_tracker(stopped, S),
    ok;
terminate(Reason, _S) ->
    ?WARN([terminating_due_to, Reason]),
    ok.

%% @private
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%--------------------------------------------------------------------
contact_tracker(S) ->
    contact_tracker(none, S).

contact_tracker(Event, #state { url = Tiers } = S) ->
    case contact_tracker(Tiers, Event, S) of
	{none, NS} ->
	    NS;
	{ok, NS} ->
	    NS
    end.

contact_tracker(Tiers, Event, S) ->
    contact_tracker(Tiers, Event, S, []).

contact_tracker([], _Event, S, _Acc) ->
    {none, handle_timeout(S)};
contact_tracker([Tier | NextTier], Event, S, Acc) ->
    case contact_tracker_tier(Tier, Event, S) of
	{ok, NS, NewTier} ->
	    {ok, NS#state { url = lists:reverse(Acc) ++ [NewTier | NextTier] }};
	none ->
	    contact_tracker(NextTier, Event, S, [Tier | Acc])
    end.

contact_tracker_tier(Tier, S, Event) ->
    contact_tracker_tier(Tier, S, Event, []).

contact_tracker_tier([], _Event, _S, _Acc) ->
    none;
contact_tracker_tier([Url | Next], Event, S, Acc) ->
    case
	case identify_url_type(Url) of
	    http -> contact_tracker_http(Url, Event, S);
	    {udp, IP, Port}  -> contact_tracker_udp(Url, IP, Port, Event, S)
	end
    of
	{ok, NS} ->
	    {ok, NS, [Url] ++ lists:reverse(Acc) ++ Next};
	error ->
	    %% For private torrent (BEP 27), disconnect all peers coming
	    %% from the tracker before switching to another one
	    case etorrent_torrent:is_private(S#state.torrent_id) of
	        true -> disconnect_tracker(Url)
	    end,
	    contact_tracker_tier(Next, Event, S, [Url | Acc])
    end.

identify_url_type(Url) ->
    case etorrent_http_uri:parse(Url) of
	{S1, _UserInfo, Host, Port, _Path, _Query} ->
	    case S1 of
		http ->
		    http;
		udp ->
		    {udp, Host, Port}
	    end;
	{error, Reason} ->
	    ?WARN([Reason, Url]),
	    exit(identify_url_type)

    end.

%% @doc Disconnect from tracker designated by its url.
%% <p>It is called to comply with BEP 27 Private Torrents,
%% which reads: "When switching between trackers,
%% the peer MUST disconnect from all current peers".</p>
%% @end<commit_msg>Case expression should always have one match.<commit_after>%% @author <NAME> <<EMAIL>>
%% @doc Handle communication with trackers
%% <p>This module handles all communication with a tracker. It will
%% periodically announce to the tracker for an update. Eventual errors
%% and new Peers are fed back into the Peer Manager process so they
%% can be processed to completion.</p>
%% <p>For UDP tracking, we delegate the work to the UDP tracker
%% system.</p>
%% @end
-module(etorrent_tracker_communication).

-behaviour(gen_server).
-include("log.hrl").
-include("types.hrl").

-ifdef(TEST).
-include_lib("eqc/include/eqc.hrl").
-include_lib("eunit/include/eunit.hrl").
-endif.

%% API
-export([start_link/5, completed/1]).
-ignore_xref([{'start_link', 5}]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).

-record(state, {queued_message = none,
                %% The hard timer is the time we *must* wait on the tracker.
                %% soft timer may be overridden if we want to change state.
                soft_timer = none,
                hard_timer = none,
                url = [[]] :: [tier()],
                info_hash = none,
                peer_id = none,
                trackerid = none,
                control_pid = none,
                torrent_id = none}).

-define(DEFAULT_CONNECTION_TIMEOUT_INTERVAL, 1800).
-define(DEFAULT_CONNECTION_TIMEOUT_MIN_INTERVAL, 60).
-define(DEFAULT_TRACKER_OVERLOAD_INTERVAL, 300).
-define(DEFAULT_REQUEST_TIMEOUT, 240).
%%====================================================================

%% @doc Start the server
%% <p>Start the server. We are given a large amount of
%% information. The `ControlPid' refers to the Pid of the controller
%% process. The `UrlTiers' are a list of lists of string() parameters,
%% each a URL. Next comes the `Infohash' as a binary(), the `PeerId'
%% parameter and finally the `TorrentId': the identifier of the torrent.</p> 
%% @end
%% @todo What module, precisely do the control pid come from?
-spec start_link(pid(), [tier()], binary(), integer(), integer()) ->
    ignore | {ok, pid()} | {error, term()}.
start_link(ControlPid, UrlTiers, InfoHash, PeerId, TorrentId) ->
    gen_server:start_link(?MODULE,
                          [ControlPid,
                            UrlTiers, InfoHash, PeerId, TorrentId],
                          []).

%% @doc Prod the tracker and tell it we completed to torrent
%% @end
-spec completed(pid()) -> ok.
completed(Pid) ->
    gen_server:cast(Pid, completed).

%%====================================================================

%% @private
init([ControlPid, UrlTiers, InfoHash, PeerId, TorrentId]) ->
    process_flag(trap_exit, true),
    random:seed(now()),
    HardRef = erlang:send_after(0, self(), hard_timeout),
    SoftRef = erlang:send_after(timer:seconds(?DEFAULT_CONNECTION_TIMEOUT_INTERVAL),
			       self(),
			       soft_timeout),
    Url = swap_urls(shuffle_tiers(UrlTiers)),
    {ok, #state{control_pid = ControlPid,
                torrent_id = TorrentId,
                url = Url,
                info_hash = InfoHash,
                peer_id = PeerId,

                soft_timer = SoftRef,
                hard_timer = HardRef,

                queued_message = started}}.

%% @private
handle_call(_Request, _From, State) ->
    Reply = ok,
    {reply, Reply, State}.

%% @private
handle_cast(completed, S) ->
    NS = contact_tracker(completed, S),
    {noreply, NS};
handle_cast(Msg, #state { hard_timer = none } = S) ->
    NS = contact_tracker(Msg, S),
    {noreply, NS};
handle_cast(Msg, S) ->
    ?ERR([unknown_msg, Msg]),
    {noreply, S}.

%% @private
handle_info(hard_timeout,
    #state { queued_message = none } = S) ->
    %% There is nothing to do with the hard_timer, just ignore this
    {noreply, S#state { hard_timer = none }};
handle_info(hard_timeout, S) ->
    NS = contact_tracker(S#state.queued_message, S),
    {noreply, NS#state { queued_message = none}};
handle_info(soft_timeout, S) ->
    %% Soft timeout
    NS = contact_tracker(S),
    {noreply, NS};
handle_info({Ref, _M}, State) when is_reference(Ref) ->
    %% Quench late messages arriving
    {noreply, State};
handle_info(_Info, State) ->
    {noreply, State}.

%% @private
terminate(Reason, S) when Reason =:= shutdown; Reason =:= normal ->
    _NS = contact_tracker(stopped, S),
    ok;
terminate(Reason, _S) ->
    ?WARN([terminating_due_to, Reason]),
    ok.

%% @private
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%--------------------------------------------------------------------
contact_tracker(S) ->
    contact_tracker(none, S).

contact_tracker(Event, #state { url = Tiers } = S) ->
    case contact_tracker(Tiers, Event, S) of
	{none, NS} ->
	    NS;
	{ok, NS} ->
	    NS
    end.

contact_tracker(Tiers, Event, S) ->
    contact_tracker(Tiers, Event, S, []).

contact_tracker([], _Event, S, _Acc) ->
    {none, handle_timeout(S)};
contact_tracker([Tier | NextTier], Event, S, Acc) ->
    case contact_tracker_tier(Tier, Event, S) of
	{ok, NS, NewTier} ->
	    {ok, NS#state { url = lists:reverse(Acc) ++ [NewTier | NextTier] }};
	none ->
	    contact_tracker(NextTier, Event, S, [Tier | Acc])
    end.

contact_tracker_tier(Tier, S, Event) ->
    contact_tracker_tier(Tier, S, Event, []).

contact_tracker_tier([], _Event, _S, _Acc) ->
    none;
contact_tracker_tier([Url | Next], Event, S, Acc) ->
    case
	case identify_url_type(Url) of
	    http -> contact_tracker_http(Url, Event, S);
	    {udp, IP, Port}  -> contact_tracker_udp(Url, IP, Port, Event, S)
	end
    of
	{ok, NS} ->
	    {ok, NS, [Url] ++ lists:reverse(Acc) ++ Next};
	error ->
	    %% For private torrent (BEP 27), disconnect all peers coming
	    %% from the tracker before switching to another one
	    case etorrent_torrent:is_private(S#state.torrent_id) of
	        true -> disconnect_tracker(Url);
	        _ -> ok
	    end,
	    contact_tracker_tier(Next, Event, S, [Url | Acc])
    end.

identify_url_type(Url) ->
    case etorrent_http_uri:parse(Url) of
	{S1, _UserInfo, Host, Port, _Path, _Query} ->
	    case S1 of
		http ->
		    http;
		udp ->
		    {udp, Host, Port}
	    end;
	{error, Reason} ->
	    ?WARN([Reason, Url]),
	    exit(identify_url_type)

    end.

%% @doc Disconnect from tracker designated by its url.
%% <p>It is called to comply with BEP 27 Private Torrents,
%% which reads: "When switching between trackers,
%% the peer MUST disconnect from all current peers".</p>
%% @end
{'fraction_non_alphanumeric': 0.16664102169564549, 'fraction_numerical': 0.004308355131558701, 'mean_word_length': 2.988646824179196, 'pattern_counts': {'":': 0, '<': 25, '<?xml version=': 0, '>': 104, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '974367', 'n_tokens_mistral': 4471, 'n_tokens_neox': 4189, 'n_words': 1150}