Add scrape-only UDP tracker

This commit is contained in:
Lucas Gabriel Vuotto 2025-02-10 01:30:29 +00:00
parent a7ea02282f
commit c6c9166172
4 changed files with 434 additions and 1 deletions

View File

@ -1,7 +1,7 @@
PROG= bt PROG= bt
NOMAN= noman NOMAN= noman
SRCS= bt.c bcode.c SRCS= bt.c bcode.c tracker.c util.c
WARNINGS= Yes WARNINGS= Yes

36
bt.h
View File

@ -18,6 +18,14 @@
#include <stdint.h> #include <stdint.h>
/*
* CONSTANTS
*/
#define BTIH_LEN 20
/* /*
* STRUCTS * STRUCTS
*/ */
@ -25,6 +33,18 @@
struct bcode; struct bcode;
struct bttc_ctx;
struct btt_scrape_stats {
uint32_t seeders;
uint32_t completed;
uint32_t leechers;
};
struct btih {
uint8_t hash[BTIH_LEN];
} __packed;
/* /*
* PROTOTYPES * PROTOTYPES
@ -36,3 +56,19 @@ struct bcode;
struct bcode *bcode_parse(const uint8_t *, size_t); struct bcode *bcode_parse(const uint8_t *, size_t);
void bcode_free(struct bcode *); void bcode_free(struct bcode *);
void bcode_print(FILE *, const struct bcode *); void bcode_print(FILE *, const struct bcode *);
/* Tracker client */
struct bttc_ctx *bttc_ctx_new(const char *);
void bttc_ctx_free(struct bttc_ctx *);
const char *bttc_get_tracker(const struct bttc_ctx *);
int bttc_announce(struct bttc_ctx *, const uint8_t *,
const char **);
int bttc_scrape(struct bttc_ctx *, struct btt_scrape_stats *,
const struct btih *, size_t, const char **);
/* Utilities */
int btih_parse(struct btih *, const char *);

346
tracker.c Normal file
View File

