Globalize the etorrent_t_peer_group_mgr and rename it to etorrent_choker while here.
[etorrent.git] / lib / etorrent-1.0 / src / etorrent_tracker_communication.erl
blob3c6b3b7b8f83d069b4649f529881b449d502f83b
1 %%%-------------------------------------------------------------------
2 %%% File : tracker_delegate.erl
3 %%% Author : Jesper Louis Andersen <jesper.louis.andersen@gmail.com>
4 %%% License : See COPYING
5 %%% Description : Handles communication with the tracker
6 %%%
7 %%% Created : 17 Jul 2007 by Jesper Louis Andersen <jesper.louis.andersen@gmail.com>
8 %%%-------------------------------------------------------------------
9 -module(etorrent_tracker_communication).
11 -behaviour(gen_server).
13 -include("etorrent_mnesia_table.hrl").
15 %% API
16 -export([start_link/5, contact/1, stopped/1, completed/1, started/1]).
18 %% gen_server callbacks
19 -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
20 terminate/2, code_change/3]).
22 -record(state, {should_contact_tracker = false,
23 queued_message = none,
24 %% The hard timer is the time we *must* wait on the tracker.
25 %% soft timer may be overridden if we want to change state.
26 soft_timer = none,
27 hard_timer = none,
28 url = none,
29 info_hash = none,
30 peer_id = none,
31 trackerid = none,
32 control_pid = none,
33 torrent_id = none}).
35 -define(DEFAULT_REQUEST_TIMEOUT, 180).
36 -define(DEFAULT_CONNECTION_TIMEOUT_INTERVAL, 1800).
37 -define(DEFAULT_CONNECTION_TIMEOUT_MIN_INTERVAL, 60).
38 -define(DEFAULT_TRACKER_OVERLOAD_INTERVAL, 300).
40 %%====================================================================
41 %% API
42 %%====================================================================
43 %%--------------------------------------------------------------------
44 %% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
45 %% Description: Starts the server
46 %%--------------------------------------------------------------------
47 start_link(ControlPid, Url, InfoHash, PeerId, TorrentId) ->
48 gen_server:start_link(?MODULE,
49 [ControlPid,
50 Url, InfoHash, PeerId, TorrentId],
51 []).
53 %%--------------------------------------------------------------------
54 %% Function: contact(Pid)
55 %% Description: Contact the tracker right now ignoring the soft timeout
56 %% it won't ignore the hard-timeout though.
57 %%--------------------------------------------------------------------
58 contact(Pid) ->
59 gen_server:cast(Pid, contact).
61 %%--------------------------------------------------------------------
62 %% Function: stopped(Pid)
63 %% Description: Tell the tracker that we stopped the torrent
64 %%--------------------------------------------------------------------
65 stopped(Pid) ->
66 gen_server:cast(Pid, stopped).
68 %%--------------------------------------------------------------------
69 %% Function: started(Pid)
70 %% Description: Tell the tracker that we started the torrent
71 %%--------------------------------------------------------------------
72 started(Pid) ->
73 gen_server:cast(Pid, started).
75 %%--------------------------------------------------------------------
76 %% Function: started(Pid)
77 %% Description: Tell the tracker that we completed the torrent
78 %%--------------------------------------------------------------------
79 completed(Pid) ->
80 gen_server:cast(Pid, completed).
82 %%====================================================================
83 %% gen_server callbacks
84 %%====================================================================
86 %%--------------------------------------------------------------------
87 %% Function: init(Args) -> {ok, State} |
88 %% {ok, State, Timeout} |
89 %% ignore |
90 %% {stop, Reason}
91 %% Description: Initiates the server
92 %%--------------------------------------------------------------------
93 init([ControlPid, Url, InfoHash, PeerId, TorrentId]) ->
94 {ok, HardRef} = timer:send_after(0, hard_timeout),
95 {ok, SoftRef} = timer:send_after(timer:seconds(?DEFAULT_CONNECTION_TIMEOUT_INTERVAL),
96 soft_timeout),
98 {ok, #state{should_contact_tracker = false,
99 control_pid = ControlPid,
100 torrent_id = TorrentId,
101 url = Url,
102 info_hash = InfoHash,
103 peer_id = PeerId,
105 soft_timer = SoftRef,
106 hard_timer = HardRef,
108 queued_message = started}}.
110 %%--------------------------------------------------------------------
111 %% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
112 %% {reply, Reply, State, Timeout} |
113 %% {noreply, State} |
114 %% {noreply, State, Timeout} |
115 %% {stop, Reason, Reply, State} |
116 %% {stop, Reason, State}
117 %% Description: Handling call messages
118 %%--------------------------------------------------------------------
119 handle_call(_Request, _From, State) ->
120 Reply = ok,
121 {reply, Reply, State}.
123 %%--------------------------------------------------------------------
124 %% Function: handle_cast(Msg, State) -> {noreply, State} |
125 %% {noreply, State, Timeout} |
126 %% {stop, Reason, State}
127 %% Description: Handling cast messages
128 %%----------------------p----------------------------------------------
129 handle_cast(Msg, S) when S#state.hard_timer =:= none ->
130 NS = contact_tracker(Msg, S),
131 {noreply, NS};
132 handle_cast(started, S) when S#state.queued_message =:= completed ->
133 {noreply, S};
134 handle_cast(started, S) when S#state.queued_message =:= stopped ->
135 {noreply, S#state {queued_message = started }};
136 handle_cast(stopped, S) ->
137 {noreply, S#state {queued_message = stopped }};
138 handle_cast(completed, S) ->
139 {noreply, S#state {queued_message = completed }}.
142 %%--------------------------------------------------------------------
143 %% Function: handle_info(Info, State) -> {noreply, State} |
144 %% {noreply, State, Timeout} |
145 %% {stop, Reason, State}
146 %% Description: Handling all non call/cast messages
147 %%--------------------------------------------------------------------
148 handle_info(hard_timeout, S) when S#state.queued_message =:= none ->
149 %% There is nothing to do with the hard_timer, just ignore this
150 {noreply, S#state { hard_timer = none }};
151 handle_info(hard_timeout, S) ->
152 NS = contact_tracker(S#state.queued_message, S),
153 {noreply, NS#state { queued_message = none}};
154 handle_info(soft_timeout, S) ->
155 %% Soft timeout
156 NS = contact_tracker(S),
157 {noreply, NS};
158 handle_info(_Info, State) ->
159 {noreply, State}.
161 %%--------------------------------------------------------------------
162 %% Function: terminate(Reason, State) -> void()
163 %% Description: This function is called by a gen_server when it is about to
164 %% terminate. It should be the opposite of Module:init/1 and do any necessary
165 %% cleaning up. When it returns, the gen_server terminates with Reason.
166 %% The return value is ignored.
167 %%--------------------------------------------------------------------
168 %% XXX: Cancel timers for completeness.
169 terminate(normal, S) ->
170 _NS = contact_tracker(stopped, S),
172 terminate(_Reason, _S) ->
175 %%--------------------------------------------------------------------
176 %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
177 %% Description: Convert process state when code is changed
178 %%--------------------------------------------------------------------
179 code_change(_OldVsn, State, _Extra) ->
180 {ok, State}.
182 %%--------------------------------------------------------------------
183 %%% Internal functions
184 %%--------------------------------------------------------------------
186 contact_tracker(S) ->
187 contact_tracker(none, S).
189 contact_tracker(Event, S) ->
190 NewUrl = build_tracker_url(S, Event),
191 case http_gzip:request(NewUrl) of
192 {ok, {{_, 200, _}, _, Body}} ->
193 handle_tracker_response(etorrent_bcoding:decode(Body), S);
194 {error, etimedout} ->
195 handle_timeout(S);
196 {error,econnrefused} ->
197 handle_timeout(S);
198 {error, session_remotly_closed} ->
199 handle_timeout(S)
200 end.
203 handle_tracker_response(BC, S) ->
204 handle_tracker_response(BC,
205 fetch_error_message(BC),
206 fetch_warning_message(BC),
209 handle_tracker_response(BC, {string, E}, _WM, S) ->
210 etorrent_t_control:tracker_error_report(S#state.control_pid, E),
211 handle_timeout(BC, S);
212 handle_tracker_response(BC, none, {string, W}, S) ->
213 etorrent_t_control:tracker_warning_report(S#state.control_pid, W),
214 handle_tracker_response(BC, none, none, S);
215 handle_tracker_response(BC, none, none, S) ->
216 %% Add new peers
217 etorrent_choker:add_peers(S#state.torrent_id,
218 response_ips(BC)),
219 %% Update the state of the torrent
220 ok = etorrent_torrent:statechange(S#state.torrent_id,
221 {tracker_report,
222 decode_integer("complete", BC),
223 decode_integer("incomplete", BC)}),
224 %% Timeout
225 TrackerId = tracker_id(BC),
226 handle_timeout(BC, S#state { trackerid = TrackerId }).
228 handle_timeout(S) ->
229 Interval = timer:seconds(?DEFAULT_CONNECTION_TIMEOUT_INTERVAL),
230 MinInterval = timer:seconds(?DEFAULT_CONNECTION_TIMEOUT_MIN_INTERVAL),
231 handle_timeout(Interval, MinInterval, S).
233 handle_timeout(BC, S) ->
234 Interval = response_interval(BC),
235 MinInterval = response_mininterval(BC),
236 handle_timeout(Interval, MinInterval, S).
238 handle_timeout(Interval, MinInterval, S) ->
239 NS = cancel_timers(S),
240 NNS = case MinInterval of
241 none ->
243 I when is_integer(I) ->
244 {ok, TRef} = timer:send_after(timer:seconds(I), hard_timeout),
245 NS#state { hard_timer = TRef }
246 end,
247 {ok, TRef2} = timer:send_after(timer:seconds(Interval), soft_timeout),
248 NNS#state { soft_timer = TRef2 }.
250 cancel_timers(S) ->
251 NS = case S#state.hard_timer of
252 none ->
254 TRef ->
255 timer:cancel(TRef),
256 S#state { hard_timer = none }
257 end,
258 case NS#state.soft_timer of
259 none ->
261 TRef2 ->
262 timer:cancel(TRef2),
263 NS#state { soft_timer = none }
264 end.
267 construct_headers([], HeaderLines) ->
268 lists:concat(lists:reverse(HeaderLines));
269 construct_headers([{Key, Value}], HeaderLines) ->
270 Data = lists:concat([Key, "=", Value]),
271 construct_headers([], [Data | HeaderLines]);
272 construct_headers([{Key, Value} | Rest], HeaderLines) ->
273 Data = lists:concat([Key, "=", Value, "&"]),
274 construct_headers(Rest, [Data | HeaderLines]).
276 build_tracker_url(S, Event) ->
277 [R] = etorrent_torrent:select(S#state.torrent_id),
278 {ok, Port} = application:get_env(etorrent, port),
279 Request = [{"info_hash",
280 etorrent_utils:build_encoded_form_rfc1738(S#state.info_hash)},
281 {"peer_id",
282 etorrent_utils:build_encoded_form_rfc1738(S#state.peer_id)},
283 {"uploaded", R#torrent.uploaded},
284 {"downloaded", R#torrent.downloaded},
285 {"left", R#torrent.left},
286 {"port", Port},
287 {"compact", 1}],
288 EReq = case Event of
289 none -> Request;
290 started -> [{"event", "started"} | Request];
291 stopped -> [{"event", "stopped"} | Request];
292 completed -> [{"event", "completed"} | Request]
293 end,
294 lists:concat([S#state.url, "?", construct_headers(EReq, [])]).
296 %%% Tracker response lookup functions
297 response_interval(BC) ->
298 {integer, R} = etorrent_bcoding:search_dict_default({string, "interval"},
300 {integer,
301 ?DEFAULT_REQUEST_TIMEOUT}),
304 response_mininterval(BC) ->
305 X = etorrent_bcoding:search_dict_default({string, "min interval"},
307 none),
308 case X of
309 {integer, R} -> R;
310 none -> none
311 end.
313 %%--------------------------------------------------------------------
314 %% Function: decode_ips(IpData) -> [{IP, Port}]
315 %% Description: Decode the IP response from the tracker
316 %%--------------------------------------------------------------------
317 decode_ips(D) ->
318 decode_ips(D, []).
320 decode_ips([], Accum) ->
321 Accum;
322 decode_ips([IPDict | Rest], Accum) ->
323 {string, IP} = etorrent_bcoding:search_dict({string, "ip"}, IPDict),
324 {integer, Port} = etorrent_bcoding:search_dict({string, "port"},
325 IPDict),
326 decode_ips(Rest, [{IP, Port} | Accum]);
327 decode_ips(<<>>, Accum) ->
328 Accum;
329 decode_ips(<<B1:8, B2:8, B3:8, B4:8, Port:16/big, Rest/binary>>, Accum) ->
330 decode_ips(Rest, [{{B1, B2, B3, B4}, Port} | Accum]).
332 response_ips(BC) ->
333 case etorrent_bcoding:search_dict_default({string, "peers"}, BC, none) of
334 {list, Ips} ->
335 decode_ips(Ips);
336 {string, Ips} ->
337 decode_ips(list_to_binary(Ips));
338 none ->
340 end.
342 tracker_id(BC) ->
343 etorrent_bcoding:search_dict_default({string, "trackerid"},
345 tracker_id_not_given).
347 decode_integer(Target, BC) ->
348 case etorrent_bcoding:search_dict_default({string, Target}, BC, none) of
349 {integer, N} ->
351 none ->
353 end.
355 fetch_error_message(BC) ->
356 etorrent_bcoding:search_dict_default({string, "failure reason"}, BC, none).
358 fetch_warning_message(BC) ->
359 etorrent_bcoding:search_dict_default({string, "warning message"}, BC, none).