/* This software was written by Dirk Engling It is considered beerware. Prost. Skol. Cheers or whatever. Some of the stuff below is stolen from Fefes example libowfat httpd. $Id$ */ /* System */ #include #include #include #include #include #include #include #include #include #include #include #include #ifdef WANT_SYSLOGS #include #endif /* Opentracker */ #include "ot_accesslist.h" #include "ot_config.h" #include "ot_hex.h" #include "ot_http.h" #include "ot_ip.h" #include "ot_livesync.h" #include "ot_mutex.h" #include "ot_random.h" #include "ot_scan.h" #include "ot_event.h" #include "ot_fdstate.h" #include "ot_socket.h" #include "ot_stats.h" #include "ot_udp.h" #include "trackerlogic.h" /* Globals */ time_t g_now_seconds; uint32_t g_tracker_id; volatile int g_opentracker_running = 1; int g_self_pipe[2]; static void panic(const char *routine) __attribute__((noreturn)); static void panic(const char *routine) { fprintf(stderr, "%s: %s\n", routine, strerror(errno)); exit(111); } static void signal_handler(int s) { if (s == SIGINT) { /* Any new interrupt signal quits the application */ signal(SIGINT, SIG_DFL); /* Tell all other threads to not acquire any new lock on a bucket but cancel their operations and return */ g_opentracker_running = 0; trackerlogic_deinit(); #ifdef WANT_SYSLOGS closelog(); #endif exit(0); } } static void defaul_signal_handlers(void) { sigset_t signal_mask; sigemptyset(&signal_mask); sigaddset(&signal_mask, SIGPIPE); sigaddset(&signal_mask, SIGHUP); sigaddset(&signal_mask, SIGINT); sigaddset(&signal_mask, SIGALRM); pthread_sigmask(SIG_BLOCK, &signal_mask, NULL); } static void install_signal_handlers(void) { struct sigaction sa; sigset_t signal_mask; sigemptyset(&signal_mask); sa.sa_handler = signal_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if ((sigaction(SIGINT, &sa, NULL) == -1) || (sigaction(SIGALRM, &sa, NULL) == -1)) panic("install_signal_handlers"); sigaddset(&signal_mask, SIGINT); pthread_sigmask(SIG_UNBLOCK, &signal_mask, NULL); } static ssize_t header_complete(char *request, ssize_t byte_count) { ssize_t i = 0, state = 0; for (i = 1; i < byte_count; i += 2) if (request[i] <= 13) { i--; for (state = 0; i < byte_count; ++i) { char c = request[i]; if (c == '\r' || c == '\n') state = (state >> 2) | ((c << 6) & 0xc0); else break; if (state >= 0xa0 || state == 0x99) return i + 1; } } return 0; } static void handle_dead(const int64_t sock) { struct http_data *cookie = fdstate_cookie(sock); if (cookie) { if (cookie->batch) batch_reset(cookie->batch); free(cookie->batch); buf_reset(&cookie->request); if (cookie->flag & (STRUCT_HTTP_FLAG_WAITINGFORTASK | STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER)) mutex_workqueue_canceltask(sock); free(cookie); } /* The generation has to move before the kernel can reissue the number */ fdstate_close(sock); event_del((int)sock); close((int)sock); } static void handle_read(const int64_t sock, struct ot_workstruct *ws) { struct http_data *cookie = fdstate_cookie(sock); ssize_t byte_count = read((int)sock, ws->inbuf, G_INBUF_SIZE); if (byte_count == 0 || (byte_count == -1 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)) { handle_dead(sock); return; } if (byte_count == -1) return; ws->request = NULL; ws->request_size = 0; ws->header_size = 0; ws->keep_alive = 0; /* If we get the whole request in one packet, handle it without copying */ if (!cookie->request.data) { if ((ws->header_size = header_complete(ws->inbuf, byte_count))) { ws->request = ws->inbuf; ws->request_size = byte_count; http_handle_request(sock, ws); } else if (buf_cat(&cookie->request, ws->inbuf, (size_t)byte_count)) handle_dead(sock); return; } if (buf_cat(&cookie->request, ws->inbuf, (size_t)byte_count) || cookie->request.size > G_INBUF_SIZE) { http_issue_error(sock, ws, CODE_HTTPERROR_500); return; } while ((ws->header_size = header_complete(cookie->request.data, (ssize_t)cookie->request.size))) { ws->request = cookie->request.data; ws->request_size = (ssize_t)cookie->request.size; http_handle_request(sock, ws); #ifdef WANT_KEEPALIVE if (!ws->keep_alive) #endif return; } } static void handle_write(const int64_t sock) { struct http_data *cookie = fdstate_cookie(sock); int chunked = 0; int keep_alive = 0; if (cookie) { chunked = (cookie->flag & STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER) != 0; keep_alive = (cookie->flag & STRUCT_HTTP_FLAG_KEEPALIVE) != 0; if (cookie->batch) { if (batch_send(sock, cookie->batch) == BATCH_FAILED) { handle_dead(sock); return; } if (batch_pending(cookie->batch)) return; } } /* In a chunked transfer, wait for the next piece once everything queued has gone out */ if (chunked) http_interest_drop(sock, cookie, EVENT_OUT); else if (keep_alive) { /* The reply is out and the client asked to keep the connection, so stop waiting to write and go back to waiting for the next request */ http_interest_drop(sock, cookie, EVENT_OUT); http_interest_add(sock, cookie, EVENT_IN); fdstate_set_expiry(sock, g_now_seconds + OT_CLIENT_TIMEOUT); } else handle_dead(sock); } static void handle_accept(const int64_t serversocket) { struct http_data *cookie; int64_t sock; ot_ip6 ip; uint16_t port; while ((sock = socket_accept(serversocket, ip, &port)) != -1) { socket_set_blocking((int)sock, 0); if (!(cookie = (struct http_data *)malloc(sizeof(struct http_data)))) { close((int)sock); continue; } memset(cookie, 0, sizeof(struct http_data)); memcpy(cookie->ip, ip, sizeof(ot_ip6)); cookie->interest = EVENT_IN; if (event_add((int)sock, EVENT_IN, fdstate_open(sock, cookie, g_now_seconds + OT_CLIENT_TIMEOUT)) == -1) { fdstate_close(sock); free(cookie); close((int)sock); continue; } stats_issue_event(EVENT_ACCEPT, FLAG_TCP, (uintptr_t)ip); } } /* How many ready descriptors one wait hands back at a time */ #define OT_EVENT_BATCH 512 static void *server_mainloop(void *args) { struct ot_workstruct ws; time_t next_timeout_check = g_now_seconds + OT_CLIENT_TIMEOUT_CHECKINTERVAL; struct iovec *iovector; size_t iovec_entries; int is_partial; (void)args; /* Initialize our "thread local storage" */ ws.inbuf = malloc(G_INBUF_SIZE); ws.outbuf = malloc(G_OUTBUF_SIZE); #ifdef _DEBUG_HTTPERROR ws.debugbuf = malloc(G_DEBUGBUF_SIZE); #endif if (!ws.inbuf || !ws.outbuf) panic("Initializing worker failed"); random_bytes(ws.rand48_state, sizeof(ws.rand48_state)); for (;;) { ot_event ready[OT_EVENT_BATCH]; int64_t sock; int count, i; count = event_wait(ready, OT_EVENT_BATCH, OT_CLIENT_TIMEOUT_CHECKINTERVAL * 1000); for (i = 0; i < count; ++i) { const void *cookie; sock = ready[i].fd; /* A descriptor closed earlier in this same batch may already have been handed out again by accept, so an event is only ours if the generation it was registered under is still the one in place */ if (fdstate_generation((int)sock) != ready[i].generation) continue; cookie = fdstate_cookie((int)sock); if ((intptr_t)cookie == FLAG_TCP) handle_accept(sock); else if ((intptr_t)cookie == FLAG_UDP) handle_udp6(sock, &ws); else if ((intptr_t)cookie == FLAG_SELFPIPE) while (read((int)sock, ws.inbuf, G_INBUF_SIZE) > 0) ; else { if (ready[i].what & EVENT_IN) handle_read(sock, &ws); /* The read may have closed it, so look again before writing */ if ((ready[i].what & EVENT_OUT) && fdstate_generation((int)sock) == ready[i].generation) handle_write(sock); } } while ((sock = mutex_workqueue_popresult(&iovec_entries, &iovector, &is_partial)) != -1) http_sendiovecdata(sock, &ws, iovec_entries, iovector, is_partial); if (g_now_seconds > next_timeout_check) { int cursor = 0; while ((sock = fdstate_next_expired(g_now_seconds, &cursor)) != -1) handle_dead(sock); next_timeout_check = g_now_seconds + OT_CLIENT_TIMEOUT_CHECKINTERVAL; } livesync_ticker(); } return 0; } static void listen_bind_one(ot_listen *entry) { socket_sharing sharing = entry->shared ? SOCKET_SHARED : SOCKET_EXCLUSIVE; #ifdef _DEBUG { const char *protos[] = {"TCP", "UDP", "UDP mcast"}; char _debug[512]; int off = snprintf(_debug, sizeof(_debug), "Binding socket type %s to address [", protos[entry->proto]); off += ip_format(_debug + off, entry->ip); snprintf(_debug + off, sizeof(_debug) - off, "]:%d...", entry->port); fputs(_debug, stderr); } #endif #ifdef WANT_SYNC_LIVE if (entry->proto == FLAG_MCA) { livesync_bind_mcast(entry->ip, entry->port); #ifdef _DEBUG fputs(" success.\n", stderr); #endif return; } #endif if (entry->proto == FLAG_TCP) { if ((entry->sock = socket_tcp_listen(entry->ip, entry->port, sharing)) == -1) panic("socket_tcp_listen"); } else { if ((entry->sock = socket_udp_bind(entry->ip, entry->port, sharing)) == -1) panic("socket_udp_bind"); } if (event_add((int)entry->sock, EVENT_IN, fdstate_open(entry->sock, (void *)entry->proto, 0)) == -1) panic("event_add"); #ifdef _DEBUG fputs(" success.\n", stderr); #endif } /* Bound in the order the entries were given. Note: On macOS SO_REUSEPORT delivers everything to the socket that bound last. */ static void listen_bind_all(void) { size_t count, i; ot_listen *entries = config_listen_entries(&count); for (i = 0; i < count; ++i) listen_bind_one(entries + i); } static void listen_start_udp_workers(void) { size_t count, i; ot_listen *entries = config_listen_entries(&count); udp_init(); for (i = 0; i < count; ++i) { ot_listen *entry = entries + i; if (entry->proto != FLAG_UDP || entry->workers <= 0) continue; socket_set_blocking((int)entry->sock, 1); event_del((int)entry->sock); udp_workers_install(entry->sock, (unsigned int)entry->workers); } } void load_state(const char *const state_filename) { FILE *state_filehandle; char inbuf[512]; ot_hash infohash; uint64_t base, downcount; int consumed; state_filehandle = fopen(state_filename, "r"); if (state_filehandle == NULL) { fprintf(stderr, "Warning: Can't open config file: %s.", state_filename); return; } /* We do ignore anything that is not of the form "^[:xdigit:]:\d+:\d+" */ while (fgets(inbuf, sizeof(inbuf), state_filehandle)) { size_t i; if (!hex_to_hash(infohash, inbuf)) continue; i = 2 * sizeof(ot_hash); if (inbuf[i++] != ':' || !(consumed = scan_uint(inbuf + i, &base, UINT64_MAX))) continue; i += consumed; if (inbuf[i++] != ':' || !(consumed = scan_uint(inbuf + i, &downcount, UINT64_MAX))) continue; add_torrent_from_saved_state(infohash, base, downcount); } fclose(state_filehandle); } int drop_privileges(const char *const serveruser, const char *const serverdir) { struct passwd *pws = NULL; #ifdef _DEBUG if (!geteuid()) fprintf(stderr, "Dropping to user %s.\n", serveruser); if (serverdir) fprintf(stderr, "ch%s'ing to directory %s.\n", geteuid() ? "dir" : "root", serverdir); #endif /* Grab pws entry before chrooting */ pws = getpwnam(serveruser); endpwent(); if (geteuid() == 0) { /* Running as root: chroot and drop privileges */ if (serverdir && chroot(serverdir)) { fprintf(stderr, "Could not chroot to %s, because: %s\n", serverdir, strerror(errno)); return -1; } if (chdir("/")) panic("chdir() failed after chrooting: "); /* If we can't find server user, revert to nobody's default uid */ if (!pws) { fprintf(stderr, "Warning: Could not get password entry for %s. Reverting to uid -2.\n", serveruser); if (setegid((gid_t)-2) || setgid((gid_t)-2) || setuid((uid_t)-2) || seteuid((uid_t)-2)) panic("Could not set uid to value -2"); } else { if (setegid(pws->pw_gid) || setgid(pws->pw_gid) || setuid(pws->pw_uid) || seteuid(pws->pw_uid)) panic("Could not set uid to specified value"); } if (geteuid() == 0 || getegid() == 0) panic("Still running with root privileges?!"); } else { /* Normal user, just chdir() */ if (serverdir && chdir(serverdir)) { fprintf(stderr, "Could not chroot to %s, because: %s\n", serverdir, strerror(errno)); return -1; } } return 0; } /* Maintain our copy of the clock. time() on BSDs is very expensive. */ static void *time_caching_worker(void *args) { (void)args; while (1) { g_now_seconds = time(NULL); sleep(5); } return NULL; } int main(int argc, char **argv) { pthread_t thread_id; /* time cacher */ random_init(); if (fdstate_init() == -1) panic("fdstate_init"); if (event_init() == -1) panic("event_init"); config_parse(argc, argv); listen_bind_all(); defaul_signal_handlers(); #ifdef WANT_SYSLOGS openlog("opentracker", 0, LOG_USER); setlogmask(LOG_UPTO(LOG_INFO)); #endif if (drop_privileges(g_serveruser ? g_serveruser : "nobody", g_serverdir) == -1) panic("drop_privileges failed, exiting. Last error"); g_now_seconds = time(NULL); pthread_create(&thread_id, NULL, time_caching_worker, NULL); /* Create our self pipe which allows us to interrupt mainloops wait in case some data is available to send out */ if (pipe(g_self_pipe) == -1) panic("selfpipe failed: "); /* Both ends have to be non blocking: the loop drains the read end until it is empty, and a worker nudging a full pipe has already made its point */ socket_set_blocking(g_self_pipe[0], 0); socket_set_blocking(g_self_pipe[1], 0); if (event_add(g_self_pipe[0], EVENT_IN, fdstate_open(g_self_pipe[0], (void *)FLAG_SELFPIPE, 0)) == -1) panic("selfpipe event_add"); /* Init all sub systems. This call may fail with an exit() */ trackerlogic_init(); #ifdef _DEBUG_RANDOMTORRENTS fprintf(stderr, "DEBUG: Generating %d random peers on random torrents. This may take a while. (Setting RANDOMTORRENTS in trackerlogic.h)\n", RANDOMTORRENTS); trackerlogic_add_random_torrents(RANDOMTORRENTS); fprintf(stderr, "... done.\n"); #endif if (g_statefile) load_state(g_statefile); install_signal_handlers(); listen_start_udp_workers(); server_mainloop(0); event_deinit(); fdstate_deinit(); return 0; }