Handle HAVE_ALL and HAVE_NONE. Cleanup the BITFIELD message.
[etorrent.git] / lib / etorrent-1.0 / src / etorrent_tracker_communication.erl
blobc25765185bf2fcfb8c94b3691f8a3ee9776f4422
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 process_flag(trap_exit, true),
95 {ok, HardRef} = timer:send_after(0, hard_timeout),
96 {ok, SoftRef} = timer:send_after(timer:seconds(?DEFAULT_CONNECTION_TIMEOUT_INTERVAL),
97 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(_Reason, S) ->
170 _NS = contact_tracker(stopped, S),
173 %%--------------------------------------------------------------------
174 %% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
175 %% Description: Convert process state when code is changed
176 %%--------------------------------------------------------------------
177 code_change(_OldVsn, State, _Extra) ->
178 {ok, State}.
180 %%--------------------------------------------------------------------
181 %%% Internal functions
182 %%--------------------------------------------------------------------
184 contact_tracker(S) ->
185 contact_tracker(none, S).
187 contact_tracker(Event, S) ->
188 NewUrl = build_tracker_url(S, Event),
189 error_logger:info_report([{contacting_tracker, NewUrl}]),
190 case http_gzip:request(NewUrl) of
191 {ok, {{_, 200, _}, _, Body}} ->
192 handle_tracker_response(etorrent_bcoding:decode(Body), S);
193 {error, etimedout} ->
194 handle_timeout(S);
195 {error,econnrefused} ->
196 handle_timeout(S);
197 {error, session_remotly_closed} ->
198 handle_timeout(S)
199 end.
202 handle_tracker_response(BC, S) ->
203 handle_tracker_response(BC,
204 fetch_error_message(BC),
205 fetch_warning_message(BC),
208 handle_tracker_response(BC, {string, E}, _WM, S) ->
209 etorrent_t_control:tracker_error_report(S#state.control_pid, E),
210 handle_timeout(BC, S);
211 handle_tracker_response(BC, none, {string, W}, S) ->
212 etorrent_t_control:tracker_warning_report(S#state.control_pid, W),
213 handle_tracker_response(BC, none, none, S);
214 handle_tracker_response(BC, none, none, S) ->
215 %% Add new peers
216 etorrent_peer_mgr:add_peers(S#state.torrent_id,
217 response_ips(BC)),
218 %% Update the state of the torrent
219 ok = etorrent_torrent:statechange(S#state.torrent_id,
220 {tracker_report,
221 decode_integer("complete", BC),
222 decode_integer("incomplete", BC)}),
223 %% Timeout
224 TrackerId = tracker_id(BC),
225 handle_timeout(BC, S#state { trackerid = TrackerId }).
227 handle_timeout(S) ->
228 Interval = timer:seconds(?DEFAULT_CONNECTION_TIMEOUT_INTERVAL),
229 MinInterval = timer:seconds(?DEFAULT_CONNECTION_TIMEOUT_MIN_INTERVAL),
230 handle_timeout(Interval, MinInterval, S).
232 handle_timeout(BC, S) ->
233 Interval = response_interval(BC),
234 MinInterval = response_mininterval(BC),
235 handle_timeout(Interval, MinInterval, S).
237 handle_timeout(Interval, MinInterval, S) ->
238 NS = cancel_timers(S),
239 NNS = case MinInterval of
240 none ->
242 I when is_integer(I) ->
243 {ok, TRef} = timer:send_after(timer:seconds(I), hard_timeout),
244 NS#state { hard_timer = TRef }
245 end,
246 {ok, TRef2} = timer:send_after(timer:seconds(Interval), soft_timeout),
247 NNS#state { soft_timer = TRef2 }.
249 cancel_timers(S) ->
250 NS = case S#state.hard_timer of
251 none ->
253 TRef ->
254 timer:cancel(TRef),
255 S#state { hard_timer = none }
256 end,
257 case NS#state.soft_timer of
258 none ->
260 TRef2 ->
261 timer:cancel(TRef2),
262 NS#state { soft_timer = none }
263 end.
266 construct_headers([], HeaderLines) ->
267 lists:concat(lists:reverse(HeaderLines));
268 construct_headers([{Key, Value}], HeaderLines) ->
269 Data = lists:concat([Key, "=", Value]),
270 construct_headers([], [Data | HeaderLines]);
271 construct_headers([{Key, Value} | Rest], HeaderLines) ->
272 Data = lists:concat([Key, "=", Value, "&"]),
273 construct_headers(Rest, [Data | HeaderLines]).
275 build_tracker_url(S, Event) ->
276 [R] = etorrent_torrent:select(S#state.torrent_id),
277 {ok, Port} = application:get_env(etorrent, port),
278 Request = [{"info_hash",
279 etorrent_utils:build_encoded_form_rfc1738(S#state.info_hash)},
280 {"peer_id",
281 etorrent_utils:build_encoded_form_rfc1738(S#state.peer_id)},
282 {"uploaded", R#torrent.uploaded},
283 {"downloaded", R#torrent.downloaded},
284 {"left", R#torrent.left},
285 {"port", Port},
286 {"compact", 1}],
287 EReq = case Event of
288 none -> Request;
289 started -> [{"event", "started"} | Request];
290 stopped -> [{"event", "stopped"} | Request];
291 completed -> [{"event", "completed"} | Request]
292 end,
293 lists:concat([S#state.url, "?", construct_headers(EReq, [])]).
295 %%% Tracker response lookup functions
296 response_interval(BC) ->
297 {integer, R} = etorrent_bcoding:search_dict_default({string, "interval"},
299 {integer,
300 ?DEFAULT_REQUEST_TIMEOUT}),
303 response_mininterval(BC) ->
304 X = etorrent_bcoding:search_dict_default({string, "min interval"},
306 none),
307 case X of
308 {integer, R} -> R;
309 none -> none
310 end.
312 %%--------------------------------------------------------------------
313 %% Function: decode_ips(IpData) -> [{IP, Port}]
314 %% Description: Decode the IP response from the tracker
315 %%--------------------------------------------------------------------
316 decode_ips(D) ->
317 decode_ips(D, []).
319 decode_ips([], Accum) ->
320 Accum;
321 decode_ips([IPDict | Rest], Accum) ->
322 {string, IP} = etorrent_bcoding:search_dict({string, "ip"}, IPDict),
323 {integer, Port} = etorrent_bcoding:search_dict({string, "port"},
324 IPDict),
325 decode_ips(Rest, [{IP, Port} | Accum]);
326 decode_ips(<<>>, Accum) ->
327 Accum;
328 decode_ips(<<B1:8, B2:8, B3:8, B4:8, Port:16/big, Rest/binary>>, Accum) ->
329 decode_ips(Rest, [{{B1, B2, B3, B4}, Port} | Accum]).
331 response_ips(BC) ->
332 case etorrent_bcoding:search_dict_default({string, "peers"}, BC, none) of
333 {list, Ips} ->
334 decode_ips(Ips);
335 {string, Ips} ->
336 decode_ips(list_to_binary(Ips));
337 none ->
339 end.
341 tracker_id(BC) ->
342 etorrent_bcoding:search_dict_default({string, "trackerid"},
344 tracker_id_not_given).
346 decode_integer(Target, BC) ->
347 case etorrent_bcoding:search_dict_default({string, Target}, BC, none) of
348 {integer, N} ->
350 none ->
352 end.
354 fetch_error_message(BC) ->
355 etorrent_bcoding:search_dict_default({string, "failure reason"}, BC, none).
357 fetch_warning_message(BC) ->
358 etorrent_bcoding:search_dict_default({string, "warning message"}, BC, none).