summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile40
-rw-r--r--ot_config.c7
-rw-r--r--ot_fmt.c42
-rw-r--r--ot_fmt.h21
-rw-r--r--ot_fullscrape.c38
-rw-r--r--ot_http.c42
-rw-r--r--ot_stats.c306
-rw-r--r--ot_stats.h4
-rw-r--r--ot_sync.c13
-rw-r--r--ot_vector.c47
-rw-r--r--tests/integration/.gitignore2
-rw-r--r--tests/integration/harness.py198
-rw-r--r--tests/integration/test_announce.py129
-rw-r--r--tests/integration/test_dualstack.py89
-rw-r--r--tests/runner.c73
-rw-r--r--tests/t_fmt.c108
-rw-r--r--tests/t_vector.c46
-rw-r--r--tests/test.h1
-rw-r--r--trackerlogic.c55
-rw-r--r--trackerlogic.h2
20 files changed, 1012 insertions, 251 deletions
diff --git a/Makefile b/Makefile
index d74742a..b172af6 100644
--- a/Makefile
+++ b/Makefile
@@ -69,8 +69,8 @@ CFLAGS_libowfat=-I$(LIBOWFAT_HEADERS)
69LDFLAGS_libowfat=-L$(LIBOWFAT_LIBRARY) -lowfat 69LDFLAGS_libowfat=-L$(LIBOWFAT_LIBRARY) -lowfat
70 70
71BINARY =opentracker 71BINARY =opentracker
72HEADERS=trackerlogic.h ot_urlencoded.h ot_batch.h ot_buf.h ot_event.h ot_fdstate.h ot_hex.h ot_ip.h ot_config.h ot_scan.h ot_socket.h ot_mutex.h ot_random.h ot_stats.h ot_vector.h ot_clean.h ot_udp.h ot_iovec.h ot_fullscrape.h ot_accesslist.h ot_http.h ot_livesync.h ot_rijndael.h 72HEADERS=trackerlogic.h ot_urlencoded.h ot_batch.h ot_buf.h ot_event.h ot_fdstate.h ot_fmt.h ot_hex.h ot_ip.h ot_config.h ot_scan.h ot_socket.h ot_mutex.h ot_random.h ot_stats.h ot_vector.h ot_clean.h ot_udp.h ot_iovec.h ot_fullscrape.h ot_accesslist.h ot_http.h ot_livesync.h ot_rijndael.h
73SOURCES=opentracker.c trackerlogic.c ot_urlencoded.c ot_batch.c ot_buf.c ot_event.c ot_fdstate.c ot_hex.c ot_ip.c ot_config.c ot_scan.c ot_socket.c ot_mutex.c ot_random.c ot_stats.c ot_vector.c ot_clean.c ot_udp.c ot_iovec.c ot_fullscrape.c ot_accesslist.c ot_http.c ot_livesync.c ot_rijndael.c 73SOURCES=opentracker.c trackerlogic.c ot_urlencoded.c ot_batch.c ot_buf.c ot_event.c ot_fdstate.c ot_fmt.c ot_hex.c ot_ip.c ot_config.c ot_scan.c ot_socket.c ot_mutex.c ot_random.c ot_stats.c ot_vector.c ot_clean.c ot_udp.c ot_iovec.c ot_fullscrape.c ot_accesslist.c ot_http.c ot_livesync.c ot_rijndael.c
74SOURCES_proxy=proxy.c ot_batch.c ot_fdstate.c ot_vector.c ot_mutex.c ot_random.c ot_scan.c ot_ip.c ot_socket.c 74SOURCES_proxy=proxy.c ot_batch.c ot_fdstate.c ot_vector.c ot_mutex.c ot_random.c ot_scan.c ot_ip.c ot_socket.c
75 75
76OBJECTS = $(SOURCES:%.c=%.o) 76OBJECTS = $(SOURCES:%.c=%.o)
@@ -120,8 +120,8 @@ $(OBJECTS_proxy_debug): $(HEADERS)
120# has to. tests/stubs.c supplies the few symbols those modules reference but 120# has to. tests/stubs.c supplies the few symbols those modules reference but
121# the tests never reach. 121# the tests never reach.
122TESTBIN=tests/runner 122TESTBIN=tests/runner
123TEST_SOURCES=tests/runner.c tests/test.c tests/stubs.c tests/t_urlencoded.c tests/t_iovec.c tests/t_vector.c tests/t_random.c tests/t_hex.c tests/t_accesslist.c tests/t_scan.c tests/t_buf.c tests/t_ip.c tests/t_socket.c tests/t_mutex.c tests/t_batch.c tests/t_fdstate.c tests/t_event.c 123TEST_SOURCES=tests/runner.c tests/test.c tests/stubs.c tests/t_urlencoded.c tests/t_iovec.c tests/t_vector.c tests/t_random.c tests/t_hex.c tests/t_accesslist.c tests/t_scan.c tests/t_buf.c tests/t_ip.c tests/t_socket.c tests/t_mutex.c tests/t_batch.c tests/t_fdstate.c tests/t_event.c tests/t_fmt.c
124TEST_UNITS=ot_urlencoded.c ot_event.c ot_batch.c ot_fdstate.c ot_buf.c ot_ip.c ot_socket.c ot_iovec.c ot_vector.c ot_random.c ot_hex.c ot_scan.c ot_accesslist.c ot_mutex.c 124TEST_UNITS=ot_urlencoded.c ot_fmt.c ot_event.c ot_batch.c ot_fdstate.c ot_buf.c ot_ip.c ot_socket.c ot_iovec.c ot_vector.c ot_random.c ot_hex.c ot_scan.c ot_accesslist.c ot_mutex.c
125TEST_HEADERS=tests/test.h $(HEADERS) 125TEST_HEADERS=tests/test.h $(HEADERS)
126# Tests reach code that sits behind feature flags, so the units under test are 126# Tests reach code that sits behind feature flags, so the units under test are
127# built with those features enabled. A test binary is not a production binary. 127# built with those features enabled. A test binary is not a production binary.
@@ -134,10 +134,38 @@ $(TESTBIN): $(TEST_SOURCES) $(TEST_UNITS) $(TEST_HEADERS)
134check: $(TESTBIN) $(BINARY) 134check: $(TESTBIN) $(BINARY)
135 ./$(TESTBIN) 135 ./$(TESTBIN)
136 136
137.PHONY: all check clean install 137# The same unit tests under the sanitizers. Use clang over $(CC) because the
138# two toolchains do not check the same things
139# Override CC_sanitize to use another compiler.
140CC_sanitize?=clang
141CFLAGS_sanitize=$(CFLAGS_test) -fsanitize=address,undefined -fno-omit-frame-pointer
142LDFLAGS_sanitize=$(LDFLAGS) -fsanitize=address,undefined
143
144$(TESTBIN).sanitize: $(TEST_SOURCES) $(TEST_UNITS) $(TEST_HEADERS)
145 $(CC_sanitize) -o $@ $(CFLAGS_sanitize) $(TEST_SOURCES) $(TEST_UNITS) $(LDFLAGS_sanitize)
146
147check-sanitize: $(TESTBIN).sanitize
148 ./$(TESTBIN).sanitize
149
150# Black box tests: these drive the real binary over the wire, so they cover
151# the event loop and the parsers working together, which the unit tests
152# cannot reach. Built with the sanitizers on, so the tracker reports its own
153# memory errors and the harness turns those into failures.
154PYTHON?=python3
155FEATURES_integration=$(FEATURES) -DWANT_IP_FROM_QUERY_STRING
156CFLAGS_integration=$(CFLAGS) $(OPTS_debug) $(FEATURES_integration) -fsanitize=address,undefined -fno-omit-frame-pointer
157LDFLAGS_integration=$(LDFLAGS) -fsanitize=address,undefined
158
159opentracker.integration: $(SOURCES) $(HEADERS)
160 $(CC) -o $@ $(CFLAGS_integration) $(SOURCES) $(LDFLAGS_integration)
161
162check-integration: opentracker.integration
163 OPENTRACKER=./opentracker.integration $(PYTHON) -m unittest discover -s tests/integration
164
165.PHONY: all check check-sanitize check-integration clean install
138 166
139clean: 167clean:
140 rm -rf opentracker opentracker.debug proxy proxy.debug $(TESTBIN) *.o *~ 168 rm -rf opentracker opentracker.debug opentracker.integration proxy proxy.debug $(TESTBIN) $(TESTBIN).sanitize *.o *~
141 169
142install: 170install:
143 install -m 755 opentracker $(DESTDIR)$(BINDIR) 171 install -m 755 opentracker $(DESTDIR)$(BINDIR)
diff --git a/ot_config.c b/ot_config.c
index 32625bd..ee6bc12 100644
--- a/ot_config.c
+++ b/ot_config.c
@@ -544,8 +544,11 @@ void config_parse(int argc, char **argv) {
544 help(argv[0]); 544 help(argv[0]);
545 exit(0); 545 exit(0);
546 case 'v': { 546 case 'v': {
547 char buffer[8192]; 547 char buffer[8192];
548 stats_return_tracker_version(buffer); 548 char *cursor = buffer;
549
550 stats_return_tracker_version(&cursor, buffer + sizeof(buffer) - 1);
551 *cursor = 0;
549 fputs(buffer, stderr); 552 fputs(buffer, stderr);
550 exit(0); 553 exit(0);
551 } 554 }
diff --git a/ot_fmt.c b/ot_fmt.c
new file mode 100644
index 0000000..9cab25f
--- /dev/null
+++ b/ot_fmt.c
@@ -0,0 +1,42 @@
1/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
2 It is considered beerware. Prost. Skol. Cheers or whatever.
3
4 $id$ */
5
6/* System */
7#include <stdarg.h>
8#include <stdio.h>
9#include <string.h>
10
11/* Opentracker */
12#include "ot_fmt.h"
13
14void fmt_append(char **cursor, const char *end, const char *format, ...) {
15 va_list arguments;
16 size_t room;
17 int written;
18
19 if (*cursor >= end) {
20 *cursor = (char *)end;
21 return;
22 }
23
24 room = (size_t)(end - *cursor);
25 va_start(arguments, format);
26 written = vsnprintf(*cursor, room, format, arguments);
27 va_end(arguments);
28
29 if (written < 0)
30 return;
31 *cursor = (size_t)written >= room ? (char *)end : *cursor + written;
32}
33
34void fmt_bytes(char **cursor, const char *end, const void *bytes, size_t length) {
35 if (*cursor >= end || (size_t)(end - *cursor) < length) {
36 *cursor = (char *)end;
37 return;
38 }
39
40 memcpy(*cursor, bytes, length);
41 *cursor += length;
42}
diff --git a/ot_fmt.h b/ot_fmt.h
new file mode 100644
index 0000000..851b2d8
--- /dev/null
+++ b/ot_fmt.h
@@ -0,0 +1,21 @@
1/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
2 It is considered beerware. Prost. Skol. Cheers or whatever.
3
4 $id$ */
5
6#ifndef OT_FMT_H__
7#define OT_FMT_H__
8
9#include <stddef.h>
10
11/* Appending to a cursor that walks a fixed buffer. Both take the cursor by
12 reference and advance it, so a reply is written as a run of statements and
13 no caller does pointer arithmetic of its own. */
14
15void fmt_append(char **cursor, const char *end, const char *format, ...) __attribute__((format(printf, 3, 4)));
16
17/* Append raw bytes for the binary stretches of a bencoded reply.
18 Either everything is appended or nothing at all */
19void fmt_bytes(char **cursor, const char *end, const void *bytes, size_t length);
20
21#endif
diff --git a/ot_fullscrape.c b/ot_fullscrape.c
index 2ba3b24..8518bd2 100644
--- a/ot_fullscrape.c
+++ b/ot_fullscrape.c
@@ -18,6 +18,7 @@
18#endif 18#endif
19 19
20/* Opentracker */ 20/* Opentracker */
21#include "ot_fmt.h"
21#include "ot_hex.h" 22#include "ot_hex.h"
22#include "ot_fullscrape.h" 23#include "ot_fullscrape.h"
23#include "ot_iovec.h" 24#include "ot_iovec.h"
@@ -83,10 +84,15 @@ void fullscrape_deliver( int64_t sock, ot_tasktype tasktype ) {
83 mutex_workqueue_pushtask( sock, TASK_FULLSCRAPE | tasktype ); 84 mutex_workqueue_pushtask( sock, TASK_FULLSCRAPE | tasktype );
84} 85}
85 86
86static char * fullscrape_write_one( ot_tasktype mode, char *r, ot_torrent *torrent, ot_hash *hash ) { 87static void fullscrape_write_one( ot_tasktype mode, char **cursor, ot_torrent *torrent, ot_hash *hash ) {
87 size_t seed_count = torrent->peer_list6->seed_count + torrent->peer_list4->seed_count; 88 /* Callers guarantee this much room: the chunk writer keeps its low
88 size_t peer_count = torrent->peer_list6->peer_count + torrent->peer_list4->peer_count; 89 watermark that far from the end, the compressors hand over a buffer of
89 size_t down_count = torrent->peer_list6->down_count + torrent->peer_list4->down_count; 90 exactly that size */
91 char *r = *cursor;
92 const char *r_end = r + OT_SCRAPE_MAXENTRYLEN;
93 size_t seed_count = torrent->peer_list6->seed_count + torrent->peer_list4->seed_count;
94 size_t peer_count = torrent->peer_list6->peer_count + torrent->peer_list4->peer_count;
95 size_t down_count = torrent->peer_list6->down_count + torrent->peer_list4->down_count;
90 96
91 switch (mode & TASK_TASK_MASK) { 97 switch (mode & TASK_TASK_MASK) {
92 case TASK_FULLSCRAPE: 98 case TASK_FULLSCRAPE:
@@ -98,18 +104,18 @@ static char * fullscrape_write_one( ot_tasktype mode, char *r, ot_torrent *torre
98 memcpy(r, hash, sizeof(ot_hash)); 104 memcpy(r, hash, sizeof(ot_hash));
99 r += sizeof(ot_hash); 105 r += sizeof(ot_hash);
100 /* push rest of the scrape string */ 106 /* push rest of the scrape string */
101 r += sprintf(r, "d8:completei%zde10:downloadedi%zde10:incompletei%zdee", seed_count, down_count, peer_count - seed_count); 107 fmt_append(&r, r_end, "d8:completei%zde10:downloadedi%zde10:incompletei%zdee", seed_count, down_count, peer_count - seed_count);
102 108
103 break; 109 break;
104 case TASK_FULLSCRAPE_TPB_ASCII: 110 case TASK_FULLSCRAPE_TPB_ASCII:
105 hex_from_hash(r, *hash); 111 hex_from_hash(r, *hash);
106 r += 2 * sizeof(ot_hash); 112 r += 2 * sizeof(ot_hash);
107 r += sprintf(r, ":%zd:%zd\n", seed_count, peer_count - seed_count); 113 fmt_append(&r, r_end, ":%zd:%zd\n", seed_count, peer_count - seed_count);
108 break; 114 break;
109 case TASK_FULLSCRAPE_TPB_ASCII_PLUS: 115 case TASK_FULLSCRAPE_TPB_ASCII_PLUS:
110 hex_from_hash(r, *hash); 116 hex_from_hash(r, *hash);
111 r += 2 * sizeof(ot_hash); 117 r += 2 * sizeof(ot_hash);
112 r += sprintf(r, ":%zd:%zd:%zd\n", seed_count, peer_count - seed_count, down_count); 118 fmt_append(&r, r_end, ":%zd:%zd:%zd\n", seed_count, peer_count - seed_count, down_count);
113 break; 119 break;
114 case TASK_FULLSCRAPE_TPB_BINARY: 120 case TASK_FULLSCRAPE_TPB_BINARY:
115 memcpy(r, *hash, sizeof(ot_hash)); 121 memcpy(r, *hash, sizeof(ot_hash));
@@ -120,15 +126,15 @@ static char * fullscrape_write_one( ot_tasktype mode, char *r, ot_torrent *torre
120 break; 126 break;
121 case TASK_FULLSCRAPE_TPB_URLENCODED: 127 case TASK_FULLSCRAPE_TPB_URLENCODED:
122 r += urlencoded_format(r, *hash, sizeof(ot_hash)); 128 r += urlencoded_format(r, *hash, sizeof(ot_hash));
123 r += sprintf(r, ":%zd:%zd\n", seed_count, peer_count - seed_count); 129 fmt_append(&r, r_end, ":%zd:%zd\n", seed_count, peer_count - seed_count);
124 break; 130 break;
125 case TASK_FULLSCRAPE_TRACKERSTATE: 131 case TASK_FULLSCRAPE_TRACKERSTATE:
126 hex_from_hash(r, *hash); 132 hex_from_hash(r, *hash);
127 r += 2 * sizeof(ot_hash); 133 r += 2 * sizeof(ot_hash);
128 r += sprintf(r, ":%zd:%zd\n", torrent->peer_list6->base, down_count); 134 fmt_append(&r, r_end, ":%zd:%zd\n", torrent->peer_list6->base, down_count);
129 break; 135 break;
130 } 136 }
131 return r; 137 *cursor = r;
132} 138}
133 139
134static void fullscrape_make(int taskid, ot_tasktype mode) { 140static void fullscrape_make(int taskid, ot_tasktype mode) {
@@ -145,7 +151,7 @@ static void fullscrape_make(int taskid, ot_tasktype mode) {
145 re = r + OT_SCRAPE_CHUNK_SIZE - OT_SCRAPE_MAXENTRYLEN; 151 re = r + OT_SCRAPE_CHUNK_SIZE - OT_SCRAPE_MAXENTRYLEN;
146 152
147 if ((mode & TASK_TASK_MASK) == TASK_FULLSCRAPE) 153 if ((mode & TASK_TASK_MASK) == TASK_FULLSCRAPE)
148 r += sprintf(r, "d5:filesd"); 154 fmt_append(&r, (char *)iovector.iov_base + OT_SCRAPE_CHUNK_SIZE, "d5:filesd");
149 155
150 /* For each bucket... */ 156 /* For each bucket... */
151 for (bucket = 0; bucket < OT_BUCKET_COUNT; ++bucket) { 157 for (bucket = 0; bucket < OT_BUCKET_COUNT; ++bucket) {
@@ -156,7 +162,7 @@ static void fullscrape_make(int taskid, ot_tasktype mode) {
156 162
157 /* For each torrent in this bucket.. */ 163 /* For each torrent in this bucket.. */
158 for (i = 0; i < torrents_list->size; ++i) { 164 for (i = 0; i < torrents_list->size; ++i) {
159 r = fullscrape_write_one(mode, r, torrents + i, &torrents[i].hash); 165 fullscrape_write_one(mode, &r, torrents + i, &torrents[i].hash);
160 166
161 if (r > re) { 167 if (r > re) {
162 iovector.iov_len = r - (char *)iovector.iov_base; 168 iovector.iov_len = r - (char *)iovector.iov_base;
@@ -187,7 +193,7 @@ static void fullscrape_make(int taskid, ot_tasktype mode) {
187 } 193 }
188 194
189 if ((mode & TASK_TASK_MASK) == TASK_FULLSCRAPE) 195 if ((mode & TASK_TASK_MASK) == TASK_FULLSCRAPE)
190 r += sprintf(r, "ee"); 196 fmt_append(&r, (char *)iovector.iov_base + OT_SCRAPE_CHUNK_SIZE, "ee");
191 197
192 /* Send rest of data */ 198 /* Send rest of data */
193 iovector.iov_len = r - (char *)iovector.iov_base; 199 iovector.iov_len = r - (char *)iovector.iov_base;
@@ -230,7 +236,8 @@ static void fullscrape_make_gzip(int taskid, ot_tasktype mode) {
230 /* For each torrent in this bucket.. */ 236 /* For each torrent in this bucket.. */
231 for (i = 0; i < torrents_list->size; ++i) { 237 for (i = 0; i < torrents_list->size; ++i) {
232 char compress_buffer[OT_SCRAPE_MAXENTRYLEN]; 238 char compress_buffer[OT_SCRAPE_MAXENTRYLEN];
233 r = fullscrape_write_one(mode, compress_buffer, torrents + i, &torrents[i].hash); 239 r = compress_buffer;
240 fullscrape_write_one(mode, &r, torrents + i, &torrents[i].hash);
234 strm.next_in = (uint8_t *)compress_buffer; 241 strm.next_in = (uint8_t *)compress_buffer;
235 strm.avail_in = r - compress_buffer; 242 strm.avail_in = r - compress_buffer;
236 zres = deflate(&strm, Z_NO_FLUSH); 243 zres = deflate(&strm, Z_NO_FLUSH);
@@ -359,7 +366,8 @@ static void fullscrape_make_zstd(int taskid, ot_tasktype mode) {
359 /* For each torrent in this bucket.. */ 366 /* For each torrent in this bucket.. */
360 for (i = 0; i < torrents_list->size; ++i) { 367 for (i = 0; i < torrents_list->size; ++i) {
361 char compress_buffer[OT_SCRAPE_MAXENTRYLEN]; 368 char compress_buffer[OT_SCRAPE_MAXENTRYLEN];
362 r = fullscrape_write_one(mode, compress_buffer, torrents + i, &torrents[i].hash); 369 r = compress_buffer;
370 fullscrape_write_one(mode, &r, torrents + i, &torrents[i].hash);
363 inbuf.src = compress_buffer; 371 inbuf.src = compress_buffer;
364 inbuf.size = r - compress_buffer; 372 inbuf.size = r - compress_buffer;
365 inbuf.pos = 0; 373 inbuf.pos = 0;
diff --git a/ot_http.c b/ot_http.c
index 5e10d59..36819c7 100644
--- a/ot_http.c
+++ b/ot_http.c
@@ -24,6 +24,7 @@
24#include "ot_scan.h" 24#include "ot_scan.h"
25#include "ot_event.h" 25#include "ot_event.h"
26#include "ot_fdstate.h" 26#include "ot_fdstate.h"
27#include "ot_fmt.h"
27#include "ot_stats.h" 28#include "ot_stats.h"
28#include "ot_urlencoded.h" 29#include "ot_urlencoded.h"
29#include "trackerlogic.h" 30#include "trackerlogic.h"
@@ -257,6 +258,7 @@ static ssize_t http_handle_stats(const int64_t sock, struct ot_workstruct *ws, c
257 {"txtp", TASK_FULLSCRAPE_TPB_ASCII_PLUS}, {NULL, -3}}; 258 {"txtp", TASK_FULLSCRAPE_TPB_ASCII_PLUS}, {NULL, -3}};
258 259
259 struct http_data *cookie = fdstate_cookie(sock); 260 struct http_data *cookie = fdstate_cookie(sock);
261 char *reply;
260 int mode = TASK_STATS_PEERS, scanon = 1, format = 0; 262 int mode = TASK_STATS_PEERS, scanon = 1, format = 0;
261 263
262 /* A socket without one is a dead socket, which is what http_sendiovecdata 264 /* A socket without one is a dead socket, which is what http_sendiovecdata
@@ -333,7 +335,9 @@ static ssize_t http_handle_stats(const int64_t sock, struct ot_workstruct *ws, c
333 } 335 }
334 336
335 /* Simple stats can be answerred immediately */ 337 /* Simple stats can be answerred immediately */
336 return ws->reply_size = return_stats_for_tracker(ws->reply, mode, 0); 338 reply = ws->reply;
339 return_stats_for_tracker(&reply, ws->outbuf + G_OUTBUF_SIZE, mode, 0);
340 return ws->reply_size = reply - ws->reply;
337} 341}
338 342
339#ifdef WANT_MODEST_FULLSCRAPES 343#ifdef WANT_MODEST_FULLSCRAPES
@@ -417,9 +421,10 @@ static ssize_t http_handle_fullscrape(const int64_t sock, struct ot_workstruct *
417static ssize_t http_handle_scrape(const int64_t sock, struct ot_workstruct *ws, char *read_ptr) { 421static ssize_t http_handle_scrape(const int64_t sock, struct ot_workstruct *ws, char *read_ptr) {
418 static const ot_keywords keywords_scrape[] = {{"info_hash", 1}, {NULL, -3}}; 422 static const ot_keywords keywords_scrape[] = {{"info_hash", 1}, {NULL, -3}};
419 423
420 ot_hash *multiscrape_buf = (ot_hash *)ws->request; 424 ot_hash *multiscrape_buf = (ot_hash *)ws->request;
421 int scanon = 1; 425 char *reply = ws->reply;
422 size_t numwant = 0; 426 int scanon = 1;
427 size_t numwant = 0;
423 428
424 /* This is to hack around stupid clients that send "scrape ?info_hash" */ 429 /* This is to hack around stupid clients that send "scrape ?info_hash" */
425 if (read_ptr[-1] != '?') { 430 if (read_ptr[-1] != '?') {
@@ -457,7 +462,8 @@ static ssize_t http_handle_scrape(const int64_t sock, struct ot_workstruct *ws,
457 numwant = OT_MAXMULTISCRAPE_COUNT; 462 numwant = OT_MAXMULTISCRAPE_COUNT;
458 463
459 /* Enough for http header + whole scrape string */ 464 /* Enough for http header + whole scrape string */
460 ws->reply_size = return_tcp_scrape_for_torrent((const ot_hash *)multiscrape_buf, numwant, ws->reply); 465 return_tcp_scrape_for_torrent((const ot_hash *)multiscrape_buf, numwant, &reply, ws->outbuf + G_OUTBUF_SIZE);
466 ws->reply_size = reply - ws->reply;
461 stats_issue_event(EVENT_SCRAPE, FLAG_TCP, ws->reply_size); 467 stats_issue_event(EVENT_SCRAPE, FLAG_TCP, ws->reply_size);
462 return ws->reply_size; 468 return ws->reply_size;
463} 469}
@@ -493,6 +499,16 @@ static ot_keywords keywords_announce[] = {{"port", 1}, {"left", 2}, {"event",
493#endif 499#endif
494 {"peer_id", 9}, {NULL, -3}}; 500 {"peer_id", 9}, {NULL, -3}};
495static ot_keywords keywords_announce_event[] = {{"completed", 1}, {"stopped", 2}, {NULL, -3}}; 501static ot_keywords keywords_announce_event[] = {{"completed", 1}, {"stopped", 2}, {NULL, -3}};
502
503/* For the replies that are one fixed string and nothing else, where the only
504 thing the caller wants back is how long it turned out */
505static ssize_t reply_literal(struct ot_workstruct *ws, const char *text) {
506 char *reply = ws->reply;
507
508 fmt_bytes(&reply, ws->outbuf + G_OUTBUF_SIZE, text, strlen(text));
509 return reply - ws->reply;
510}
511
496static ssize_t http_handle_announce(const int64_t sock, struct ot_workstruct *ws, char *read_ptr) { 512static ssize_t http_handle_announce(const int64_t sock, struct ot_workstruct *ws, char *read_ptr) {
497 size_t numwant; 513 size_t numwant;
498 int64_t tmp; 514 int64_t tmp;
@@ -601,7 +617,7 @@ static ssize_t http_handle_announce(const int64_t sock, struct ot_workstruct
601 char *tmp_buf1 = ws->reply, *tmp_buf2 = ws->reply + 16; 617 char *tmp_buf1 = ws->reply, *tmp_buf2 = ws->reply + 16;
602 len = urlencoded_parse(&read_ptr, tmp_buf2, SCAN_SEARCHPATH_VALUE); 618 len = urlencoded_parse(&read_ptr, tmp_buf2, SCAN_SEARCHPATH_VALUE);
603 tmp_buf2[len] = 0; 619 tmp_buf2[len] = 0;
604 if ((len <= 0) || !ip_parse(tmp_buf2, tmp_buf1)) 620 if ((len <= 0) || ip_parse(tmp_buf2, tmp_buf1) != (size_t)len)
605 HTTPERROR_400_PARAM; 621 HTTPERROR_400_PARAM;
606 OT_SETIP(&ws->peer, tmp_buf1); 622 OT_SETIP(&ws->peer, tmp_buf1);
607 } break; 623 } break;
@@ -621,7 +637,7 @@ static ssize_t http_handle_announce(const int64_t sock, struct ot_workstruct
621 HTTPERROR_400_PARAM; 637 HTTPERROR_400_PARAM;
622 if (*tmp_buf == '-') { 638 if (*tmp_buf == '-') {
623 loglist_reset(); 639 loglist_reset();
624 return ws->reply_size = sprintf(ws->reply, "Successfully removed.\n"); 640 return ws->reply_size = reply_literal(ws, "Successfully removed.\n");
625 } 641 }
626 parsed = ip_parse(tmp_buf, net.address); 642 parsed = ip_parse(tmp_buf, net.address);
627 if (!parsed) 643 if (!parsed)
@@ -638,7 +654,7 @@ static ssize_t http_handle_announce(const int64_t sock, struct ot_workstruct
638 } 654 }
639 net.bits = bits; 655 net.bits = bits;
640 loglist_add_network(&net); 656 loglist_add_network(&net);
641 return ws->reply_size = sprintf(ws->reply, "Successfully added.\n"); 657 return ws->reply_size = reply_literal(ws, "Successfully added.\n");
642 //} 658 //}
643 } break; 659 } break;
644#endif 660#endif
@@ -661,7 +677,7 @@ static ssize_t http_handle_announce(const int64_t sock, struct ot_workstruct
661 677
662 /* Scanned whole query string */ 678 /* Scanned whole query string */
663 if (!ws->hash) 679 if (!ws->hash)
664 return ws->reply_size = sprintf(ws->reply, "d14:failure reason80:Your client forgot to send your torrent's info_hash. Please upgrade your client.e"); 680 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");
665 681
666 if (OT_PEERFLAG(&ws->peer) & PEER_FLAG_STOPPED) 682 if (OT_PEERFLAG(&ws->peer) & PEER_FLAG_STOPPED)
667 ws->reply_size = remove_peer_from_torrent(FLAG_TCP, ws); 683 ws->reply_size = remove_peer_from_torrent(FLAG_TCP, ws);
@@ -772,9 +788,11 @@ ssize_t http_handle_request(const int64_t sock, struct ot_workstruct *ws) {
772 reply_off = SUCCESS_HTTP_SIZE_OFF - snprintf(ws->outbuf, 0, "%zd", ws->reply_size); 788 reply_off = SUCCESS_HTTP_SIZE_OFF - snprintf(ws->outbuf, 0, "%zd", ws->reply_size);
773 ws->reply = ws->outbuf + reply_off; 789 ws->reply = ws->outbuf + reply_off;
774 790
775 /* 2. Now we sprintf our header so that sprintf writes its terminating '\0' exactly one byte before content starts. Complete 791 /* 2. Now we write the header so that its terminating '\0' lands exactly one byte before the content starts. The bound is the
776 packet size is increased by size of header plus one byte '\n', we will copy over '\0' in next step */ 792 space step 1 reserved, so the '\0' falls where step 3 expects it. Complete packet size is increased by size of header plus
777 ws->reply_size += 1 + sprintf(ws->reply, "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %zd\r\n\r", ws->reply_size); 793 one byte '\n', we will copy over '\0' in next step */
794 ws->reply_size += 1 + snprintf(ws->reply, (size_t)(SUCCESS_HTTP_HEADER_LENGTH - reply_off),
795 "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: %zd\r\n\r", ws->reply_size);
778 796
779 /* 3. Finally we join both blocks neatly */ 797 /* 3. Finally we join both blocks neatly */
780 ws->outbuf[SUCCESS_HTTP_HEADER_LENGTH - 1] = '\n'; 798 ws->outbuf[SUCCESS_HTTP_HEADER_LENGTH - 1] = '\n';
diff --git a/ot_stats.c b/ot_stats.c
index bee1f93..45dc8db 100644
--- a/ot_stats.c
+++ b/ot_stats.c
@@ -22,6 +22,7 @@
22#include "ot_iovec.h" 22#include "ot_iovec.h"
23#include "ot_ip.h" 23#include "ot_ip.h"
24#include "ot_mutex.h" 24#include "ot_mutex.h"
25#include "ot_fmt.h"
25#include "ot_stats.h" 26#include "ot_stats.h"
26#include "trackerlogic.h" 27#include "trackerlogic.h"
27 28
@@ -175,8 +176,8 @@ static size_t stats_get_highscore_networks(stats_network_node *node, int depth,
175 ++j; 176 ++j;
176 --j; 177 --j;
177 178
178 memcpy(scores, scores + 1, j * sizeof(*scores)); 179 memmove(scores, scores + 1, j * sizeof(*scores));
179 memcpy(networks, networks + 1, j * sizeof(*networks)); 180 memmove(networks, networks + 1, j * sizeof(*networks));
180 scores[j] = node_score; 181 scores[j] = node_score;
181 memcpy(networks + j, node_value, sizeof(*networks)); 182 memcpy(networks + j, node_value, sizeof(*networks));
182 } 183 }
@@ -184,12 +185,11 @@ static size_t stats_get_highscore_networks(stats_network_node *node, int depth,
184 return score; 185 return score;
185} 186}
186 187
187static size_t stats_return_busy_networks(char *reply, stats_network_node *tree, size_t amount, int limit) { 188static void stats_return_busy_networks(char **reply, const char *reply_end, stats_network_node *tree, size_t amount, int limit) {
188 ot_ip6 networks[amount]; 189 ot_ip6 networks[amount];
189 ot_ip6 node_value; 190 ot_ip6 node_value;
190 size_t scores[amount]; 191 size_t scores[amount];
191 size_t i; 192 size_t i;
192 char *r = reply;
193 193
194 memset(scores, 0, sizeof(scores)); 194 memset(scores, 0, sizeof(scores));
195 memset(networks, 0, sizeof(networks)); 195 memset(networks, 0, sizeof(networks));
@@ -197,24 +197,21 @@ static size_t stats_return_busy_networks(char *reply, stats_network_node *tree,
197 197
198 stats_get_highscore_networks(tree, 0, node_value, scores, networks, amount, limit); 198 stats_get_highscore_networks(tree, 0, node_value, scores, networks, amount, limit);
199 199
200 r += sprintf(r, "Networks, limit /%d:\n", limit + STATS_NETWORK_NODE_BITWIDTH); 200 fmt_append(reply, reply_end, "Networks, limit /%d:\n", limit + STATS_NETWORK_NODE_BITWIDTH);
201 for (i = amount; i > 0; --i) { 201 for (i = amount; i > 0; --i) {
202 if (scores[i - 1]) { 202 if (scores[i - 1]) {
203 r += sprintf(r, "%08zd: ", scores[i - 1]); 203 char ip_readable[64];
204 // #ifdef WANT_V6 204
205 r += ip_format(r, networks[i - 1]); 205 ip_readable[ip_format(ip_readable, networks[i - 1])] = 0;
206 *r++ = '\n'; 206 fmt_append(reply, reply_end, "%08zd: %s\n", scores[i - 1], ip_readable);
207 } 207 }
208 } 208 }
209 *r++ = '\n'; 209 fmt_append(reply, reply_end, "\n");
210
211 return r - reply;
212} 210}
213 211
214static size_t stats_slash24s_txt(char *reply, size_t amount) { 212static void stats_slash24s_txt(char **reply, const char *reply_end, size_t amount) {
215 stats_network_node *slash24s_network_counters_root = NULL; 213 stats_network_node *slash24s_network_counters_root = NULL;
216 char *r = reply; 214 size_t i, bucket, peer_size = OT_PEER_SIZE4;
217 size_t i, bucket, peer_size = OT_PEER_SIZE4;
218 215
219 for (bucket = 0; bucket < OT_BUCKET_COUNT; ++bucket) { 216 for (bucket = 0; bucket < OT_BUCKET_COUNT; ++bucket) {
220 ot_vector *torrents_list = mutex_bucket_lock(bucket); 217 ot_vector *torrents_list = mutex_bucket_lock(bucket);
@@ -241,35 +238,28 @@ static size_t stats_slash24s_txt(char *reply, size_t amount) {
241 } 238 }
242 mutex_bucket_unlock(bucket, 0); 239 mutex_bucket_unlock(bucket, 0);
243 if (!g_opentracker_running) 240 if (!g_opentracker_running)
244 goto bailout_error; 241 goto done;
245 } 242 }
246 243
247 /* The tree is built. Now analyze */ 244 /* The tree is built. Now analyze */
248 r += stats_return_busy_networks(r, slash24s_network_counters_root, amount, STATS_NETWORK_NODE_MAXDEPTH); 245 stats_return_busy_networks(reply, reply_end, slash24s_network_counters_root, amount, STATS_NETWORK_NODE_MAXDEPTH);
249 r += stats_return_busy_networks(r, slash24s_network_counters_root, amount, STATS_NETWORK_NODE_LIMIT); 246 stats_return_busy_networks(reply, reply_end, slash24s_network_counters_root, amount, STATS_NETWORK_NODE_LIMIT);
250 goto success; 247 goto done;
251 248
252bailout_unlock: 249bailout_unlock:
253 mutex_bucket_unlock(bucket, 0); 250 mutex_bucket_unlock(bucket, 0);
254bailout_error: 251done:
255 r = reply;
256success:
257 stats_shift_down_network_count(&slash24s_network_counters_root, 0, sizeof(int) * 8 - 1); 252 stats_shift_down_network_count(&slash24s_network_counters_root, 0, sizeof(int) * 8 - 1);
258
259 return r - reply;
260} 253}
261 254
262#ifdef WANT_SPOT_WOODPECKER 255#ifdef WANT_SPOT_WOODPECKER
263static stats_network_node *stats_woodpeckers_tree; 256static stats_network_node *stats_woodpeckers_tree;
264static pthread_mutex_t g_woodpeckers_mutex = PTHREAD_MUTEX_INITIALIZER; 257static pthread_mutex_t g_woodpeckers_mutex = PTHREAD_MUTEX_INITIALIZER;
265 258
266static size_t stats_return_woodpeckers(char *reply, int amount) { 259static void stats_return_woodpeckers(char **reply, const char *reply_end, int amount) {
267 char *r = reply;
268
269 pthread_mutex_lock(&g_woodpeckers_mutex); 260 pthread_mutex_lock(&g_woodpeckers_mutex);
270 r += stats_return_busy_networks(r, stats_woodpeckers_tree, amount, STATS_NETWORK_NODE_MAXDEPTH); 261 stats_return_busy_networks(reply, reply_end, stats_woodpeckers_tree, amount, STATS_NETWORK_NODE_MAXDEPTH);
271 pthread_mutex_unlock(&g_woodpeckers_mutex); 262 pthread_mutex_unlock(&g_woodpeckers_mutex);
272 return r - reply;
273} 263}
274#endif 264#endif
275 265
@@ -309,10 +299,10 @@ static inline void stats_insert_into_highscore(size_t val, ot_record* list, size
309 299
310 300
311/* Fetches stats from tracker */ 301/* Fetches stats from tracker */
312size_t stats_top_txt(char *reply, size_t amount) { 302void stats_top_txt(char **reply, const char *reply_end, size_t amount) {
313 size_t j, idx, bucket; 303 size_t j, idx, bucket;
314 ot_record top100s[100], top100c[100], top100l[100]; 304 ot_record top100s[100], top100c[100], top100l[100];
315 char *r = reply, hex_out[42]; 305 char hex_out[42];
316 306
317 if (amount > 100) 307 if (amount > 100)
318 amount = 100; 308 amount = 100;
@@ -323,7 +313,7 @@ size_t stats_top_txt(char *reply, size_t amount) {
323 313
324 /* Iterate over complete torrent list */ 314 /* Iterate over complete torrent list */
325 for (bucket = 0; bucket < OT_BUCKET_COUNT; ++bucket) { 315 for (bucket = 0; bucket < OT_BUCKET_COUNT; ++bucket) {
326 ot_vector *torrents_list = mutex_bucket_lock(bucket); 316 ot_vector *torrents_list = mutex_bucket_lock(bucket);
327 for (j = 0; j < torrents_list->size; ++j) { 317 for (j = 0; j < torrents_list->size; ++j) {
328 ot_torrent *torrent = (ot_torrent *)(torrents_list->data) + j; 318 ot_torrent *torrent = (ot_torrent *)(torrents_list->data) + j;
329 size_t peer_count = torrent->peer_list6->peer_count + torrent->peer_list4->peer_count; 319 size_t peer_count = torrent->peer_list6->peer_count + torrent->peer_list4->peer_count;
@@ -336,114 +326,110 @@ size_t stats_top_txt(char *reply, size_t amount) {
336 } 326 }
337 mutex_bucket_unlock(bucket, 0); 327 mutex_bucket_unlock(bucket, 0);
338 if (!g_opentracker_running) 328 if (!g_opentracker_running)
339 return 0; 329 return;
340 } 330 }
341 331
342 r += sprintf(r, "Top %zd torrents by peers:\n", amount); 332 fmt_append(reply, reply_end, "Top %zd torrents by peers:\n", amount);
343 for (idx = 0; idx < amount; ++idx) 333 for (idx = 0; idx < amount; ++idx)
344 if (top100c[idx].val) 334 if (top100c[idx].val)
345 r += sprintf(r, "\t%zd\t%s\n", top100c[idx].val, hex_from_hash(hex_out, top100c[idx].hash)); 335 fmt_append(reply, reply_end, "\t%zd\t%s\n", top100c[idx].val, hex_from_hash(hex_out, top100c[idx].hash));
346 r += sprintf(r, "Top %zd torrents by seeds:\n", amount); 336 fmt_append(reply, reply_end, "Top %zd torrents by seeds:\n", amount);
347 for (idx = 0; idx < amount; ++idx) 337 for (idx = 0; idx < amount; ++idx)
348 if (top100s[idx].val) 338 if (top100s[idx].val)
349 r += sprintf(r, "\t%zd\t%s\n", top100s[idx].val, hex_from_hash(hex_out, top100s[idx].hash)); 339 fmt_append(reply, reply_end, "\t%zd\t%s\n", top100s[idx].val, hex_from_hash(hex_out, top100s[idx].hash));
350 r += sprintf(r, "Top %zd torrents by leechers:\n", amount); 340 fmt_append(reply, reply_end, "Top %zd torrents by leechers:\n", amount);
351 for (idx = 0; idx < amount; ++idx) 341 for (idx = 0; idx < amount; ++idx)
352 if (top100l[idx].val) 342 if (top100l[idx].val)
353 r += sprintf(r, "\t%zd\t%s\n", top100l[idx].val, hex_from_hash(hex_out, top100l[idx].hash)); 343 fmt_append(reply, reply_end, "\t%zd\t%s\n", top100l[idx].val, hex_from_hash(hex_out, top100l[idx].hash));
354
355 return r - reply;
356} 344}
357 345
358static unsigned long events_per_time(unsigned long long events, time_t t) { return events / ((unsigned int)t ? (unsigned int)t : 1); } 346static unsigned long events_per_time(unsigned long long events, time_t t) { return events / ((unsigned int)t ? (unsigned int)t : 1); }
359 347
360static size_t stats_connections_mrtg(char *reply) { 348static void stats_connections_mrtg(char **reply, const char *reply_end) {
361 ot_time t = time(NULL) - ot_start_time; 349 ot_time t = time(NULL) - ot_start_time;
362 return sprintf(reply, "%llu\n%llu\n%i seconds (%i hours)\nopentracker connections, %lu conns/s :: %lu success/s.", 350 fmt_append(reply, reply_end, "%llu\n%llu\n%i seconds (%i hours)\nopentracker connections, %lu conns/s :: %lu success/s.",
363 ot_overall_tcp_connections + ot_overall_udp_connections, 351 ot_overall_tcp_connections + ot_overall_udp_connections,
364 ot_overall_tcp_successfulannounces + ot_overall_udp_successfulannounces + ot_overall_udp_connects, (int)t, (int)(t / 3600), 352 ot_overall_tcp_successfulannounces + ot_overall_udp_successfulannounces + ot_overall_udp_connects, (int)t, (int)(t / 3600),
365 events_per_time(ot_overall_tcp_connections + ot_overall_udp_connections, t), 353 events_per_time(ot_overall_tcp_connections + ot_overall_udp_connections, t),
366 events_per_time(ot_overall_tcp_successfulannounces + ot_overall_udp_successfulannounces + ot_overall_udp_connects, t)); 354 events_per_time(ot_overall_tcp_successfulannounces + ot_overall_udp_successfulannounces + ot_overall_udp_connects, t));
367} 355}
368 356
369static size_t stats_udpconnections_mrtg(char *reply) { 357static void stats_udpconnections_mrtg(char **reply, const char *reply_end) {
370 ot_time t = time(NULL) - ot_start_time; 358 ot_time t = time(NULL) - ot_start_time;
371 return sprintf(reply, "%llu\n%llu\n%i seconds (%i hours)\nopentracker udp4 stats, %lu conns/s :: %lu success/s.", ot_overall_udp_connections, 359 fmt_append(reply, reply_end, "%llu\n%llu\n%i seconds (%i hours)\nopentracker udp4 stats, %lu conns/s :: %lu success/s.", ot_overall_udp_connections,
372 ot_overall_udp_successfulannounces + ot_overall_udp_connects, (int)t, (int)(t / 3600), events_per_time(ot_overall_udp_connections, t), 360 ot_overall_udp_successfulannounces + ot_overall_udp_connects, (int)t, (int)(t / 3600), events_per_time(ot_overall_udp_connections, t),
373 events_per_time(ot_overall_udp_successfulannounces + ot_overall_udp_connects, t)); 361 events_per_time(ot_overall_udp_successfulannounces + ot_overall_udp_connects, t));
374} 362}
375 363
376static size_t stats_tcpconnections_mrtg(char *reply) { 364static void stats_tcpconnections_mrtg(char **reply, const char *reply_end) {
377 time_t t = time(NULL) - ot_start_time; 365 time_t t = time(NULL) - ot_start_time;
378 return sprintf(reply, "%llu\n%llu\n%i seconds (%i hours)\nopentracker tcp4 stats, %lu conns/s :: %lu success/s.", ot_overall_tcp_connections, 366 fmt_append(reply, reply_end, "%llu\n%llu\n%i seconds (%i hours)\nopentracker tcp4 stats, %lu conns/s :: %lu success/s.", ot_overall_tcp_connections,
379 ot_overall_tcp_successfulannounces, (int)t, (int)(t / 3600), events_per_time(ot_overall_tcp_connections, t), 367 ot_overall_tcp_successfulannounces, (int)t, (int)(t / 3600), events_per_time(ot_overall_tcp_connections, t),
380 events_per_time(ot_overall_tcp_successfulannounces, t)); 368 events_per_time(ot_overall_tcp_successfulannounces, t));
381} 369}
382 370
383static size_t stats_scrape_mrtg(char *reply) { 371static void stats_scrape_mrtg(char **reply, const char *reply_end) {
384 time_t t = time(NULL) - ot_start_time; 372 time_t t = time(NULL) - ot_start_time;
385 return sprintf(reply, "%llu\n%llu\n%i seconds (%i hours)\nopentracker scrape stats, %lu scrape/s (tcp and udp)", ot_overall_tcp_successfulscrapes, 373 fmt_append(reply, reply_end, "%llu\n%llu\n%i seconds (%i hours)\nopentracker scrape stats, %lu scrape/s (tcp and udp)",
386 ot_overall_udp_successfulscrapes, (int)t, (int)(t / 3600), 374 ot_overall_tcp_successfulscrapes, ot_overall_udp_successfulscrapes, (int)t, (int)(t / 3600),
387 events_per_time((ot_overall_tcp_successfulscrapes + ot_overall_udp_successfulscrapes), t)); 375 events_per_time((ot_overall_tcp_successfulscrapes + ot_overall_udp_successfulscrapes), t));
388} 376}
389 377
390static size_t stats_fullscrapes_mrtg(char *reply) { 378static void stats_fullscrapes_mrtg(char **reply, const char *reply_end) {
391 ot_time t = time(NULL) - ot_start_time; 379 ot_time t = time(NULL) - ot_start_time;
392 return sprintf(reply, "%llu\n%llu\n%i seconds (%i hours)\nopentracker full scrape stats, %lu conns/s :: %lu bytes/s.", ot_full_scrape_count * 1000, 380 fmt_append(reply, reply_end, "%llu\n%llu\n%i seconds (%i hours)\nopentracker full scrape stats, %lu conns/s :: %lu bytes/s.",
393 ot_full_scrape_size, (int)t, (int)(t / 3600), events_per_time(ot_full_scrape_count, t), events_per_time(ot_full_scrape_size, t)); 381 ot_full_scrape_count * 1000, ot_full_scrape_size, (int)t, (int)(t / 3600), events_per_time(ot_full_scrape_count, t),
382 events_per_time(ot_full_scrape_size, t));
394} 383}
395 384
396static size_t stats_peers_mrtg(char *reply) { 385static void stats_peers_mrtg(char **reply, const char *reply_end) {
397 torrent_stats stats = {0, 0, 0}; 386 torrent_stats stats = {0, 0, 0};
398 387
399 iterate_all_torrents(torrent_statter, (uintptr_t)&stats); 388 iterate_all_torrents(torrent_statter, (uintptr_t)&stats);
400 389
401 return sprintf(reply, "%llu\n%llu\nopentracker serving %llu torrents\nopentracker", stats.peer_count, stats.seed_count, stats.torrent_count); 390 fmt_append(reply, reply_end, "%llu\n%llu\nopentracker serving %llu torrents\nopentracker", stats.peer_count, stats.seed_count,
391 stats.torrent_count);
402} 392}
403 393
404static size_t stats_torrents_mrtg(char *reply) { 394static void stats_torrents_mrtg(char **reply, const char *reply_end) {
405 size_t torrent_count = mutex_get_torrent_count(); 395 size_t torrent_count = mutex_get_torrent_count();
406 396
407 return sprintf(reply, "%zd\n%zd\nopentracker serving %zd torrents\nopentracker", torrent_count, (size_t)0, torrent_count); 397 fmt_append(reply, reply_end, "%zd\n%zd\nopentracker serving %zd torrents\nopentracker", torrent_count, (size_t)0, torrent_count);
408} 398}
409 399
410static size_t stats_httperrors_txt(char *reply) { 400static void stats_httperrors_txt(char **reply, const char *reply_end) {
411 return sprintf(reply, "302 RED %llu\n400 ... %llu\n400 PAR %llu\n400 COM %llu\n403 IP %llu\n404 INV %llu\n500 SRV %llu\n", ot_failed_request_counts[0], 401 fmt_append(reply, reply_end, "302 RED %llu\n400 ... %llu\n400 PAR %llu\n400 COM %llu\n403 IP %llu\n404 INV %llu\n500 SRV %llu\n",
412 ot_failed_request_counts[1], ot_failed_request_counts[2], ot_failed_request_counts[3], ot_failed_request_counts[4], 402 ot_failed_request_counts[0], ot_failed_request_counts[1], ot_failed_request_counts[2], ot_failed_request_counts[3],
413 ot_failed_request_counts[5], ot_failed_request_counts[6]); 403 ot_failed_request_counts[4], ot_failed_request_counts[5], ot_failed_request_counts[6]);
414} 404}
415 405
416static size_t stats_return_renew_bucket(char *reply) { 406static void stats_return_renew_bucket(char **reply, const char *reply_end) {
417 char *r = reply; 407 int i;
418 int i;
419 408
420 for (i = 0; i < OT_PEER_TIMEOUT; ++i) 409 for (i = 0; i < OT_PEER_TIMEOUT; ++i)
421 r += sprintf(r, "%02i %llu\n", i, ot_renewed[i]); 410 fmt_append(reply, reply_end, "%02i %llu\n", i, ot_renewed[i]);
422 return r - reply;
423} 411}
424 412
425static size_t stats_return_sync_mrtg(char *reply) { 413static void stats_return_sync_mrtg(char **reply, const char *reply_end) {
426 ot_time t = time(NULL) - ot_start_time; 414 ot_time t = time(NULL) - ot_start_time;
427 return sprintf(reply, "%llu\n%llu\n%i seconds (%i hours)\nopentracker connections, %lu conns/s :: %lu success/s.", ot_overall_sync_count, 0LL, (int)t, 415 fmt_append(reply, reply_end, "%llu\n%llu\n%i seconds (%i hours)\nopentracker connections, %lu conns/s :: %lu success/s.", ot_overall_sync_count, 0LL,
428 (int)(t / 3600), events_per_time(ot_overall_tcp_connections + ot_overall_udp_connections, t), 416 (int)t, (int)(t / 3600), events_per_time(ot_overall_tcp_connections + ot_overall_udp_connections, t),
429 events_per_time(ot_overall_tcp_successfulannounces + ot_overall_udp_successfulannounces + ot_overall_udp_connects, t)); 417 events_per_time(ot_overall_tcp_successfulannounces + ot_overall_udp_successfulannounces + ot_overall_udp_connects, t));
430} 418}
431 419
432static size_t stats_return_completed_mrtg(char *reply) { 420static void stats_return_completed_mrtg(char **reply, const char *reply_end) {
433 ot_time t = time(NULL) - ot_start_time; 421 ot_time t = time(NULL) - ot_start_time;
434 422
435 return sprintf(reply, "%llu\n%llu\n%i seconds (%i hours)\nopentracker, %lu completed/h.", ot_overall_completed, 0LL, (int)t, (int)(t / 3600), 423 fmt_append(reply, reply_end, "%llu\n%llu\n%i seconds (%i hours)\nopentracker, %lu completed/h.", ot_overall_completed, 0LL, (int)t, (int)(t / 3600),
436 events_per_time(ot_overall_completed, t / 3600)); 424 events_per_time(ot_overall_completed, t / 3600));
437} 425}
438 426
439#ifdef WANT_LOG_NUMWANT 427#ifdef WANT_LOG_NUMWANT
440extern unsigned long long numwants[201]; 428extern unsigned long long numwants[201];
441static size_t stats_return_numwants(char *reply) { 429static void stats_return_numwants(char **reply, const char *reply_end) {
442 char *r = reply; 430 int i;
443 int i;
444 for (i = 0; i <= 200; ++i) 431 for (i = 0; i <= 200; ++i)
445 r += sprintf(r, "%03d => %lld\n", i, numwants[i]); 432 fmt_append(reply, reply_end, "%03d => %lld\n", i, numwants[i]);
446 return r - reply;
447} 433}
448#endif 434#endif
449 435
@@ -461,17 +447,14 @@ static void stats_return_fulllog(size_t *iovec_entries, struct iovec **iovector,
461 return; 447 return;
462 re = r + 32 * OT_STATS_TMPSIZE; 448 re = r + 32 * OT_STATS_TMPSIZE;
463 } 449 }
464 r += sprintf(r, "%08ld: ", loglist->time); 450 char ip_readable[64];
465 r += ip_format(r, loglist->ip); 451
466 *r++ = '\n'; 452 ip_readable[ip_format(ip_readable, loglist->ip)] = 0;
467 memcpy(r, loglist->data, loglist->size); 453 fmt_append(&r, re, "%08ld: %s\n", loglist->time, ip_readable);
468 r += loglist->size; 454 fmt_bytes(&r, re, loglist->data, loglist->size);
469 *r++ = '\n'; 455 fmt_append(&r, re, "\n*\n\n");
470 *r++ = '*';
471 *r++ = '\n';
472 *r++ = '\n';
473 456
474 llnext = loglist->next; 457 llnext = loglist->next;
475 free(loglist->data); 458 free(loglist->data);
476 free(loglist); 459 free(loglist);
477 loglist = llnext; 460 loglist = llnext;
@@ -480,124 +463,137 @@ static void stats_return_fulllog(size_t *iovec_entries, struct iovec **iovector,
480} 463}
481#endif 464#endif
482 465
483static size_t stats_return_everything(char *reply) { 466static void stats_return_everything(char **reply, const char *reply_end) {
484 torrent_stats stats = {0, 0, 0}; 467 torrent_stats stats = {0, 0, 0};
485 int i; 468 int i;
486 char *r = reply;
487 469
488 iterate_all_torrents(torrent_statter, (uintptr_t)&stats); 470 iterate_all_torrents(torrent_statter, (uintptr_t)&stats);
489 471
490 r += sprintf(r, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); 472 fmt_append(reply, reply_end, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
491 r += sprintf(r, "<stats>\n"); 473 fmt_append(reply, reply_end, "<stats>\n");
492 r += sprintf(r, " <tracker_id>%" PRIu32 "</tracker_id>\n", g_tracker_id); 474 fmt_append(reply, reply_end, " <tracker_id>%" PRIu32 "</tracker_id>\n", g_tracker_id);
493 r += sprintf(r, " <version>\n"); 475 fmt_append(reply, reply_end, " <version>\n");
494 r += stats_return_tracker_version(r); 476 stats_return_tracker_version(reply, reply_end);
495 r += sprintf(r, " </version>\n"); 477 fmt_append(reply, reply_end, " </version>\n");
496 r += sprintf(r, " <uptime>%llu</uptime>\n", (unsigned long long)(time(NULL) - ot_start_time)); 478 fmt_append(reply, reply_end, " <uptime>%llu</uptime>\n", (unsigned long long)(time(NULL) - ot_start_time));
497 r += sprintf(r, " <torrents>\n"); 479 fmt_append(reply, reply_end, " <torrents>\n");
498 r += sprintf(r, " <count_mutex>%zd</count_mutex>\n", mutex_get_torrent_count()); 480 fmt_append(reply, reply_end, " <count_mutex>%zd</count_mutex>\n", mutex_get_torrent_count());
499 r += sprintf(r, " <count_iterator>%llu</count_iterator>\n", stats.torrent_count); 481 fmt_append(reply, reply_end, " <count_iterator>%llu</count_iterator>\n", stats.torrent_count);
500 r += sprintf(r, " </torrents>\n"); 482 fmt_append(reply, reply_end, " </torrents>\n");
501 r += sprintf(r, " <peers>\n <count>%llu</count>\n </peers>\n", stats.peer_count); 483 fmt_append(reply, reply_end, " <peers>\n <count>%llu</count>\n </peers>\n", stats.peer_count);
502 r += sprintf(r, " <seeds>\n <count>%llu</count>\n </seeds>\n", stats.seed_count); 484 fmt_append(reply, reply_end, " <seeds>\n <count>%llu</count>\n </seeds>\n", stats.seed_count);
503 r += sprintf(r, " <completed>\n <count>%llu</count>\n </completed>\n", ot_overall_completed); 485 fmt_append(reply, reply_end, " <completed>\n <count>%llu</count>\n </completed>\n", ot_overall_completed);
504 r += sprintf(r, " <connections>\n"); 486 fmt_append(reply, reply_end, " <connections>\n");
505 r += sprintf(r, " <tcp>\n <accept>%llu</accept>\n <announce>%llu</announce>\n <scrape>%llu</scrape>\n </tcp>\n", 487 fmt_append(reply, reply_end, " <tcp>\n <accept>%llu</accept>\n <announce>%llu</announce>\n <scrape>%llu</scrape>\n </tcp>\n",
506 ot_overall_tcp_connections, ot_overall_tcp_successfulannounces, ot_overall_tcp_successfulscrapes); 488 ot_overall_tcp_connections, ot_overall_tcp_successfulannounces, ot_overall_tcp_successfulscrapes);
507 r += sprintf(r, " <udp>\n <overall>%llu</overall>\n <connect>%llu</connect>\n <announce>%llu</announce>\n <scrape>%llu</scrape>\n <missmatch>%llu</missmatch>\n </udp>\n", 489 fmt_append(reply, reply_end, " <udp>\n <overall>%llu</overall>\n <connect>%llu</connect>\n <announce>%llu</announce>\n <scrape>%llu</scrape>\n <missmatch>%llu</missmatch>\n </udp>\n",
508 ot_overall_udp_connections, ot_overall_udp_connects, ot_overall_udp_successfulannounces, ot_overall_udp_successfulscrapes, 490 ot_overall_udp_connections, ot_overall_udp_connects, ot_overall_udp_successfulannounces, ot_overall_udp_successfulscrapes,
509 ot_overall_udp_connectionidmissmatches); 491 ot_overall_udp_connectionidmissmatches);
510 r += sprintf(r, " <livesync>\n <count>%llu</count>\n </livesync>\n", ot_overall_sync_count); 492 fmt_append(reply, reply_end, " <livesync>\n <count>%llu</count>\n </livesync>\n", ot_overall_sync_count);
511 r += sprintf(r, " </connections>\n"); 493 fmt_append(reply, reply_end, " </connections>\n");
512 r += sprintf(r, " <debug>\n"); 494 fmt_append(reply, reply_end, " <debug>\n");
513 r += sprintf(r, " <renew>\n"); 495 fmt_append(reply, reply_end, " <renew>\n");
514 for (i = 0; i < OT_PEER_TIMEOUT; ++i) 496 for (i = 0; i < OT_PEER_TIMEOUT; ++i)
515 r += sprintf(r, " <count interval=\"%02i\">%llu</count>\n", i, ot_renewed[i]); 497 fmt_append(reply, reply_end, " <count interval=\"%02i\">%llu</count>\n", i, ot_renewed[i]);
516 r += sprintf(r, " </renew>\n"); 498 fmt_append(reply, reply_end, " </renew>\n");
517 r += sprintf(r, " <http_error>\n"); 499 fmt_append(reply, reply_end, " <http_error>\n");
518 for (i = 0; i < CODE_HTTPERROR_COUNT; ++i) 500 for (i = 0; i < CODE_HTTPERROR_COUNT; ++i)
519 r += sprintf(r, " <count code=\"%s\">%llu</count>\n", ot_failed_request_names[i], ot_failed_request_counts[i]); 501 fmt_append(reply, reply_end, " <count code=\"%s\">%llu</count>\n", ot_failed_request_names[i], ot_failed_request_counts[i]);
520 r += sprintf(r, " </http_error>\n"); 502 fmt_append(reply, reply_end, " </http_error>\n");
521 r += sprintf(r, " <mutex_stall>\n <count>%llu</count>\n </mutex_stall>\n", ot_overall_stall_count); 503 fmt_append(reply, reply_end, " <mutex_stall>\n <count>%llu</count>\n </mutex_stall>\n", ot_overall_stall_count);
522 r += sprintf(r, " </debug>\n"); 504 fmt_append(reply, reply_end, " </debug>\n");
523 r += sprintf(r, "</stats>"); 505 fmt_append(reply, reply_end, "</stats>");
524 return r - reply;
525} 506}
526 507
527size_t stats_return_tracker_version(char *reply) { 508void stats_return_tracker_version(char **reply, const char *reply_end) {
528#define QUOTE(name) #name 509#define QUOTE(name) #name
529#define SQUOTE(name) QUOTE(name) 510#define SQUOTE(name) QUOTE(name)
530 return sprintf(reply, "https://erdgeist.org/gitweb/opentracker/commit/?id=" SQUOTE(GIT_VERSION) "\n"); 511 fmt_append(reply, reply_end, "https://erdgeist.org/gitweb/opentracker/commit/?id=" SQUOTE(GIT_VERSION) "\n");
531} 512}
532 513
533size_t return_stats_for_tracker(char *reply, int mode, int format) { 514void return_stats_for_tracker(char **reply, const char *reply_end, int mode, int format) {
534 (void)format; 515 (void)format;
535 switch (mode & TASK_TASK_MASK) { 516 switch (mode & TASK_TASK_MASK) {
536 case TASK_STATS_CONNS: 517 case TASK_STATS_CONNS:
537 return stats_connections_mrtg(reply); 518 stats_connections_mrtg(reply, reply_end);
519 break;
538 case TASK_STATS_SCRAPE: 520 case TASK_STATS_SCRAPE:
539 return stats_scrape_mrtg(reply); 521 stats_scrape_mrtg(reply, reply_end);
522 break;
540 case TASK_STATS_UDP: 523 case TASK_STATS_UDP:
541 return stats_udpconnections_mrtg(reply); 524 stats_udpconnections_mrtg(reply, reply_end);
525 break;
542 case TASK_STATS_TCP: 526 case TASK_STATS_TCP:
543 return stats_tcpconnections_mrtg(reply); 527 stats_tcpconnections_mrtg(reply, reply_end);
528 break;
544 case TASK_STATS_FULLSCRAPE: 529 case TASK_STATS_FULLSCRAPE:
545 return stats_fullscrapes_mrtg(reply); 530 stats_fullscrapes_mrtg(reply, reply_end);
531 break;
546 case TASK_STATS_COMPLETED: 532 case TASK_STATS_COMPLETED:
547 return stats_return_completed_mrtg(reply); 533 stats_return_completed_mrtg(reply, reply_end);
534 break;
548 case TASK_STATS_HTTPERRORS: 535 case TASK_STATS_HTTPERRORS:
549 return stats_httperrors_txt(reply); 536 stats_httperrors_txt(reply, reply_end);
537 break;
550 case TASK_STATS_VERSION: 538 case TASK_STATS_VERSION:
551 return stats_return_tracker_version(reply); 539 stats_return_tracker_version(reply, reply_end);
540 break;
552 case TASK_STATS_RENEW: 541 case TASK_STATS_RENEW:
553 return stats_return_renew_bucket(reply); 542 stats_return_renew_bucket(reply, reply_end);
543 break;
554 case TASK_STATS_SYNCS: 544 case TASK_STATS_SYNCS:
555 return stats_return_sync_mrtg(reply); 545 stats_return_sync_mrtg(reply, reply_end);
546 break;
556#ifdef WANT_LOG_NUMWANT 547#ifdef WANT_LOG_NUMWANT
557 case TASK_STATS_NUMWANTS: 548 case TASK_STATS_NUMWANTS:
558 return stats_return_numwants(reply); 549 stats_return_numwants(reply, reply_end);
550 break;
559#endif 551#endif
560 default: 552 default:
561 return 0; 553 break;
562 } 554 }
563} 555}
564 556
565static void stats_make(size_t *iovec_entries, struct iovec **iovector, ot_tasktype mode) { 557static void stats_make(size_t *iovec_entries, struct iovec **iovector, ot_tasktype mode) {
566 char *r; 558 char *r;
559 const char *r_end;
567 560
568 *iovec_entries = 0; 561 *iovec_entries = 0;
569 *iovector = NULL; 562 *iovector = NULL;
570 if (!(r = iovec_new_chunk(iovec_entries, iovector, OT_STATS_TMPSIZE))) 563 if (!(r = iovec_new_chunk(iovec_entries, iovector, OT_STATS_TMPSIZE)))
571 return; 564 return;
565 r_end = r + OT_STATS_TMPSIZE;
572 566
573 switch (mode & TASK_TASK_MASK) { 567 switch (mode & TASK_TASK_MASK) {
574 case TASK_STATS_TORRENTS: 568 case TASK_STATS_TORRENTS:
575 r += stats_torrents_mrtg(r); 569 stats_torrents_mrtg(&r, r_end);
576 break; 570 break;
577 case TASK_STATS_PEERS: 571 case TASK_STATS_PEERS:
578 r += stats_peers_mrtg(r); 572 stats_peers_mrtg(&r, r_end);
579 break; 573 break;
580 case TASK_STATS_SLASH24S: 574 case TASK_STATS_SLASH24S:
581 r += stats_slash24s_txt(r, 128); 575 stats_slash24s_txt(&r, r_end, 128);
582 break; 576 break;
583 case TASK_STATS_TOP10: 577 case TASK_STATS_TOP10:
584 r += stats_top_txt(r, 10); 578 stats_top_txt(&r, r_end, 10);
585 break; 579 break;
586 case TASK_STATS_TOP100: 580 case TASK_STATS_TOP100:
587 r = iovec_next_chunk(iovec_entries, iovector, r, 4 * OT_STATS_TMPSIZE); 581 r = iovec_next_chunk(iovec_entries, iovector, r, 4 * OT_STATS_TMPSIZE);
588 if (!r) 582 if (!r)
589 return; 583 return;
590 r += stats_top_txt(r, 100); 584 r_end = r + 4 * OT_STATS_TMPSIZE;
585 stats_top_txt(&r, r_end, 100);
591 break; 586 break;
592 case TASK_STATS_EVERYTHING: 587 case TASK_STATS_EVERYTHING:
593 r = iovec_next_chunk(iovec_entries, iovector, r, OT_STATS_TMPSIZE + 64 * OT_PEER_TIMEOUT); 588 r = iovec_next_chunk(iovec_entries, iovector, r, OT_STATS_TMPSIZE + 64 * OT_PEER_TIMEOUT);
594 if (!r) 589 if (!r)
595 return; 590 return;
596 r += stats_return_everything(r); 591 r_end = r + OT_STATS_TMPSIZE + 64 * OT_PEER_TIMEOUT;
592 stats_return_everything(&r, r_end);
597 break; 593 break;
598#ifdef WANT_SPOT_WOODPECKER 594#ifdef WANT_SPOT_WOODPECKER
599 case TASK_STATS_WOODPECKERS: 595 case TASK_STATS_WOODPECKERS:
600 r += stats_return_woodpeckers(r, 128); 596 stats_return_woodpeckers(&r, r_end, 128);
601 break; 597 break;
602#endif 598#endif
603#ifdef WANT_FULLLOG_NETWORKS 599#ifdef WANT_FULLLOG_NETWORKS
diff --git a/ot_stats.h b/ot_stats.h
index 203831c..fb6aad8 100644
--- a/ot_stats.h
+++ b/ot_stats.h
@@ -44,8 +44,8 @@ enum {
44void stats_issue_event(ot_status_event event, PROTO_FLAG proto, uintptr_t event_data); 44void stats_issue_event(ot_status_event event, PROTO_FLAG proto, uintptr_t event_data);
45void stats_deliver(int64_t sock, int tasktype); 45void stats_deliver(int64_t sock, int tasktype);
46void stats_cleanup(void); 46void stats_cleanup(void);
47size_t return_stats_for_tracker(char *reply, int mode, int format); 47void return_stats_for_tracker(char **reply, const char *reply_end, int mode, int format);
48size_t stats_return_tracker_version(char *reply); 48void stats_return_tracker_version(char **reply, const char *reply_end);
49void stats_init(void); 49void stats_init(void);
50void stats_deinit(void); 50void stats_deinit(void);
51 51
diff --git a/ot_sync.c b/ot_sync.c
index f958a20..f656bef 100644
--- a/ot_sync.c
+++ b/ot_sync.c
@@ -11,6 +11,7 @@
11#include <sys/uio.h> 11#include <sys/uio.h>
12 12
13/* Opentracker */ 13/* Opentracker */
14#include "ot_fmt.h"
14#include "ot_iovec.h" 15#include "ot_iovec.h"
15#include "ot_mutex.h" 16#include "ot_mutex.h"
16#include "ot_scan.h" 17#include "ot_scan.h"
@@ -109,14 +110,10 @@ static void sync_make(size_t *iovec_entries, struct iovec **iovector) {
109 re = r + OT_SYNC_CHUNK_SIZE; 110 re = r + OT_SYNC_CHUNK_SIZE;
110 } 111 }
111 112
112 *r++ = '2'; 113 fmt_append(&r, re, "20:");
113 *r++ = '0'; 114 fmt_bytes(&r, re, hash, sizeof(ot_hash));
114 *r++ = ':'; 115 fmt_append(&r, re, "%zd:", byte_count);
115 memmove(r, hash, sizeof(ot_hash)); 116 fmt_bytes(&r, re, peer_list->changeset.data, byte_count);
116 r += sizeof(ot_hash);
117 r += sprintf(r, "%zd:", byte_count);
118 memmove(r, peer_list->changeset.data, byte_count);
119 r += byte_count;
120 } 117 }
121 118
122 /* All torrents done: release lock on currenct bucket */ 119 /* All torrents done: release lock on currenct bucket */
diff --git a/ot_vector.c b/ot_vector.c
index 924d468..a56d55b 100644
--- a/ot_vector.c
+++ b/ot_vector.c
@@ -57,34 +57,39 @@ static uint8_t vector_hash_peer(ot_peer const *peer, size_t compare_size, size_t
57*/ 57*/
58void *vector_find_or_insert(ot_vector *vector, void *key, size_t member_size, size_t compare_size, int *exactmatch) { 58void *vector_find_or_insert(ot_vector *vector, void *key, size_t member_size, size_t compare_size, int *exactmatch) {
59 uint8_t *match = vector_binary_search(key, vector->data, vector->size, member_size, compare_size, exactmatch); 59 uint8_t *match = vector_binary_search(key, vector->data, vector->size, member_size, compare_size, exactmatch);
60 size_t match_offset, bytes_after;
60 61
61 if (*exactmatch) 62 if (*exactmatch)
62 return match; 63 return match;
63 64
65 /* Hold the insertion point as an offset rather than a pointer: it survives
66 the realloc below, and an empty vector has no data pointer to offset from
67 in the first place */
68 match_offset = vector->size ? (size_t)(match - (uint8_t *)vector->data) : 0;
69 bytes_after = member_size * vector->size - match_offset;
70
64 if (vector->size + 1 > vector->space) { 71 if (vector->size + 1 > vector->space) {
65 size_t new_space = vector->space ? OT_VECTOR_GROW_RATIO * vector->space : OT_VECTOR_MIN_MEMBERS; 72 size_t new_space = vector->space ? OT_VECTOR_GROW_RATIO * vector->space : OT_VECTOR_MIN_MEMBERS;
66 ptrdiff_t match_off = match - (uint8_t *)vector->data; 73 uint8_t *new_data = realloc(vector->data, new_space * member_size);
67 uint8_t *new_data = realloc(vector->data, new_space * member_size);
68 74
69 if (!new_data) 75 if (!new_data)
70 return NULL; 76 return NULL;
71 77
72 /* Adjust pointer if it moved by realloc */
73 match = new_data + match_off;
74
75 vector->data = new_data; 78 vector->data = new_data;
76 vector->space = new_space; 79 vector->space = new_space;
77 } 80 }
78 memmove(match + member_size, match, ((uint8_t *)vector->data) + member_size * vector->size - match); 81
82 match = (uint8_t *)vector->data + match_offset;
83 memmove(match + member_size, match, bytes_after);
79 84
80 vector->size++; 85 vector->size++;
81 return match; 86 return match;
82} 87}
83 88
84ot_peer *vector_find_or_insert_peer(ot_vector *vector, ot_peer const *peer, size_t peer_size, int *exactmatch) { 89ot_peer *vector_find_or_insert_peer(ot_vector *vector, ot_peer const *peer, size_t peer_size, int *exactmatch) {
85 ot_peer *match, *end; 90 ot_peer *match;
86 const size_t compare_size = OT_PEER_COMPARE_SIZE_FROM_PEER_SIZE(peer_size); 91 const size_t compare_size = OT_PEER_COMPARE_SIZE_FROM_PEER_SIZE(peer_size);
87 size_t match_to_end; 92 size_t match_offset, bytes_after;
88 93
89 /* If space is zero but size is set, we're dealing with a list of vector->size buckets */ 94 /* If space is zero but size is set, we're dealing with a list of vector->size buckets */
90 if (vector->space < vector->size) 95 if (vector->space < vector->size)
@@ -94,26 +99,26 @@ ot_peer *vector_find_or_insert_peer(ot_vector *vector, ot_peer const *peer, size
94 if (*exactmatch) 99 if (*exactmatch)
95 return match; 100 return match;
96 101
97 /* This is the amount of bytes that needs to be pushed backwards by peer_size bytes to make room for new peer */ 102 /* Hold the insertion point as an offset rather than a pointer: it survives
98 end = (ot_peer *)vector->data + vector->size * peer_size; 103 the realloc below, and an empty vector has no data pointer to offset from
99 match_to_end = end - match; 104 in the first place. bytes_after is what has to move up by peer_size to
105 make room for the new peer */
106 match_offset = vector->size ? (size_t)(match - (ot_peer *)vector->data) : 0;
107 bytes_after = vector->size * peer_size - match_offset;
100 108
101 if (vector->size + 1 > vector->space) { 109 if (vector->size + 1 > vector->space) {
102 ptrdiff_t offset = match - (ot_peer *)vector->data; 110 size_t new_space = vector->space ? OT_VECTOR_GROW_RATIO * vector->space : OT_VECTOR_MIN_MEMBERS;
103 size_t new_space = vector->space ? OT_VECTOR_GROW_RATIO * vector->space : OT_VECTOR_MIN_MEMBERS; 111 ot_peer *new_data = realloc(vector->data, new_space * peer_size);
104 ot_peer *new_data = realloc(vector->data, new_space * peer_size);
105 112
106 if (!new_data) 113 if (!new_data)
107 return NULL; 114 return NULL;
108 /* Adjust pointer if it moved by realloc */
109 match = new_data + offset;
110 115
111 vector->data = new_data; 116 vector->data = new_data;
112 vector->space = new_space; 117 vector->space = new_space;
113 } 118 }
114 119
115 /* Here we're guaranteed to have enough space in vector to move the block of peers after insertion point */ 120 match = (ot_peer *)vector->data + match_offset;
116 memmove(match + peer_size, match, match_to_end); 121 memmove(match + peer_size, match, bytes_after);
117 122
118 vector->size++; 123 vector->size++;
119 return match; 124 return match;
@@ -150,11 +155,13 @@ int vector_remove_peer(ot_vector *vector, ot_peer const *peer, size_t peer_size)
150} 155}
151 156
152void vector_remove_torrent(ot_vector *vector, ot_torrent *match) { 157void vector_remove_torrent(ot_vector *vector, ot_torrent *match) {
153 ot_torrent *end = ((ot_torrent *)vector->data) + vector->size; 158 ot_torrent *end;
154 159
155 if (!vector->size) 160 if (!vector->size)
156 return; 161 return;
157 162
163 end = ((ot_torrent *)vector->data) + vector->size;
164
158 free_peerlist(match->peer_list6); 165 free_peerlist(match->peer_list6);
159 free_peerlist(match->peer_list4); 166 free_peerlist(match->peer_list4);
160 167
diff --git a/tests/integration/.gitignore b/tests/integration/.gitignore
new file mode 100644
index 0000000..7a60b85
--- /dev/null
+++ b/tests/integration/.gitignore
@@ -0,0 +1,2 @@
1__pycache__/
2*.pyc
diff --git a/tests/integration/harness.py b/tests/integration/harness.py
new file mode 100644
index 0000000..868075d
--- /dev/null
+++ b/tests/integration/harness.py
@@ -0,0 +1,198 @@
1"""Starts a real opentracker and talks to it over the wire.
2
3Everything here drives the binary rather than the code, so these tests cover
4what the unit suite cannot reach: the event loop, the HTTP parser and the
5torrent store working together. Run against a sanitizer build and the tracker
6reports its own memory errors, which the teardown turns into test failures.
7"""
8
9import os
10import shutil
11import signal
12import socket
13import struct
14import subprocess
15import sys
16import tempfile
17import time
18import unittest
19import urllib.error
20import urllib.parse
21import urllib.request
22
23TRACKER = os.environ.get("OPENTRACKER", os.path.join(os.path.dirname(__file__), "..", "..", "opentracker.debug"))
24
25
26def bdecode(data):
27 """Enough bencode for what a tracker answers."""
28
29 def value(i):
30 if data[i : i + 1] == b"d":
31 i, out = i + 1, {}
32 while data[i : i + 1] != b"e":
33 i, key = value(i)
34 i, out[key] = value(i)
35 return i + 1, out
36 if data[i : i + 1] == b"l":
37 i, out = i + 1, []
38 while data[i : i + 1] != b"e":
39 i, item = value(i)
40 out.append(item)
41 return i + 1, out
42 if data[i : i + 1] == b"i":
43 end = data.index(b"e", i)
44 return end + 1, int(data[i + 1 : end])
45 colon = data.index(b":", i)
46 length = int(data[i:colon])
47 return colon + 1 + length, data[colon + 1 : colon + 1 + length]
48
49 return value(0)[1]
50
51
52def free_port():
53 probe = socket.socket()
54 probe.bind(("127.0.0.1", 0))
55 port = probe.getsockname()[1]
56 probe.close()
57 return port
58
59
60class Tracker:
61 """One tracker process, listening on loopback."""
62
63 def __init__(self, extra_config=""):
64 self.port = free_port()
65 self.dir = tempfile.mkdtemp(prefix="ot-integration-")
66 self.log = None
67 self.process = None
68 try:
69 self.config = os.path.join(self.dir, "opentracker.conf")
70 with open(self.config, "w") as handle:
71 handle.write("listen.tcp 127.0.0.1:%d\n" % self.port)
72 handle.write("listen.udp 127.0.0.1:%d\n" % self.port)
73 handle.write(extra_config)
74 self.log = open(os.path.join(self.dir, "tracker.log"), "w+b")
75 # Leak detection is left at the sanitizer's own default, which is on
76 # where LeakSanitizer exists and off where it does not. Asking for it
77 # by name makes the tracker refuse to start on macOS and FreeBSD.
78 environment = dict(os.environ, ASAN_OPTIONS="abort_on_error=0")
79 self.process = subprocess.Popen([TRACKER, "-f", self.config], stdout=self.log, stderr=self.log, env=environment)
80 self._wait_for_port()
81 except BaseException:
82 self._cleanup()
83 raise
84
85 def _cleanup(self):
86 """Close what we opened and take the scratch directory with it."""
87 if self.process is not None and self.process.poll() is None:
88 self.process.kill()
89 self.process.wait()
90 if self.log is not None:
91 self.log.close()
92 self.log = None
93 shutil.rmtree(self.dir, ignore_errors=True)
94
95 def _wait_for_port(self):
96 for _ in range(200):
97 if self.process.poll() is not None:
98 raise AssertionError("tracker exited at once:\n" + self.output())
99 try:
100 probe = socket.create_connection(("127.0.0.1", self.port), timeout=0.2)
101 probe.close()
102 return
103 except OSError:
104 time.sleep(0.05)
105 raise AssertionError("tracker never listened on %d:\n%s" % (self.port, self.output()))
106
107 def output(self):
108 if self.log is None:
109 return ""
110 self.log.flush()
111 self.log.seek(0)
112 return self.log.read().decode("utf-8", "replace")
113
114 def stop(self):
115 """Returns anything the tracker complained about on the way out.
116
117 The interrupt rather than a kill is what makes the leak check work at
118 all: LeakSanitizer reports from an exit handler, so a killed tracker
119 reports nothing and the suite passes over leaks it never looked for.
120 """
121 if self.process is not None and self.process.poll() is None:
122 self.process.send_signal(signal.SIGINT)
123 try:
124 self.process.wait(timeout=5)
125 except subprocess.TimeoutExpired:
126 self.process.kill()
127 self.process.wait()
128 complaints = [
129 line
130 for line in self.output().splitlines()
131 if "Sanitizer" in line or "runtime error" in line or line.startswith("ERROR")
132 ]
133 self._cleanup()
134 return complaints
135
136 # ----------------------------------------------------------- over http
137
138 def get(self, path, timeout=10):
139 return urllib.request.urlopen("http://127.0.0.1:%d%s" % (self.port, path), timeout=timeout).read()
140
141 def announce(self, info_hash, peer_id=b"-OT0001-aaaaaaaaaaaa", port=6881, left=0, **extra):
142 query = {
143 "info_hash": urllib.parse.quote_from_bytes(info_hash, safe=""),
144 "peer_id": urllib.parse.quote_from_bytes(peer_id, safe=""),
145 "port": port,
146 "left": left,
147 }
148 query.update(extra)
149 return bdecode(self.get("/announce?" + "&".join("%s=%s" % item for item in query.items())))
150
151 def scrape(self, info_hash):
152 return bdecode(self.get("/scrape?info_hash=" + urllib.parse.quote_from_bytes(info_hash, safe="")))
153
154 def stats(self, mode):
155 return self.get("/stats?mode=" + mode)
156
157 def torrent_count(self):
158 return int(self.stats("torr").split()[0])
159
160 # ------------------------------------------------------------ over udp
161
162 def udp_connect(self):
163 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
164 sock.settimeout(5)
165 sock.sendto(struct.pack(">QII", 0x41727101980, 0, 1), ("127.0.0.1", self.port))
166 answer = sock.recv(2048)
167 action, _, connection_id = struct.unpack(">IIQ", answer[:16])
168 assert action == 0, "udp connect answered action %d" % action
169 return sock, connection_id
170
171 def udp_announce(self, sock, connection_id, info_hash, left=0, event=0, port=6881, numwant=-1):
172 sock.sendto(
173 struct.pack(
174 ">QII20s20sQQQIIIiH", connection_id, 1, 2, info_hash, b"p" * 20, 0, left, 0, event, 0, 0, numwant, port
175 ),
176 ("127.0.0.1", self.port),
177 )
178 answer = sock.recv(2048)
179 action, _, interval, leechers, seeders = struct.unpack(">IIIII", answer[:20])
180 assert action == 1, "udp announce answered action %d" % action
181 return {"interval": interval, "leechers": leechers, "seeders": seeders, "peers": answer[20:]}
182
183
184class TrackerTestCase(unittest.TestCase):
185 """Gives every test its own tracker and fails it on any sanitizer output."""
186
187 config = ""
188
189 def setUp(self):
190 self.tracker = Tracker(self.config)
191
192 def tearDown(self):
193 complaints = self.tracker.stop()
194 self.assertEqual(complaints, [], "the tracker reported:\n" + "\n".join(complaints))
195
196 def hash_for(self, name):
197 """A distinct info hash per test, so tests cannot leak into each other."""
198 return (name.encode() + b"\0" * 20)[:20]
diff --git a/tests/integration/test_announce.py b/tests/integration/test_announce.py
new file mode 100644
index 0000000..ffbc07b
--- /dev/null
+++ b/tests/integration/test_announce.py
@@ -0,0 +1,129 @@
1"""What a peer's announce does to the swarm it joins."""
2
3import os
4import socket
5import sys
6import unittest
7
8sys.path.insert(0, os.path.dirname(__file__))
9from harness import TrackerTestCase
10
11
12def peers_of(answer, key=b"peers"):
13 """Compact peer list as a list of (address, port)."""
14 blob, out = answer.get(key, b""), []
15 size = 6 if key == b"peers" else 18
16 for offset in range(0, len(blob), size):
17 entry = blob[offset : offset + size]
18 address = socket.inet_ntop(socket.AF_INET if size == 6 else socket.AF_INET6, entry[:-2])
19 out.append((address, entry[-2] << 8 | entry[-1]))
20 return out
21
22
23class AnnounceLifetime(TrackerTestCase):
24 config = ""
25
26 def test_a_leecher_is_counted_as_incomplete(self):
27 info_hash = self.hash_for("leecher")
28 answer = self.tracker.announce(info_hash, left=100)
29 self.assertEqual(answer[b"complete"], 0)
30 self.assertEqual(answer[b"incomplete"], 1)
31
32 def test_no_bytes_left_means_seeding(self):
33 info_hash = self.hash_for("seeder")
34 answer = self.tracker.announce(info_hash, left=0)
35 self.assertEqual(answer[b"complete"], 1)
36 self.assertEqual(answer[b"incomplete"], 0)
37
38 def test_a_peer_that_finishes_moves_from_one_column_to_the_other(self):
39 info_hash = self.hash_for("finishes")
40 peer_id = b"-OT0001-finisher0001"
41 self.tracker.announce(info_hash, peer_id=peer_id, left=100)
42 self.assertEqual(self.tracker.announce(info_hash, peer_id=peer_id, left=100)[b"incomplete"], 1)
43
44 answer = self.tracker.announce(info_hash, peer_id=peer_id, left=0, event="completed")
45 self.assertEqual(answer[b"complete"], 1, "the peer should now count as a seeder")
46 self.assertEqual(answer[b"incomplete"], 0, "and no longer as a leecher")
47
48 def test_stopped_removes_the_peer(self):
49 info_hash = self.hash_for("stopped")
50 peer_id = b"-OT0001-leaves000001"
51 self.tracker.announce(info_hash, peer_id=peer_id, left=100)
52 self.assertEqual(self.tracker.scrape(info_hash)[b"files"][info_hash][b"incomplete"], 1)
53
54 self.tracker.announce(info_hash, peer_id=peer_id, left=100, event="stopped")
55 self.assertEqual(
56 self.tracker.scrape(info_hash)[b"files"][info_hash][b"incomplete"], 0, "the peer should be gone"
57 )
58
59 def test_the_same_peer_announcing_twice_is_still_one_peer(self):
60 info_hash = self.hash_for("twice")
61 peer_id = b"-OT0001-repeats00001"
62 for _ in range(5):
63 answer = self.tracker.announce(info_hash, peer_id=peer_id, left=100)
64 self.assertEqual(answer[b"incomplete"], 1, "re-announcing must not duplicate the peer")
65
66 def test_a_peer_is_its_address_and_port_not_its_peer_id(self):
67 """Two announces from one address and port are one peer, whatever
68 peer_id they carry, because that pair is the whole identity."""
69 info_hash = self.hash_for("identity")
70 self.tracker.announce(info_hash, peer_id=b"-OT0001-a00000000001", port=6881, left=100)
71 answer = self.tracker.announce(info_hash, peer_id=b"-OT0001-b00000000002", port=6881, left=100)
72 self.assertEqual(answer[b"incomplete"], 1, "same address and port, so still one peer")
73
74 answer = self.tracker.announce(info_hash, peer_id=b"-OT0001-c00000000003", port=6882, left=100)
75 self.assertEqual(answer[b"incomplete"], 2, "a different port makes a different peer")
76
77 def test_a_downloaded_count_is_kept(self):
78 info_hash = self.hash_for("downloaded")
79 self.tracker.announce(info_hash, port=7001, left=0, event="completed")
80 self.tracker.announce(info_hash, port=7002, left=0, event="completed")
81 self.assertEqual(self.tracker.scrape(info_hash)[b"files"][info_hash][b"downloaded"], 2)
82
83 def test_a_peer_is_given_the_whole_swarm_including_itself(self):
84 """opentracker does not filter the announcing peer out of its own
85 answer. Clients are expected to ignore themselves."""
86 info_hash = self.hash_for("sees")
87 self.tracker.announce(info_hash, port=1111, left=100)
88 answer = self.tracker.announce(info_hash, port=2222, left=100)
89
90 ports = sorted(port for _, port in peers_of(answer))
91 self.assertEqual(ports, [1111, 2222])
92
93 def test_an_unknown_torrent_scrapes_as_empty(self):
94 answer = self.tracker.scrape(self.hash_for("never-seen"))
95 self.assertEqual(answer[b"files"], {}, "a torrent nobody announced has no entry")
96
97
98class AnnounceRejects(TrackerTestCase):
99 def assert_rejected(self, query):
100 import urllib.error
101
102 with self.assertRaises(urllib.error.HTTPError) as caught:
103 self.tracker.get("/announce?" + query)
104 self.assertEqual(caught.exception.code, 400)
105
106 def test_a_short_info_hash_is_rejected(self):
107 self.assert_rejected("info_hash=tooshort&peer_id=" + "b" * 20 + "&port=6881&left=0")
108
109 def test_a_missing_info_hash_answers_a_failure_reason(self):
110 """Not a 400: the tracker answers 200 with a bencoded complaint, so
111 that a client shows the message to its user."""
112 from harness import bdecode
113
114 answer = bdecode(self.tracker.get("/announce?peer_id=" + "b" * 20 + "&port=6881&left=0"))
115 self.assertIn(b"failure reason", answer)
116 self.assertIn(b"info_hash", answer[b"failure reason"])
117
118 def test_a_port_out_of_range_is_rejected(self):
119 self.assert_rejected("info_hash=" + "a" * 20 + "&peer_id=" + "b" * 20 + "&port=65536&left=0")
120
121 def test_a_port_that_is_not_a_number_is_rejected(self):
122 self.assert_rejected("info_hash=" + "a" * 20 + "&peer_id=" + "b" * 20 + "&port=abc&left=0")
123
124 def test_two_info_hashes_are_rejected(self):
125 self.assert_rejected("info_hash=" + "a" * 20 + "&info_hash=" + "c" * 20 + "&peer_id=" + "b" * 20 + "&port=6881&left=0")
126
127
128if __name__ == "__main__":
129 unittest.main()
diff --git a/tests/integration/test_dualstack.py b/tests/integration/test_dualstack.py
new file mode 100644
index 0000000..cd5ba7d
--- /dev/null
+++ b/tests/integration/test_dualstack.py
@@ -0,0 +1,89 @@
1"""A tracker serving both families from one socket.
2
3These need WANT_IP_FROM_QUERY_STRING so a peer's address can be dictated
4rather than being whatever loopback the test happened to come from.
5"""
6
7import os
8import socket
9import sys
10import unittest
11
12sys.path.insert(0, os.path.dirname(__file__))
13from harness import TrackerTestCase
14
15
16def addresses(answer, key):
17 blob, out = answer.get(key, b""), []
18 size = 6 if key == b"peers" else 18
19 for offset in range(0, len(blob), size):
20 entry = blob[offset : offset + size]
21 out.append(socket.inet_ntop(socket.AF_INET if size == 6 else socket.AF_INET6, entry[:-2]))
22 return out
23
24
25class DualStack(TrackerTestCase):
26 def test_an_ipv4_peer_lands_in_the_v4_list(self):
27 info_hash = self.hash_for("v4only")
28 self.tracker.announce(info_hash, ip="10.1.2.3", port=5001, left=100)
29 answer = self.tracker.announce(info_hash, ip="10.1.2.4", port=5002, left=100)
30
31 self.assertEqual(sorted(addresses(answer, b"peers")), ["10.1.2.3", "10.1.2.4"])
32 self.assertEqual(addresses(answer, b"peers6"), [], "no v6 peer was announced")
33
34 def test_an_ipv6_peer_lands_in_the_v6_list(self):
35 info_hash = self.hash_for("v6only")
36 self.tracker.announce(info_hash, ip="2001:db8::1", port=5001, left=100)
37 answer = self.tracker.announce(info_hash, ip="2001:db8::2", port=5002, left=100)
38
39 self.assertEqual(sorted(addresses(answer, b"peers6")), ["2001:db8::1", "2001:db8::2"])
40 self.assertEqual(addresses(answer, b"peers"), [], "no v4 peer was announced")
41
42 def test_the_two_families_are_kept_apart_but_both_returned(self):
43 info_hash = self.hash_for("both")
44 self.tracker.announce(info_hash, ip="10.9.9.9", port=6001, left=100)
45 answer = self.tracker.announce(info_hash, ip="2001:db8::9", port=6002, left=100)
46
47 self.assertEqual(addresses(answer, b"peers"), ["10.9.9.9"])
48 self.assertEqual(addresses(answer, b"peers6"), ["2001:db8::9"])
49
50 def test_a_v4_mapped_address_is_the_same_peer_as_the_plain_one(self):
51 info_hash = self.hash_for("v4mapped")
52 self.tracker.announce(info_hash, ip="10.5.5.5", port=7001, left=100)
53 answer = self.tracker.announce(info_hash, ip="::ffff:10.5.5.5", port=7001, left=100)
54
55 self.assertEqual(answer[b"incomplete"], 1, "the same address written two ways is one peer")
56 self.assertEqual(addresses(answer, b"peers"), ["10.5.5.5"], "and it is held as v4")
57
58 def test_the_two_families_count_into_one_swarm(self):
59 info_hash = self.hash_for("counts")
60 self.tracker.announce(info_hash, ip="10.4.4.4", port=8001, left=0)
61 answer = self.tracker.announce(info_hash, ip="2001:db8::4", port=8002, left=100)
62
63 self.assertEqual(answer[b"complete"], 1)
64 self.assertEqual(answer[b"incomplete"], 1)
65 self.assertEqual(self.tracker.scrape(info_hash)[b"files"][info_hash][b"complete"], 1)
66
67 def test_stopped_removes_only_the_family_it_came_from(self):
68 info_hash = self.hash_for("stopfamily")
69 self.tracker.announce(info_hash, ip="10.3.3.3", port=9001, left=100)
70 self.tracker.announce(info_hash, ip="2001:db8::3", port=9002, left=100)
71 self.tracker.announce(info_hash, ip="10.3.3.3", port=9001, left=100, event="stopped")
72
73 answer = self.tracker.announce(info_hash, ip="2001:db8::33", port=9003, left=100)
74 self.assertEqual(addresses(answer, b"peers"), [], "the v4 peer left")
75 self.assertIn("2001:db8::3", addresses(answer, b"peers6"), "the v6 one stayed")
76
77 def test_a_malformed_address_is_rejected(self):
78 import urllib.error
79
80 # The whole field has to be the address: a prefix that happens to
81 # parse would quietly record the peer somewhere else
82 for bad in ("999.1.2.3", "nonsense", "2001:db8:::1", "10.1.2.3.4", "10.1.2.3junk", "2001:db8::1%eth0"):
83 with self.assertRaises(urllib.error.HTTPError, msg="ip=%s should be refused" % bad) as caught:
84 self.tracker.announce(self.hash_for("badip"), ip=bad, port=6881, left=0)
85 self.assertEqual(caught.exception.code, 400)
86
87
88if __name__ == "__main__":
89 unittest.main()
diff --git a/tests/runner.c b/tests/runner.c
index c2350c3..71a5e2a 100644
--- a/tests/runner.c
+++ b/tests/runner.c
@@ -4,7 +4,11 @@
4 $id$ */ 4 $id$ */
5 5
6/* System */ 6/* System */
7#include <stdio.h>
8#include <stdlib.h>
7#include <string.h> 9#include <string.h>
10#include <sys/wait.h>
11#include <unistd.h>
8 12
9/* Opentracker */ 13/* Opentracker */
10#include "test.h" 14#include "test.h"
@@ -27,18 +31,83 @@ static const struct module modules[] = {{"ot_urlencoded", test_urlencoded},
27 {"ot_batch", test_batch}, 31 {"ot_batch", test_batch},
28 {"ot_fdstate", test_fdstate}, 32 {"ot_fdstate", test_fdstate},
29 {"ot_event", test_event}, 33 {"ot_event", test_event},
34 {"ot_fmt", test_fmt},
30 {"ot_accesslist", test_accesslist}, 35 {"ot_accesslist", test_accesslist},
31 {NULL, NULL}}; 36 {NULL, NULL}};
32 37
33/* Run every module, or only those whose name contains argv[1] */ 38struct tally {
39 unsigned long checks;
40 unsigned long failures;
41};
42
43/* Each module runs in a child of its own, so that a crash or a sanitizer
44 abort costs that module's results rather than every module after it. The
45 child reports its tally through a pipe; one that dies without reporting is
46 counted as a failure under its own name. */
47static void run_forked(const struct module *m) {
48 struct tally tally = {0, 0};
49 int pipefd[2], status = 0;
50 pid_t child;
51 ssize_t got;
52
53 if (pipe(pipefd)) {
54 test_failf(__FILE__, __LINE__, "pipe failed, cannot isolate %s", m->name);
55 return;
56 }
57
58 fflush(stdout); /* or the child inherits a copy and prints it again */
59 child = fork();
60 if (child < 0) {
61 close(pipefd[0]);
62 close(pipefd[1]);
63 test_failf(__FILE__, __LINE__, "fork failed, cannot isolate %s", m->name);
64 return;
65 }
66
67 if (!child) {
68 close(pipefd[0]);
69 test_checks = test_failures = 0;
70 m->run();
71 tally.checks = test_checks;
72 tally.failures = test_failures;
73 if (write(pipefd[1], &tally, sizeof(tally)) != (ssize_t)sizeof(tally))
74 _exit(1);
75 fflush(stdout);
76 _exit(0);
77 }
78
79 close(pipefd[1]);
80 got = read(pipefd[0], &tally, sizeof(tally));
81 close(pipefd[0]);
82 while (waitpid(child, &status, 0) < 0)
83 ;
84
85 if (got == (ssize_t)sizeof(tally)) {
86 test_checks += tally.checks;
87 test_failures += tally.failures;
88 }
89 if (got != (ssize_t)sizeof(tally) || !WIFEXITED(status) || WEXITSTATUS(status))
90 test_failf(__FILE__, __LINE__, "%s did not run to the end", m->name);
91}
92
93/* Run every module, or only those whose name contains argv[1].
94 OT_TEST_FORK=0 keeps everything in one process, which is what a debugger
95 wants. */
34int main(int argc, char **argv) { 96int main(int argc, char **argv) {
97 const char *fork_setting = getenv("OT_TEST_FORK");
98 int isolate = !fork_setting || strcmp(fork_setting, "0");
35 const struct module *m; 99 const struct module *m;
36 100
37 for (m = modules; m->name; ++m) { 101 for (m = modules; m->name; ++m) {
38 if (argc > 1 && !strstr(m->name, argv[1])) 102 if (argc > 1 && !strstr(m->name, argv[1]))
39 continue; 103 continue;
40 test_module(m->name); 104 test_module(m->name);
41 m->run(); 105 if (!isolate) {
106 m->run();
107 continue;
108 }
109 test_case("the module runs to the end");
110 run_forked(m);
42 } 111 }
43 112
44 return test_summary(); 113 return test_summary();
diff --git a/tests/t_fmt.c b/tests/t_fmt.c
new file mode 100644
index 0000000..bc23ed5
--- /dev/null
+++ b/tests/t_fmt.c
@@ -0,0 +1,108 @@
1/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
2 It is considered beerware. Prost. Skol. Cheers or whatever.
3
4 $id$ */
5
6/* System */
7#include <string.h>
8
9/* Opentracker */
10#include "ot_fmt.h"
11#include "test.h"
12
13static void case_appends_in_sequence(void) {
14 char buffer[64];
15 char *r = buffer, *re = buffer + sizeof(buffer);
16
17 test_case("appends follow each other and advance the cursor");
18 fmt_append(&r, re, "d8:completei%zde", (size_t)7);
19 fmt_append(&r, re, "ee");
20 *r = 0;
21 CHECK_STR(buffer, "d8:completei7eee");
22 CHECK_INT(r - buffer, 16);
23}
24
25static void case_truncation_stops_at_the_end(void) {
26 char buffer[16];
27 char *r = buffer, *re = buffer + 8; /* pretend the buffer ends early */
28
29 memset(buffer, '#', sizeof(buffer));
30 test_case("a value too long for the room left stops at the end");
31 fmt_append(&r, re, "%s", "0123456789");
32 CHECK_PTR(r, re);
33
34 test_case("and writes nothing past it");
35 CHECK_INT(buffer[8], '#');
36 CHECK_INT(buffer[15], '#');
37}
38
39static void case_a_full_buffer_takes_nothing_more(void) {
40 char buffer[16];
41 char *r = buffer + 8, *re = buffer + 8;
42
43 memset(buffer, '#', sizeof(buffer));
44 test_case("appending to a cursor already at the end changes nothing");
45 fmt_append(&r, re, "more");
46 CHECK_PTR(r, re);
47 CHECK_INT(buffer[8], '#');
48}
49
50static void case_an_append_that_just_fits(void) {
51 char buffer[8];
52 char *r = buffer, *re = buffer + 3;
53
54 memset(buffer, '#', sizeof(buffer));
55 test_case("three characters into three bytes of room still truncate");
56 fmt_append(&r, re, "abc");
57 CHECK_PTR(r, re);
58 CHECK_INT(buffer[0], 'a');
59 CHECK_INT(buffer[1], 'b');
60
61 test_case("because snprintf spends the last byte on its terminator");
62 CHECK_INT(buffer[2], 0);
63 CHECK_INT(buffer[3], '#');
64}
65
66static void case_bytes_are_all_or_nothing(void) {
67 char buffer[16];
68 char *r = buffer, *re = buffer + 8;
69
70 memset(buffer, '#', sizeof(buffer));
71 test_case("a run of bytes is written when it fits");
72 fmt_bytes(&r, re, "\0\1\2\3", 4);
73 CHECK_PTR(r, buffer + 4);
74 CHECK_MEM(buffer, "\0\1\2\3", 4);
75
76 test_case("a run that would not fit is refused whole");
77 fmt_bytes(&r, re, "abcde", 5);
78 CHECK_PTR(r, re);
79 CHECK_INT(buffer[4], '#');
80
81 test_case("a run of exactly the room left is written");
82 r = buffer + 4;
83 fmt_bytes(&r, re, "wxyz", 4);
84 CHECK_PTR(r, re);
85 CHECK_MEM(buffer + 4, "wxyz", 4);
86 CHECK_INT(buffer[8], '#');
87}
88
89static void case_bytes_do_not_terminate(void) {
90 char buffer[8];
91 char *r = buffer, *re = buffer + 4;
92
93 memset(buffer, '#', sizeof(buffer));
94 test_case("unlike an append, a run of bytes may fill the room exactly");
95 fmt_bytes(&r, re, "abcd", 4);
96 CHECK_PTR(r, re);
97 CHECK_MEM(buffer, "abcd", 4);
98 CHECK_INT(buffer[4], '#');
99}
100
101void test_fmt(void) {
102 case_appends_in_sequence();
103 case_truncation_stops_at_the_end();
104 case_a_full_buffer_takes_nothing_more();
105 case_an_append_that_just_fits();
106 case_bytes_are_all_or_nothing();
107 case_bytes_do_not_terminate();
108}
diff --git a/tests/t_vector.c b/tests/t_vector.c
index 541a6da..6e048e1 100644
--- a/tests/t_vector.c
+++ b/tests/t_vector.c
@@ -112,6 +112,51 @@ static void case_insert_keeps_order(void) {
112 free(vector.data); 112 free(vector.data);
113} 113}
114 114
115/* The peer variant is reached with an empty vector on every first announce
116 for a torrent, and again whenever a swarm empties and refills, because
117 vector_fixup_peers drops the allocation and nulls the data pointer. */
118
119static void case_insert_peer_into_empty_vector(void) {
120 ot_vector vector = {NULL, 0, 0};
121 ot_peer6 low, high;
122 int exactmatch;
123 ot_peer *slot;
124
125 memset(low, 0x11, sizeof(low));
126 memset(high, 0x99, sizeof(high));
127
128 test_case("the first peer goes into a vector that has no allocation yet");
129 slot = vector_find_or_insert_peer(&vector, low, OT_PEER_SIZE6, &exactmatch);
130 CHECK(slot != NULL);
131 CHECK_INT(exactmatch, 0);
132 memcpy(slot, low, OT_PEER_SIZE6);
133 CHECK_INT(vector.size, 1);
134 CHECK_INT(vector.space, OT_VECTOR_MIN_MEMBERS);
135 CHECK_PTR(slot, vector.data);
136
137 test_case("a second peer keeps the order");
138 slot = vector_find_or_insert_peer(&vector, high, OT_PEER_SIZE6, &exactmatch);
139 CHECK(slot != NULL);
140 CHECK_INT(exactmatch, 0);
141 memcpy(slot, high, OT_PEER_SIZE6);
142 CHECK_INT(vector.size, 2);
143 CHECK_MEM((char *)vector.data, low, OT_PEER_COMPARE_SIZE6);
144 CHECK_MEM((char *)vector.data + OT_PEER_SIZE6, high, OT_PEER_COMPARE_SIZE6);
145
146 test_case("and emptying the vector hands the allocation back");
147 CHECK(vector_remove_peer(&vector, low, OT_PEER_SIZE6) != 0);
148 CHECK(vector_remove_peer(&vector, high, OT_PEER_SIZE6) != 0);
149 CHECK_INT(vector.size, 0);
150 CHECK_PTR(vector.data, NULL);
151
152 test_case("so the next peer starts from no allocation again");
153 slot = vector_find_or_insert_peer(&vector, low, OT_PEER_SIZE6, &exactmatch);
154 CHECK(slot != NULL);
155 CHECK_INT(vector.size, 1);
156
157 free(vector.data);
158}
159
115static void case_insert_finds_existing(void) { 160static void case_insert_finds_existing(void) {
116 ot_vector vector = {NULL, 0, 0}; 161 ot_vector vector = {NULL, 0, 0};
117 const unsigned char five[4] = {0, 0, 0, 5}; 162 const unsigned char five[4] = {0, 0, 0, 5};
@@ -257,6 +302,7 @@ void test_vector(void) {
257 case_search_past_the_end(); 302 case_search_past_the_end();
258 case_search_empty(); 303 case_search_empty();
259 case_insert_keeps_order(); 304 case_insert_keeps_order();
305 case_insert_peer_into_empty_vector();
260 case_insert_finds_existing(); 306 case_insert_finds_existing();
261 case_insert_compare_prefix(); 307 case_insert_compare_prefix();
262 case_redistribute_holds_every_peer(); 308 case_redistribute_holds_every_peer();
diff --git a/tests/test.h b/tests/test.h
index aa4eb00..0eddc82 100644
--- a/tests/test.h
+++ b/tests/test.h
@@ -90,5 +90,6 @@ void test_mutex(void);
90void test_batch(void); 90void test_batch(void);
91void test_fdstate(void); 91void test_fdstate(void);
92void test_event(void); 92void test_event(void);
93void test_fmt(void);
93 94
94#endif 95#endif
diff --git a/trackerlogic.c b/trackerlogic.c
index 9705bd6..1f38f07 100644
--- a/trackerlogic.c
+++ b/trackerlogic.c
@@ -14,6 +14,7 @@
14/* Opentracker */ 14/* Opentracker */
15#include "ot_accesslist.h" 15#include "ot_accesslist.h"
16#include "ot_clean.h" 16#include "ot_clean.h"
17#include "ot_fmt.h"
17#include "ot_fullscrape.h" 18#include "ot_fullscrape.h"
18#include "ot_http.h" 19#include "ot_http.h"
19#include "ot_ip.h" 20#include "ot_ip.h"
@@ -299,11 +300,12 @@ static size_t return_peers_for_torrent_udp(struct ot_workstruct *ws, ot_torrent
299} 300}
300 301
301static size_t return_peers_for_torrent_tcp(struct ot_workstruct *ws, ot_torrent *torrent, size_t amount, char *reply) { 302static size_t return_peers_for_torrent_tcp(struct ot_workstruct *ws, ot_torrent *torrent, size_t amount, char *reply) {
302 char *r = reply; 303 char *r = reply;
303 int erval = OT_CLIENT_REQUEST_INTERVAL_RANDOM; 304 const char *r_end = ws->outbuf + G_OUTBUF_SIZE;
304 size_t seed_count = torrent->peer_list6->seed_count + torrent->peer_list4->seed_count; 305 int erval = OT_CLIENT_REQUEST_INTERVAL_RANDOM;
305 size_t down_count = torrent->peer_list6->down_count + torrent->peer_list4->down_count; 306 size_t seed_count = torrent->peer_list6->seed_count + torrent->peer_list4->seed_count;
306 size_t peer_count = torrent->peer_list6->peer_count + torrent->peer_list4->peer_count - seed_count; 307 size_t down_count = torrent->peer_list6->down_count + torrent->peer_list4->down_count;
308 size_t peer_count = torrent->peer_list6->peer_count + torrent->peer_list4->peer_count - seed_count;
307 309
308 /* Simple case: amount of peers in both lists is less than requested, here we return all results */ 310 /* Simple case: amount of peers in both lists is less than requested, here we return all results */
309 size_t amount_v4 = torrent->peer_list4->peer_count; 311 size_t amount_v4 = torrent->peer_list4->peer_count;
@@ -341,11 +343,11 @@ static size_t return_peers_for_torrent_tcp(struct ot_workstruct *ws, ot_torrent
341 ++amount_v4; 343 ++amount_v4;
342 } 344 }
343 345
344 r += 346 fmt_append(&r, r_end, "d8:completei%zde10:downloadedi%zde10:incompletei%zde8:intervali%ie12:min intervali%ie", seed_count, down_count, peer_count, erval,
345 sprintf(r, "d8:completei%zde10:downloadedi%zde10:incompletei%zde8:intervali%ie12:min intervali%ie", seed_count, down_count, peer_count, erval, erval / 2); 347 erval / 2);
346 348
347 if (amount_v4) { 349 if (amount_v4) {
348 r += sprintf(r, PEERS_BENCODED4 "%zd:", OT_PEER_COMPARE_SIZE4 * amount_v4); 350 fmt_append(&r, r_end, PEERS_BENCODED4 "%zd:", OT_PEER_COMPARE_SIZE4 * amount_v4);
349 if (amount_v4 == torrent->peer_list4->peer_count) 351 if (amount_v4 == torrent->peer_list4->peer_count)
350 r += return_peers_all(torrent->peer_list4, OT_PEER_SIZE4, r); 352 r += return_peers_all(torrent->peer_list4, OT_PEER_SIZE4, r);
351 else 353 else
@@ -353,14 +355,14 @@ static size_t return_peers_for_torrent_tcp(struct ot_workstruct *ws, ot_torrent
353 } 355 }
354 356
355 if (amount_v6) { 357 if (amount_v6) {
356 r += sprintf(r, PEERS_BENCODED6 "%zd:", OT_PEER_COMPARE_SIZE6 * amount_v6); 358 fmt_append(&r, r_end, PEERS_BENCODED6 "%zd:", OT_PEER_COMPARE_SIZE6 * amount_v6);
357 if (amount_v6 == torrent->peer_list6->peer_count) 359 if (amount_v6 == torrent->peer_list6->peer_count)
358 r += return_peers_all(torrent->peer_list6, OT_PEER_SIZE6, r); 360 r += return_peers_all(torrent->peer_list6, OT_PEER_SIZE6, r);
359 else 361 else
360 r += return_peers_selection(ws, torrent->peer_list6, OT_PEER_SIZE6, amount_v6, r); 362 r += return_peers_selection(ws, torrent->peer_list6, OT_PEER_SIZE6, amount_v6, r);
361 } 363 }
362 364
363 *r++ = 'e'; 365 fmt_append(&r, r_end, "e");
364 366
365 return r - reply; 367 return r - reply;
366} 368}
@@ -403,12 +405,11 @@ size_t return_udp_scrape_for_torrent(ot_hash const hash, char *reply) {
403} 405}
404 406
405/* Fetches scrape info for a specific torrent */ 407/* Fetches scrape info for a specific torrent */
406size_t return_tcp_scrape_for_torrent(ot_hash const *hash_list, size_t amount, char *reply) { 408void return_tcp_scrape_for_torrent(ot_hash const *hash_list, size_t amount, char **reply, const char *reply_end) {
407 char *r = reply; 409 int exactmatch;
408 int exactmatch;
409 size_t i; 410 size_t i;
410 411
411 r += sprintf(r, "d5:filesd"); 412 fmt_append(reply, reply_end, "d5:filesd");
412 413
413 for (i = 0; i < amount; ++i) { 414 for (i = 0; i < amount; ++i) {
414 int delta_torrentcount = 0; 415 int delta_torrentcount = 0;
@@ -421,22 +422,17 @@ size_t return_tcp_scrape_for_torrent(ot_hash const *hash_list, size_t amount, ch
421 vector_remove_torrent(torrents_list, torrent); 422 vector_remove_torrent(torrents_list, torrent);
422 delta_torrentcount = -1; 423 delta_torrentcount = -1;
423 } else { 424 } else {
424 *r++ = '2'; 425 fmt_append(reply, reply_end, "20:");
425 *r++ = '0'; 426 fmt_bytes(reply, reply_end, hash, sizeof(ot_hash));
426 *r++ = ':'; 427 fmt_append(reply, reply_end, "d8:completei%zde10:downloadedi%zde10:incompletei%zdee",
427 memcpy(r, hash, sizeof(ot_hash)); 428 torrent->peer_list6->seed_count + torrent->peer_list4->seed_count, torrent->peer_list6->down_count + torrent->peer_list4->down_count,
428 r += sizeof(ot_hash); 429 torrent->peer_list6->peer_count + torrent->peer_list4->peer_count - torrent->peer_list6->seed_count - torrent->peer_list4->seed_count);
429 r += sprintf(r, "d8:completei%zde10:downloadedi%zde10:incompletei%zdee", torrent->peer_list6->seed_count + torrent->peer_list4->seed_count,
430 torrent->peer_list6->down_count + torrent->peer_list4->down_count,
431 torrent->peer_list6->peer_count + torrent->peer_list4->peer_count - torrent->peer_list6->seed_count - torrent->peer_list4->seed_count);
432 } 430 }
433 } 431 }
434 mutex_bucket_unlock_by_hash(*hash, delta_torrentcount); 432 mutex_bucket_unlock_by_hash(*hash, delta_torrentcount);
435 } 433 }
436 434
437 *r++ = 'e'; 435 fmt_append(reply, reply_end, "ee");
438 *r++ = 'e';
439 return r - reply;
440} 436}
441 437
442static ot_peerlist dummy_list; 438static ot_peerlist dummy_list;
@@ -472,9 +468,12 @@ size_t remove_peer_from_torrent(PROTO_FLAG proto, struct ot_workstruct *ws) {
472 } 468 }
473 469
474 if (proto == FLAG_TCP) { 470 if (proto == FLAG_TCP) {
475 int erval = OT_CLIENT_REQUEST_INTERVAL_RANDOM; 471 int erval = OT_CLIENT_REQUEST_INTERVAL_RANDOM;
476 ws->reply_size = sprintf(ws->reply, "d8:completei%zde10:incompletei%zde8:intervali%ie12:min intervali%ie%s0:e", seed_count, peer_count - seed_count, erval, 472 char *r = ws->reply;
477 erval / 2, peer_size == OT_PEER_SIZE6 ? PEERS_BENCODED6 : PEERS_BENCODED4); 473
474 fmt_append(&r, ws->outbuf + G_OUTBUF_SIZE, "d8:completei%zde10:incompletei%zde8:intervali%ie12:min intervali%ie%s0:e", seed_count, peer_count - seed_count,
475 erval, erval / 2, peer_size == OT_PEER_SIZE6 ? PEERS_BENCODED6 : PEERS_BENCODED4);
476 ws->reply_size = r - ws->reply;
478 } 477 }
479 478
480 /* Handle UDP reply */ 479 /* Handle UDP reply */
diff --git a/trackerlogic.h b/trackerlogic.h
index 42018c3..63abf78 100644
--- a/trackerlogic.h
+++ b/trackerlogic.h
@@ -187,7 +187,7 @@ void exerr(const char *message);
187 otherwise it is released in return_peers_for_torrent */ 187 otherwise it is released in return_peers_for_torrent */
188size_t add_peer_to_torrent_and_return_peers(PROTO_FLAG proto, struct ot_workstruct *ws, size_t amount); 188size_t add_peer_to_torrent_and_return_peers(PROTO_FLAG proto, struct ot_workstruct *ws, size_t amount);
189size_t remove_peer_from_torrent(PROTO_FLAG proto, struct ot_workstruct *ws); 189size_t remove_peer_from_torrent(PROTO_FLAG proto, struct ot_workstruct *ws);
190size_t return_tcp_scrape_for_torrent(ot_hash const *hash_list, size_t amount, char *reply); 190void return_tcp_scrape_for_torrent(ot_hash const *hash_list, size_t amount, char **reply, const char *reply_end);
191size_t return_udp_scrape_for_torrent(ot_hash const hash, char *reply); 191size_t return_udp_scrape_for_torrent(ot_hash const hash, char *reply);
192void add_torrent_from_saved_state(ot_hash const hash, ot_time base, size_t down_count); 192void add_torrent_from_saved_state(ot_hash const hash, ot_time base, size_t down_count);
193#ifdef _DEBUG_RANDOMTORRENTS 193#ifdef _DEBUG_RANDOMTORRENTS