From ad541c67761b8aa6da1566802c347cac4018d0c2 Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Mon, 2 Nov 2026 20:50:24 +0100 Subject: Move the build sources into src/ --- src/ot_http.c | 810 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 810 insertions(+) create mode 100644 src/ot_http.c (limited to 'src/ot_http.c') diff --git a/src/ot_http.c b/src/ot_http.c new file mode 100644 index 0000000..96b8f4e --- /dev/null +++ b/src/ot_http.c @@ -0,0 +1,810 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. */ + +/* System */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Opentracker */ +#include "ot_accesslist.h" +#include "ot_fullscrape.h" +#include "ot_http.h" +#include "ot_iovec.h" +#include "ot_ip.h" +#include "ot_mutex.h" +#include "ot_workqueue.h" +#include "ot_scan.h" +#include "ot_event.h" +#include "ot_fdstate.h" +#include "ot_vector.h" +#include "ot_fmt.h" +#include "ot_stats.h" +#include "ot_urlencoded.h" +#include "trackerlogic.h" + +#define OT_MAXMULTISCRAPE_COUNT 64 +extern const char *g_redirecturl; + +const char *g_stats_path; +ssize_t g_stats_path_len; + +enum { SUCCESS_HTTP_HEADER_LENGTH = 80, SUCCESS_HTTP_SIZE_OFF = 17 }; + +struct http_data *http_adopt(const int64_t sock) { + struct http_data *cookie = fdstate_cookie(sock), *heap; + int interest; + + if (!cookie || !(cookie->flag & STRUCT_HTTP_FLAG_ONSTACK)) + return cookie; + + if (!(heap = malloc(sizeof(*heap)))) + return NULL; + + *heap = *cookie; + heap->flag &= ~STRUCT_HTTP_FLAG_ONSTACK; + + /* Publishing the cookie is what lets another thread reach this connection, + so nothing may touch it afterwards, not even to read what it asked for */ + interest = heap->interest; + fdstate_set_cookie((int)sock, heap); + + if (event_add((int)sock, interest, fdstate_generation((int)sock)) == -1) { + fdstate_set_cookie((int)sock, cookie); + free(heap); + return NULL; + } + return heap; +} + +/* Give up a connection: the one place that knows everything hanging off it. */ +void http_close(const int64_t sock) { + struct http_data *cookie = fdstate_cookie(sock); + + fdstate_close((int)sock); + + if (cookie) { + if (cookie->batch) + batch_reset(cookie->batch); + free(cookie->batch); + buf_reset(&cookie->request); + if (cookie->flag & (STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER)) + workqueue_canceltask(sock); + if (!(cookie->flag & STRUCT_HTTP_FLAG_ONSTACK)) + free(cookie); + } + + event_close((int)sock); +} + +static void http_senddata(const int64_t sock, struct ot_workstruct *ws) { + struct http_data *cookie = fdstate_cookie(sock); + ssize_t written_size; + + if (!cookie) { + http_close(sock); + return; + } + + /* whoever sends data is not interested in its input buffer */ + if (ws->keep_alive && ws->header_size != ws->request_size) { + size_t rest = ws->request_size - ws->header_size; + if (cookie->request.data) { + memmove(cookie->request.data, ws->request + ws->header_size, rest); + buf_truncate(&cookie->request, rest); + } else if (buf_cat(&cookie->request, ws->request + ws->header_size, rest)) { + /* Without room for what the client pipelined behind this request, answer + this one and let the connection close, rather than keeping it open + around a request we have silently dropped */ + buf_reset(&cookie->request); + ws->keep_alive = 0; + } + } else + buf_reset(&cookie->request); + + written_size = write(sock, ws->reply, ws->reply_size); + /* A send buffer with no room in it is not a failure: nothing went out, so + the whole reply goes into the batch below and leaves when the socket + drains. Only a real error ends the connection here. */ + if (written_size < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) + written_size = 0; + + if ((written_size < 0) || ((written_size == ws->reply_size) && !ws->keep_alive)) { + http_close(sock); + return; + } + + if (written_size < ws->reply_size) { + char *outbuf; + + if (!(outbuf = malloc(ws->reply_size - written_size))) { + http_close(sock); + return; + } + + memcpy(outbuf, ws->reply + written_size, ws->reply_size - written_size); + if (!cookie->batch) { + cookie->batch = calloc(1, sizeof(ot_batch)); + if (!cookie->batch) { + free(outbuf); + http_close(sock); + return; + } + } + + batch_add(cookie->batch, outbuf, ws->reply_size - written_size); + + /* writeable short data sockets just have a tcp timeout */ + if (!ws->keep_alive) + fdstate_set_expiry(sock, 0); + http_interest_set(sock, cookie, EVENT_OUT); + } +} + +#define HTTPERROR_302 return http_issue_error(sock, ws, CODE_HTTPERROR_302) +#define HTTPERROR_400 return http_issue_error(sock, ws, CODE_HTTPERROR_400) +#define HTTPERROR_400_PARAM return http_issue_error(sock, ws, CODE_HTTPERROR_400_PARAM) +#define HTTPERROR_400_COMPACT return http_issue_error(sock, ws, CODE_HTTPERROR_400_COMPACT) +#define HTTPERROR_400_DOUBLEHASH return http_issue_error(sock, ws, CODE_HTTPERROR_400_PARAM) +#define HTTPERROR_402_NOTMODEST return http_issue_error(sock, ws, CODE_HTTPERROR_402_NOTMODEST) +#define HTTPERROR_403_IP return http_issue_error(sock, ws, CODE_HTTPERROR_403_IP) +#define HTTPERROR_404 return http_issue_error(sock, ws, CODE_HTTPERROR_404) +#define HTTPERROR_500 return http_issue_error(sock, ws, CODE_HTTPERROR_500) +/* A connection is either being read or being written, never both, so what it + waits for is set outright rather than added to and taken away again. The + event layer only hears about it when the answer changes. */ +void http_interest_set(const int64_t sock, struct http_data *cookie, int what) { + if (cookie->interest == what) + return; + cookie->interest = what; + /* Still on a stack, so not in the event loop yet: whoever adopts this + connection registers whatever it asked for last */ + if (cookie->flag & STRUCT_HTTP_FLAG_ONSTACK) + return; + event_modify((int)sock, what, fdstate_generation((int)sock)); +} + +ssize_t http_issue_error(const int64_t sock, struct ot_workstruct *ws, int code) { + const char *error_code[] = {"302 Found", "400 Invalid Request", "400 Invalid Request", "400 Invalid Request", "402 Payment Required", + "403 Not Modest", "403 Access Denied", "404 Not Found", "500 Internal Server Error"}; + const char *title = error_code[code]; + + ws->reply = ws->outbuf; + if (code == CODE_HTTPERROR_302) + ws->reply_size = snprintf(ws->reply, G_OUTBUF_SIZE, "HTTP/1.0 302 Found\r\nContent-Length: 0\r\nLocation: %s\r\n\r\n", g_redirecturl); + else + ws->reply_size = snprintf(ws->reply, G_OUTBUF_SIZE, "HTTP/1.0 %s\r\nContent-Type: text/html\r\nContent-Length: %zd\r\n\r\n%s\n", title, + strlen(title) + 16 - 4, title + 4); + +#ifdef _DEBUG_HTTPERROR + fprintf(stderr, "DEBUG: invalid request was: %s\n", ws->debugbuf); +#endif + stats_issue_event(ws, EVENT_FAILED, FLAG_TCP, code); + http_senddata(sock, ws); + return ws->reply_size = -2; +} + +ssize_t http_sendiovecdata(const int64_t sock, struct ot_workstruct *ws, size_t iovec_entries, struct iovec *iovector, int is_partial) { + struct http_data *cookie = fdstate_cookie(sock); + char *header; + const char *encoding = ""; + size_t header_size, size = iovec_length(&iovec_entries, (const struct iovec **)&iovector); + + /* No cookie? Bad socket. Leave. */ + if (!cookie) { + iovec_free(&iovec_entries, &iovector); + HTTPERROR_500; + } + + /* Descriptor numbers get recycled, so a result can only belong to this + connection if this connection is in fact waiting for one. */ + if (!(cookie->flag & (STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER))) { + iovec_free(&iovec_entries, &iovector); + return 0; + } + + /* If this socket collected request in a buffer, free it now */ + buf_reset(&cookie->request); + + /* If we came here, wait for the answer is over */ + if (cookie->flag & STRUCT_HTTP_FLAG_WAITINGFORTASK) { + http_interest_set(sock, cookie, 0); + cookie->flag &= ~STRUCT_HTTP_FLAG_WAITINGFORTASK; + } + + if (iovec_entries) { + + if (cookie->flag & STRUCT_HTTP_FLAG_ZSTD) + encoding = "Content-Encoding: zstd\r\n"; + else if (cookie->flag & STRUCT_HTTP_FLAG_GZIP) + encoding = "Content-Encoding: gzip\r\n"; + else if (cookie->flag & STRUCT_HTTP_FLAG_BZIP2) + encoding = "Content-Encoding: bzip2\r\n"; + + if (!(cookie->flag & STRUCT_HTTP_FLAG_CHUNKED)) + header_size = asprintf(&header, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n%sContent-Length: %zd\r\n\r\n", encoding, size); + else { + if (!(cookie->flag & STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER)) { + header_size = + asprintf(&header, "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\n%sTransfer-Encoding: chunked\r\n\r\n%zx\r\n", encoding, size); + cookie->flag |= STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER; + } else + header_size = asprintf(&header, "%zx\r\n", size); + } + if (!header) { + iovec_free(&iovec_entries, &iovector); + HTTPERROR_500; + } + + if (!cookie->batch) { + cookie->batch = calloc(1, sizeof(ot_batch)); + if (!cookie->batch) { + free(header); + iovec_free(&iovec_entries, &iovector); + HTTPERROR_500; + } + } + + batch_add(cookie->batch, header, header_size); + batch_add_vector(cookie->batch, iovector, iovec_entries); + free(iovector); + + if (cookie->flag & STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER) + batch_add_copy(cookie->batch, "\r\n", 2); + } + + if ((cookie->flag & STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER) && cookie->batch && !is_partial) { + batch_add_copy(cookie->batch, "0\r\n\r\n", 5); + cookie->flag &= ~STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER; + } + + fdstate_set_expiry(sock, time_future_seconds(OT_CLIENT_TIMEOUT_SEND)); + http_interest_set(sock, cookie, EVENT_OUT); + return 0; +} + +static ssize_t http_handle_stats(const int64_t sock, struct ot_workstruct *ws, char *read_ptr) { + static const ot_keywords keywords_main[] = {{"mode", 1}, {"format", 2}, {"info_hash", 3}, {NULL, -3}}; + static const ot_keywords keywords_mode[] = {{"peer", TASK_STATS_PEERS}, + {"conn", TASK_STATS_CONNS}, + {"scrp", TASK_STATS_SCRAPE}, + {"udp4", TASK_STATS_UDP}, + {"tcp4", TASK_STATS_TCP}, + {"torr", TASK_STATS_TORRENTS}, + {"fscr", TASK_STATS_FULLSCRAPE}, + {"s24s", TASK_STATS_SLASH24S}, + {"tpbs", TASK_STATS_TPB}, + {"herr", TASK_STATS_HTTPERRORS}, + {"completed", TASK_STATS_COMPLETED}, + {"top100", TASK_STATS_TOP100}, + {"top10", TASK_STATS_TOP10}, + {"renew", TASK_STATS_RENEW}, + {"syncs", TASK_STATS_SYNCS}, + {"everything", TASK_STATS_EVERYTHING}, + {"statedump", TASK_FULLSCRAPE_TRACKERSTATE}, + {"woodpeckers", TASK_STATS_WOODPECKERS}, +#ifdef WANT_LOG_NUMWANT + {"numwants", TASK_STATS_NUMWANTS}, +#endif + {NULL, -3}}; + static const ot_keywords keywords_format[] = {{"bin", TASK_FULLSCRAPE_TPB_BINARY}, {"ben", TASK_FULLSCRAPE}, + {"url", TASK_FULLSCRAPE_TPB_URLENCODED}, {"txt", TASK_FULLSCRAPE_TPB_ASCII}, + {"txtp", TASK_FULLSCRAPE_TPB_ASCII_PLUS}, {NULL, -3}}; + + struct http_data *cookie = fdstate_cookie(sock); + char *reply; + int mode = TASK_STATS_PEERS, scanon = 1, format = 0; + + /* A socket without one is a dead socket, which is what http_sendiovecdata + already answers 500 to. */ + if (!cookie) + HTTPERROR_500; + +#ifdef WANT_RESTRICT_STATS + if (!accesslist_is_blessed(cookie->ip, OT_PERMISSION_MAY_STAT)) + HTTPERROR_403_IP; +#endif + + while (scanon) { + switch (urlencoded_match_keyword(keywords_main, &read_ptr, SCAN_SEARCHPATH_PARAM)) { + case -2: + scanon = 0; + break; /* TERMINATOR */ + case -1: + HTTPERROR_400_PARAM; /* PARSE ERROR */ + case -3: + urlencoded_skip_value(&read_ptr); + break; + case 1: /* matched "mode" */ + if ((mode = urlencoded_match_keyword(keywords_mode, &read_ptr, SCAN_SEARCHPATH_VALUE)) <= 0) + HTTPERROR_400_PARAM; + break; + case 2: /* matched "format" */ + if ((format = urlencoded_match_keyword(keywords_format, &read_ptr, SCAN_SEARCHPATH_VALUE)) <= 0) + HTTPERROR_400_PARAM; + break; + case 3: + HTTPERROR_400_PARAM; /* If the stats URL was mistakenly added as announce URL, return a 400 */ + } + } + +#ifdef WANT_FULLSCRAPE + if (mode == TASK_FULLSCRAPE_TRACKERSTATE) { + format = mode; + mode = TASK_STATS_TPB; + } + + if (mode == TASK_STATS_TPB) { +#ifdef WANT_COMPRESSION_GZIP + ws->request[ws->request_size - 1] = 0; +#ifndef WANT_COMPRESSION_GZIP_ALWAYS + if (strstr(read_ptr - 1, "gzip")) { +#endif + cookie->flag |= STRUCT_HTTP_FLAG_GZIP; + format |= TASK_FLAG_GZIP; +#ifndef WANT_COMPRESSION_GZIP_ALWAYS + } +#endif +#endif + cookie->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED; + + /* Clients waiting for us should not easily timeout */ + fdstate_set_expiry(sock, 0); + http_interest_set(sock, cookie, 0); + + /* Everything this connection needs has been written, so it can be given + a home of its own and only then be made reachable by a worker thread */ + if (!http_adopt(sock)) + HTTPERROR_500; + fullscrape_deliver(sock, format); + return ws->reply_size = -2; + } +#endif + + /* default format for now */ + if ((mode & TASK_CLASS_MASK) == TASK_STATS) { + + cookie->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK; + + /* Complex stats also include expensive memory debugging tools */ + fdstate_set_expiry(sock, 0); + + if (!http_adopt(sock)) + HTTPERROR_500; + stats_deliver(sock, mode); + return ws->reply_size = -2; + } + + /* Simple stats can be answerred immediately */ + reply = ws->reply; + return_stats_for_tracker(&reply, ws->outbuf + G_OUTBUF_SIZE, mode, 0); + return ws->reply_size = reply - ws->reply; +} + +#ifdef WANT_MODEST_FULLSCRAPES +static pthread_mutex_t g_modest_fullscrape_mutex = PTHREAD_MUTEX_INITIALIZER; +static ot_vector g_modest_fullscrape_timeouts; +typedef struct { + ot_ip6 ip; + ot_time last_fullscrape; +} ot_scrape_log; +#endif + +#ifdef WANT_FULLSCRAPE +static ssize_t http_handle_fullscrape(const int64_t sock, struct ot_workstruct *ws) { + struct http_data *cookie = fdstate_cookie(sock); + int format = 0; + +#ifdef WANT_MODEST_FULLSCRAPES + { + ot_scrape_log this_peer, *new_peer; + int exactmatch; + memcpy(this_peer.ip, cookie->ip, sizeof(ot_ip6)); + this_peer.last_fullscrape = time_now_seconds(); + pthread_mutex_lock(&g_modest_fullscrape_mutex); + new_peer = vector_find_or_insert(&g_modest_fullscrape_timeouts, &this_peer, sizeof(ot_scrape_log), sizeof(ot_ip6), &exactmatch); + if (!new_peer) { + pthread_mutex_unlock(&g_modest_fullscrape_mutex); + HTTPERROR_500; + } + if (exactmatch && (this_peer.last_fullscrape - new_peer->last_fullscrape) < OT_MODEST_PEER_TIMEOUT) { + pthread_mutex_unlock(&g_modest_fullscrape_mutex); + HTTPERROR_402_NOTMODEST; + } + memcpy(new_peer, &this_peer, sizeof(ot_scrape_log)); + pthread_mutex_unlock(&g_modest_fullscrape_mutex); + } +#endif + + +#if defined(WANT_COMPRESSION_GZIP) || defined(WANT_COMPRESSION_ZSTD) + ws->request[ws->request_size - 1] = 0; +#ifdef WANT_COMPRESSION_GZIP + if (strstr(ws->request, "gzip")) { + cookie->flag |= STRUCT_HTTP_FLAG_GZIP; + format |= TASK_FLAG_GZIP; + } +#endif +#ifdef WANT_COMPRESSION_ZSTD + if (strstr(ws->request, "zstd")) { + cookie->flag |= STRUCT_HTTP_FLAG_ZSTD; + format |= TASK_FLAG_ZSTD; + } +#endif + +#if defined(WANT_COMPRESSION_ZSTD) && defined(WANT_COMPRESSION_ZSTD_ALWAYS) + cookie->flag |= STRUCT_HTTP_FLAG_ZSTD; + format |= TASK_FLAG_ZSTD; +#endif + +#if defined(WANT_COMPRESSION_GZIP) && defined(WANT_COMPRESSION_GZIP_ALWAYS) + cookie->flag |= STRUCT_HTTP_FLAG_GZIP; + format |= TASK_FLAG_GZIP; +#endif +#endif + + stats_issue_event(ws, EVENT_FULLSCRAPE_REQUEST, 0, (uintptr_t)cookie->ip); + +#ifdef _DEBUG_HTTPERROR + fprintf(stderr, "%s", ws->debugbuf); +#endif + + /* Pass this task to the worker thread */ + cookie->flag |= STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED; + /* Clients waiting for us should not easily timeout */ + fdstate_set_expiry(sock, 0); + http_interest_set(sock, cookie, 0); + + if (!http_adopt(sock)) + HTTPERROR_500; + fullscrape_deliver(sock, format); + return ws->reply_size = -2; +} +#endif + +static int compare_hash(const void *left, const void *right) { return memcmp(left, right, sizeof(ot_hash)); } + +static ssize_t http_handle_scrape(const int64_t sock, struct ot_workstruct *ws, char *read_ptr) { + static const ot_keywords keywords_scrape[] = {{"info_hash", 1}, {NULL, -3}}; + + ot_hash *multiscrape_buf = (ot_hash *)ws->request; + char *reply = ws->reply; + int scanon = 1; + size_t i, unique, numwant = 0; + + /* This is to hack around stupid clients that send "scrape ?info_hash" */ + if (read_ptr[-1] != '?') { + while ((*read_ptr != '?') && (*read_ptr != '\n')) + ++read_ptr; + if (*read_ptr == '\n') + HTTPERROR_400_PARAM; + ++read_ptr; + } + + while (scanon) { + switch (urlencoded_match_keyword(keywords_scrape, &read_ptr, SCAN_SEARCHPATH_PARAM)) { + case -2: + scanon = 0; + break; /* TERMINATOR */ + default: + HTTPERROR_400_PARAM; /* PARSE ERROR */ + case -3: + urlencoded_skip_value(&read_ptr); + break; + case 1: /* matched "info_hash" */ + /* ignore this, when we have less than 20 bytes */ + if (urlencoded_parse(&read_ptr, (char *)(multiscrape_buf + numwant++), SCAN_SEARCHPATH_VALUE) != (ssize_t)sizeof(ot_hash)) + HTTPERROR_400_PARAM; + break; + } + } + + /* No info_hash found? Inform user */ + if (!numwant) + HTTPERROR_400_PARAM; + + /* Limit number of hashes to process */ + if (numwant > OT_MAXMULTISCRAPE_COUNT) + numwant = OT_MAXMULTISCRAPE_COUNT; + + /* A bencoded dictionary has its keys in order and names each one once, so + the answer cannot just follow the order the client asked in */ + qsort(multiscrape_buf, numwant, sizeof(ot_hash), compare_hash); + for (i = unique = 1; i < numwant; ++i) + if (memcmp(multiscrape_buf[i], multiscrape_buf[unique - 1], sizeof(ot_hash))) + memcpy(multiscrape_buf[unique++], multiscrape_buf[i], sizeof(ot_hash)); + numwant = unique; + + /* Enough for http header + whole scrape string */ + return_tcp_scrape_for_torrent((const ot_hash *)multiscrape_buf, numwant, &reply, ws->outbuf + G_OUTBUF_SIZE); + ws->reply_size = reply - ws->reply; + stats_issue_event(ws, EVENT_SCRAPE, FLAG_TCP, ws->reply_size); + return ws->reply_size; +} + +#ifdef WANT_LOG_NUMWANT +unsigned long long numwants[201]; +#endif + +#if defined(WANT_KEEPALIVE) || defined(WANT_IP_FROM_PROXY) +static char *http_header(char *data, size_t byte_count, const char *header) { + size_t i; + long sl = strlen(header); + for (i = 0; i + sl + 2 < byte_count; ++i) { + if (data[i] != '\n' || data[i + sl + 1] != ':') + continue; + if (strncasecmp(data + i + 1, header, sl)) + continue; + data += i + sl + 2; + while (*data == ' ' || *data == '\t') + ++data; + return data; + } + return 0; +} +#endif + +static ot_keywords keywords_announce[] = {{"port", 1}, {"left", 2}, {"event", 3}, {"numwant", 4}, {"compact", 5}, {"compact6", 5}, {"info_hash", 6}, +#ifdef WANT_IP_FROM_QUERY_STRING + {"ip", 7}, +#endif + {"peer_id", 8}, {NULL, -3}}; +static ot_keywords keywords_announce_event[] = {{"completed", 1}, {"stopped", 2}, {NULL, -3}}; + +/* For the replies that are one fixed string and nothing else, where the only + thing the caller wants back is how long it turned out */ +static ssize_t reply_literal(struct ot_workstruct *ws, const char *text) { + char *reply = ws->reply; + + fmt_bytes(&reply, ws->outbuf + G_OUTBUF_SIZE, text, strlen(text)); + return reply - ws->reply; +} + +static ssize_t http_handle_announce(const int64_t sock, struct ot_workstruct *ws, char *read_ptr) { + size_t numwant; + int64_t tmp; + int scanon; + unsigned short port = 0; + char *write_ptr; + ssize_t len; + struct http_data *cookie = fdstate_cookie(sock); + + /* This is to hack around stupid clients that send "announce ?info_hash" */ + if (read_ptr[-1] != '?') { + while ((*read_ptr != '?') && (*read_ptr != '\n')) + ++read_ptr; + if (*read_ptr == '\n') + HTTPERROR_400_PARAM; + ++read_ptr; + } + +#ifdef WANT_IP_FROM_PROXY + if (accesslist_is_blessed(cookie->ip, OT_PERMISSION_MAY_PROXY)) { + ot_ip6 proxied_ip; + char *fwd = http_header(ws->request, ws->header_size, "x-forwarded-for"); + if (fwd && ip_parse(fwd, proxied_ip)) { + OT_SETIP(ws->peer, proxied_ip); + } else + OT_SETIP(ws->peer, cookie->ip); + } else +#endif + OT_SETIP(ws->peer, cookie->ip); + + ws->peer_id = NULL; + ws->hash = NULL; + + OT_SETPORT(ws->peer, &port); + OT_PEERFLAG(ws->peer) = 0; + numwant = 50; + scanon = 1; + + while (scanon) { + switch (urlencoded_match_keyword(keywords_announce, &read_ptr, SCAN_SEARCHPATH_PARAM)) { + case -2: + scanon = 0; + break; /* TERMINATOR */ + case -1: + HTTPERROR_400_PARAM; /* PARSE ERROR */ + case -3: + urlencoded_skip_value(&read_ptr); + break; + case 1: /* matched "port" */ + len = urlencoded_parse(&read_ptr, write_ptr = read_ptr, SCAN_SEARCHPATH_VALUE); + if ((len <= 0) || scan_int(write_ptr, len, &tmp) || (tmp > 0xffff)) + HTTPERROR_400_PARAM; + port = htons((uint16_t)tmp); + OT_SETPORT(&ws->peer, &port); + break; + case 2: /* matched "left" */ + if ((len = urlencoded_parse(&read_ptr, write_ptr = read_ptr, SCAN_SEARCHPATH_VALUE)) <= 0) + HTTPERROR_400_PARAM; + if (scan_int(write_ptr, len, &tmp)) + tmp = 0; + if (!tmp) + OT_PEERFLAG(&ws->peer) |= PEER_FLAG_SEEDING; + break; + case 3: /* matched "event" */ + switch (urlencoded_match_keyword(keywords_announce_event, &read_ptr, SCAN_SEARCHPATH_VALUE)) { + case -1: + HTTPERROR_400_PARAM; + case 1: /* matched "completed" */ + OT_PEERFLAG(&ws->peer) |= PEER_FLAG_COMPLETED; + break; + case 2: /* matched "stopped" */ + OT_PEERFLAG(&ws->peer) |= PEER_FLAG_STOPPED; + break; + default: + break; + } + break; + case 4: /* matched "numwant" */ + len = urlencoded_parse(&read_ptr, write_ptr = read_ptr, SCAN_SEARCHPATH_VALUE); + if ((len <= 0) || scan_int(write_ptr, len, &tmp)) + HTTPERROR_400_PARAM; + if (tmp < 0) + tmp = 50; + if (tmp > 200) + tmp = 200; + numwant = (size_t)tmp; + break; + case 5: /* matched "compact" */ + len = urlencoded_parse(&read_ptr, write_ptr = read_ptr, SCAN_SEARCHPATH_VALUE); + if ((len <= 0) || scan_int(write_ptr, len, &tmp)) + HTTPERROR_400_PARAM; + if (!tmp) + HTTPERROR_400_COMPACT; + break; + case 6: /* matched "info_hash" */ + if (ws->hash) + HTTPERROR_400_DOUBLEHASH; + /* ignore this, when we have less than 20 bytes */ + if (urlencoded_parse(&read_ptr, write_ptr = read_ptr, SCAN_SEARCHPATH_VALUE) != 20) + HTTPERROR_400_PARAM; + ws->hash = (ot_hash *)write_ptr; + break; +#ifdef WANT_IP_FROM_QUERY_STRING + case 7: /* matched "ip" */ + { + char *tmp_buf1 = ws->reply, *tmp_buf2 = ws->reply + 16; + len = urlencoded_parse(&read_ptr, tmp_buf2, SCAN_SEARCHPATH_VALUE); + tmp_buf2[len] = 0; + if ((len <= 0) || ip_parse(tmp_buf2, tmp_buf1) != (size_t)len) + HTTPERROR_400_PARAM; + OT_SETIP(&ws->peer, tmp_buf1); + } break; +#endif + case 8: /* matched "peer_id" */ + /* ignore this, when we have less than 20 bytes */ + if (urlencoded_parse(&read_ptr, write_ptr = read_ptr, SCAN_SEARCHPATH_VALUE) != 20) + HTTPERROR_400_PARAM; + ws->peer_id = write_ptr; + break; + } + } + +#ifdef WANT_LOG_NUMWANT + numwants[numwant]++; +#endif + + /* XXX DEBUG + stats_issue_event(ws, EVENT_ACCEPT, FLAG_TCP, (uintptr_t)ws->reply ); + */ + + /* Scanned whole query string */ + if (!ws->hash) + return ws->reply_size = reply_literal(ws, "d14:failure reason80:Your client forgot to send your torrent's info_hash. Please upgrade your client.e"); + + if (OT_PEERFLAG(&ws->peer) & PEER_FLAG_STOPPED) + ws->reply_size = remove_peer_from_torrent(FLAG_TCP, ws); + else + ws->reply_size = add_peer_to_torrent_and_return_peers(FLAG_TCP, ws, numwant); + + stats_issue_event(ws, EVENT_ANNOUNCE, FLAG_TCP, ws->reply_size); + return ws->reply_size; +} + +ssize_t http_handle_request(const int64_t sock, struct ot_workstruct *ws) { + ssize_t reply_off, len; + char *read_ptr = ws->request, *write_ptr; + +#ifdef _DEBUG_HTTPERROR + reply_off = ws->request_size; + if (ws->request_size >= G_DEBUGBUF_SIZE) + reply_off = G_DEBUGBUF_SIZE - 1; + memcpy(ws->debugbuf, ws->request, reply_off); + ws->debugbuf[reply_off] = 0; +#endif + + /* Tell subroutines where to put reply data */ + ws->reply = ws->outbuf + SUCCESS_HTTP_HEADER_LENGTH; + + /* Find out if the client wants to keep this connection alive, before any + subroutine gets to answer: an error reply finishes with the connection + on its way out, and ws outlives the connection it was serving */ + ws->keep_alive = 0; +#ifdef WANT_KEEPALIVE + if ((write_ptr = http_header(ws->request, ws->header_size, "connection")) && (*write_ptr == 'K' || *write_ptr == 'k')) + ws->keep_alive = 1; +#endif + { + struct http_data *connection = fdstate_cookie(sock); + if (connection) { + if (ws->keep_alive) + connection->flag |= STRUCT_HTTP_FLAG_KEEPALIVE; + else + connection->flag &= ~STRUCT_HTTP_FLAG_KEEPALIVE; + } + } + + /* This one implicitely tests strlen < 5, too -- remember, it is \n terminated */ + if (memcmp(read_ptr, "GET /", 5)) + HTTPERROR_400; + + /* Skip leading '/' */ + for (read_ptr += 4; *read_ptr == '/'; ++read_ptr) + ; + + /* Try to parse the request. + In reality we abandoned requiring the url to be correct. This now + only decodes url encoded characters, we check for announces and + scrapes by looking for "a*" or "sc" */ + len = urlencoded_parse(&read_ptr, write_ptr = read_ptr, SCAN_PATH); + + /* If parsing returned an error, leave with not found */ + if (g_redirecturl && (len == -2)) + HTTPERROR_302; + if (len <= 0) + HTTPERROR_404; + + /* This is the hardcore match for announce*/ + if ((*write_ptr == 'a') || (*write_ptr == '?')) + http_handle_announce(sock, ws, read_ptr); +#ifdef WANT_FULLSCRAPE + else if (!memcmp(write_ptr, "scrape HTTP/", 12)) + http_handle_fullscrape(sock, ws); +#endif + /* This is the hardcore match for scrape */ + else if (!memcmp(write_ptr, "sc", 2)) + http_handle_scrape(sock, ws, read_ptr); + /* All the rest is matched the standard way */ + else if (len == g_stats_path_len && !memcmp(write_ptr, g_stats_path, len)) + http_handle_stats(sock, ws, read_ptr); + else + HTTPERROR_404; + + /* If routines handled sending themselves, just return */ + if (ws->reply_size == -2) + return 0; + /* If routine failed, let http error take over */ + if (ws->reply_size <= 0) + HTTPERROR_500; + + /* This one is rather ugly, so I take you step by step through it. + + 1. In order to avoid having two buffers, one for header and one for content, we allow all above functions from trackerlogic to + write to a fixed location, leaving SUCCESS_HTTP_HEADER_LENGTH bytes in our work buffer, which is enough for the static string + plus dynamic space needed to expand our Content-Length value. We reserve SUCCESS_HTTP_SIZE_OFF for its expansion and calculate + the space NOT needed to expand in reply_off + */ + reply_off = SUCCESS_HTTP_SIZE_OFF - snprintf(ws->outbuf, 0, "%zd", ws->reply_size); + ws->reply = ws->outbuf + reply_off; + + /* 2. Now we write the header so that its terminating '\0' lands exactly one byte before the content starts. The bound is the + space step 1 reserved, so the '\0' falls where step 3 expects it. Complete packet size is increased by size of header plus + one byte '\n', we will copy over '\0' in next step */ + ws->reply_size += 1 + snprintf(ws->reply, (size_t)(SUCCESS_HTTP_HEADER_LENGTH - reply_off), + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %zd\r\n\r", ws->reply_size); + + /* 3. Finally we join both blocks neatly */ + ws->outbuf[SUCCESS_HTTP_HEADER_LENGTH - 1] = '\n'; + + http_senddata(sock, ws); + return ws->reply_size; +} -- cgit v1.3