/* 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 /* Libowfat */ #include "io.h" /* 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_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); io_close(sock); } static void handle_read(const int64_t sock, struct ot_workstruct *ws) { struct http_data *cookie = fdstate_cookie(sock); ssize_t byte_count = io_tryread(sock, ws->inbuf, G_INBUF_SIZE); if (byte_count == 0 || byte_count == -3) { 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; if (cookie) { if (cookie->flag & STRUCT_HTTP_FLAG_CHUNKED_IN_TRANSFER) chunked = 1; 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) io_dontwantwrite(sock); 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) { /* Put fd into a non-blocking mode */ io_nonblock(sock); if (!io_fd(sock) || !(cookie = (struct http_data *)malloc(sizeof(struct http_data)))) { io_close(sock); continue; } memset(cookie, 0, sizeof(struct http_data)); memcpy(cookie->ip, ip, sizeof(ot_ip6)); fdstate_open(sock, cookie, g_now_seconds + OT_CLIENT_TIMEOUT); io_wantread(sock); stats_issue_event(EVENT_ACCEPT, FLAG_TCP, (uintptr_t)ip); } io_eagain(serversocket); } 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 (;;) { int64_t sock; io_wait(); while ((sock = io_canread()) != -1) { const void *cookie = fdstate_cookie(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) io_tryread(sock, ws.inbuf, G_INBUF_SIZE); else handle_read(sock, &ws); } while ((sock = mutex_workqueue_popresult(&iovec_entries, &iovector, &is_partial)) != -1) http_sendiovecdata(sock, &ws, iovec_entries, iovector, is_partial); while ((sock = io_canwrite()) != -1) handle_write(sock); 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 (!io_fd(entry->sock)) panic("io_fd"); fdstate_open(entry->sock, (void *)entry->proto, 0); io_wantread(entry->sock); #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; io_block(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_tohash(infohash, inbuf)) continue; i = 2 * sizeof(ot_hash); if (inbuf[i++] != ':' || !(consumed = scan_udec(inbuf + i, &base, UINT64_MAX))) continue; i += consumed; if (inbuf[i++] != ':' || !(consumed = scan_udec(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"); 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 io_wait in case some data is available to send out */ if (pipe(g_self_pipe) == -1) panic("selfpipe failed: "); if (!io_fd(g_self_pipe[0])) panic("selfpipe io_fd failed: "); if (!io_fd(g_self_pipe[1])) panic("selfpipe io_fd failed: "); fdstate_open(g_self_pipe[0], (void *)FLAG_SELFPIPE, 0); io_wantread(g_self_pipe[0]); /* 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); fdstate_deinit(); return 0; }