/* * This file Copyright (C) 2010-2014 Mnemosyne LLC * * It may be used under the GNU Public License v2 or v3 licenses, * or any future license endorsed by Mnemosyne LLC. * * $Id$ */ #define __LIBTRANSMISSION_ANNOUNCER_MODULE___ #include /* errno, EAFNOSUPPORT */ #include /* memcpy (), memset () */ #include #include #include #include "transmission.h" #include "announcer.h" #include "announcer-common.h" #include "crypto.h" /* tr_cryptoRandBuf () */ #include "log.h" #include "peer-io.h" #include "peer-mgr.h" /* tr_peerMgrCompactToPex () */ #include "ptrarray.h" #include "tr-udp.h" #include "utils.h" #define dbgmsg(name, ...) \ do \ { \ if (tr_logGetDeepEnabled ()) \ tr_logAddDeep (__FILE__, __LINE__, name, __VA_ARGS__); \ } \ while (0) /**** ***** ****/ static void tau_sockaddr_setport (struct sockaddr * sa, tr_port port) { if (sa->sa_family == AF_INET) ((struct sockaddr_in *)sa)->sin_port = htons (port); else if (sa->sa_family == AF_INET6) ((struct sockaddr_in6 *)sa)->sin6_port = htons (port); } static int tau_sendto (tr_session * session, struct evutil_addrinfo * ai, tr_port port, const void * buf, size_t buflen) { int sockfd; if (ai->ai_addr->sa_family == AF_INET) sockfd = session->udp_socket; else if (ai->ai_addr->sa_family == AF_INET6) sockfd = session->udp6_socket; else sockfd = -1; if (sockfd < 0) { errno = EAFNOSUPPORT; return -1; } tau_sockaddr_setport (ai->ai_addr, port); return sendto (sockfd, buf, buflen, 0, ai->ai_addr, ai->ai_addrlen); } /**** ***** ****/ static uint32_t evbuffer_read_ntoh_32 (struct evbuffer * buf) { uint32_t val; evbuffer_remove (buf, &val, sizeof (uint32_t)); return ntohl (val); } static uint64_t evbuffer_read_ntoh_64 (struct evbuffer * buf) { uint64_t val; evbuffer_remove (buf, &val, sizeof (uint64_t)); return tr_ntohll (val); } /**** ***** ****/ typedef uint64_t tau_connection_t; enum { TAU_CONNECTION_TTL_SECS = 60 }; typedef uint32_t tau_transaction_t; static tau_transaction_t tau_transaction_new (void) { tau_transaction_t tmp; tr_cryptoRandBuf (&tmp, sizeof (tau_transaction_t)); return tmp; } /* used in the "action" field of a request */ typedef enum { TAU_ACTION_CONNECT = 0, TAU_ACTION_ANNOUNCE = 1, TAU_ACTION_SCRAPE = 2, TAU_ACTION_ERROR = 3 } tau_action_t; static bool is_tau_response_message (int action, int msglen) { if (action == TAU_ACTION_CONNECT) return msglen == 16; if (action == TAU_ACTION_ANNOUNCE) return msglen >= 20; if (action == TAU_ACTION_SCRAPE ) return msglen >= 20; if (action == TAU_ACTION_ERROR ) return msglen >= 8; return false; } enum { TAU_REQUEST_TTL = 60 }; /**** ***** ***** SCRAPE ***** ****/ struct tau_scrape_request { void * payload; size_t payload_len; time_t sent_at; time_t created_at; tau_transaction_t transaction_id; tr_scrape_response response; tr_scrape_response_func callback; void * user_data; }; static struct tau_scrape_request * tau_scrape_request_new (const tr_scrape_request * in, tr_scrape_response_func callback, void * user_data) { int i; struct evbuffer * buf; struct tau_scrape_request * req; const tau_transaction_t transaction_id = tau_transaction_new (); /* build the payload */ buf = evbuffer_new (); evbuffer_add_hton_32 (buf, TAU_ACTION_SCRAPE); evbuffer_add_hton_32 (buf, transaction_id); for (i=0; iinfo_hash_count; ++i) evbuffer_add (buf, in->info_hash[i], SHA_DIGEST_LENGTH); /* build the tau_scrape_request */ req = tr_new0 (struct tau_scrape_request, 1); req->created_at = tr_time (); req->transaction_id = transaction_id; req->callback = callback; req->user_data = user_data; req->response.url = tr_strdup (in->url); req->response.row_count = in->info_hash_count; req->payload_len = evbuffer_get_length (buf); req->payload = tr_memdup (evbuffer_pullup (buf, -1), req->payload_len); for (i=0; iresponse.row_count; ++i) { req->response.rows[i].seeders = -1; req->response.rows[i].leechers = -1; req->response.rows[i].downloads = -1; memcpy (req->response.rows[i].info_hash, in->info_hash[i], SHA_DIGEST_LENGTH); } /* cleanup */ evbuffer_free (buf); return req; } static void tau_scrape_request_free (struct tau_scrape_request * req) { tr_free (req->response.errmsg); tr_free (req->response.url); tr_free (req->payload); tr_free (req); } static void tau_scrape_request_finished (const struct tau_scrape_request * request) { if (request->callback != NULL) request->callback (&request->response, request->user_data); } static void tau_scrape_request_fail (struct tau_scrape_request * request, bool did_connect, bool did_timeout, const char * errmsg) { request->response.did_connect = did_connect; request->response.did_timeout = did_timeout; request->response.errmsg = tr_strdup (errmsg); tau_scrape_request_finished (request); } static void on_scrape_response (struct tau_scrape_request * request, tau_action_t action, struct evbuffer * buf) { request->response.did_connect = true; request->response.did_timeout = false; if (action == TAU_ACTION_SCRAPE) { int i; for (i=0; iresponse.row_count; ++i) { struct tr_scrape_response_row * row; if (evbuffer_get_length (buf) < (sizeof (uint32_t) * 3)) break; row = &request->response.rows[i]; row->seeders = evbuffer_read_ntoh_32 (buf); row->downloads = evbuffer_read_ntoh_32 (buf); row->leechers = evbuffer_read_ntoh_32 (buf); } tau_scrape_request_finished (request); } else { char * errmsg; const size_t buflen = evbuffer_get_length (buf); if ((action == TAU_ACTION_ERROR) && (buflen > 0)) errmsg = tr_strndup (evbuffer_pullup (buf, -1), buflen); else errmsg = tr_strdup (_("Unknown error")); tau_scrape_request_fail (request, true, false, errmsg); tr_free (errmsg); } } /**** ***** ***** ANNOUNCE ***** ****/ struct tau_announce_request { void * payload; size_t payload_len; time_t created_at; time_t sent_at; tau_transaction_t transaction_id; tr_announce_response response; tr_announce_response_func callback; void * user_data; }; typedef enum { /* used in the "event" field of an announce request */ TAU_ANNOUNCE_EVENT_NONE = 0, TAU_ANNOUNCE_EVENT_COMPLETED = 1, TAU_ANNOUNCE_EVENT_STARTED = 2, TAU_ANNOUNCE_EVENT_STOPPED = 3 } tau_announce_event; static tau_announce_event get_tau_announce_event (tr_announce_event e) { switch (e) { case TR_ANNOUNCE_EVENT_COMPLETED: return TAU_ANNOUNCE_EVENT_COMPLETED; case TR_ANNOUNCE_EVENT_STARTED: return TAU_ANNOUNCE_EVENT_STARTED; case TR_ANNOUNCE_EVENT_STOPPED: return TAU_ANNOUNCE_EVENT_STOPPED; default: return TAU_ANNOUNCE_EVENT_NONE; } } static struct tau_announce_request * tau_announce_request_new (const tr_announce_request * in, tr_announce_response_func callback, void * user_data) { struct evbuffer * buf; struct tau_announce_request * req; const tau_transaction_t transaction_id = tau_transaction_new (); /* build the payload */ buf = evbuffer_new (); evbuffer_add_hton_32 (buf, TAU_ACTION_ANNOUNCE); evbuffer_add_hton_32 (buf, transaction_id); evbuffer_add (buf, in->info_hash, SHA_DIGEST_LENGTH); evbuffer_add (buf, in->peer_id, PEER_ID_LEN); evbuffer_add_hton_64 (buf, in->down); evbuffer_add_hton_64 (buf, in->leftUntilComplete); evbuffer_add_hton_64 (buf, in->up); evbuffer_add_hton_32 (buf, get_tau_announce_event (in->event)); evbuffer_add_hton_32 (buf, 0); evbuffer_add_hton_32 (buf, in->key); evbuffer_add_hton_32 (buf, in->numwant); evbuffer_add_hton_16 (buf, in->port); /* build the tau_announce_request */ req = tr_new0 (struct tau_announce_request, 1); req->created_at = tr_time (); req->transaction_id = transaction_id; req->callback = callback; req->user_data = user_data; req->payload_len = evbuffer_get_length (buf); req->payload = tr_memdup (evbuffer_pullup (buf, -1), req->payload_len); req->response.seeders = -1; req->response.leechers = -1; req->response.downloads = -1; memcpy (req->response.info_hash, in->info_hash, SHA_DIGEST_LENGTH); evbuffer_free (buf); return req; } static void tau_announce_request_free (struct tau_announce_request * req) { tr_free (req->response.tracker_id_str); tr_free (req->response.warning); tr_free (req->response.errmsg); tr_free (req->response.pex6); tr_free (req->response.pex); tr_free (req->payload); tr_free (req); } static void tau_announce_request_finished (const struct tau_announce_request * request) { if (request->callback != NULL) request->callback (&request->response, request->user_data); } static void tau_announce_request_fail (struct tau_announce_request * request, bool did_connect, bool did_timeout, const char * errmsg) { request->response.did_connect = did_connect; request->response.did_timeout = did_timeout; request->response.errmsg = tr_strdup (errmsg); tau_announce_request_finished (request); } static void on_announce_response (struct tau_announce_request * request, tau_action_t action, struct evbuffer * buf) { const size_t buflen = evbuffer_get_length (buf); request->response.did_connect = true; request->response.did_timeout = false; if ((action == TAU_ACTION_ANNOUNCE) && (buflen >= 3*sizeof (uint32_t))) { tr_announce_response * resp = &request->response; resp->interval = evbuffer_read_ntoh_32 (buf); resp->leechers = evbuffer_read_ntoh_32 (buf); resp->seeders = evbuffer_read_ntoh_32 (buf); resp->pex = tr_peerMgrCompactToPex (evbuffer_pullup (buf, -1), evbuffer_get_length (buf), NULL, 0, &request->response.pex_count); tau_announce_request_finished (request); } else { char * errmsg; if ((action == TAU_ACTION_ERROR) && (buflen > 0)) errmsg = tr_strndup (evbuffer_pullup (buf, -1), buflen); else errmsg = tr_strdup (_("Unknown error")); tau_announce_request_fail (request, true, false, errmsg); tr_free (errmsg); } } /**** ***** ***** TRACKERS ***** ****/ struct tau_tracker { tr_session * session; char * key; char * host; int port; struct evdns_getaddrinfo_request * dns_request; struct evutil_addrinfo * addr; time_t addr_expiration_time; time_t connecting_at; time_t connection_expiration_time; tau_connection_t connection_id; tau_transaction_t connection_transaction_id; time_t close_at; tr_ptrArray announces; tr_ptrArray scrapes; }; static void tau_tracker_upkeep (struct tau_tracker *); static void tau_tracker_free (struct tau_tracker * t) { if (t->addr) evutil_freeaddrinfo (t->addr); if (t->dns_request != NULL) evdns_getaddrinfo_cancel (t->dns_request); tr_ptrArrayDestruct (&t->announces, (PtrArrayForeachFunc)tau_announce_request_free); tr_ptrArrayDestruct (&t->scrapes, (PtrArrayForeachFunc)tau_scrape_request_free); tr_free (t->host); tr_free (t->key); tr_free (t); } static void tau_tracker_fail_all (struct tau_tracker * tracker, bool did_connect, bool did_timeout, const char * errmsg) { int i; int n; tr_ptrArray * reqs; /* fail all the scrapes */ reqs = &tracker->scrapes; for (i=0, n=tr_ptrArraySize (reqs); iannounces; for (i=0, n=tr_ptrArraySize (reqs); idns_request = NULL; if (errcode) { char * errmsg = tr_strdup_printf (_("DNS Lookup failed: %s"), evdns_err_to_string (errcode)); dbgmsg (tracker->key, "%s", errmsg); tau_tracker_fail_all (tracker, false, false, errmsg); tr_free (errmsg); } else { dbgmsg (tracker->key, "DNS lookup succeeded"); tracker->addr = addr; tracker->addr_expiration_time = tr_time () + (60*60); /* one hour */ tau_tracker_upkeep (tracker); } } static void tau_tracker_send_request (struct tau_tracker * tracker, const void * payload, size_t payload_len) { struct evbuffer * buf = evbuffer_new (); dbgmsg (tracker->key, "sending request w/connection id %"PRIu64"\n", tracker->connection_id); evbuffer_add_hton_64 (buf, tracker->connection_id); evbuffer_add_reference (buf, payload, payload_len, NULL, NULL); tau_sendto (tracker->session, tracker->addr, tracker->port, evbuffer_pullup (buf, -1), evbuffer_get_length (buf)); evbuffer_free (buf); } static void tau_tracker_send_reqs (struct tau_tracker * tracker) { int i, n; tr_ptrArray * reqs; const time_t now = tr_time (); assert (tracker->dns_request == NULL); assert (tracker->connecting_at == 0); assert (tracker->addr != NULL); assert (tracker->connection_expiration_time > now); reqs = &tracker->announces; for (i=0, n=tr_ptrArraySize (reqs); isent_at) { dbgmsg (tracker->key, "sending announce req %p", (void*)req); req->sent_at = now; tau_tracker_send_request (tracker, req->payload, req->payload_len); if (req->callback == NULL) { tau_announce_request_free (req); tr_ptrArrayRemove (reqs, i); --i; --n; } } } reqs = &tracker->scrapes; for (i=0, n=tr_ptrArraySize (reqs); isent_at) { dbgmsg (tracker->key, "sending scrape req %p", (void*)req); req->sent_at = now; tau_tracker_send_request (tracker, req->payload, req->payload_len); if (req->callback == NULL) { tau_scrape_request_free (req); tr_ptrArrayRemove (reqs, i); --i; --n; } } } } static void on_tracker_connection_response (struct tau_tracker * tracker, tau_action_t action, struct evbuffer * buf) { const time_t now = tr_time (); tracker->connecting_at = 0; tracker->connection_transaction_id = 0; if (action == TAU_ACTION_CONNECT) { tracker->connection_id = evbuffer_read_ntoh_64 (buf); tracker->connection_expiration_time = now + TAU_CONNECTION_TTL_SECS; dbgmsg (tracker->key, "Got a new connection ID from tracker: %"PRIu64, tracker->connection_id); } else { char * errmsg; const size_t buflen = buf ? evbuffer_get_length (buf) : 0; if ((action == TAU_ACTION_ERROR) && (buflen > 0)) errmsg = tr_strndup (evbuffer_pullup (buf, -1), buflen); else errmsg = tr_strdup (_("Connection failed")); dbgmsg (tracker->key, "%s", errmsg); tau_tracker_fail_all (tracker, true, false, errmsg); tr_free (errmsg); } tau_tracker_upkeep (tracker); } static void tau_tracker_timeout_reqs (struct tau_tracker * tracker) { int i, n; tr_ptrArray * reqs; const time_t now = time (NULL); const bool cancel_all = tracker->close_at && (tracker->close_at <= now); if (tracker->connecting_at && (tracker->connecting_at + TAU_REQUEST_TTL < now)) { on_tracker_connection_response (tracker, TAU_ACTION_ERROR, NULL); } reqs = &tracker->announces; for (i=0, n=tr_ptrArraySize (reqs); icreated_at + TAU_REQUEST_TTL < now)) { dbgmsg (tracker->key, "timeout announce req %p", (void*)req); tau_announce_request_fail (req, false, true, NULL); tau_announce_request_free (req); tr_ptrArrayRemove (reqs, i); --i; --n; } } reqs = &tracker->scrapes; for (i=0, n=tr_ptrArraySize (reqs); icreated_at + TAU_REQUEST_TTL < now)) { dbgmsg (tracker->key, "timeout scrape req %p", (void*)req); tau_scrape_request_fail (req, false, true, NULL); tau_scrape_request_free (req); tr_ptrArrayRemove (reqs, i); --i; --n; } } } static bool tau_tracker_is_idle (const struct tau_tracker * tracker) { return tr_ptrArrayEmpty (&tracker->announces) && tr_ptrArrayEmpty (&tracker->scrapes); } static void tau_tracker_upkeep (struct tau_tracker * tracker) { const time_t now = tr_time (); /* if the address info is too old, expire it */ if (tracker->addr && (tracker->addr_expiration_time <= now)) { dbgmsg (tracker->host, "Expiring old DNS result"); evutil_freeaddrinfo (tracker->addr); tracker->addr = NULL; } /* are there any requests pending? */ if (tau_tracker_is_idle (tracker)) return; /* if we don't have an address yet, try & get one now. */ if (!tracker->addr && (tracker->dns_request == NULL)) { struct evutil_addrinfo hints; memset (&hints, 0, sizeof (hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; hints.ai_protocol = IPPROTO_UDP; dbgmsg (tracker->host, "Trying a new DNS lookup"); tracker->dns_request = evdns_getaddrinfo (tracker->session->evdns_base, tracker->host, NULL, &hints, tau_tracker_on_dns, tracker); return; } dbgmsg (tracker->key, "addr %p -- connected %d (%"TR_PRIuSIZE" %"TR_PRIuSIZE") -- connecting_at %"TR_PRIuSIZE, (void*)tracker->addr, (int)(tracker->connection_expiration_time > now), (size_t)tracker->connection_expiration_time, (size_t)now, (size_t)tracker->connecting_at); /* also need a valid connection ID... */ if (tracker->addr && (tracker->connection_expiration_time <= now) && (!tracker->connecting_at)) { struct evbuffer * buf = evbuffer_new (); tracker->connecting_at = now; tracker->connection_transaction_id = tau_transaction_new (); dbgmsg (tracker->key, "Trying to connect. Transaction ID is %u", tracker->connection_transaction_id); evbuffer_add_hton_64 (buf, 0x41727101980LL); evbuffer_add_hton_32 (buf, TAU_ACTION_CONNECT); evbuffer_add_hton_32 (buf, tracker->connection_transaction_id); tau_sendto (tracker->session, tracker->addr, tracker->port, evbuffer_pullup (buf, -1), evbuffer_get_length (buf)); evbuffer_free (buf); return; } tau_tracker_timeout_reqs (tracker); if ((tracker->addr != NULL) && (tracker->connection_expiration_time > now)) tau_tracker_send_reqs (tracker); } /**** ***** ***** SESSION ***** ****/ struct tr_announcer_udp { /* tau_tracker */ tr_ptrArray trackers; tr_session * session; }; static struct tr_announcer_udp* announcer_udp_get (tr_session * session) { struct tr_announcer_udp * tau; if (session->announcer_udp != NULL) return session->announcer_udp; tau = tr_new0 (struct tr_announcer_udp, 1); tau->trackers = TR_PTR_ARRAY_INIT; tau->session = session; session->announcer_udp = tau; return tau; } /* Finds the tau_tracker struct that corresponds to this url. If it doesn't exist yet, create one. */ static struct tau_tracker * tau_session_get_tracker (struct tr_announcer_udp * tau, const char * url) { int i; int n; int port; char * host; char * key; struct tau_tracker * tracker = NULL; /* see if we've already got a tracker that matches this host + port */ tr_urlParse (url, -1, NULL, &host, &port, NULL); key = tr_strdup_printf ("%s:%d", host, port); for (i=0, n=tr_ptrArraySize (&tau->trackers); !tracker && itrackers, i); if (!tr_strcmp0 (tmp->key, key)) tracker = tmp; } /* if we don't have a match, build a new tracker */ if (tracker == NULL) { tracker = tr_new0 (struct tau_tracker, 1); tracker->session = tau->session; tracker->key = key; tracker->host = host; tracker->port = port; tracker->scrapes = TR_PTR_ARRAY_INIT; tracker->announces = TR_PTR_ARRAY_INIT; tr_ptrArrayAppend (&tau->trackers, tracker); dbgmsg (tracker->key, "New tau_tracker created"); } else { tr_free (key); tr_free (host); } return tracker; } /**** ***** ***** PUBLIC API ***** ****/ void tr_tracker_udp_upkeep (tr_session * session) { struct tr_announcer_udp * tau = session->announcer_udp; if (tau != NULL) tr_ptrArrayForeach (&tau->trackers, (PtrArrayForeachFunc)tau_tracker_upkeep); } bool tr_tracker_udp_is_idle (const tr_session * session) { int i; int n; struct tr_announcer_udp * tau = session->announcer_udp; if (tau != NULL) for (i=0, n=tr_ptrArraySize (&tau->trackers); itrackers, i))) return false; return true; } /* drop dead now. */ void tr_tracker_udp_close (tr_session * session) { struct tr_announcer_udp * tau = session->announcer_udp; if (tau != NULL) { session->announcer_udp = NULL; tr_ptrArrayDestruct (&tau->trackers, (PtrArrayForeachFunc)tau_tracker_free); tr_free (tau); } } /* start shutting down. This doesn't destroy everything if there are requests, but sets a deadline on how much longer to wait for the remaining ones */ void tr_tracker_udp_start_shutdown (tr_session * session) { const time_t now = time (NULL); struct tr_announcer_udp * tau = session->announcer_udp; if (tau != NULL) { int i, n; for (i=0, n=tr_ptrArraySize (&tau->trackers); itrackers, i); tracker->close_at = now + 3; tau_tracker_upkeep (tracker); } } } /* @brief process an incoming udp message if it's a tracker response. * @return true if msg was a tracker response; false otherwise */ bool tau_handle_message (tr_session * session, const uint8_t * msg, size_t msglen) { int i; int n; struct tr_announcer_udp * tau; tau_action_t action_id; tau_transaction_t transaction_id; struct evbuffer * buf; /*fprintf (stderr, "got an incoming udp message w/len %"TR_PRIuSIZE"\n", msglen);*/ if (!session || !session->announcer_udp) return false; if (msglen < (sizeof (uint32_t)*2)) return false; /* extract the action_id and see if it makes sense */ buf = evbuffer_new (); evbuffer_add_reference (buf, msg, msglen, NULL, NULL); action_id = evbuffer_read_ntoh_32 (buf); if (!is_tau_response_message (action_id, msglen)) { evbuffer_free (buf); return false; } /* extract the transaction_id and look for a match */ tau = session->announcer_udp; transaction_id = evbuffer_read_ntoh_32 (buf); /*fprintf (stderr, "UDP got a transaction_id %u...\n", transaction_id);*/ for (i=0, n=tr_ptrArraySize (&tau->trackers); itrackers, i); /* is it a connection response? */ if (tracker->connecting_at && (transaction_id == tracker->connection_transaction_id)) { dbgmsg (tracker->key, "%"PRIu32" is my connection request!", transaction_id); on_tracker_connection_response (tracker, action_id, buf); evbuffer_free (buf); return true; } /* is it a response to one of this tracker's announces? */ reqs = &tracker->announces; for (j=0, jn=tr_ptrArraySize (reqs); jsent_at && (transaction_id == req->transaction_id)) { dbgmsg (tracker->key, "%"PRIu32" is an announce request!", transaction_id); tr_ptrArrayRemove (reqs, j); on_announce_response (req, action_id, buf); tau_announce_request_free (req); evbuffer_free (buf); return true; } } /* is it a response to one of this tracker's scrapes? */ reqs = &tracker->scrapes; for (j=0, jn=tr_ptrArraySize (reqs); jsent_at && (transaction_id == req->transaction_id)) { dbgmsg (tracker->key, "%"PRIu32" is a scrape request!", transaction_id); tr_ptrArrayRemove (reqs, j); on_scrape_response (req, action_id, buf); tau_scrape_request_free (req); evbuffer_free (buf); return true; } } } /* no match... */ evbuffer_free (buf); return false; } void tr_tracker_udp_announce (tr_session * session, const tr_announce_request * request, tr_announce_response_func response_func, void * user_data) { struct tr_announcer_udp * tau = announcer_udp_get (session); struct tau_tracker * tracker = tau_session_get_tracker (tau, request->url); struct tau_announce_request * r = tau_announce_request_new (request, response_func, user_data); tr_ptrArrayAppend (&tracker->announces, r); tau_tracker_upkeep (tracker); } void tr_tracker_udp_scrape (tr_session * session, const tr_scrape_request * request, tr_scrape_response_func response_func, void * user_data) { struct tr_announcer_udp * tau = announcer_udp_get (session); struct tau_tracker * tracker = tau_session_get_tracker (tau, request->url); struct tau_scrape_request * r = tau_scrape_request_new (request, response_func, user_data); tr_ptrArrayAppend (&tracker->scrapes, r); tau_tracker_upkeep (tracker); }