diff --git a/Makefile b/Makefile index c6a067a..33f375f 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ PROG= bt NOMAN= noman -SRCS= bt.c bcode.c +SRCS= bt.c bcode.c tracker.c util.c WARNINGS= Yes diff --git a/bt.h b/bt.h index d0eccfa..e242ff1 100644 --- a/bt.h +++ b/bt.h @@ -18,6 +18,14 @@ #include +/* + * CONSTANTS + */ + + +#define BTIH_LEN 20 + + /* * STRUCTS */ @@ -25,6 +33,18 @@ 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 @@ -36,3 +56,19 @@ struct bcode; struct bcode *bcode_parse(const uint8_t *, size_t); void bcode_free(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 *); diff --git a/tracker.c b/tracker.c new file mode 100644 index 0000000..869657f --- /dev/null +++ b/tracker.c @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2025 Lucas Gabriel Vuotto + * + * 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/util.c b/util.c new file mode 100644 index 0000000..129c303 --- /dev/null +++ b/util.c @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 Lucas Gabriel Vuotto + * + * 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 +#include + +#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; +}