@ -0,0 +1,346 @@
/*
* Copyright (c) 2025 Lucas Gabriel Vuotto <lucas@lgv5.net>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <endian.h>
#include <errno.h>
#include <inttypes.h>
#include <netdb.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "bt.h"
#ifndef nitems
#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
#endif
#define BTTC_UDP_ANNOUNCE_MAGIC UINT64_C(0x41727101980)
#define BTT_UDP_ACTION_CONNECT 0
#define BTT_UDP_ACTION_ANNOUNCE 1
#define BTT_UDP_ACTION_SCRAPE 2
struct bttc_ctx {
const char *url;
char *host;
char *port;
int socktype;
int sock;
uint64_t cid;
};
struct udpc_req_head {
uint64_t connection_id;
uint32_t action;
uint32_t transaction_id;
} __packed;
struct udpc_res_head {
uint32_t action;
uint32_t transaction_id;
} __packed;
struct udpc_connect_req {
uint64_t protocol_id;
uint32_t action;
uint32_t transaction_id;
} __packed;
struct udpc_connect_res {
uint32_t action;
uint32_t transaction_id;
uint64_t connection_id;
} __packed;
struct udpc_scrape_res {
uint32_t *seeders;
uint32_t *completed;
uint32_t *leechers;
};
static int tracker_dial(const char *, const char *, int, const char **);
static int url_parse(const char *, char **, char **);
static int udpc_action_connect(int, uint64_t *);
static int udpc_action_scrape(int, uint64_t, struct btt_scrape_stats *,
const struct btih *, size_t);
static int
tracker_dial(const char *host, const char *port, int socktype,
const char **cause)
{
struct addrinfo hints, *res, *res0;
int error, save_errno, s;
*cause = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = socktype;
error = getaddrinfo(host, port, &hints, &res0);
if (error) {
*cause = gai_strerror(error);
return -1;
}
s = -1;
for (res = res0; res != NULL; res = res->ai_next) {
s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (s == -1) {
*cause = "socket";
continue;
}
if (connect(s, res->ai_addr, res->ai_addrlen) == -1) {
*cause = "connect";
save_errno = errno;
close(s);
errno = save_errno;
s = -1;
continue;
}
break;
}
return s;
}
static int
url_parse(const char *tracker, char **host, char **port)
{
const char *colon, *end;
size_t hostlen, portlen;
colon = strchr(tracker, ':');
if (colon == NULL || colon == tracker)
return 0;
hostlen = colon - tracker + 1;
colon++;
end = strchr(colon, '/');
if (end == NULL)
end = strchr(colon, '\0');
if (end == colon)
return 0;
portlen = end - colon + 1;
*host = malloc(hostlen);
*port = malloc(portlen);
if (*host == NULL || *port == NULL) {
free(*host);
free(*port);
*host = *port = NULL;
return 0;
}
(void)strlcpy(*host, tracker, hostlen);
(void)strlcpy(*port, colon, portlen);
/* XXX handle extensions. */
return 1;
}
static int
udpc_action_connect(int s, uint64_t *cid)
{
char buf[512];
struct udpc_connect_req req;
struct udpc_connect_res res;
uint32_t tid;
tid = arc4random();
req.protocol_id = htobe64(BTTC_UDP_ANNOUNCE_MAGIC);
req.action = htobe32(BTT_UDP_ACTION_CONNECT);
req.transaction_id = htobe32(tid);
if (send(s, &req, sizeof(req), 0) != sizeof(req))
return 0;
if (recv(s, buf, sizeof(buf), 0) < (ssize_t)sizeof(res))
return 0;
memcpy(&res, buf, sizeof(res));
if (be32toh(res.action) != BTT_UDP_ACTION_CONNECT ||
be32toh(res.transaction_id) != tid)
return 0;
*cid = be64toh(res.connection_id);
return 1;
}
static int
udpc_action_scrape(int s, uint64_t cid, struct btt_scrape_stats *stats,
const struct btih *btih, size_t btihlen)
{
struct iovec reqiov[2], resiov[2];
uint32_t *rawstats;
uint8_t *rawbtih;
struct msghdr reqmsg, resmsg;
struct udpc_req_head reqhead;
struct udpc_res_head reshead;
size_t i;
ssize_t n;
uint32_t tid;
if (btihlen == 0)
return 0;
rawbtih = reallocarray(NULL, btihlen, BTIH_LEN * sizeof(uint8_t));
if (rawbtih == NULL)
return 0;
tid = arc4random();
reqhead.connection_id = htobe64(cid);
reqhead.action = htobe32(BTT_UDP_ACTION_SCRAPE);
reqhead.transaction_id = htobe32(tid);
reqiov[0].iov_base = &reqhead;
reqiov[0].iov_len = sizeof(reqhead);
for (i = 0; i < btihlen; i++)
memcpy(&rawbtih[i * BTIH_LEN], btih[i].hash,
BTIH_LEN * sizeof(uint8_t));
reqiov[1].iov_base = rawbtih;
reqiov[1].iov_len = btihlen * BTIH_LEN * sizeof(uint8_t);
memset(&reqmsg, 0, sizeof(reqmsg));
reqmsg.msg_iov = reqiov;
reqmsg.msg_iovlen = nitems(reqiov);
n = sendmsg(s, &reqmsg, 0);
free(rawbtih);
if (n == -1)
return 0;
rawstats = reallocarray(NULL, btihlen, 3 * sizeof(uint32_t));
if (rawstats == NULL)
return 0;
resiov[0].iov_base = &reshead;
resiov[0].iov_len = sizeof(reshead);
resiov[1].iov_base = rawstats;
resiov[1].iov_len = btihlen * 3 * sizeof(uint32_t);
memset(&resmsg, 0, sizeof(resmsg));
resmsg.msg_iov = resiov;
resmsg.msg_iovlen = nitems(resiov);
n = recvmsg(s, &resmsg, 0);
if (n == -1) {
free(rawstats);
return 0;
}
if (be32toh(reshead.action) != BTT_UDP_ACTION_SCRAPE ||
be32toh(reshead.transaction_id) != tid) {
free(rawstats);
return 0;
}
for (i = 0; i < btihlen; i++) {
stats[i].seeders = be32toh(rawstats[3 * i]);
stats[i].completed = be32toh(rawstats[3 * i + 1]);
stats[i].leechers = be32toh(rawstats[3 * i + 2]);
}
free(rawstats);
return 1;
}
struct bttc_ctx *
bttc_ctx_new(const char *tracker)
{
struct bttc_ctx *ctx;
if (tracker == NULL)
return NULL;
ctx = calloc(1, sizeof(*ctx));
if (ctx == NULL)
return NULL;
if (strncmp(tracker, "udp://", 6) == 0) {
if (!url_parse(tracker + 6, &ctx->host, &ctx->port))
return 0;
ctx->socktype = SOCK_DGRAM;
} else {
free(ctx);
return NULL;
}
ctx->url = tracker;
ctx->sock = -1;
return ctx;
}
void
bttc_ctx_free(struct bttc_ctx *ctx)
{
if (ctx == NULL)
return;
if (ctx->sock != -1)
(void)close(ctx->sock);
free(ctx->host);
free(ctx->port);
free(ctx);
}
const char *
bttc_get_tracker(const struct bttc_ctx *ctx)
{
return ctx->url;
}
int
bttc_scrape(struct bttc_ctx *ctx, struct btt_scrape_stats *stats,
const struct btih *btih, size_t btihlen, const char **cause)
{
*cause = NULL;
if (ctx->sock == -1) {
ctx->sock = tracker_dial(ctx->host, ctx->port, ctx->socktype,
cause);
if (ctx->sock == -1)
return 0;
}
if (!udpc_action_connect(ctx->sock, &ctx->cid)) {
*cause = "failed connection handshake";
return 0;
}
if (!udpc_action_scrape(ctx->sock, ctx->cid, stats, btih, btihlen)) {
*cause = "failed scrape";
return 0;
}
return 1;
}

51
util.c Normal file
View File

@ -0,0 +1,51 @@
/*
* Copyright (c) 2025 Lucas Gabriel Vuotto <lucas@lgv5.net>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <ctype.h>
#include <string.h>
#include "bt.h"
static inline unsigned int xdigittonum(char c);
static inline unsigned int
xdigittonum(char c)
{
return c >= 'a' ? c - 'a' + 10 : c >= 'A' ? c - 'A' + 10 : c - '0';
}
int
btih_parse(struct btih *btih, const char *s)
{
const unsigned char *us = s;
struct btih h;
size_t i;
if (strlen(s) != 2 * BTIH_LEN)
return 0;
for (i = 0; i < BTIH_LEN; i++) {
if (!isxdigit(us[2 * i]) || !isxdigit(us[2 * i + 1]))
return 0;
h.hash[i] = (xdigittonum(s[2 * i]) << 4) |
xdigittonum(s[2 * i + 1]);
}
memcpy(btih, &h, sizeof(h));
return 1;
}