From 8ff08a04093e51dfdbde0b6a4f8f3e58dcbe43be Mon Sep 17 00:00:00 2001 From: solusipse Date: Sat, 2 Sep 2017 17:51:43 +0200 Subject: [PATCH] Entirely rewritten fiche --- .gitignore | 6 + Makefile | 12 +- fiche.c | 1234 +++++++++++++++++++++++++++++----------------------- fiche.h | 159 ++++--- main.c | 133 ++++++ 5 files changed, 910 insertions(+), 634 deletions(-) create mode 100644 main.c diff --git a/.gitignore b/.gitignore index 116d45f..60faeb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,8 @@ # ignore binaries /fiche + +# ignore default outpit dir +code/ + +# ignore log files +*.log diff --git a/Makefile b/Makefile index b75fc63..092eebe 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,9 @@ -# ----------------------------------- -# Fiche MAKEFILE -# https://github.com/solusipse/fiche -# solusipse.net -# ----------------------------------- - -CFLAGS+=-pthread -O2 +# for debug add -g -O0 to line below +CFLAGS+=-pthread -O2 -Wall -Wextra -Wpedantic -Wstrict-overflow -fno-strict-aliasing -std=gnu11 -g -O0 prefix=/usr/local -all: fiche +all: + ${CC} main.c fiche.c $(CFLAGS) -o fiche install: fiche install -m 0755 fiche $(prefix)/bin diff --git a/fiche.c b/fiche.c index 9a60195..f5c62c7 100644 --- a/fiche.c +++ b/fiche.c @@ -5,7 +5,7 @@ Fiche - Command line pastebin for sharing terminal output. License: MIT (http://www.opensource.org/licenses/mit-license.php) Repository: https://github.com/solusipse/fiche/ -Live example: http://code.solusipse.net/ +Live example: http://termbin.com ------------------------------------------------------------------------------- @@ -13,566 +13,720 @@ usage: fiche [-DepbsdolBuw]. [-D] [-e] [-d domain] [-p port] [-s slug size] [-o output directory] [-B buffer size] [-u user name] [-l log file] [-b banlist] [-w whitelist] - -D option is for daemonizing fiche - -e option is for using an extended character set for the URL Compile with Makefile or manually with -O2 and -pthread flags. + To install use `make install` command. Use netcat to push text - example: - $ cat fiche.c | nc localhost 9999 ------------------------------------------------------------------------------- */ -#include #include "fiche.h" -int main(int argc, char **argv) -{ - time_seed = time(0); +#include +#include +#include +#include - parse_parameters(argc, argv); - set_domain_name(); +#include +#include +#include +#include - if (getuid() == 0) +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +/****************************************************************************** + * Various declarations + */ +const char *Fiche_Symbols = "abcdefghijklmnopqrstuvwxyz0123456789"; + + +/****************************************************************************** + * Inner structs + */ + +struct fiche_connection { + int socket; + struct sockaddr_in address; + + Fiche_Settings *settings; +}; + + +/****************************************************************************** + * Static function declarations + */ + +// Settings-related + +/** + * @brief Sets domain name + * @warning settings.domain has to be freed after using this function! + */ +static int set_domain_name(Fiche_Settings *settings); + +/** + * @brief Changes user running this program to requested one + * @warning Application has to be run as root to use this function + */ +static int perform_user_change(const Fiche_Settings *settings); + + +// Server-related + +/** + * @brief Starts server with settings provided in Fiche_Settings struct + */ +static int start_server(Fiche_Settings *settings); + +/** + * @brief Dispatches incoming connections by spawning threads + */ +static void dispatch_connection(int socket, Fiche_Settings *settings); + +/** + * @brief Handles connections + * @remarks Is being run by dispatch_connection in separate threads + * @arg args Struct fiche_connection containing connection details + */ +static void *handle_connection(void *args); + +// Server-related utils + + +/** + * @brief Generates a slug that will be used for paste creation + * @warning output has to be freed after using! + * + * @arg output pointer to output string containing full path to directory + * @arg length default or user-requested length of a slug + * @arg extra_length additional length that was added to speed-up the + * generation process + * + * This function is used in connection with create_directory function + * It generates strings that are used to create a directory for + * user-provided data. If directory already exists, we ask this function + * to generate another slug with increased size. + */ +static void generate_slug(char **output, uint8_t length, uint8_t extra_length); + + +/** + * @brief Creates a directory at requested path using requested slug + * @returns 0 if succeded, 1 if failed or dir already existed + * + * @arg output_dir root directory for all pastes + * @arg slug directory name for a particular paste + */ +static int create_directory(char *output_dir, char *slug); + + +/** + * @brief Saves data to file at requested path + * + * @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(uint8_t *data, char *output_dir, char *path); + + +// Logging-related + +/** + * @brief Displays error messages + */ +static void print_error(const char *format, ...); + + +/** + * @brief Displays status messages + */ +static void print_status(const char *format, ...); + + +/** + * @brief Displays horizontal line + */ +static void print_separator(); + + +/** + * @brief Saves connection entry to the logfile + */ +static void log_entry(const Fiche_Settings *s, const char *ip, + const char *hostname, const char *slug); + + +/** + * @brief Returns string containing current date + * @warning Output has to be freed! + */ +static char *get_date(); + + +/** + * @brief Time seed + */ +unsigned int seed; + +/****************************************************************************** + * Public fiche functions + */ + +void fiche_init(Fiche_Settings *settings) { + + // Initialize everything to default values + // or to NULL in case of pointers + + struct Fiche_Settings def = { + // domain + "example.com", + // output dir + "code", + // port + 9999, + // slug length + 4, + // buffer length + 32768, + // user name + NULL, + // path to log file + NULL, + // path to banlist + NULL, + // path to whitelist + NULL + }; + + // Copy default settings to provided instance + *settings = def; +} + +int fiche_run(Fiche_Settings settings) { + + seed = time(NULL); + + // Display welcome message { - if (UID == -1) - error("user not set"); - if (setgid(GID) != 0) - error("Unable to drop group privileges"); - if (setuid(UID) != 0) - error("Unable to drop user privileges"); + char *date = get_date(); + print_status("Starting fiche on %s...", date); + free(date); } - if (BASEDIR == NULL) - set_basedir(); - - startup_message(); - - int listen_socket, optval = 1; - struct sockaddr_in server_address; - - listen_socket = create_socket(); - setsockopt(listen_socket, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval , sizeof(int)); - -#if (HAVE_INET6) - struct sockaddr_in6 server_address6; - if (IPv6) - { - server_address6 = set_address6(server_address6); - bind_to_port6(listen_socket, server_address6); - } - else - { -#else - if (1) { -#endif - server_address = set_address(server_address); - bind_to_port(listen_socket, server_address); + // Try to set requested user + if ( perform_user_change(&settings) != 0) { + print_error("Was not able to change the user!"); + return -1; } - if (DAEMON) + // Check if output directory is writable + // - First we try to create it { - pid_t pid; - - pid = fork(); - if (pid == -1) - error("Failed to fork"); - if (pid == 0) - while (1) perform_connection(listen_socket); - } - else - while (1) perform_connection(listen_socket); - - return 0; -} - -void *thread_connection(void *args) -{ - int connection_socket = ((struct thread_arguments *) args ) -> connection_socket; - struct sockaddr_in client_address; - struct client_data data; -#if (HAVE_INET6) - struct sockaddr_in6 client_address6; - if (IPv6) - { - client_address6 = ((struct thread_arguments *) args ) -> client_address6; - data = get_client_address6(client_address6); - } - else - { -#else - if (1) { -#endif - client_address = ((struct thread_arguments *) args ) -> client_address; - data = get_client_address(client_address); - } - - char buffer[BUFSIZE]; - bzero(buffer, BUFSIZE); - int status = recv(connection_socket, buffer, BUFSIZE, MSG_WAITALL); - - if (WHITELIST != NULL && check_whitelist(data.ip_address) == NULL) - { - display_info(data, NULL, "Rejected connection from unknown user."); - save_log(NULL, data.ip_address, data.hostname); - if (write(connection_socket, "You are not whitelisted!\n", 26) < 0) - printf("Error writing on stream socket\n"); - close(connection_socket); - pthread_exit(NULL); - } - - if (BANLIST != NULL && check_banlist(data.ip_address) != NULL) - { - display_info(data, NULL, "Rejected connection from banned user."); - save_log(NULL, data.ip_address, data.hostname); - if (write(connection_socket, "You are banned!\n", 17) < 0) - printf("Error writing on stream socket\n"); - close(connection_socket); - pthread_exit(NULL); - } - - if (check_protocol(buffer) == 1) - status = -1; - - if (status != -1) - { - char slug[SLUG_SIZE+8]; - generate_url(buffer, slug, SLUG_SIZE+8, data); - save_log(slug, data.ip_address, data.hostname); - char response[strlen(slug) + strlen(DOMAIN) + 2]; - snprintf(response, sizeof response, "%s%s\n", DOMAIN, slug); - if (write(connection_socket, response, strlen(response)) < 0) - printf("Error writing on stream socket\n"); - } - else - { - display_info(data, NULL, "Invalid connection."); - save_log(NULL, data.ip_address, data.hostname); - if (write(connection_socket, "Use netcat.\n", 12) < 0) - printf("Error writing on stream socket\n"); - } - - close(connection_socket); - pthread_exit(NULL); -} - -void perform_connection(int listen_socket) -{ - pthread_t thread_id; - struct sockaddr_in client_address; - - int address_length; - int connection_socket; - -#if (HAVE_INET6) - struct sockaddr_in6 client_address6; - if (IPv6) - { - address_length = sizeof(client_address6); - connection_socket = accept(listen_socket, (struct sockaddr *) &client_address6, (void *) &address_length); - } - else - { -#else - if (1) { -#endif - address_length = sizeof(client_address); - connection_socket = accept(listen_socket, (struct sockaddr *) &client_address, (void *) &address_length); - } - - struct timeval timeout; - timeout.tv_sec = 5; - timeout.tv_usec = 0; - - if (setsockopt (connection_socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout)) < 0) - error("while setting setsockopt timeout"); - if (setsockopt (connection_socket, SOL_SOCKET, SO_SNDTIMEO, (char *)&timeout, sizeof(timeout)) < 0) - error("while setting setsockopt timeout"); - - struct thread_arguments arguments; - arguments.connection_socket = connection_socket; -#if (HAVE_INET6) - if (IPv6) - arguments.client_address6 = client_address6; - else -#endif - arguments.client_address = client_address; - - if (pthread_create(&thread_id, NULL, &thread_connection, &arguments) != 0) - error("on thread creation"); - else - pthread_detach(thread_id); -} - -char *get_date() -{ - time_t rawtime; - struct tm *timeinfo; - char *timechar; - - time(&rawtime); - timeinfo = localtime(&rawtime); - timechar = asctime(timeinfo); - timechar[strlen(timechar)-1] = 0; - - return timechar; -} - -struct client_data get_client_address(struct sockaddr_in client_address) -{ - struct hostent *hostp; - struct client_data data; - char *hostaddrp; - - hostp = gethostbyaddr((const char *)&client_address.sin_addr.s_addr, sizeof(client_address.sin_addr.s_addr), AF_INET); - if (hostp == NULL) - { - printf("WARNING: Couldn't obtain client's hostname\n"); - data.hostname = "n/a"; - } - else - data.hostname = hostp->h_name; - - hostaddrp = inet_ntoa(client_address.sin_addr); - if (hostaddrp == NULL) - { - printf("WARNING: Couldn't obtain client's address\n"); - data.ip_address = "n/a"; - } - else - data.ip_address = hostaddrp; - - return data; -} - -#if (HAVE_INET6) -struct client_data get_client_address6(struct sockaddr_in6 client_address6) -{ - struct hostent *hostp; - struct client_data data; - static char hostaddrp[INET6_ADDRSTRLEN]; - - hostp = gethostbyaddr((const char *)&client_address6.sin6_addr, sizeof(client_address6.sin6_addr), AF_INET6); - if (hostp == NULL) - { - printf("WARNING: Couldn't obtain client's hostname\n"); - data.hostname = "n/a"; - } - else - data.hostname = hostp->h_name; - - inet_ntop(AF_INET6, &(client_address6.sin6_addr), hostaddrp, - INET6_ADDRSTRLEN); - if (hostaddrp == NULL) - { - printf("WARNING: Couldn't obtain client's address\n"); - data.ip_address = "n/a"; - } - else - data.ip_address = hostaddrp; - - return data; -} -#endif - -void save_log(char *slug, char *hostaddrp, char *h_name) -{ - if (LOG != NULL) - { - char contents[256]; - - if (slug != NULL) - snprintf(contents, sizeof contents, "%s -- %s -- %s (%s)\n", slug, get_date(), hostaddrp, h_name); - else - snprintf(contents, sizeof contents, "%s -- %s -- %s (%s)\n", "rej", get_date(), hostaddrp, h_name); - - FILE *fp; - fp = fopen(LOG, "a"); - fprintf(fp, "%s", contents); - fclose(fp); - } -} - -void display_info(struct client_data data, char *slug, char *message) -{ - if (DAEMON) - return; - - if (slug == NULL) - printf("%s\n", message); - else - printf("Saved to: %s\n", slug); - - printf("%s\n", get_date()); - printf("Client: %s (%s)\n", data.ip_address, data.hostname); - printf("====================================\n"); -} - -char *check_banlist(char *ip_address) -{ - load_list(BANFILE, 0); - return strstr(BANLIST, ip_address); -} - -char *check_whitelist(char *ip_address) -{ - load_list(WHITEFILE, 1); - return strstr(WHITELIST, ip_address); -} - -void load_list(char *file_path, int type) -{ - FILE *fp; - - if (( fp = fopen(file_path, "r")) == NULL ) - error("cannot load list"); - - fseek(fp, 0, SEEK_END); - long fsize = ftell(fp); - fseek(fp, 0, SEEK_SET); - - char *buffer = malloc(fsize + 1); - if (fread(buffer, fsize, 1, fp) != fsize) - error("reading list failed"); - fclose(fp); - - buffer[fsize] = 0; - - if (type == 0) - BANLIST = buffer; - else - WHITELIST = buffer; - - free(buffer); -} - -int create_socket() -{ - int lsocket; -#if (HAVE_INET6) - if (IPv6) - lsocket = socket(AF_INET6, SOCK_STREAM, 0); - else -#endif - lsocket = socket(AF_INET, SOCK_STREAM, 0); - - if (lsocket < 0) - error("Couldn't open socket"); - - return lsocket; -} - -struct sockaddr_in set_address(struct sockaddr_in server_address) -{ - bzero((char *) &server_address, sizeof(server_address)); - server_address.sin_family = AF_INET; - server_address.sin_addr.s_addr = htonl(INADDR_ANY); - server_address.sin_port = htons((unsigned short)PORT); - return server_address; -} - -#if (HAVE_INET6) -struct sockaddr_in6 set_address6(struct sockaddr_in6 server_address6) -{ - bzero((char *) &server_address6, sizeof(server_address6)); - server_address6.sin6_family = AF_INET6; - server_address6.sin6_addr = in6addr_any; - server_address6.sin6_port = htons((unsigned short)PORT); - return server_address6; -} -#endif - -void bind_to_port(int listen_socket, struct sockaddr_in server_address) -{ - if (bind(listen_socket, (struct sockaddr *) &server_address, sizeof(server_address)) < 0) - error("while binding to port"); - if (listen(listen_socket, QUEUE_SIZE) < 0) - error("while starting listening"); -} - -#if (HAVE_INET6) -void bind_to_port6(int listen_socket, struct sockaddr_in6 server_address6) -{ - if (bind(listen_socket, (struct sockaddr *) &server_address6, sizeof(server_address6)) < 0) - error("while binding to port"); - if (listen(listen_socket, QUEUE_SIZE) < 0) - error("while starting listening"); -} -#endif - -void generate_url(char *buffer, char *slug, size_t slug_length, struct client_data data) -{ - int i; - memset(slug, '\0', slug_length); - - for (i = 0; i <= SLUG_SIZE - 1; i++) - { -#if defined(BSD) - int symbol_id = arc4random() % strlen(symbols); -#else - int symbol_id = rand_r(&time_seed) % strlen(symbols); -#endif - slug[i] = symbols[symbol_id]; - } - - while (create_directory(slug) == -1) - { -#if defined(BSD) - int symbol_id = arc4random() % strlen(symbols); -#else - int symbol_id = rand_r(&time_seed) % strlen(symbols); -#endif - slug[strlen(slug)] = symbols[symbol_id]; - } - - save_to_file(slug, buffer, data); -} - -int create_directory(char *slug) -{ - char *directory = malloc(strlen(BASEDIR) + strlen(slug) + sizeof(char) + 1); - - snprintf(directory, strlen(BASEDIR) + strlen(slug) + sizeof(char) + 1, "%s%s%s", BASEDIR, "/", slug); - - mkdir(BASEDIR, S_IRWXU | S_IRGRP | S_IROTH | S_IXOTH | S_IXGRP); - int result = mkdir(directory, S_IRWXU | S_IRGRP | S_IROTH | S_IXOTH | S_IXGRP); - - free(directory); - - return result; -} - -void save_to_file(char *slug, char *buffer, struct client_data data) -{ - char *directory = malloc(strlen(BASEDIR) + strlen(slug) + 11 * sizeof(char) + 1 ); - - snprintf(directory, strlen(BASEDIR) + strlen(slug) + 11 * sizeof(char) + 1, "%s%s%s%s", BASEDIR , "/", slug, "/index.txt"); - - FILE *fp; - fp = fopen(directory, "w"); - fprintf(fp, "%s", buffer); - fclose(fp); - - display_info(data, directory, ""); - - free(directory); -} - -void set_uid_gid(char *username) -{ - struct passwd *userdata = getpwnam(username); - if (userdata == NULL) - error("Provided user doesn't exist"); - - UID = userdata->pw_uid; - GID = userdata->pw_gid; -} - -int check_protocol(char *buffer) -{ - if (strlen(buffer) < 3) - return 1; - if ((strncmp(buffer, "GET", 3) == 0)||(strncmp(buffer, "POST", 4) == 0)) - if (strstr(buffer, "HTTP/1.")) - return 1; - return 0; -} - -void set_basedir() -{ - BASEDIR = getenv("HOME"); - strncat(BASEDIR, "/code", 5 * sizeof(char)); -} - -void startup_message() -{ - if (DAEMON) - return; - - printf("====================================\n"); - printf("Domain name: %s\n", DOMAIN); - printf("Saving files to: %s\n", BASEDIR); - printf("Fiche started listening on port %d.\n", PORT); - printf("Buffer size set to: %d.\n", BUFSIZE); - printf("Slug size set to: %d.\n", SLUG_SIZE); - printf("Log file: %s\n", LOG); - printf("====================================\n"); -} - -void error(char *buffer) -{ - printf("Error: %s\n", buffer); - exit(1); -} - -void set_domain_name() { - char b[128]; - memcpy(b, DOMAIN, sizeof DOMAIN); - - if (HTTPS) - snprintf(DOMAIN, sizeof DOMAIN, "%s%s", "https://", b); - else - snprintf(DOMAIN, sizeof DOMAIN, "%s%s", "http://", b); -} - -void parse_parameters(int argc, char **argv) -{ - int c; - - while ((c = getopt (argc, argv, "D6eSp:b:s:d:o:l:B:u:w:")) != -1) - switch (c) - { - case 'D': - DAEMON = 1; - break; - case '6': - IPv6 = 1; - break; - case 'e': - snprintf(symbols, sizeof symbols, "%s", "abcdefghijklmnopqrstuvwxyz0123456789-+_=.ABCDEFGHIJKLMNOPQRSTUVWXYZ"); - break; - case 'S': - HTTPS = 1; - break; - case 'd': - snprintf(DOMAIN, sizeof DOMAIN, "%s%s", optarg, "/"); - break; - case 'p': - PORT = atoi(optarg); - break; - case 'B': - BUFSIZE = atoi(optarg); - break; - case 'b': - BANFILE = optarg; - load_list(BANFILE, 0); - break; - case 's': - SLUG_SIZE = atoi(optarg); - break; - case 'o': - BASEDIR = optarg; - break; - case 'l': - LOG = optarg; - break; - case 'u': - set_uid_gid(optarg); - break; - case 'w': - WHITEFILE = optarg; - load_list(WHITEFILE, 1); - break; - default: - printf("usage: fiche [-D6epbsdSolBuw].\n"); - printf(" [-d domain] [-p port] [-s slug_size]\n"); - printf(" [-o output directory] [-B buffer_size] [-u user name]\n"); - printf(" [-l log file] [-b banlist] [-w whitelist]\n"); - exit(1); + mkdir( + settings.output_dir_path, + S_IRWXU | S_IRGRP | S_IROTH | S_IXOTH | S_IXGRP + ); + // - Then we check if we can write there + if ( access(settings.output_dir_path, W_OK) != 0 ) { + print_error("Output directory not writable!"); + return -1; } + } + + // Check if log file is writable (if set) + if ( settings.log_file_path ) { + + // Create log file if it doesn't exist + creat(settings.log_file_path, S_IRWXU); + + // Then check if it's accessible + if ( access(settings.log_file_path, W_OK) != 0 ) { + print_error("Log file not writable!"); + return -1; + } + + } + + // Try to set domain name + if ( set_domain_name(&settings) != 0 ) { + print_error("Was not able to set domain name!"); + return -1; + } + + // Main loop in this method + start_server(&settings); + + // Perform final cleanup + + // This is allways allocated on the heap + free(settings.domain); + + return 0; + +} + + +/****************************************************************************** + * Static functions below + */ + +static void print_error(const char *format, ...) { + va_list args; + va_start(args, format); + + printf("[Fiche][ERROR] "); + vprintf(format, args); + printf("\n"); + + va_end(args); +} + + +static void print_status(const char *format, ...) { + va_list args; + va_start(args, format); + + printf("[Fiche][STATUS] "); + vprintf(format, args); + printf("\n"); + + va_end(args); +} + + +static void print_separator() { + printf("============================================================\n"); +} + + +static void log_entry(const Fiche_Settings *s, const char *ip, + const char *hostname, const char *slug) +{ + // Logging to file not enabled, finish here + if (!s->log_file_path) { + return; + } + + FILE *f = fopen(s->log_file_path, "a"); + if (!f) { + print_status("Was not able to save entry to the log!"); + return; + } + + char *date = get_date(); + + // Write entry to file + fprintf(f, "%s -- %s -- %s (%s)\n", slug, date, ip, hostname); + fclose(f); + + free(date); +} + + +static char *get_date() { + struct tm curtime; + time_t ltime; + + ltime=time(<ime); + localtime_r(<ime, &curtime); + + // Much more than required, date string is usually about 25 chars + char buf[128]; + asctime_r(&curtime, buf); + + char *out = malloc(strlen(buf) + 1); + strcpy(out, buf); + + // Remove newline char + out[strlen(buf)-1] = 0; + + return out; +} + + +static int set_domain_name(Fiche_Settings *settings) { + + const char *prefix = "http://"; + const int len = strlen(settings->domain) + strlen(prefix) + 1; + + char *b = malloc(len); + if (b == NULL) { + return -1; + } + + strcpy(b, prefix); + strcat(b, settings->domain); + + settings->domain = b; + + print_status("Domain set to: %s.", settings->domain); + + return 0; +} + + +static int perform_user_change(const Fiche_Settings *settings) { + + // User change wasn't requested, finish here + if (settings->user_name == NULL) { + return 0; + } + + // Check if root, if not - finish here + if (getuid() != 0) { + print_error("Run as root if you want to change the user!"); + return -1; + } + + // Get user details + const struct passwd *userdata = getpwnam(settings->user_name); + + const int uid = userdata->pw_uid; + const int gid = userdata->pw_gid; + + if (uid == -1 || gid == -1) { + print_error("Could find requested user: %s!", settings->user_name); + return -1; + } + + if (setgid(gid) != 0) { + print_error("Couldn't switch to requested user: %s!", settings->user_name); + } + + if (setuid(uid) != 0) { + print_error("Couldn't switch to requested user: %s!", settings->user_name); + } + + print_status("User changed to: %s.", settings->user_name); + + return 0; +} + + +static int start_server(Fiche_Settings *settings) { + + // Perform socket creation + int s = socket(AF_INET, SOCK_STREAM, 0); + if (s < 0) { + print_error("Couldn't create a socket!"); + return -1; + } + + // Set socket settings + if ( setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &(int){ 1 } , sizeof(int)) != 0 ) { + print_error("Couldn't prepare the socket!"); + return -1; + } + + // Prepare address and port handler + struct sockaddr_in address; + address.sin_family = AF_INET; + address.sin_addr.s_addr = INADDR_ANY; + address.sin_port = htons(settings->port); + + // Bind to port + if ( bind(s, (struct sockaddr *) &address, sizeof(address)) != 0) { + print_error("Couldn't bind to the port: %d!", settings->port); + return -1; + } + + // Start listening + if ( listen(s, 128) != 0 ) { + print_error("Couldn't start listening on the socket!"); + return -1; + } + + print_status("Server started listening on port: %d.", settings->port); + print_separator(); + + // Run dispatching loop + while (1) { + dispatch_connection(s, settings); + } + + // Give some time for all threads to finish + // NOTE: this code is reached only in testing environment + // There is currently no way to kill the main thread from any thread + // Something like this can be done for testing purpouses: + // int i = 0; + // while (i < 3) { + // dispatch_connection(s, settings); + // i++; + // } + + sleep(5); + + return 0; +} + + +static void dispatch_connection(int socket, Fiche_Settings *settings) { + + // Create address structs for this socket + struct sockaddr_in address; + socklen_t addlen = sizeof(address); + + // Accept a connection and get a new socket id + const int s = accept(socket, (struct sockaddr *) &address, &addlen); + if (s < 0 ) { + print_error("Error on accepting connection!"); + return; + } + + // Set timeout for accepted socket + const struct timeval timeout = { 5, 0 }; + + if ( setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) != 0 ) { + print_error("Couldn't set a timeout!"); + } + + if ( setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) != 0 ) { + print_error("Couldn't set a timeout!"); + } + + // Create an argument for the thread function + //struct fiche_connection c = { s, address, settings }; + struct fiche_connection *c = malloc(sizeof(*c)); + if (!c) { + print_error("Couldn't allocate memory!"); + return; + } + c->socket = s; + c->address = address; + c->settings = settings; + + // Spawn a new thread to handle this connection + pthread_t id; + + if ( pthread_create(&id, NULL, &handle_connection, c) != 0 ) { + print_error("Couldn't spawn a thread!"); + return; + } + + // Detach thread if created succesfully + // TODO: consider using pthread_tryjoin_np + pthread_detach(id); + +} + + +static void *handle_connection(void *args) { + + // Cast args to it's previous type + struct fiche_connection *c = (struct fiche_connection *) args; + + // Get client's IP + const char *ip = inet_ntoa(c->address.sin_addr); + + // Get client's hostname + char hostname[1024]; + + if (getnameinfo((struct sockaddr *)&c->address, sizeof(c->address), + hostname, sizeof(hostname), NULL, 0, 0) != 0 ) { + + // Couldn't resolve a hostname + strcpy(hostname, "n/a"); + } + + // Print status on this connection + { + char *date = get_date(); + print_status("%s", date); + free(date); + + 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); + + const int r = recv(c->socket, buffer, sizeof(buffer), MSG_WAITALL); + if (r <= 0) { + print_error("No data received from the client!"); + print_separator(); + + // Close the socket + close(c->socket); + + // Cleanup + free(c); + pthread_exit(NULL); + + return 0; + } + + // - Check if request was performed with a known protocol + // TODO + + // - Check if on whitelist + // TODO + + // - Check if on banlist + // TODO + + // Generate slug and use it to create an url + char *slug; + uint8_t extra = 0; + + do { + + // Generate slugs until it's possible to create a directory + // with generated slug on disk + generate_slug(&slug, c->settings->slug_len, extra); + + // Increment counter for additional letters needed + ++extra; + + // If i was incremented more than 128 times, something + // for sure went wrong. We are closing connection and + // killing this thread in such case + if (extra > 128) { + print_error("Couldn't generate a valid slug!"); + print_separator(); + + // Cleanup + free(c); + free(slug); + close(c->socket); + pthread_exit(NULL); + return NULL; + } + + } + while(create_directory(c->settings->output_dir_path, slug) != 0); + + if ( save_to_file(buffer, c->settings->output_dir_path, slug) != 0 ) { + print_error("Couldn't save a file!"); + print_separator(); + + // Cleanup + free(c); + free(slug); + close(c->socket); + pthread_exit(NULL); + return NULL; + } + + // 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); + } + + print_status("Received %d bytes, saved to: %s.", r, slug); + print_separator(); + + // Log connection + // TODO: log unsuccessful and rejected connections + log_entry(c->settings, ip, hostname, slug); + + // Close the connection + close(c->socket); + + // Perform cleanup of values used in this thread + free(slug); + free(c); + + 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 + // This happens in case when directory with this name already + // exists. To save time, we don't generate new slugs until + // we spot an available one. We add another letter instead. + + if (extra_length > 0) { + free(*output); + } + + // Create a buffer for slug with extra_length if any + *output = calloc(length + 1 + extra_length, sizeof(char)); + + if (*output == NULL) { + // TODO + } + + // Take n-th symbol from symbol table and use it for slug generation + for (int i = 0; i < length + extra_length; i++) { + int n = rand_r(&seed) % strlen(Fiche_Symbols); + *(output[0] + sizeof(char) * i) = Fiche_Symbols[n]; + } + +} + + +static int create_directory(char *output_dir, char *slug) { + // Additional byte is for the slash + size_t len = strlen(output_dir) + strlen(slug) + 2; + + // Generate a path + char *path = malloc(len); + snprintf(path, len, "%s%s%s", output_dir, "/", slug); + + // Create output directory, just in case + mkdir(output_dir, S_IRWXU | S_IRGRP | S_IROTH | S_IXOTH | S_IXGRP); + + // Create slug directory + const int r = mkdir( + path, + S_IRWXU | S_IRGRP | S_IROTH | S_IXOTH | S_IXGRP + ); + + free(path); + + return r; +} + + +static int save_to_file(uint8_t *data, char *output_dir, char *slug) { + char *file_name = "index.txt"; + + // Additional 2 bytes are for 2 slashes + size_t len = strlen(output_dir) + strlen(slug) + strlen(file_name) + 3; + + // Generate a path + char *path = malloc(len); + snprintf(path, len, "%s%s%s%s%s", output_dir, "/", slug, "/", file_name); + + // Attempt file saving + FILE *f = fopen(path, "w"); + if (!f) { + return -1; + } + + if ( fprintf(f, "%s", data) < 0 ) { + return -1; + } + + fclose(f); + free(path); + + return 0; } diff --git a/fiche.h b/fiche.h index 9ef6d9c..4b8c958 100644 --- a/fiche.h +++ b/fiche.h @@ -5,7 +5,7 @@ Fiche - Command line pastebin for sharing terminal output. License: MIT (http://www.opensource.org/licenses/mit-license.php) Repository: https://github.com/solusipse/fiche/ -Live example: http://code.solusipse.net/ +Live example: http://termbin.com ------------------------------------------------------------------------------- @@ -14,15 +14,7 @@ usage: fiche [-DepbsdolBuw]. [-o output directory] [-B buffer size] [-u user name] [-l log file] [-b banlist] [-w whitelist] --D option is for daemonizing fiche - --e option is for using an extended character set for the URL - -Compile with Makefile or manually with -O2 and -pthread flags. -To install use `make install` command. - Use netcat to push text - example: - $ cat fiche.c | nc localhost 9999 ------------------------------------------------------------------------------- @@ -31,91 +23,86 @@ $ cat fiche.c | nc localhost 9999 #ifndef FICHE_H #define FICHE_H -#ifndef HAVE_INET6 -#define HAVE_INET6 1 -#endif +#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -int UID = -1; -int GID = -1; -char *LOG; -char *BASEDIR; -char *BANLIST; -char *BANFILE; -char *WHITEFILE; -char *WHITELIST; -int DAEMON = 0; -int HTTPS = 0; -int PORT = 9999; -int IPv6 = 0; -int SLUG_SIZE = 4; -int BUFSIZE = 32768; -int QUEUE_SIZE = 500; -char DOMAIN[128] = "localhost/"; -char symbols[67] = "abcdefghijklmnopqrstuvwxyz0123456789"; +/** + * @brief Used as a container for fiche settings. Create before + * the initialization + * + */ +typedef struct Fiche_Settings { + /** + * @brief Domain used in output links + */ + char *domain; -unsigned int time_seed; + /** + * @brief Path to directory used for storing uploaded pastes + */ + char *output_dir_path; -struct thread_arguments -{ - int connection_socket; - struct sockaddr_in client_address; -#if (HAVE_INET6) - struct sockaddr_in6 client_address6; -#endif -}; + /** + * @brief Port on which fiche is waiting for connections + */ + uint16_t port; -struct client_data -{ - char *ip_address; - char *hostname; -}; + /** + * @brief Length of a paste's name + */ + uint8_t slug_len; -int create_socket(); -int create_directory(char *slug); -int check_protocol(char *buffer); + /** + * @brief Connection buffer length + * + * @remarks Length of this buffer limits max size of uploaded files + */ + uint32_t buffer_len; -void bind_to_port(int listen_socket, struct sockaddr_in serveraddr); -#if (HAVE_INET6) -void bind_to_port6(int listen_socket, struct sockaddr_in6 serveraddr6); -#endif -void error(char *buffer); -void perform_connection(int listen_socket); -void generate_url(char *buffer, char *slug, size_t slug_length, struct client_data data); -void save_to_file(char *buffer, char *slug, struct client_data data); -void display_info(struct client_data data, char *slug, char *message); -void startup_message(); -void set_basedir(); -void set_domain_name(); -void load_list(char *file_path, int type); -void parse_parameters(int argc, char **argv); -void save_log(char *slug, char *hostaddrp, char *h_name); -void set_uid_gid(); + /** + * @brief Name of the user that runs fiche process + */ + char *user_name; -char *check_banlist(char *ip_address); -char *check_whitelist(char *ip_address); -char *get_date(); + /** + * @brief Path to the log file + */ + char *log_file_path; + + /** + * @brief Path to the file with banned IPs + */ + char *banlist_path; + + /** + * @brief Path to the file with whitelisted IPs + */ + char *whitelist_path; + + + +} Fiche_Settings; + + +/** + * @brief Initializes Fiche_Settings instance + */ +void fiche_init(Fiche_Settings *settings); + + +/** + * @brief Runs fiche server + * + * @return 0 if it was able to start, any other value otherwise + */ +int fiche_run(Fiche_Settings settings); + + +/** + * @brief array of symbols used in slug generation + * @remarks defined in fiche.c + */ +extern const char *Fiche_Symbols; -struct sockaddr_in set_address(struct sockaddr_in serveraddr); -#if (HAVE_INET6) -struct sockaddr_in6 set_address6(struct sockaddr_in6 serveraddr6); -#endif -struct client_data get_client_address(struct sockaddr_in client_address); -#if (HAVE_INET6) -struct client_data get_client_address6(struct sockaddr_in6 client_address6); -#endif #endif diff --git a/main.c b/main.c new file mode 100644 index 0000000..269633c --- /dev/null +++ b/main.c @@ -0,0 +1,133 @@ +/* +Fiche - Command line pastebin for sharing terminal output. + +------------------------------------------------------------------------------- + +License: MIT (http://www.opensource.org/licenses/mit-license.php) +Repository: https://github.com/solusipse/fiche/ +Live example: http://termbin.com + +------------------------------------------------------------------------------- + +usage: fiche [-DepbsdolBuw]. + [-D] [-e] [-d domain] [-p port] [-s slug size] + [-o output directory] [-B buffer size] [-u user name] + [-l log file] [-b banlist] [-w whitelist] + +Use netcat to push text - example: +$ cat fiche.c | nc localhost 9999 + +------------------------------------------------------------------------------- +*/ + +#include "fiche.h" + +#include +#include +#include +#include + + +int main(int argc, char **argv) { + + // Fiche settings instance + Fiche_Settings fs; + + // Initialize settings instance to default values + fiche_init(&fs); + + // Note: fiche_run is responsible for checking if these values + // were set correctly + + // Note: according to getopt documentation, we don't need to + // copy strings, so we decided to go with pointer approach for these + + // Parse input arguments + int c; + while ((c = getopt(argc, argv, "D6eSp:b:s:d:o:l:B:u:w:")) != -1) { + switch (c) { + + // domain + case 'd': + { + fs.domain = optarg; + } + break; + + // port + case 'p': + { + fs.port = atoi(optarg); + } + break; + + // slug size + case 's': + { + fs.slug_len = atoi(optarg); + } + break; + + // output directory path + case 'o': + { + fs.output_dir_path = optarg; + } + break; + + // buffer size + case 'B': + { + fs.buffer_len = atoi(optarg); + } + break; + + // user name + case 'u': + { + fs.user_name = optarg; + } + break; + + // log file path + case 'l': + { + fs.log_file_path = optarg; + } + break; + + // banlist file path + case 'b': + { + fs.banlist_path = optarg; + } + break; + + // whitelist file path + case 'w': + { + fs.whitelist_path = optarg; + } + break; + + // Display help in case of any unsupported argument + default: + { + printf("usage: fiche [-dpsoBulbw].\n"); + printf(" [-d domain] [-p port] [-s slug size]\n"); + printf(" [-o output directory] [-B buffer size] [-u user name]\n"); + printf(" [-l log file] [-b banlist] [-w whitelist]\n"); + return 0; + } + break; + } + } + + + fiche_run(fs); + + + return 0; +} + +