From 0c4a9915e11b41564b83efa334d1f942f114da15 Mon Sep 17 00:00:00 2001 From: Leprechaun Date: Mon, 9 Mar 2026 23:20:39 +0100 Subject: [PATCH 1/3] fix: replace VLA buffer with heap alloc, fix MSG_WAITALL recv loop --- fiche.c | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/fiche.c b/fiche.c index 99d140d..ad83014 100644 --- a/fiche.c +++ b/fiche.c @@ -560,11 +560,26 @@ 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); + // Create a buffer on the heap (avoids stack overflow with large buffer sizes) + uint8_t *buffer = calloc(c->settings->buffer_len, sizeof(uint8_t)); + if (!buffer) { + print_error("Couldn't allocate buffer!"); + print_separator(); + close(c->socket); + free(c); + pthread_exit(NULL); + return NULL; + } + + // Read in a loop: MSG_WAITALL + SO_RCVTIMEO is unreliable when the client + // sends fewer bytes than buffer_len and then closes the connection. + ssize_t r = 0, chunk; + while ((chunk = recv(c->socket, buffer + r, + c->settings->buffer_len - (size_t)r, 0)) > 0) { + r += chunk; + if ((size_t)r >= c->settings->buffer_len) break; + } - 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 +588,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 +630,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 +650,7 @@ static void *handle_connection(void *args) { close(c->socket); // Cleanup + free(buffer); free(c); pthread_exit(NULL); return NULL; @@ -647,6 +665,7 @@ static void *handle_connection(void *args) { close(c->socket); // Cleanup + free(buffer); free(c); free(slug); pthread_exit(NULL); @@ -665,7 +684,7 @@ static void *handle_connection(void *args) { 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,6 +695,7 @@ static void *handle_connection(void *args) { close(c->socket); // Perform cleanup of values used in this thread + free(buffer); free(slug); free(c); From d0187867185f6be1de8c358aa78205bbc66cc355 Mon Sep 17 00:00:00 2001 From: Leprechaun Date: Mon, 9 Mar 2026 23:42:35 +0100 Subject: [PATCH 2/3] fix: dynamic receive buffer and malloc_trim --- fiche.c | 69 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/fiche.c b/fiche.c index ad83014..5dd3a17 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,9 @@ Use netcat to push text - example: */ const char *Fiche_Symbols = "abcdefghijklmnopqrstuvwxyz0123456789"; +/* Initial chunk size for the dynamic receive buffer */ +#define FICHE_CHUNK (65536U) + /****************************************************************************** * Inner structs @@ -141,7 +145,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 @@ -560,8 +564,13 @@ static void *handle_connection(void *args) { print_status("Incoming connection from: %s (%s).", ip, hostname); } - // Create a buffer on the heap (avoids stack overflow with large buffer sizes) - uint8_t *buffer = calloc(c->settings->buffer_len, sizeof(uint8_t)); + // 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(); @@ -571,13 +580,38 @@ static void *handle_connection(void *args) { return NULL; } - // Read in a loop: MSG_WAITALL + SO_RCVTIMEO is unreliable when the client - // sends fewer bytes than buffer_len and then closes the connection. + // Read in a loop, growing the buffer as needed up to buffer_len. ssize_t r = 0, chunk; - while ((chunk = recv(c->socket, buffer + r, - c->settings->buffer_len - (size_t)r, 0)) > 0) { + 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; - if ((size_t)r >= c->settings->buffer_len) break; } if (r <= 0) { @@ -658,7 +692,7 @@ 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(); @@ -699,6 +733,12 @@ static void *handle_connection(void *args) { 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; @@ -762,7 +802,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 @@ -783,10 +823,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; @@ -796,4 +835,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 From ef2f4d89e73edd6748120e9ae48bc08293619406 Mon Sep 17 00:00:00 2001 From: Leprechaun Date: Tue, 10 Mar 2026 03:40:44 +0100 Subject: [PATCH 3/3] Self paste delete command (port 9998) --- fiche.c | 321 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- fiche.h | 5 +- main.c | 9 +- 3 files changed, 324 insertions(+), 11 deletions(-) diff --git a/fiche.c b/fiche.c index 5dd3a17..8394a74 100644 --- a/fiche.c +++ b/fiche.c @@ -58,6 +58,10 @@ 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 @@ -182,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 */ @@ -218,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 @@ -277,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); @@ -706,16 +752,59 @@ static void *handle_connection(void *args) { 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 %zd bytes, saved to: %s.", r, slug); @@ -745,6 +834,220 @@ static void *handle_connection(void *args) { } +/****************************************************************************** + * 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 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': {