diff --git a/fiche.c b/fiche.c index 99d140d..8394a74 100644 --- a/fiche.c +++ b/fiche.c @@ -31,6 +31,7 @@ Use netcat to push text - example: #include #include #include +#include #include #include @@ -54,6 +55,13 @@ Use netcat to push text - example: */ const char *Fiche_Symbols = "abcdefghijklmnopqrstuvwxyz0123456789"; +/* Initial chunk size for the dynamic receive buffer */ +#define FICHE_CHUNK (65536U) + +/* Delete token */ +#define TOKEN_LEN 32 +#define TOKEN_SYMBOLS "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + /****************************************************************************** * Inner structs @@ -141,7 +149,7 @@ static int create_directory(char *output_dir, char *slug); * @arg data Buffer with data received from the user * @arg path Path at which file containing data from the buffer will be created */ -static int save_to_file(const Fiche_Settings *s, uint8_t *data, char *slug); +static int save_to_file(const Fiche_Settings *s, uint8_t *data, size_t data_len, char *slug); // Logging-related @@ -178,6 +186,26 @@ static void log_entry(const Fiche_Settings *s, const char *ip, static void get_date(char *buf); +/** + * @brief Generates a random delete token + */ +static void generate_token(char *out); + +/** + * @brief Saves delete token to .token file in slug directory + */ +static int save_token(const Fiche_Settings *s, const char *token, const char *slug); + +/** + * @brief Handles a delete connection + */ +static void *handle_delete(void *args); + +/** + * @brief Runs the delete service on a separate thread + */ +static void *run_delete_server(void *arg); + /** * @brief Time seed */ @@ -214,7 +242,9 @@ void fiche_init(Fiche_Settings *settings) { // path to banlist NULL, // path to whitelist - NULL + NULL, + // delete port + 0 }; // Copy default settings to provided instance @@ -273,6 +303,26 @@ int fiche_run(Fiche_Settings settings) { return -1; } + // Start delete service on a separate thread if port is configured + if (settings.delete_port > 0) { + Fiche_Settings *sp = malloc(sizeof(*sp)); + if (!sp) { + print_error("Couldn't allocate delete service settings!"); + return -1; + } + *sp = settings; + pthread_t del_tid; + if (pthread_create(&del_tid, NULL, run_delete_server, sp) != 0) { + print_error("Couldn't start delete server!"); + free(sp); + } else { + pthread_detach(del_tid); + print_status("Delete service listening on: %s:%d.", + settings.listen_addr, settings.delete_port); + print_separator(); + } + } + // Main loop in this method start_server(&settings); @@ -560,11 +610,56 @@ static void *handle_connection(void *args) { print_status("Incoming connection from: %s (%s).", ip, hostname); } - // Create a buffer - uint8_t buffer[c->settings->buffer_len]; - memset(buffer, 0, c->settings->buffer_len); + // Allocate an initial receive buffer; grow dynamically up to buffer_len. + // This avoids reserving the full -B allocation (e.g. 10 MB) for every + // connection regardless of actual upload size. + size_t buf_size = (FICHE_CHUNK < c->settings->buffer_len) + ? FICHE_CHUNK : c->settings->buffer_len; + + uint8_t *buffer = malloc(buf_size); + if (!buffer) { + print_error("Couldn't allocate buffer!"); + print_separator(); + close(c->socket); + free(c); + pthread_exit(NULL); + return NULL; + } + + // Read in a loop, growing the buffer as needed up to buffer_len. + ssize_t r = 0, chunk; + while (1) { + // Buffer full: try to grow before the next read + if ((size_t)r == buf_size) { + size_t new_size = buf_size * 2; + if (new_size > c->settings->buffer_len) + new_size = c->settings->buffer_len; + + if (new_size == buf_size) { + // Hard limit reached — stop reading + print_error("Upload size limit reached (%u bytes).", + c->settings->buffer_len); + break; + } + + uint8_t *tmp = realloc(buffer, new_size); + if (!tmp) { + print_error("Couldn't grow receive buffer!"); + free(buffer); + close(c->socket); + free(c); + pthread_exit(NULL); + return NULL; + } + buffer = tmp; + buf_size = new_size; + } + + chunk = recv(c->socket, buffer + r, buf_size - (size_t)r, 0); + if (chunk <= 0) break; + r += chunk; + } - const int r = recv(c->socket, buffer, sizeof(buffer), MSG_WAITALL); if (r <= 0) { print_error("No data received from the client!"); print_separator(); @@ -573,10 +668,11 @@ static void *handle_connection(void *args) { close(c->socket); // Cleanup + free(buffer); free(c); pthread_exit(NULL); - return 0; + return NULL; } // - Check if request was performed with a known protocol @@ -614,9 +710,10 @@ static void *handle_connection(void *args) { print_separator(); // Cleanup - free(c); + free(buffer); free(slug); close(c->socket); + free(c); pthread_exit(NULL); return NULL; } @@ -633,6 +730,7 @@ static void *handle_connection(void *args) { close(c->socket); // Cleanup + free(buffer); free(c); pthread_exit(NULL); return NULL; @@ -640,32 +738,76 @@ static void *handle_connection(void *args) { // Save to file failed, we have to finish here - if ( save_to_file(c->settings, buffer, slug) != 0 ) { + if ( save_to_file(c->settings, buffer, (size_t)r, slug) != 0 ) { print_error("Couldn't save a file!"); print_separator(); close(c->socket); // Cleanup + free(buffer); free(c); free(slug); pthread_exit(NULL); return NULL; } + // Generate and save delete token (best-effort; non-fatal if it fails) + char token[TOKEN_LEN + 1]; + int has_token = 0; + if (c->settings->delete_port > 0) { + generate_token(token); + has_token = (save_token(c->settings, token, slug) == 0); + if (!has_token) { + print_error("Couldn't save delete token for: %s.", slug); + } + } + // Write a response to the user { - // Create an url (additional byte for slash and one for new line) - const size_t len = strlen(c->settings->domain) + strlen(slug) + 3; - - char url[len]; - snprintf(url, len, "%s%s%s%s", c->settings->domain, "/", slug, "\n"); - - // Send the response - write(c->socket, url, len); + const char *domain = c->settings->domain; + + if (has_token) { + // URL + delete instructions + // Format: + // https://domain.com/slug\n + // \n + // To delete this paste:\n + // echo -e "slug\\ntoken" | nc domain.com PORT\n + char host[256]; + // Extract bare hostname from domain (strip scheme if present) + const char *h = strstr(domain, "://"); + h = h ? h + 3 : domain; + snprintf(host, sizeof(host), "%s", h); + // Strip trailing slash if any + size_t hlen = strlen(host); + if (hlen > 0 && host[hlen - 1] == '/') host[hlen - 1] = '\0'; + + size_t len = strlen(domain) + strlen(slug) + + strlen(host) + strlen(slug) + strlen(token) + + 64; + char *resp = malloc(len); + if (resp) { + snprintf(resp, len, + "%s/%s\n" + "\n" + "To delete this paste:\n" + "echo -e \"%s\\\\n%s\" | nc %s %d\n", + domain, slug, + slug, token, host, c->settings->delete_port); + write(c->socket, resp, strlen(resp)); + free(resp); + } + } else { + // Plain URL only + const size_t len = strlen(domain) + strlen(slug) + 3; + char url[len]; + snprintf(url, len, "%s/%s\n", domain, slug); + write(c->socket, url, len); + } } - print_status("Received %d bytes, saved to: %s.", r, slug); + print_status("Received %zd bytes, saved to: %s.", r, slug); print_separator(); // Log connection @@ -676,15 +818,236 @@ static void *handle_connection(void *args) { close(c->socket); // Perform cleanup of values used in this thread + free(buffer); free(slug); free(c); + // Return freed memory to the OS immediately. + // By default glibc holds freed heap memory in its arena; for a server + // that may allocate large buffers per connection this prevents the + // process RSS from growing unboundedly. + malloc_trim(0); + pthread_exit(NULL); return NULL; } +/****************************************************************************** + * Token helpers + */ + +static void generate_token(char *out) { + static const char syms[] = TOKEN_SYMBOLS; + const size_t nsyms = sizeof(syms) - 1; + for (int i = 0; i < TOKEN_LEN; i++) { + out[i] = syms[rand_r(&seed) % nsyms]; + } + out[TOKEN_LEN] = '\0'; +} + +static int save_token(const Fiche_Settings *s, const char *token, const char *slug) { + // Path: //.token + size_t len = strlen(s->output_dir_path) + strlen(slug) + 9; // /.token + null + char *path = malloc(len); + if (!path) return -1; + snprintf(path, len, "%s/%s/.token", s->output_dir_path, slug); + + FILE *f = fopen(path, "w"); + free(path); + if (!f) return -1; + fprintf(f, "%s", token); + fclose(f); + return 0; +} + + +/****************************************************************************** + * Delete service + * Listens on delete_port. + * Client sends: \n\n + * Server verifies token, deletes paste directory if match. + */ + +static void *handle_delete(void *args) { + struct fiche_connection *c = (struct fiche_connection *)args; + + // Read slug\ntoken\n in one shot — client sends both lines at once + char buf[128]; + memset(buf, 0, sizeof(buf)); + ssize_t n = recv(c->socket, buf, sizeof(buf) - 1, 0); + if (n <= 0) { + close(c->socket); free(c); pthread_exit(NULL); return NULL; + } + + // Split on first \n to get slug and token + char slug[65]; + char token_in[TOKEN_LEN + 4]; + memset(slug, 0, sizeof(slug)); + memset(token_in, 0, sizeof(token_in)); + + char *nl = memchr(buf, '\n', (size_t)n); + if (!nl) { + write(c->socket, "invalid request\n", 16); + close(c->socket); free(c); pthread_exit(NULL); return NULL; + } + + size_t slug_len = (size_t)(nl - buf); + if (slug_len == 0 || slug_len >= sizeof(slug)) { + write(c->socket, "invalid slug\n", 13); + close(c->socket); free(c); pthread_exit(NULL); return NULL; + } + memcpy(slug, buf, slug_len); + // Strip \r if present + if (slug[slug_len - 1] == '\r') slug[slug_len - 1] = '\0'; + + char *token_start = nl + 1; + size_t token_raw_len = (size_t)(buf + n - token_start); + if (token_raw_len == 0 || token_raw_len >= sizeof(token_in)) { + write(c->socket, "invalid token\n", 14); + close(c->socket); free(c); pthread_exit(NULL); return NULL; + } + memcpy(token_in, token_start, token_raw_len); + // Strip trailing \n and \r + for (int i = 0; token_in[i]; i++) { + if (token_in[i] == '\n' || token_in[i] == '\r') { token_in[i] = '\0'; break; } + } + + // Validate slug: only alphanumeric, no path traversal + for (int i = 0; slug[i]; i++) { + char ch = slug[i]; + if (!((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9'))) { + write(c->socket, "invalid slug\n", 13); + close(c->socket); free(c); pthread_exit(NULL); return NULL; + } + } + + // Build path to .token file + size_t plen = strlen(c->settings->output_dir_path) + strlen(slug) + 9; + char *token_path = malloc(plen); + if (!token_path) { + close(c->socket); free(c); pthread_exit(NULL); return NULL; + } + snprintf(token_path, plen, "%s/%s/.token", c->settings->output_dir_path, slug); + + // Read stored token + FILE *f = fopen(token_path, "r"); + free(token_path); + if (!f) { + write(c->socket, "not found\n", 10); + close(c->socket); free(c); pthread_exit(NULL); return NULL; + } + char token_stored[TOKEN_LEN + 4]; + memset(token_stored, 0, sizeof(token_stored)); + fgets(token_stored, sizeof(token_stored), f); + fclose(f); + // Strip trailing \n and \r left by fgets + for (int i = 0; token_stored[i]; i++) { + if (token_stored[i] == '\n' || token_stored[i] == '\r') { + token_stored[i] = '\0'; break; + } + } + + // Constant-time comparison to avoid timing attacks + int match = 1; + if (strlen(token_in) != strlen(token_stored)) { + match = 0; + } else { + for (size_t i = 0; i < strlen(token_stored); i++) { + if (token_in[i] != token_stored[i]) match = 0; + } + } + + if (!match) { + write(c->socket, "unauthorized\n", 13); + close(c->socket); free(c); pthread_exit(NULL); return NULL; + } + + // Delete paste directory: remove index.txt, .token, then the directory + size_t dlen = strlen(c->settings->output_dir_path) + strlen(slug) + 2; + char *dir_path = malloc(dlen + 12); // extra for /index.txt + if (!dir_path) { + close(c->socket); free(c); pthread_exit(NULL); return NULL; + } + + snprintf(dir_path, dlen + 12, "%s/%s/index.txt", c->settings->output_dir_path, slug); + unlink(dir_path); + + snprintf(dir_path, dlen + 12, "%s/%s/.token", c->settings->output_dir_path, slug); + unlink(dir_path); + + snprintf(dir_path, dlen, "%s/%s", c->settings->output_dir_path, slug); + if (rmdir(dir_path) == 0) { + write(c->socket, "deleted\n", 8); + print_status("Paste deleted: %s.", slug); + } else { + write(c->socket, "error\n", 6); + } + free(dir_path); + + close(c->socket); + free(c); + pthread_exit(NULL); + return NULL; +} + + +static void *run_delete_server(void *arg) { + Fiche_Settings *settings = (Fiche_Settings *)arg; + + int s = socket(AF_INET, SOCK_STREAM, 0); + if (s < 0) { + print_error("Delete: couldn't create socket!"); + free(settings); pthread_exit(NULL); return NULL; + } + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)); + + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = inet_addr(settings->listen_addr); + addr.sin_port = htons(settings->delete_port); + + if (bind(s, (struct sockaddr *)&addr, sizeof(addr)) != 0) { + print_error("Delete: couldn't bind to port %d!", settings->delete_port); + close(s); free(settings); pthread_exit(NULL); return NULL; + } + if (listen(s, 128) != 0) { + print_error("Delete: couldn't listen!"); + close(s); free(settings); pthread_exit(NULL); return NULL; + } + + while (1) { + struct sockaddr_in caddr; + socklen_t clen = sizeof(caddr); + int cs = accept(s, (struct sockaddr *)&caddr, &clen); + if (cs < 0) continue; + + const struct timeval timeout = {5, 0}; + setsockopt(cs, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + setsockopt(cs, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); + + struct fiche_connection *conn = malloc(sizeof(*conn)); + if (!conn) { close(cs); continue; } + conn->socket = cs; + conn->address = caddr; + conn->settings = settings; + + pthread_t tid; + if (pthread_create(&tid, NULL, handle_delete, conn) != 0) { + close(cs); free(conn); + } else { + pthread_detach(tid); + } + } + + close(s); + free(settings); + pthread_exit(NULL); + return NULL; +} + + static void generate_slug(char **output, uint8_t length, uint8_t extra_length) { // Realloc buffer for slug when we want it to be bigger @@ -742,7 +1105,7 @@ static int create_directory(char *output_dir, char *slug) { } -static int save_to_file(const Fiche_Settings *s, uint8_t *data, char *slug) { +static int save_to_file(const Fiche_Settings *s, uint8_t *data, size_t data_len, char *slug) { char *file_name = "index.txt"; // Additional 2 bytes are for 2 slashes @@ -763,10 +1126,9 @@ static int save_to_file(const Fiche_Settings *s, uint8_t *data, char *slug) { return -1; } - // Null-terminate buffer if not null terminated already - data[s->buffer_len - 1] = 0; - - if ( fprintf(f, "%s", data) < 0 ) { + // Write exactly data_len bytes; fwrite handles binary data and null bytes + // correctly, unlike fprintf("%s") which would stop at the first null byte. + if ( fwrite(data, 1, data_len, f) != data_len ) { fclose(f); free(path); return -1; @@ -776,4 +1138,4 @@ static int save_to_file(const Fiche_Settings *s, uint8_t *data, char *slug) { free(path); return 0; -} +} \ No newline at end of file diff --git a/fiche.h b/fiche.h index b1c97a2..82ceeb1 100644 --- a/fiche.h +++ b/fiche.h @@ -90,7 +90,10 @@ typedef struct Fiche_Settings { */ char *whitelist_path; - + /** + * @brief Port for the delete service (0 = disabled) + */ + uint16_t delete_port; } Fiche_Settings; diff --git a/main.c b/main.c index da503c0..5a047c0 100644 --- a/main.c +++ b/main.c @@ -44,7 +44,7 @@ int main(int argc, char **argv) { // Parse input arguments int c; - while ((c = getopt(argc, argv, "D6eSL:p:b:s:d:o:l:B:u:w:")) != -1) { + while ((c = getopt(argc, argv, "D6eSL:p:b:s:d:o:l:B:u:w:P:")) != -1) { switch (c) { // domain @@ -82,6 +82,13 @@ int main(int argc, char **argv) { } break; + // delete service port + case 'P': + { + fs.delete_port = (uint16_t)atoi(optarg); + } + break; + // output directory path case 'o': {