3 Commits
Author SHA1 Message Date
Spine 54370ad892 lock entire reap operations in a mutex 2026-02-14 12:31:19 +00:00
Spine d4aaff7bb5 refactor tracker report functionality 2024-03-10 21:13:13 +01:00
Spine 37685bd8da add resource usage to grafana and report endpoints 2024-03-10 21:12:45 +01:00
17 changed files with 1039 additions and 334 deletions

No files matched your search

+31
View File
@@ -1,3 +1,34 @@
-- 2.1.5 (2026-02-12)
FEATURES
* connection_mother() does not need db access
* validate client whitelist before acquiring torrent lock
* add timers to measure lock acquisition, announce() and reap_peers() duration
BUGFIXES
* Wrap peer reaper code in a mutex to avoid stats desynchronization.
* Prevent upspeed and downspeed calculation from exceeding 2^31-1.
* Fix error when seeders reannounce with left > 0.
NOTE
* 2.1.4 was not officially released.
-- 2.1.3 (2024-03-09)
BREAKING
* API user endpoint output changed to JSON
FEATURES
* Peak number of simultaneous open connections is now tracked.
* Maximum size of client http request is now tracked.
* Number of errors (bad tracker secrets, bad client requests) is now tracked.
* Show jemalloc stats in Gazelle and Grafana report endpoints.
BUILD
* refactor report functions to produce only report payload
-- 2.1.2 (2024-02-17)
FEATURES
* show resource usage in Gazelle and Grafana report endpoints
-- 2.1.1 (2024-02-15)
BUILD
* Detabbed source code in first step towards having the code lint cleanly, no code changes
-- 2.1.0 (2024-02-11)
FEATURES
* announce jitter is now a uniform distribution and can be modified on the fly
+3 -1
View File
@@ -28,7 +28,9 @@ RUN cmake -Wno-dev -S /srv -B /srv/build \
&& apt-get autoremove -y \
&& apt-get clean -y \
&& mv /srv/build/ocelot /srv/ocelot \
&& mv /srv/ocelot.conf.dist /srv/ocelot.conf
&& mv /srv/ocelot.conf.dist /srv/ocelot.conf \
&& rm -rf /srv/build \
&& mkdir -p /tmp/ocelot
# default listen_port value in ocelot.conf
EXPOSE 34000
+4
View File
@@ -69,6 +69,10 @@ docker run -v $(pwd)/ocelot.conf:/srv/ocelot.conf ocelot
* `-c <path/to/ocelot.conf>` - Path to config file. If unspecified, the current working directory is used.
* `-v` - Print queue status every time a flush is initiated.
You can run a test ocelot daemon alongside a production daemon by specifying
a separate configuration file, and setting `readonly = true`. This will
prevent the database peer tables from being reset.
### Signals
* `SIGHUP` - Reload config
+5
View File
@@ -34,4 +34,9 @@ schedule_interval = 3
log = false
log_path = /tmp/ocelot
# the report path should be placed on a tmpfs partition in production
report_path = /tmp/ocelot
# set to true if prevent peer data from being zeroed out on startup
# useful to test a new version alongside an instance running in production
readonly = false
+2
View File
@@ -100,6 +100,8 @@ void config::init() {
add("log", false);
add("log_path", "ocelot"); // path to where to write log + name of log (don't need to put file extension)
add("report_path", "/tmp"); // path for transfer to website (should be a tmpfs in production)
// Debugging
add("readonly", false);
}
+11 -3
View File
@@ -1,4 +1,4 @@
// Copyright [2017-2024] Orpheus
// Copyright [2017-2026] Orpheus
#include <spdlog/spdlog.h>
@@ -12,11 +12,13 @@
#include "response.h"
#include "events.h"
std::mutex peak_open_mutex;
// Define the connection mother (first half) and connection middlemen (second half)
//---------- Connection mother - spawns middlemen and lets them deal with the connection
connection_mother::connection_mother(config * conf, worker * worker_obj, mysql * db_obj, site_comm * sc_obj, schedule * sched) : work(worker_obj), db(db_obj) {
connection_mother::connection_mother(config * conf, worker * worker_obj, site_comm * sc_obj, schedule * sched) : work(worker_obj) {
logger = spdlog::get("logger");
// Handle config stuff first
@@ -119,7 +121,13 @@ void connection_mother::handle_connect(ev::io &watcher, int events_flags) {
// Spawn a new middleman
if (stats.open_connections < max_middlemen) {
stats.opened_connections++;
stats.open_connections++;
{
const std::lock_guard<std::mutex> lock(peak_open_mutex);
unsigned int current_open = stats.open_connections++;
if (stats.peak_connections < current_open) {
stats.peak_connections = current_open;
}
}
new connection_middleman(listen_socket, work, this);
}
}
+2 -4
View File
@@ -1,7 +1,7 @@
#ifndef SRC_EVENTS_H_
#define SRC_EVENTS_H_
// Copyright [2017-2024] Orpheus
// Copyright [2017-2026] Orpheus
#ifdef EV_ERROR
#undef EV_ERROR
@@ -29,7 +29,6 @@
#include "config.h"
#include "worker.h"
#include "schedule.h"
#include "db.h"
#include "site_comm.h"
/*
@@ -58,13 +57,12 @@ class connection_mother {
int listen_socket;
worker * work;
mysql * db;
ev::io listen_event;
ev::timer schedule_event;
std::shared_ptr<spdlog::logger> logger;
public:
connection_mother(config * conf, worker * worker_obj, mysql * db_obj, site_comm * sc_obj, schedule * sched_obj);
connection_mother(config * conf, worker * worker_obj, site_comm * sc_obj, schedule * sched_obj);
~connection_mother();
void reload_config(config * conf);
int create_listen_socket();
+200
View File
@@ -0,0 +1,200 @@
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "jemalloc_parse.h"
/* parse a size_t decimal text representation */
const char *parse_sz(const char *in, const char *find, size_t *target) {
const char *p = strstr(in, find);
if (!p) {
// didn't find what we were looking for
return NULL;
}
p += strlen(find);
// copy the digits to a null-terminated string in order to pass it to strtoul()
// the largest unsigned long decimal representation is 4294967295 (10 bytes)
char convert[11];
char *c = convert;
while (isspace(*p)) {
p++;
}
while (isdigit(*p) && c < convert + sizeof(convert) - 1) {
*c++ = *p++;
}
*c = '\0';
*target = strtoul(convert, &c, 10);
return p + 1; // one past the last digit
}
/* parse an unsigned long long decimal text representation */
const char *parse_ull(const char *in, const char *find, unsigned long long *target) {
const char *p = strstr(in, find);
if (!p) {
// didn't find what we were looking for
return NULL;
}
p += strlen(find);
// copy the digits to a null-terminated string in order to pass it to strtoull()
// the largest unsigned long long decimal representation is 18446744073709551615 (21 bytes)
char convert[22];
char *c = convert;
while (isspace(*p)) {
p++;
}
while(isdigit(*p) && c < convert + sizeof(convert) - 1) {
*c++ = *p++;
}
*c = '\0';
*target = strtoull(convert, &c, 10);
return p + 1; // one past the last digit
}
int jemalloc_parse(const char *in, struct ocelot_alloc_info *out) {
/* Parse the output of jemalloc_status() plain output
* This is an ugly hack; it would be faster to build directly what we want
* rather than walk down a formatted string that has generated lots of
* information that is discarded.
* If the input breaks, the error code indicates where the parsing failed.
*/
// set everything to zero
memset (out, 0, sizeof(struct ocelot_alloc_info));
/*
___ Begin jemalloc statistics ___
Version: "5.3.0-0-g54eaed1d8b56b1aa528be3bdd1877e59c56fa90c"
Build-time option settings
...
Run-time option settings
...
Profiling settings
...
Arenas: 17
Quantum size: 16
Page size: 4096
Maximum thread-cached size class: 32768
Number of bin size classes: 36
Number of thread-cache bin size classes: 41
Number of large size classes: 196
*/
if (!(in = strstr(in, "___ Begin jemalloc statistics ___"))) {
return 1;
}
if (!(in = parse_sz(in, "Arenas: ", &out->nr_arena))) {
return 10;
}
if (!(in = parse_sz(in, "Number of bin size classes: ", &out->nr_bin_small))) {
return 11;
}
if (!(in = parse_sz(in, "Number of thread-cache bin size classes: ", &out->nr_bin_tc))) {
return 12;
}
if (!(in = parse_sz(in, "Number of large size classes: ", &out->nr_bin_large))) {
return 13;
}
// Allocated: 895744, active: 1024000, metadata: 3047664 (n_thp 0), resident: 3985408, mapped: 7315456, retained: 1073152
if (!(in = parse_ull(in, "Allocated: ", &out->mem_allocated))) {
return 20;
}
if (!(in = parse_ull(in, "active: ", &out->mem_active))) {
return 21;
}
if (!(in = parse_ull(in, "metadata: ", &out->mem_metadata))) {
return 22;
}
if (!(in = parse_ull(in, "resident: ", &out->mem_resident))) {
return 23;
}
if (!(in = parse_ull(in, "mapped: ", &out->mem_mapped))) {
return 24;
}
if (!(in = parse_ull(in, "retained: ", &out->mem_retained))) {
return 25;
}
/*
allocated nmalloc (#/sec) ndalloc (#/sec) nrequests (#/sec) nfill (#/sec) nflush (#/sec)
small: 247840 131452 0 129036 0 24219 0 2402 0 8543 0
large: 290816 815 0 808 0 971 0 815 0 2351 0
total: 538656 132267 0 129844 0
*/
if (!(in = strstr(in, "(#/sec)\nsmall:"))) {
return 30;
}
if (!(in = parse_sz(in, " ", &out->small.allocated))) {
return 40;
}
if (!(in = parse_sz(in, " ", &out->small.nmalloc_total))) {
return 41;
}
if (!(in = parse_sz(in, " ", &out->small.ndalloc_rate))) {
return 42;
}
if (!(in = parse_sz(in, " ", &out->small.ndalloc_total))) {
return 43;
}
if (!(in = parse_sz(in, " ", &out->small.nrequests_rate))) {
return 44;
}
if (!(in = parse_sz(in, " ", &out->small.nrequests_total))) {
return 45;
}
if (!(in = parse_sz(in, " ", &out->small.nfill_rate))) {
return 46;
}
if (!(in = parse_sz(in, " ", &out->small.nfill_total))) {
return 47;
}
if (!(in = parse_sz(in, " ", &out->small.nflush_rate))) {
return 48;
}
if (!(in = parse_sz(in, " ", &out->small.nflush_total))) {
return 49;
}
if (!(in = strstr(in, "\nlarge:"))) {
return 60;
}
if (!(in = parse_sz(in, " ", &out->large.allocated))) {
return 70;
}
if (!(in = parse_sz(in, " ", &out->large.nmalloc_total))) {
return 71;
}
if (!(in = parse_sz(in, " ", &out->large.ndalloc_rate))) {
return 72;
}
if (!(in = parse_sz(in, " ", &out->large.ndalloc_total))) {
return 73;
}
if (!(in = parse_sz(in, " ", &out->large.nrequests_rate))) {
return 74;
}
if (!(in = parse_sz(in, " ", &out->large.nrequests_total))) {
return 75;
}
if (!(in = parse_sz(in, " ", &out->large.nfill_rate))) {
return 76;
}
if (!(in = parse_sz(in, " ", &out->large.nfill_total))) {
return 77;
}
if (!(in = parse_sz(in, " ", &out->large.nflush_rate))) {
return 78;
}
if (!(in = parse_sz(in, " ", &out->large.nflush_total))) {
return 79;
}
return 0;
}
+37
View File
@@ -0,0 +1,37 @@
#ifndef SRC_PARSE_JEMALLOC_H_
#define SRC_PARSE_JEMALLOC_H_
struct ocelot_alloc_stat {
size_t allocated;
size_t nmalloc_total;
size_t nmalloc_rate;
size_t ndalloc_total;
size_t ndalloc_rate;
size_t nrequests_total;
size_t nrequests_rate;
size_t nfill_total;
size_t nfill_rate;
size_t nflush_total;
size_t nflush_rate;
};
struct ocelot_alloc_info {
size_t nr_arena;
size_t nr_bin_small;
size_t nr_bin_tc;
size_t nr_bin_large;
unsigned long long mem_allocated;
unsigned long long mem_active;
unsigned long long mem_metadata;
unsigned long long mem_resident;
unsigned long long mem_mapped;
unsigned long long mem_retained;
struct ocelot_alloc_stat small;
struct ocelot_alloc_stat large;
};
int jemalloc_parse(const char *in, struct ocelot_alloc_info *out);
#endif // SRC_PARSE_JEMALLOC_H_
+25 -7
View File
@@ -1,4 +1,4 @@
// Copyright [2017-2024] Orpheus
// Copyright [2017-2026] Orpheus
#include <sys/stat.h>
#include <syslog.h>
@@ -26,10 +26,6 @@ static schedule *sched;
struct stats_t stats;
const char * version() {
return "2.1.1";
}
static void create_daemon() {
pid_t pid;
@@ -113,7 +109,8 @@ int main(int argc, char **argv) {
conf_arg = true;
conf_file_path = argv[++i];
} else if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--version") == 0) {
std::cout << "Ocelot version " << version() << std::endl;
// preprocessor concatenation
std::cout << "Ocelot version " OCELOT_VERSION ", compiled " __DATE__ " " __TIME__ << std::endl;
return 0;
} else {
std::cout << "Usage: " << argv[0] << " [-v] [-c configfile] [--daemonize]" << std::endl;
@@ -146,6 +143,9 @@ int main(int argc, char **argv) {
auto combined_logger = std::make_shared<spdlog::logger>("logger", begin(sinks), end(sinks));
// If we don't set flush on info, the file log takes a long while to actually flush
combined_logger->flush_on(spdlog::level::info);
combined_logger->info(
"Ocelot version " OCELOT_VERSION ", compiled " __DATE__ " " __TIME__
);
spdlog::register_logger(combined_logger);
db = new mysql(conf);
@@ -167,17 +167,35 @@ int main(int argc, char **argv) {
db->load_whitelist(whitelist);
stats.open_connections = 0;
stats.peak_connections = 0;
stats.opened_connections = 0;
stats.connection_rate = 0;
stats.requests = 0;
stats.request_rate = 0;
stats.leechers = 0;
stats.seeders = 0;
stats.user_queue_size = 0;
stats.torrent_queue_size = 0;
stats.peer_queue_size = 0;
stats.snatch_queue_size = 0;
stats.token_queue_size = 0;
stats.max_client_request_len = 0;
stats.announcements = 0;
stats.succ_announcements = 0;
stats.scrapes = 0;
stats.announce_duration = 0;
stats.torrent_lock_total = 0;
stats.torrent_lock_duration = 0;
stats.reap_total = 0;
stats.reap_lock_duration = 0;
stats.reap_duration = 0;
stats.bytes_read = 0;
stats.bytes_written = 0;
stats.auth_error_secret = 0;
stats.auth_error_report = 0;
stats.auth_error_announce_key = 0;
stats.client_error = 0;
stats.http_error = 0;
stats.start_time = time(NULL);
// Create worker object, which handles announces and scrapes and all that jazz
@@ -187,7 +205,7 @@ int main(int argc, char **argv) {
sched = new schedule(conf, work, db, sc);
// Create connection mother, which binds to its socket and handles the event stuff
mother = new connection_mother(conf, work, db, sc, sched);
mother = new connection_mother(conf, work, sc, sched);
// Add signal handlers now that all objects have been created
struct sigaction handler{}, ignore{};
+20 -1
View File
@@ -1,7 +1,13 @@
#ifndef SRC_OCELOT_H_
#define SRC_OCELOT_H_
// Copyright [2017-2024] Orpheus
// Copyright [2017-2026] Orpheus
#define OCELOT_VERSION_MAJOR 2
#define OCELOT_VERSION_MINOR 1
#define OCELOT_VERSION_BUGFIX 5
#define OCELOT_VERSION_NREV 0
#define OCELOT_VERSION "2.1.5"
#include <time.h>
@@ -94,6 +100,7 @@ typedef std::unordered_map<std::string, std::string> params_type;
struct stats_t {
std::atomic<uint32_t> open_connections;
std::atomic<uint32_t> peak_connections;
std::atomic<uint64_t> opened_connections;
std::atomic<uint64_t> connection_rate;
std::atomic<uint32_t> leechers;
@@ -103,13 +110,25 @@ struct stats_t {
std::atomic<uint32_t> peer_queue_size;
std::atomic<uint32_t> snatch_queue_size;
std::atomic<uint32_t> token_queue_size;
std::atomic<uint32_t> max_client_request_len;
std::atomic<uint64_t> requests;
std::atomic<uint64_t> request_rate;
std::atomic<uint64_t> announcements;
std::atomic<uint64_t> succ_announcements;
std::atomic<uint64_t> scrapes;
std::atomic<uint64_t> announce_duration; // all durations measure microseconds
std::atomic<uint64_t> torrent_lock_total;
std::atomic<uint64_t> torrent_lock_duration;
std::atomic<uint64_t> reap_total;
std::atomic<uint64_t> reap_lock_duration;
std::atomic<uint64_t> reap_duration;
std::atomic<uint64_t> bytes_read;
std::atomic<uint64_t> bytes_written;
std::atomic<uint64_t> auth_error_secret;
std::atomic<uint64_t> auth_error_report;
std::atomic<uint64_t> auth_error_announce_key;
std::atomic<uint64_t> client_error;
std::atomic<uint64_t> http_error;
time_t start_time;
};
extern struct stats_t stats;
+279 -79
View File
@@ -1,91 +1,291 @@
// Copyright [2017-2024] Orpheus
// Copyright [2017-2026] Orpheus
#include <iostream>
#include <map>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/resource.h>
#include <jemalloc/jemalloc.h>
#include <spdlog/spdlog.h> // debug
#include <fstream>
#include <sstream>
#include <iostream>
#include <iomanip> // for std::setfill() and std::setw()
#include <map>
#include <mutex>
#include <thread>
#include "ocelot.h"
#include "misc_functions.h"
#include "jemalloc_parse.h"
#include "report.h"
#include "response.h"
#include "user.h"
std::string report(params_type &params, user_list &users_list, client_opts_t &client_opts, unsigned int announce_interval, unsigned int announce_jitter) {
std::stringstream output;
std::string action = params["get"];
if (action == "stats") {
time_t uptime = time(NULL) - stats.start_time;
int up_d = uptime / 86400;
uptime -= up_d * 86400;
int up_h = uptime / 3600;
uptime -= up_h * 3600;
int up_m = uptime / 60;
int up_s = uptime - up_m * 60;
int jemalloc_fd;
std::mutex jemalloc_fd_mutex;
output << "Uptime: " << up_d << " days, "
<< (up_h < 10 ? "0" : "") << inttostr(up_h) << ':'
<< (up_m < 10 ? "0" : "") << inttostr(up_m) << ':'
<< (up_s < 10 ? "0" : "") << inttostr(up_s) << "\n"
<< "version: " << version() << "\n"
<< stats.opened_connections << " connections opened\n"
<< stats.open_connections << " open connections\n"
<< stats.connection_rate << " connections/s\n"
<< stats.requests << " requests handled\n"
<< stats.request_rate << " requests/s\n"
<< stats.succ_announcements << " successful announcements\n"
<< (stats.announcements - stats.succ_announcements) << " failed announcements\n"
<< stats.scrapes << " scrapes\n"
<< stats.leechers << " leechers tracked\n"
<< stats.seeders << " seeders tracked\n"
<< stats.user_queue_size << " items in user queue\n"
<< stats.torrent_queue_size << " items in torrent queue\n"
<< stats.peer_queue_size << " items in peer queue\n"
<< stats.snatch_queue_size << " items in snatch queue\n"
<< stats.token_queue_size << " items in token queue\n"
<< stats.bytes_read << " bytes read\n"
<< stats.bytes_written << " bytes written\n"
<< announce_interval << " announce interval\n"
<< announce_jitter << " announce jitter\n"
;
} else if (action == "prom_stats") {
time_t uptime = time(NULL) - stats.start_time;
output << "ocelot_uptime " << uptime << "\n"
<< "ocelot_open_connections " << stats.open_connections << "\n"
<< "ocelot_connection_rate " << stats.connection_rate << "\n"
<< "ocelot_requests " << stats.requests << "\n"
<< "ocelot_request_rate " << stats.request_rate << "\n"
<< "ocelot_succ_announcements " << stats.succ_announcements << "\n"
<< "ocelot_total_announcements " << stats.announcements << "\n"
<< "ocelot_scrapes " << stats.scrapes << "\n"
<< "ocelot_leechers " << stats.leechers << "\n"
<< "ocelot_seeders " << stats.seeders << "\n"
<< "ocelot_user_queue " << stats.user_queue_size << "\n"
<< "ocelot_torrent_queue " << stats.torrent_queue_size << "\n"
<< "ocelot_peer_queue " << stats.peer_queue_size << "\n"
<< "ocelot_snatch_queue " << stats.snatch_queue_size << "\n"
<< "ocelot_token_queue " << stats.token_queue_size << "\n"
<< "ocelot_bytes_read " << stats.bytes_read << "\n"
<< "ocelot_bytes_written " << stats.bytes_written << "\n#";
} else if (action == "user") {
std::string key = params["key"];
if (key.empty()) {
output << "Invalid action\n";
} else {
user_list::const_iterator u = users_list.find(key);
if (u != users_list.end()) {
output << "{\"id\":" << u->second->get_id()
<< ",\"leeching\":" << u->second->get_leeching()
<< ",\"seeding\":" << u->second->get_seeding()
<< ",\"deleted\":" << u->second->is_deleted()
<< ",\"protected\":" << u->second->is_protected()
<< ",\"can_leech\":" << u->second->can_leech()
<< "}" << "\n";
return response(output.str(), client_opts);
}
}
} else {
output << "Invalid action\n";
/* Getting jemalloc stats back to Gazelle is a bit cumbersome
* because all jemalloc can do is write to a file descriptor.
* So we let it write to a file (which is why using a ramdisk
* is important) and then read it back to return as an HTTP
* response.
*/
// callback function for malloc_stats_print()
void jemalloc_stats_export(void *opaque, const char *buf) {
if (jemalloc_fd > 0) {
write(jemalloc_fd, buf, strlen(buf));
}
output << "success";
return response(output.str(), client_opts);
}
int dump_jemalloc(const char *filename, const char *opts) {
// open the file descriptor
std::lock_guard<std::mutex> guard(jemalloc_fd_mutex);
jemalloc_fd = open(filename, O_CREAT|O_WRONLY, 0600);
if (jemalloc_fd == -1) {
return jemalloc_fd;
}
// dump stats
malloc_stats_print(jemalloc_stats_export, NULL, opts);
// tidy up
int result = close(jemalloc_fd);
jemalloc_fd = 0;
return result;
}
std::string report_jemalloc_plain(const char *opts, const std::string path) {
std::string filename(path + "/jemalloc.json." + std::to_string(pthread_self()));
if (dump_jemalloc(filename.c_str(), opts) != 0) {
// return an empty string if something went bananas
unlink(filename.c_str());
return std::string("");
}
// slurp the contents into a string
std::ifstream input(filename);
std::ostringstream output;
while(input >> output.rdbuf())
;
unlink(filename.c_str());
output << "\n";
std::shared_ptr<spdlog::logger> logger(spdlog::get("logger"));
logger->debug("jemalloc stats written to " + filename + ", size=" + std::to_string(output.str().size()));
return output.str();
}
#define ITEM_BYTE(LABEL, VALUE) "\"" LABEL "\":{\"type\":\"byte\",\"value\":" << VALUE << "}"
#define ITEM_ELAPSED(LABEL, VALUE) "\"" LABEL "\":{\"type\":\"elapsed\",\"value\":" << VALUE << "}"
#define ITEM_NUM(LABEL, VALUE) "\"" LABEL "\":{\"type\":\"number\",\"value\":" << VALUE << "}"
#define ITEM_STR(LABEL, VALUE) "\"" LABEL "\":{\"type\":\"string\",\"value\":\"" VALUE "\"}"
#define ITEM_VERSION(LABEL, MAJ, MIN, BUGFIX) "\"" LABEL "\":{\"type\":\"string\",\"value\":\"" << MAJ << "." << MIN << "." << BUGFIX << "\"}"
std::string report(const uint32_t announce_interval, const uint32_t announce_jitter) {
std::ostringstream output;
output << "{"
<< ITEM_STR("version", OCELOT_VERSION) << ','
<< ITEM_VERSION("jemalloc version", JEMALLOC_VERSION_MAJOR, JEMALLOC_VERSION_MINOR, JEMALLOC_VERSION_BUGFIX) << ','
<< ITEM_NUM("connections opened", stats.opened_connections) << ','
<< ITEM_NUM("open connections", stats.open_connections) << ','
<< ITEM_NUM("peak connections", stats.peak_connections) << ','
<< ITEM_NUM("connections/s", stats.connection_rate) << ','
<< ITEM_NUM("requests handled", stats.requests) << ','
<< ITEM_NUM("requests/s", stats.request_rate) << ','
<< ITEM_NUM("successful announcements", stats.succ_announcements) << ','
<< ITEM_NUM("failed announcements", (stats.announcements - stats.succ_announcements)) << ','
<< ITEM_NUM("scrapes", stats.scrapes) << ','
<< ITEM_NUM("leechers tracked", stats.leechers) << ','
<< ITEM_NUM("seeders tracked", stats.seeders) << ','
<< ITEM_NUM("announce duration", stats.announce_duration) << ','
<< ITEM_NUM("torrent lock total", stats.torrent_lock_total) << ','
<< ITEM_NUM("torrent lock duration", stats.torrent_lock_duration) << ','
<< ITEM_NUM("reap lock total", stats.reap_total) << ','
<< ITEM_NUM("reap lock duration", stats.reap_lock_duration) << ','
<< ITEM_NUM("reap duration", stats.reap_duration) << ','
<< ITEM_NUM("items in user queue", stats.user_queue_size) << ','
<< ITEM_NUM("items in torrent queue", stats.torrent_queue_size) << ','
<< ITEM_NUM("items in peer queue", stats.peer_queue_size) << ','
<< ITEM_NUM("items in snatch queue", stats.snatch_queue_size) << ','
<< ITEM_NUM("items in token queue", stats.token_queue_size) << ','
<< ITEM_BYTE("max client request length", stats.max_client_request_len) << ','
<< ITEM_BYTE("bytes read", stats.bytes_read) << ','
<< ITEM_BYTE("bytes written", stats.bytes_written) << ','
<< ITEM_NUM("bad tracker secret received", stats.auth_error_secret) << ','
<< ITEM_NUM("bad report secret received", stats.auth_error_report) << ','
<< ITEM_NUM("bad announce key received", stats.auth_error_announce_key) << ','
<< ITEM_NUM("bad client configuration", stats.client_error) << ','
<< ITEM_NUM("bad http request", stats.http_error) << ','
<< ITEM_NUM("announce interval", announce_interval) << ','
<< ITEM_NUM("announce jitter", announce_jitter) << ','
<< ITEM_ELAPSED("uptime", (time(NULL) - stats.start_time))
;
struct rusage r;
if (getrusage(RUSAGE_SELF, &r) == 0) {
output << ','
<< ITEM_ELAPSED("user time", r.ru_utime.tv_sec << '.' << std::setfill('0') << std::setw(6) << r.ru_utime.tv_usec) << ','
<< ITEM_ELAPSED("system time", r.ru_stime.tv_sec << '.' << std::setfill('0') << std::setw(6) << r.ru_stime.tv_usec) << ','
<< ITEM_BYTE("maximum resident set size", r.ru_maxrss * 1024) << ','
<< ITEM_NUM("minor page faults", r.ru_minflt) << ','
<< ITEM_NUM("major page faults", r.ru_majflt) << ','
<< ITEM_NUM("blocks in", r.ru_inblock) << ','
<< ITEM_NUM("blocks out", r.ru_oublock) << ','
<< ITEM_NUM("voluntary context switches", r.ru_nvcsw ) << ','
<< ITEM_NUM("involuntary context switches", r.ru_nivcsw )
;
}
output << "}\n";
return output.str();
}
std::string report_prom_stats(const char *jemalloc) {
std::ostringstream output;
struct ocelot_alloc_info ji;
int result = jemalloc_parse(jemalloc, &ji);
output << "ocelot_uptime " << time(NULL) - stats.start_time << "\n"
"ocelot_version " << OCELOT_VERSION_MAJOR
<< std::setfill('0') << std::setw(3) << OCELOT_VERSION_MINOR
<< std::setfill('0') << std::setw(3) << OCELOT_VERSION_BUGFIX
<< std::setfill('0') << std::setw(3) << OCELOT_VERSION_NREV << "\n"
"jemalloc_version " << JEMALLOC_VERSION_MAJOR
<< std::setfill('0') << std::setw(3) << JEMALLOC_VERSION_MINOR
<< std::setfill('0') << std::setw(3) << JEMALLOC_VERSION_BUGFIX << "\n"
"ocelot_open_connections " << stats.open_connections << "\n"
"#TYPE ocelot_peak_connections counter\n"
"ocelot_peak_connections " << stats.peak_connections << "\n"
"ocelot_connection_rate " << stats.connection_rate << "\n"
"ocelot_requests " << stats.requests << "\n"
"ocelot_request_rate " << stats.request_rate << "\n"
"ocelot_succ_announcements " << stats.succ_announcements << "\n"
"ocelot_total_announcements " << stats.announcements << "\n"
"ocelot_scrapes " << stats.scrapes << "\n"
"ocelot_leechers " << stats.leechers << "\n"
"ocelot_seeders " << stats.seeders << "\n"
"ocelot_user_queue " << stats.user_queue_size << "\n"
"ocelot_torrent_queue " << stats.torrent_queue_size << "\n"
"ocelot_peer_queue " << stats.peer_queue_size << "\n"
"ocelot_snatch_queue " << stats.snatch_queue_size << "\n"
"ocelot_token_queue " << stats.token_queue_size << "\n"
"ocelot_bytes_read " << stats.bytes_read << "\n"
"ocelot_bytes_written " << stats.bytes_written << "\n"
"#TYPE ocelot_max_client_request_len counter\n"
"ocelot_max_client_request_len " << stats.max_client_request_len << "\n"
"#TYPE ocelot_error counter\n"
"ocelot_error{kind=\"secret\"} " << stats.auth_error_secret << "\n"
"ocelot_error{kind=\"report\"} " << stats.auth_error_report << "\n"
"ocelot_error{kind=\"announce\"} " << stats.auth_error_announce_key << "\n"
"ocelot_error{kind=\"client\"} " << stats.client_error << "\n"
"ocelot_error{kind=\"http\"} " << stats.http_error << "\n"
"ocelot_error{kind=\"jemalloc_parse\"} " << result << "\n"
"#TYPE ocelot_timing counter\n"
"ocelot_timing{kind=\"announce_duration\"} " << stats.announce_duration << "\n"
"ocelot_timing{kind=\"torrent_lock_total\"} " << stats.torrent_lock_total << "\n"
"ocelot_timing{kind=\"torrent_lock_duration\"} " << stats.torrent_lock_duration << "\n"
"ocelot_timing{kind=\"reap_total\"} " << stats.reap_total << "\n"
"ocelot_timing{kind=\"reap_lock_duration\"} " << stats.reap_lock_duration << "\n"
"ocelot_timing{kind=\"reap_duration\"} " << stats.reap_duration << "\n"
"#TYPE jemalloc_arena_total counter\n"
"jemalloc_arena_total " << ji.nr_arena << "\n"
"#TYPE jemalloc_bin_total counter\n"
"jemalloc_bin_total{bin=\"small\"} " << ji.nr_bin_small << "\n"
"jemalloc_bin_total{bin=\"thread_cache\"} " << ji.nr_bin_tc << "\n"
"jemalloc_bin_total{bin=\"large\"} " << ji.nr_bin_large << "\n"
"#TYPE jemalloc_allocated counter\n"
"jemalloc_allocated{bucket=\"small\"} " << ji.small.allocated << "\n"
"jemalloc_allocated{bucket=\"large\"} " << ji.large.allocated << "\n"
"#TYPE jemalloc_nmalloc_total counter\n"
"jemalloc_nmalloc_total{bucket=\"small\"} " << ji.small.nmalloc_total << "\n"
"jemalloc_nmalloc_total{bucket=\"large\"} " << ji.large.nmalloc_total << "\n"
"#TYPE jemalloc_ndalloc_total counter\n"
"jemalloc_ndalloc_total{bucket=\"small\"} " << ji.small.ndalloc_total << "\n"
"jemalloc_ndalloc_total{bucket=\"large\"} " << ji.large.ndalloc_total << "\n"
"#TYPE jemalloc_nrequests_total counter\n"
"jemalloc_nrequests_total{bucket=\"small\"} " << ji.small.nrequests_total << "\n"
"jemalloc_nrequests_total{bucket=\"large\"} " << ji.large.nrequests_total << "\n"
"#TYPE jemalloc_nfill_total counter\n"
"jemalloc_nfill_total{bucket=\"small\"} " << ji.small.nfill_total << "\n"
"jemalloc_nfill_total{bucket=\"large\"} " << ji.large.nfill_total << "\n"
"#TYPE jemalloc_nflush_total counter\n"
"jemalloc_nflush_total{bucket=\"small\"} " << ji.small.nflush_total << "\n"
"jemalloc_nflush_total{bucket=\"large\"} " << ji.large.nflush_total << "\n"
"#TYPE jemalloc_nflush_total gauge\n"
"jemalloc_memory{global=\"allocated\"} " << ji.mem_allocated << "\n"
"jemalloc_memory{global=\"active\"} " << ji.mem_active << "\n"
"jemalloc_memory{global=\"metadata\"} " << ji.mem_metadata << "\n"
"jemalloc_memory{global=\"resident\"} " << ji.mem_resident << "\n"
"jemalloc_memory{global=\"mapped\"} " << ji.mem_mapped << "\n"
"jemalloc_memory{global=\"retained\"} " << ji.mem_retained << "\n"
"#TYPE jemalloc_nflush_rate gauge\n"
"jemalloc_allocated{bucket=\"small\"} " << ji.small.allocated << "\n"
"jemalloc_allocated{bucket=\"large\"} " << ji.large.allocated << "\n"
"#TYPE jemalloc_nflush_rate gauge\n"
"jemalloc_nmalloc_rate{bucket=\"small\"} " << ji.small.nmalloc_rate << "\n"
"jemalloc_nmalloc_rate{bucket=\"large\"} " << ji.large.nmalloc_rate << "\n"
"#TYPE jemalloc_nflush_total gauge\n"
"jemalloc_ndalloc_rate{bucket=\"small\"} " << ji.small.ndalloc_rate << "\n"
"jemalloc_ndalloc_rate{bucket=\"large\"} " << ji.large.ndalloc_rate << "\n"
"#TYPE jemalloc_nflush_rate gauge\n"
"jemalloc_nrequests_rate{bucket=\"small\"} " << ji.small.nrequests_rate << "\n"
"jemalloc_nrequests_rate{bucket=\"large\"} " << ji.large.nrequests_rate << "\n"
"#TYPE jemalloc_nflush_rate gauge\n"
"jemalloc_fill_rate{bucket=\"small\"} " << ji.small.nfill_rate << "\n"
"jemalloc_fill_rate{bucket=\"large\"} " << ji.large.nfill_rate << "\n"
"#TYPE jemalloc_nflush_rate gauge\n"
"jemalloc_nflush_rate{bucket=\"small\"} " << ji.small.nflush_rate << "\n"
"jemalloc_nflush_rate{bucket=\"large\"} " << ji.large.nflush_rate << "\n"
;
struct rusage r;
if (getrusage(RUSAGE_SELF, &r) == 0) {
output << "ocelot_time_user " << r.ru_utime.tv_sec << '.' << std::setfill('0') << std::setw(6) << r.ru_utime.tv_usec << "\n"
"ocelot_time_system " << r.ru_stime.tv_sec << '.' << std::setfill('0') << std::setw(6) << r.ru_stime.tv_usec << "\n"
"ocelot_max_rss " << r.ru_maxrss << "\n"
"ocelot_minor_fault " << r.ru_minflt << "\n"
"ocelot_major_fault " << r.ru_majflt << "\n"
"#TYPE ocelot_block counter\n"
"ocelot_block{op=\"in\"} " << r.ru_inblock << "\n"
"ocelot_block{op=\"out\"} " << r.ru_oublock << "\n"
"ocelot_nvcsw " << r.ru_nvcsw << "\n"
"ocelot_nivcsw " << r.ru_nivcsw << "\n"
;
}
output << '#';
return output.str();
}
std::string report_user(const user_ptr u) {
std::ostringstream output;
output << "{\"id\":" << u->get_id()
<< ",\"leeching\":" << u->get_leeching()
<< ",\"seeding\":" << u->get_seeding()
<< ",\"deleted\":" << u->is_deleted()
<< ",\"protected\":" << u->is_protected()
<< ",\"can_leech\":" << u->can_leech()
<< "}\n";
return output.str();
}
+15 -8
View File
@@ -1,16 +1,23 @@
#ifndef SRC_REPORT_H_
#define SRC_REPORT_H_
// Copyright [2017-2024] Orpheus
// Copyright [2017-2026] Orpheus
#include <string>
std::string report(
params_type &params,
user_list &users_list,
client_opts_t &client_opts,
unsigned int announce_interval,
unsigned int announce_jitter
);
// take a snapshot of the current stats
void copy_stats(const struct stats_t *in, struct stats_t *out);
// status report
std::string report(const uint32_t announce_interval, const uint32_t announce_jitter);
// a snapshot of jemalloc statistics
std::string report_jemalloc_plain(const char *opts, const std::string path);
// a prometheus scrape
std::string report_prom_stats(const char *jemalloc_stats);
// user report
std::string report_user(const user_ptr u);
#endif // SRC_REPORT_H_
+25 -23
View File
@@ -10,7 +10,23 @@
#include "misc_functions.h"
#include "response.h"
const std::string response(const std::string &body, client_opts_t &client_opts) {
const std::string http_head(size_t content_length, client_opts_t &client_opts) {
const std::string content_type = client_opts.html ? "text/html" : "text/plain";
// rely on preprocessor string concatenation
std::string head = "HTTP/1.1 200 OK\r\nServer: Ocelot " OCELOT_VERSION "\r\n"
"Content-Type: " + content_type + "\r\n"
"Content-Length: " + inttostr(content_length) + "\r\n";
if (client_opts.gzip) {
head += "Content-Encoding: gzip\r\n";
}
if (client_opts.http_close) {
head += "Connection: Close\r\n";
}
return head + "\r\n";
}
const std::string http_response(const std::string &body, client_opts_t &client_opts) {
std::string out;
bool processed = false;
if (client_opts.html) {
@@ -28,29 +44,15 @@ const std::string response(const std::string &body, client_opts_t &client_opts)
processed = true;
}
if (processed) {
return response_head(out.length(), client_opts) + out;
return http_head(out.length(), client_opts) + out;
}
return response_head(body.length(), client_opts) + body;
return http_head(body.length(), client_opts) + body;
}
const std::string response_head(size_t content_length, client_opts_t &client_opts) {
const std::string content_type = client_opts.html ? "text/html" : "text/plain";
std::string head = "HTTP/1.1 200 OK\r\nServer: Ocelot 1.0";
head += "\r\nContent-Type: " + content_type;
if (client_opts.gzip) {
head += "\r\nContent-Encoding: gzip";
}
if (client_opts.http_close) {
head += "\r\nConnection: Close";
}
head += "\r\nContent-Length: " + inttostr(content_length) + "\r\n\r\n";
return head;
}
const std::string error(const std::string &err, client_opts_t &client_opts) {
return response("d14:failure reason" + inttostr(err.length()) + ':' + err + "12:min intervali5400e8:intervali5400ee", client_opts);
}
const std::string warning(const std::string &msg) {
return "15:warning message" + inttostr(msg.length()) + ':' + msg;
const std::string error(const std::string &message, client_opts_t &client_opts) {
return http_response(
"d14:failure reason" + inttostr(message.length()) + ':' + message
+ "12:min intervali5400e8:intervali5400ee",
client_opts
);
}
+1 -3
View File
@@ -7,9 +7,7 @@
#include "ocelot.h"
const std::string response(const std::string &body, client_opts_t &client_opts);
const std::string response_head(size_t content_length, client_opts_t &client_opts);
const std::string http_response(const std::string &body, client_opts_t &client_opts);
const std::string error(const std::string &err, client_opts_t &client_opts);
const std::string warning(const std::string &msg);
#endif // SRC_RESPONSE_H_
+376 -203
View File
@@ -1,11 +1,13 @@
// Copyright [2017-2024] Orpheus
// Copyright [2017-2026] Orpheus
#include <spdlog/spdlog.h>
#include <chrono>
#include <iostream>
#include <string>
#include <map>
#include <sstream>
#include <limits>
#include <list>
#include <vector>
#include <set>
@@ -24,6 +26,8 @@
#include "report.h"
#include "user.h"
std::mutex worker::client_len_mutex;
//---------- Worker - does stuff with input
worker::worker(config * conf_obj, torrent_list &torrents, user_list &users, std::vector<std::string> &_whitelist, mysql * db_obj, site_comm * sc) :
conf(conf_obj), db(db_obj), s_comm(sc), torrents_list(torrents), users_list(users), whitelist(_whitelist), status(OPEN), reaper_active(false), randgen((std::random_device())()) {
@@ -72,23 +76,17 @@ std::string worker::work(const std::string &input, std::string &ip, client_opts_
//---------- Parse request - ugly but fast. Using substr exploded.
if (input_length < 60) { // Way too short to be anything useful
stats.http_error++;
return error("GET string too short", client_opts);
}
size_t pos = 5; // skip 'GET /'
// Get the passkey
std::string passkey;
passkey.reserve(32);
if (input[37] != '/') {
stats.http_error++;
return error("Malformed announce", client_opts);
}
for (; pos < 37; pos++) {
passkey.push_back(input[pos]);
}
pos = 38;
std::string passkey(input.substr(5, 32));
size_t pos = 38;
// Get the action
enum action_t {
@@ -117,10 +115,26 @@ std::string worker::work(const std::string &input, std::string &ip, client_opts_
break;
}
unsigned int max_len_increased = false;
{
const std::lock_guard<std::mutex> lock(worker::client_len_mutex);
unsigned int max_len = stats.max_client_request_len;
if (max_len < input_length) {
stats.max_client_request_len = input_length;
max_len_increased = true;
}
}
if (max_len_increased) {
logger->info(
"max client request length raised to {} on action {}",
input_length, action
);
}
if (input[pos] != '?') {
// No parameters given. Probably means we're not talking to a torrent client
client_opts.html = true;
return response("Nothing to see here", client_opts);
return http_response("Nothing to see here", client_opts);
}
// Parse URL params
@@ -158,6 +172,7 @@ std::string worker::work(const std::string &input, std::string &ip, client_opts_
++pos;
if (input.compare(pos, 5, "HTTP/") != 0) {
stats.http_error++;
return error("Malformed HTTP request", client_opts);
}
@@ -210,72 +225,156 @@ std::string worker::work(const std::string &input, std::string &ip, client_opts_
}
if (status != OPEN) {
return error("The tracker is temporarily unavailable.", client_opts);
}
if (action == INVALID) {
return error("Invalid action", client_opts);
}
if (action == UPDATE) {
if (passkey == site_password) {
return update(params, client_opts);
} else {
return error("Authentication failure", client_opts);
return error("the tracker is not accepting connections", client_opts);
} else if (action == INVALID) {
stats.http_error++;
return error("invalid action", client_opts);
} else if (action == UPDATE) {
if (passkey != site_password) {
stats.auth_error_secret++;
logger->error("incorrect TRACKER_SECRET received");
return error("authentication failure", client_opts);
}
}
if (action == REPORT) {
if (passkey == report_password) {
std::lock_guard<std::mutex> ul_lock(db->user_list_mutex);
return report(params, users_list, client_opts, announce_interval, conf->get_uint("announce_jitter"));
} else {
return error("Authentication failure", client_opts);
return update(params, client_opts);
} else if (action == REPORT) {
if (passkey != report_password) {
stats.auth_error_report++;
logger->error("incorrect TRACKER_REPORT received");
return error("authentication failure", client_opts);
}
}
// Either a scrape or an announce
std::unique_lock<std::mutex> ul_lock(db->user_list_mutex);
auto user_it = users_list.find(passkey);
if (user_it == users_list.end()) {
return error("Passkey not found", client_opts);
}
user_ptr u = user_it->second;
ul_lock.unlock();
if (action == ANNOUNCE) {
// Let's translate the infohash into something nice
// info_hash is a url encoded (hex) base 20 number
std::string info_hash_decoded = hex_decode(params["info_hash"]);
std::lock_guard<std::mutex> tl_lock(db->torrent_list_mutex);
auto tor = torrents_list.find(info_hash_decoded);
if (tor == torrents_list.end()) {
std::lock_guard<std::mutex> dr_lock(del_reasons_lock);
auto msg = del_reasons.find(info_hash_decoded);
if (msg != del_reasons.end()) {
if (msg->second.reason != -1) {
return error("Unregistered torrent: " + get_del_reason(msg->second.reason), client_opts);
} else {
return error("Unregistered torrent", client_opts);
}
} else {
return error("Unregistered torrent", client_opts);
std::string report_action(params["get"]);
if (report_action == "prom_stats") {
// exclude per-arena (a), destroyed merged (d), mutex (m) and extents (e) statistics
std::string jemalloc_stats(report_jemalloc_plain("adex", conf->get_str("report_path")));
return http_response(
report_prom_stats(jemalloc_stats.c_str()),
client_opts
);
} else if (report_action == "stats") {
return http_response(
report(announce_interval, conf->get_uint("announce_jitter")),
client_opts
);
} else if (report_action == "jemalloc") {
return http_response(
report_jemalloc_plain("adex", conf->get_str("report_path")),
client_opts
);
} else if (report_action == "user") {
std::string announce_key = params["key"];
if (announce_key.empty()) {
stats.auth_error_announce_key++;
logger->error("user report with no announce key");
return error("Announce key missing", client_opts);
}
user_ptr u;
{
// lock scope
std::lock_guard<std::mutex> ul_lock(db->user_list_mutex);
auto user_it = users_list.find(announce_key);
if (user_it == users_list.end()) {
stats.auth_error_announce_key++;
logger->error("user report announce key not found");
return error("Announce key not found", client_opts);
}
u = user_it->second;
}
return http_response(
report_user(u),
client_opts
);
}
return announce(input, tor->second, u, params, headers, ip, client_opts);
} else {
stats.http_error++;
logger->error("unrecognized report query");
return error("unrecognized report query", client_opts);
}
// Either a scrape or an announce, find the user
user_ptr u;
{
// lock scope
std::lock_guard<std::mutex> ul_lock(db->user_list_mutex);
auto user_it = users_list.find(passkey);
if (user_it == users_list.end()) {
stats.auth_error_announce_key++;
return error("Passkey not found", client_opts);
}
u = user_it->second;
}
if (action == SCRAPE) {
return scrape(infohashes, headers, client_opts);
}
}
std::string worker::announce(const std::string &input, torrent &tor, user_ptr &u, params_type &params, params_type &headers, std::string &ip, client_opts_t &client_opts) {
cur_time = time(NULL);
if (params["compact"] != "1") {
stats.client_error++;
return error("Your client does not support compact announces", client_opts);
}
params_type::const_iterator peer_id_iterator = params.find("peer_id");
if (peer_id_iterator == params.end()) {
stats.client_error++;
return error("No peer ID", client_opts);
}
const std::string peer_id = hex_decode(peer_id_iterator->second);
if (peer_id.length() != 20) {
stats.client_error++;
return error("Invalid peer ID ", client_opts);
}
// Let's translate the infohash into something nice
// info_hash is a url encoded (hex) base 20 number
std::string info_hash_decoded = hex_decode(params["info_hash"]);
std::chrono::steady_clock::time_point lock_begin = std::chrono::steady_clock::now();
std::lock_guard<std::mutex> tl_lock(db->torrent_list_mutex);
stats.torrent_lock_duration.fetch_add(
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - lock_begin
).count()
);
stats.torrent_lock_total++;
auto tor = torrents_list.find(info_hash_decoded);
if (tor == torrents_list.end()) {
std::lock_guard<std::mutex> dr_lock(del_reasons_lock);
auto msg = del_reasons.find(info_hash_decoded);
if (msg != del_reasons.end() && msg->second.reason != -1) {
return error("Unregistered torrent: " + get_del_reason(msg->second.reason), client_opts);
} else {
return error("Unregistered torrent", client_opts);
}
}
std::unique_lock<std::mutex> wl_lock(db->whitelist_mutex);
if (whitelist.size() > 0) {
bool found = false; // Found client in whitelist?
for (const std::string& prefix : whitelist) {
if (peer_id.compare(0, prefix.length(), prefix) == 0) {
found = true;
break;
}
}
if (!found) {
stats.client_error++;
return error("Your client is not on the whitelist", client_opts);
}
}
wl_lock.unlock();
return announce(input, peer_id, tor->second, u, params, headers, ip, client_opts);
}
const std::string bencode_warning(const std::string &message) {
return "15:warning message" + inttostr(message.length()) + ':' + message;
}
std::string worker::announce(const std::string &input, const std::string &peer_id, torrent &tor, user_ptr &u, params_type &params, params_type &headers, std::string &ip, client_opts_t &client_opts) {
std::chrono::steady_clock::time_point announce_begin = std::chrono::steady_clock::now();
cur_time = time(NULL);
int64_t left = std::max((int64_t)0, strtoint64(params["left"]));
int64_t uploaded = std::max((int64_t)0, strtoint64(params["uploaded"]));
int64_t downloaded = std::max((int64_t)0, strtoint64(params["downloaded"]));
@@ -293,32 +392,8 @@ std::string worker::announce(const std::string &input, torrent &tor, user_ptr &u
bool inc_l = false, inc_s = false, dec_l = false, dec_s = false;
userid_t userid = u->get_id();
params_type::const_iterator peer_id_iterator = params.find("peer_id");
if (peer_id_iterator == params.end()) {
return error("No peer ID", client_opts);
}
const std::string peer_id = hex_decode(peer_id_iterator->second);
if (peer_id.length() != 20) {
return error("Invalid peer ID", client_opts);
}
std::unique_lock<std::mutex> wl_lock(db->whitelist_mutex);
if (whitelist.size() > 0) {
bool found = false; // Found client in whitelist?
for (unsigned int i = 0; i < whitelist.size(); i++) {
if (peer_id.compare(0, whitelist[i].length(), whitelist[i]) == 0) {
found = true;
break;
}
}
if (!found) {
return error("Your client is not on the whitelist", client_opts);
}
}
wl_lock.unlock();
std::stringstream peer_key_stream;
// "Randomize" the element order in the peer map by prefixing with a peer id byte
std::stringstream peer_key_stream;
peer_key_stream << peer_id[12 + (tor.id & 7)]
<< userid // Include user id in the key to lower chance of peer id collisions
<< peer_id;
@@ -339,10 +414,23 @@ std::string worker::announce(const std::string &input, torrent &tor, user_ptr &u
if (left > 0) {
peer_it = tor.leechers.find(peer_key);
if (peer_it == tor.leechers.end()) {
// We could search the seed list as well, but the peer reaper will sort things out eventually
peer_it = add_peer(tor.leechers, peer_key);
inserted = true;
inc_l = true;
// Check if peer is in seeders (e.g., seeder re-announcing with left > 0)
peer_it = tor.seeders.find(peer_key);
if (peer_it != tor.seeders.end()) {
// Move from seeders to leechers
p = &peer_it->second;
auto insert = tor.leechers.insert(std::pair<std::string, peer>(peer_key, *p));
tor.seeders.erase(peer_it);
peer_it = insert.first;
peer_changed = true;
dec_s = true;
inc_l = true;
} else {
// New peer
peer_it = add_peer(tor.leechers, peer_key);
inserted = true;
inc_l = true;
}
}
} else if (completed_torrent) {
peer_it = tor.leechers.find(peer_key);
@@ -353,12 +441,19 @@ std::string worker::announce(const std::string &input, torrent &tor, user_ptr &u
inserted = true;
inc_s = true;
} else {
// Already a seeder, not a real completion
completed_torrent = false;
}
} else if (tor.seeders.find(peer_key) != tor.seeders.end()) {
// If the peer exists in both peer lists, just decrement the seed count.
// Should be cheaper than searching the seed list in the left > 0 case
dec_s = true;
} else {
// Peer is in leechers - check if also incorrectly in seeders
auto seeder_it = tor.seeders.find(peer_key);
if (seeder_it != tor.seeders.end()) {
// Peer is in both lists (invalid state) - remove from seeders
// since they're completing as a leecher, they should only be in leechers
// until we move them to seeders below
tor.seeders.erase(seeder_it);
dec_s = true;
}
}
} else {
peer_it = tor.seeders.find(peer_key);
@@ -368,9 +463,9 @@ std::string worker::announce(const std::string &input, torrent &tor, user_ptr &u
peer_it = add_peer(tor.seeders, peer_key);
inserted = true;
} else {
// Move peer from leechers to seeders
p = &peer_it->second;
std::pair<peer_list::iterator, bool> insert
= tor.seeders.insert(std::pair<std::string, peer>(peer_key, *p));
auto insert = tor.seeders.insert(std::pair<std::string, peer>(peer_key, *p));
tor.leechers.erase(peer_it);
peer_it = insert.first;
peer_changed = true;
@@ -430,8 +525,14 @@ std::string worker::announce(const std::string &input, torrent &tor, user_ptr &u
update_torrent = true;
if (cur_time > p->last_announced) {
upspeed = uploaded_change / (cur_time - p->last_announced);
downspeed = downloaded_change / (cur_time - p->last_announced);
// Note: the db stores speed values as 32 bits (which saves a lot of space).
// Therefore, clamp speeds to 2**31 - 1. The only time this ever matters is
// when there is a large change between two announces in very quick succession,
// which is probably bogus in any event.
auto delta = cur_time - p->last_announced;
auto max = std::numeric_limits<int32_t>::max();
upspeed = (uploaded_change / delta > max) ? max : uploaded_change / delta;
downspeed = (downloaded_change / delta > max) ? max : downloaded_change / delta;
}
auto sit = tor.tokened_users.find(userid);
if (tor.free_torrent == NEUTRAL) {
@@ -563,12 +664,15 @@ std::string worker::announce(const std::string &input, torrent &tor, user_ptr &u
// User is a seeder now!
if (!inserted) {
std::pair<peer_list::iterator, bool> insert
= tor.seeders.insert(std::pair<std::string, peer>(peer_key, *p));
auto insert = tor.seeders.insert(std::pair<std::string, peer>(peer_key, *p));
tor.leechers.erase(peer_it);
peer_it = insert.first;
p = &peer_it->second;
dec_l = inc_s = true;
dec_l = true;
// Only increment seeders if we actually inserted (not if key already existed)
if (insert.second) {
inc_s = true;
}
}
if (expire_token) {
s_comm->expire_token(tor.id, userid);
@@ -706,6 +810,11 @@ std::string worker::announce(const std::string &input, torrent &tor, user_ptr &u
}
if (!u->can_leech() && left > 0) {
stats.announce_duration.fetch_add(
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - announce_begin
).count()
);
return error("Access denied, leeching forbidden", client_opts);
}
@@ -729,7 +838,7 @@ std::string worker::announce(const std::string &input, torrent &tor, user_ptr &u
output += peers;
}
if (invalid_ip) {
output += warning("Illegal character found in IP address. IPv6 is not supported");
output += bencode_warning("Illegal character found in IP address. IPv6 is not supported");
}
output += 'e';
@@ -740,7 +849,13 @@ std::string worker::announce(const std::string &input, torrent &tor, user_ptr &u
/*if (headers["accept-encoding"].find("gzip") != std::string::npos) {
client_opts.gzip = true;
}*/
return response(output, client_opts);
stats.announce_duration.fetch_add(
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - announce_begin
).count()
);
return http_response(output, client_opts);
}
std::string worker::scrape(const std::list<std::string> &infohashes, params_type &headers, client_opts_t &client_opts) {
@@ -770,24 +885,30 @@ std::string worker::scrape(const std::list<std::string> &infohashes, params_type
if (headers["accept-encoding"].find("gzip") != std::string::npos) {
client_opts.gzip = true;
}
return response(output, client_opts);
return http_response(output, client_opts);
}
// TODO: Restrict to local IPs
std::string worker::update(params_type &params, client_opts_t &client_opts) {
if (params["action"] == "change_passkey") {
std::string action(params["action"]);
if (action == "change_passkey") {
std::string oldpasskey = params["oldpasskey"];
std::string newpasskey = params["newpasskey"];
std::lock_guard<std::mutex> ul_lock(db->user_list_mutex);
auto u = users_list.find(oldpasskey);
if (u == users_list.end()) {
logger->warn("No user with passkey " + oldpasskey + " exists when attempting to change passkey to " + newpasskey);
logger->warn(
"no user with passkey {} exists when attempting to change passkey to {}",
oldpasskey, newpasskey
);
} else {
users_list[newpasskey] = u->second;
users_list.erase(oldpasskey);
logger->info("Changed passkey from " + oldpasskey + " to " + newpasskey + " for user " + std::to_string(u->second->get_id()));
logger->info("changed passkey from {} to {} for user {}",
oldpasskey, newpasskey, u->second->get_id()
);
}
} else if (params["action"] == "add_torrent") {
} else if (action == "add_torrent") {
torrent *t;
std::string info_hash = params["info_hash"];
info_hash = hex_decode(info_hash);
@@ -809,8 +930,8 @@ std::string worker::update(params_type &params, client_opts_t &client_opts) {
} else {
t->free_torrent = NEUTRAL;
}
logger->info("Added torrent " + std::to_string(t->id) + ". FL: " + std::to_string(t->free_torrent) + " " + params["freetorrent"]);
} else if (params["action"] == "update_torrent") {
logger->info("added torrent {}", t->id);
} else if (action == "update_torrent") {
std::string info_hash = params["info_hash"];
info_hash = hex_decode(info_hash);
freetype fl;
@@ -825,11 +946,11 @@ std::string worker::update(params_type &params, client_opts_t &client_opts) {
auto torrent_it = torrents_list.find(info_hash);
if (torrent_it != torrents_list.end()) {
torrent_it->second.free_torrent = fl;
logger->info("Updated torrent " + std::to_string(torrent_it->second.id) + " to FL " + std::to_string(fl));
logger->info("updated torrent {} to FL {}", torrent_it->second.id, fl);
} else {
logger->warn("Failed to find torrent " + std::string(info_hash) + " to FL " + std::to_string(fl));
logger->warn("failed to find torrent {} to FL {}", info_hash, fl);
}
} else if (params["action"] == "update_torrents") {
} else if (action == "update_torrents") {
// Each decoded infohash is exactly 20 characters long.
std::string info_hashes = params["info_hashes"];
info_hashes = hex_decode(info_hashes);
@@ -847,12 +968,12 @@ std::string worker::update(params_type &params, client_opts_t &client_opts) {
auto torrent_it = torrents_list.find(info_hash);
if (torrent_it != torrents_list.end()) {
torrent_it->second.free_torrent = fl;
logger->info("Updated torrent " + std::to_string(torrent_it->second.id) + " to FL " + std::to_string(fl));
logger->info("updated torrent {} to FL {}", torrent_it->second.id, fl);
} else {
logger->warn("Failed to find torrent " + info_hash + " to FL " + std::to_string(fl));
logger->warn("failed to find torrent {} to FL {}", info_hash, fl);
}
}
} else if (params["action"] == "add_token") {
} else if (action == "add_token") {
std::string info_hash = hex_decode(params["info_hash"]);
int userid = atoi(params["userid"].c_str());
std::lock_guard<std::mutex> tl_lock(db->torrent_list_mutex);
@@ -860,9 +981,9 @@ std::string worker::update(params_type &params, client_opts_t &client_opts) {
if (torrent_it != torrents_list.end()) {
torrent_it->second.tokened_users.insert(userid);
} else {
logger->warn("Failed to find torrent to add a token for user " + std::to_string(userid));
logger->warn("failed to find torrent to add a token for user {}", userid);
}
} else if (params["action"] == "remove_token") {
} else if (action == "remove_token") {
std::string info_hash = hex_decode(params["info_hash"]);
int userid = atoi(params["userid"].c_str());
std::lock_guard<std::mutex> tl_lock(db->torrent_list_mutex);
@@ -870,9 +991,12 @@ std::string worker::update(params_type &params, client_opts_t &client_opts) {
if (torrent_it != torrents_list.end()) {
torrent_it->second.tokened_users.erase(userid);
} else {
logger->warn("Failed to find torrent " + info_hash + " to remove token for user " + std::to_string(userid));
logger->warn(
"failed to find torrent {} to remove token for user ",
info_hash, userid
);
}
} else if (params["action"] == "delete_torrent") {
} else if (action == "delete_torrent") {
std::string info_hash = params["info_hash"];
info_hash = hex_decode(info_hash);
int reason = -1;
@@ -883,7 +1007,10 @@ std::string worker::update(params_type &params, client_opts_t &client_opts) {
std::lock_guard<std::mutex> tl_lock(db->torrent_list_mutex);
auto torrent_it = torrents_list.find(info_hash);
if (torrent_it != torrents_list.end()) {
logger->info("Deleting torrent " + std::to_string(torrent_it->second.id) + " for the reason '" + get_del_reason(reason) + "'");
logger->info(
"deleted torrent {} for the reason '{}'",
torrent_it->second.id, get_del_reason(reason)
);
stats.leechers -= torrent_it->second.leechers.size();
stats.seeders -= torrent_it->second.seeders.size();
for (auto &p : torrent_it->second.leechers) {
@@ -899,9 +1026,9 @@ std::string worker::update(params_type &params, client_opts_t &client_opts) {
del_reasons[info_hash] = msg;
torrents_list.erase(torrent_it);
} else {
logger->warn("Failed to find torrent " + bintohex(info_hash) + " to delete ");
logger->warn("failed to find torrent {} to delete", bintohex(info_hash));
}
} else if (params["action"] == "add_user") {
} else if (action == "add_user") {
std::string passkey = params["passkey"];
userid_t userid = strtoint32(params["id"]);
std::lock_guard<std::mutex> ul_lock(db->user_list_mutex);
@@ -910,21 +1037,27 @@ std::string worker::update(params_type &params, client_opts_t &client_opts) {
bool protect_ip = params["visible"] == "0";
user_ptr tmp_user = std::make_shared<user>(userid, true, protect_ip);
users_list.insert(std::pair<std::string, user_ptr>(passkey, tmp_user));
logger->info("Added user " + passkey + " with id " + std::to_string(userid));
logger->info("added user {} with id {}", passkey, userid);
} else {
logger->warn("Tried to add already known user " + passkey + " with id " + std::to_string(userid));
logger->warn(
"tried to add already known user {} with id {}",
passkey, userid
);
u->second->set_deleted(false);
}
} else if (params["action"] == "remove_user") {
} else if (action == "remove_user") {
std::string passkey = params["passkey"];
std::lock_guard<std::mutex> ul_lock(db->user_list_mutex);
auto u = users_list.find(passkey);
if (u != users_list.end()) {
logger->info("Removed user " + passkey + " with id " + std::to_string(u->second->get_id()));
logger->info("removed user {} with id {}",
passkey,
u->second->get_id()
);
u->second->set_deleted(true);
users_list.erase(u);
}
} else if (params["action"] == "remove_users") {
} else if (action == "remove_users") {
// Each passkey is exactly 32 characters long.
std::string passkeys = params["passkeys"];
std::lock_guard<std::mutex> ul_lock(db->user_list_mutex);
@@ -932,12 +1065,15 @@ std::string worker::update(params_type &params, client_opts_t &client_opts) {
std::string passkey = passkeys.substr(pos, 32);
auto u = users_list.find(passkey);
if (u != users_list.end()) {
logger->info("Removed user " + passkey);
logger->info(
"removed user {} with id {}",
passkey, u->second->get_id()
);
u->second->set_deleted(true);
users_list.erase(passkey);
}
}
} else if (params["action"] == "update_user") {
} else if (action == "update_user") {
std::string passkey = params["passkey"];
bool can_leech = true;
bool protect_ip = false;
@@ -950,28 +1086,34 @@ std::string worker::update(params_type &params, client_opts_t &client_opts) {
std::lock_guard<std::mutex> ul_lock(db->user_list_mutex);
user_list::iterator i = users_list.find(passkey);
if (i == users_list.end()) {
logger->warn("No user with passkey " + passkey + " found when attempting to change leeching status!");
logger->warn(
"no user with passkey {} found when attempting to change leeching status!",
passkey
);
} else {
i->second->set_protected(protect_ip);
i->second->set_leechstatus(can_leech);
logger->info("Updated user " + passkey);
logger->info(
"updated user {} protect({}) leech({})",
passkey, protect_ip, can_leech
);
}
} else if (params["action"] == "add_whitelist") {
} else if (action == "add_whitelist") {
std::string peer_id = params["peer_id"];
std::lock_guard<std::mutex> wl_lock(db->whitelist_mutex);
whitelist.push_back(peer_id);
logger->info("Whitelisted " + peer_id);
} else if (params["action"] == "remove_whitelist") {
logger->info("whitelisted {}", peer_id);
} else if (action == "remove_whitelist") {
std::string peer_id = params["peer_id"];
std::lock_guard<std::mutex> wl_lock(db->whitelist_mutex);
for (unsigned int i = 0; i < whitelist.size(); i++) {
if (whitelist[i].compare(peer_id) == 0) {
whitelist.erase(whitelist.begin() + i);
logger->info("dewhitelisted {}", peer_id);
break;
}
}
logger->info("De-whitelisted " + peer_id);
} else if (params["action"] == "edit_whitelist") {
} else if (action == "edit_whitelist") {
std::string new_peer_id = params["new_peer_id"];
std::string old_peer_id = params["old_peer_id"];
std::lock_guard<std::mutex> wl_lock(db->whitelist_mutex);
@@ -982,18 +1124,18 @@ std::string worker::update(params_type &params, client_opts_t &client_opts) {
}
}
whitelist.push_back(new_peer_id);
logger->info("Edited whitelist item from " + old_peer_id + " to " + new_peer_id);
} else if (params["action"] == "update_announce_interval") {
logger->info("whitelist edited from {} to {}", old_peer_id, new_peer_id);
} else if (action == "update_announce_interval") {
const std::string interval = params["new_announce_interval"];
conf->set("announce_interval", interval);
announce_interval = conf->get_uint("announce_interval");
logger->info("Edited announce interval to " + std::to_string(announce_interval));
} else if (params["action"] == "update_announce_jitter") {
logger->info("edited announce interval to {}", announce_interval);
} else if (action == "update_announce_jitter") {
const std::string new_jitter = params["new_announce_jitter"];
conf->set("announce_jitter", new_jitter);
jitter = std::uniform_int_distribution<int>(0, conf->get_uint("announce_jitter"));
logger->info("Edited announce jitter to " + std::to_string(conf->get_uint("announce_jitter")));
} else if (params["action"] == "info_torrent") {
logger->info("edited announce jitter to {}" , conf->get_uint("announce_jitter"));
} else if (action == "info_torrent") {
std::stringstream output;
std::string info_hash_hex = params["info_hash"];
std::string info_hash = hex_decode(info_hash_hex);
@@ -1007,9 +1149,9 @@ std::string worker::update(params_type &params, client_opts_t &client_opts) {
output << ",\"fail\":1";
}
output << "}\n";
return response(output.str(), client_opts);
return http_response(output.str(), client_opts);
}
return response("success", client_opts);
return http_response("success", client_opts);
}
peer_list::iterator worker::add_peer(peer_list &peer_list, const std::string &peer_key) {
@@ -1033,71 +1175,102 @@ void worker::do_start_reaper() {
}
void worker::reap_peers() {
logger->info("Starting peer reaper");
cur_time = time(NULL);
time_t max_time = time(NULL) - peers_timeout;
logger->info("starting peer reaper, max time {}, timeout {}", max_time, peers_timeout);
unsigned int reaped_l = 0, reaped_s = 0;
unsigned int cleared_torrents = 0;
for (auto t = torrents_list.begin(); t != torrents_list.end(); ++t) {
bool reaped_this = false; // True if at least one peer was deleted from the current torrent
auto p = t->second.leechers.begin();
peer_list::iterator del_p;
while (p != t->second.leechers.end()) {
if (p->second.last_announced + peers_timeout < cur_time) {
std::lock_guard<std::mutex> tl_lock(db->torrent_list_mutex);
del_p = p++;
del_p->second.user->decr_leeching();
t->second.leechers.erase(del_p);
reaped_this = true;
reaped_l++;
} else {
++p;
unsigned int total_torrent = 0;
unsigned int total_peer = 0;
unsigned int total_leecher = 0;
std::chrono::steady_clock::time_point lock_begin;
std::chrono::steady_clock::time_point lock_end;
std::chrono::steady_clock::time_point reap_end;
{
// Hold the lock for the entire reap operation to prevent iterator
// invalidation from concurrent announce() calls modifying the peer lists.
lock_begin = std::chrono::steady_clock::now();
std::lock_guard<std::mutex> tl_lock(db->torrent_list_mutex);
lock_end = std::chrono::steady_clock::now();
for (auto t = torrents_list.begin(); t != torrents_list.end(); ++t) {
total_torrent++;
bool reaped_this = false; // True if at least one peer was deleted from the current torrent
auto p = t->second.leechers.begin();
while (p != t->second.leechers.end()) {
total_leecher++;
if (p->second.last_announced > max_time) {
p++;
} else {
p->second.user->decr_leeching();
p = t->second.leechers.erase(p);
reaped_this = true;
reaped_l++;
}
}
p = t->second.seeders.begin();
while (p != t->second.seeders.end()) {
total_peer++;
if (p->second.last_announced > max_time) {
p++;
} else {
p->second.user->decr_seeding();
p = t->second.seeders.erase(p);
reaped_this = true;
reaped_s++;
}
}
if (reaped_this && t->second.seeders.empty() && t->second.leechers.empty()) {
std::stringstream record;
record << '(' << t->second.id << ",0,0,0," << t->second.balance << ')';
std::string record_str = record.str();
db->record_torrent(record_str);
cleared_torrents++;
}
}
p = t->second.seeders.begin();
while (p != t->second.seeders.end()) {
if (p->second.last_announced + peers_timeout < cur_time) {
std::lock_guard<std::mutex> tl_lock(db->torrent_list_mutex);
del_p = p++;
del_p->second.user->decr_seeding();
t->second.seeders.erase(del_p);
reaped_this = true;
reaped_s++;
} else {
++p;
}
}
if (reaped_this && t->second.seeders.empty() && t->second.leechers.empty()) {
std::stringstream record;
record << '(' << t->second.id << ",0,0,0," << t->second.balance << ')';
std::string record_str = record.str();
db->record_torrent(record_str);
cleared_torrents++;
}
reap_end = std::chrono::steady_clock::now();
}
if (reaped_l || reaped_s) {
stats.leechers -= reaped_l;
stats.seeders -= reaped_s;
}
logger->info("Reaped " + std::to_string(reaped_l) + " leechers and " + std::to_string(reaped_s)
+ " seeders. Reset " + std::to_string(cleared_torrents) + " torrents");
auto lock_duration = std::chrono::duration_cast<std::chrono::microseconds>(lock_end - lock_begin);
auto reap_duration = std::chrono::duration_cast<std::chrono::microseconds>(reap_end - lock_end);
stats.reap_total++;
stats.leechers = total_leecher;
stats.seeders = total_peer;
stats.reap_lock_duration.fetch_add(
lock_duration.count()
);
stats.reap_duration.fetch_add(
reap_duration.count()
);
logger->info(
"reaped {} leechers and {} seeders. Reset {} torrents."
" Lock acquired in {}us, scanned {} torrents ({} peers, {} leechers) in {}us.",
reaped_l, reaped_s, cleared_torrents,
lock_duration.count(), total_torrent, total_peer, total_leecher, reap_duration.count()
);
}
void worker::reap_del_reasons() {
logger->info("Starting del reason reaper");
time_t max_time = time(NULL) - del_reason_lifetime;
auto it = del_reasons.begin();
logger->info("starting deleted reason reaper, max time {}, timeout {}", max_time, del_reason_lifetime);
unsigned int reaped = 0;
for (; it != del_reasons.end(); ) {
if (it->second.time <= max_time) {
auto del_it = it++;
std::lock_guard<std::mutex> dr_lock(del_reasons_lock);
del_reasons.erase(del_it);
reaped++;
continue;
// Hold the lock for the entire operation to prevent iterator invalidation
{
std::lock_guard<std::mutex> dr_lock(del_reasons_lock);
for (auto it = del_reasons.begin(); it != del_reasons.end(); ) {
if (it->second.time > max_time) {
it++;
} else {
it = del_reasons.erase(it);
reaped++;
}
}
++it;
}
logger->info("Reaped " + std::to_string(reaped) + " del reasons");
logger->info("reaped {} torrent deleted reasons", reaped);
}
std::string worker::get_del_reason(int code) {
+3 -2
View File
@@ -1,7 +1,7 @@
#ifndef SRC_WORKER_H_
#define SRC_WORKER_H_
// Copyright [2017-2024] Orpheus
// Copyright [2017-2026] Orpheus
#include <spdlog/spdlog.h>
@@ -34,6 +34,7 @@ class worker {
std::shared_ptr<spdlog::logger> logger;
std::mt19937 randgen;
std::uniform_int_distribution<int> jitter;
static std::mutex client_len_mutex;
unsigned int announce_interval;
unsigned int del_reason_lifetime;
@@ -56,7 +57,7 @@ class worker {
worker(config * conf_obj, torrent_list &torrents, user_list &users, std::vector<std::string> &_whitelist, mysql * db_obj, site_comm * sc);
void reload_config(config * conf);
std::string work(const std::string &input, std::string &ip, client_opts_t &client_opts);
std::string announce(const std::string &input, torrent &tor, user_ptr &u, params_type &params, params_type &headers, std::string &ip, client_opts_t &client_opts);
std::string announce(const std::string &input, const std::string &peer_id, torrent &tor, user_ptr &u, params_type &params, params_type &headers, std::string &ip, client_opts_t &client_opts);
std::string scrape(const std::list<std::string> &infohashes, params_type &headers, client_opts_t &client_opts);
std::string update(params_type &params, client_opts_t &client_opts);