Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| /* | |
| 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] | |
| -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 "fiche.h" | |
| #include <stdio.h> | |
| #include <stdarg.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <pwd.h> | |
| #include <time.h> | |
| #include <unistd.h> | |
| #include <pthread.h> | |
| #include <fcntl.h> | |
| #include <netdb.h> | |
| #include <sys/time.h> | |
| #include <sys/stat.h> | |
| #include <sys/types.h> | |
| #include <arpa/inet.h> | |
| #include <sys/socket.h> | |
| #include <netinet/in.h> | |
| #include <netinet/in.h> | |
| /****************************************************************************** | |
| * 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(const Fiche_Settings *s, uint8_t *data, char *slug); | |
| // 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 void get_date(char *buf); | |
| /** | |
| * @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, | |
| // https | |
| false, | |
| // 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 | |
| { | |
| char date[64]; | |
| get_date(date); | |
| print_status("Starting fiche on %s...", date); | |
| } | |
| // Try to set requested user | |
| if ( perform_user_change(&settings) != 0) { | |
| print_error("Was not able to change the user!"); | |
| return -1; | |
| } | |
| // Check if output directory is writable | |
| // - First we try to create it | |
| { | |
| 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 | |
| FILE *f = fopen(settings.log_file_path, "a+"); | |
| fclose(f); | |
| // 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[64]; | |
| get_date(date); | |
| // Write entry to file | |
| fprintf(f, "%s -- %s -- %s (%s)\n", slug, date, ip, hostname); | |
| fclose(f); | |
| } | |
| static void get_date(char *buf) { | |
| struct tm curtime; | |
| time_t ltime; | |
| ltime=time(<ime); | |
| localtime_r(<ime, &curtime); | |
| // Save data to provided buffer | |
| if (asctime_r(&curtime, buf) == 0) { | |
| // Couldn't get date, setting first byte of the | |
| // buffer to zero so it won't be displayed | |
| buf[0] = 0; | |
| return; | |
| } | |
| // Remove newline char | |
| buf[strlen(buf)-1] = 0; | |
| } | |
| static int set_domain_name(Fiche_Settings *settings) { | |
| char *prefix = ""; | |
| if (settings->https) { | |
| prefix = "https://"; | |
| } else { | |
| prefix = "http://"; | |
| } | |
| const int len = strlen(settings->domain) + strlen(prefix) + 1; | |
| char *b = malloc(len); | |
| if (!b) { | |
| 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 = 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[64]; | |
| get_date(date); | |
| print_status("%s", 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); | |
| // Something went wrong in slug generation, break here | |
| if (!slug) { | |
| break; | |
| } | |
| // 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); | |
| // Slug generation failed, we have to finish here | |
| if (!slug) { | |
| print_error("Couldn't generate a slug!"); | |
| print_separator(); | |
| close(c->socket); | |
| // Cleanup | |
| free(c); | |
| pthread_exit(NULL); | |
| return NULL; | |
| } | |
| // Save to file failed, we have to finish here | |
| if ( save_to_file(c->settings, buffer, slug) != 0 ) { | |
| print_error("Couldn't save a file!"); | |
| print_separator(); | |
| close(c->socket); | |
| // Cleanup | |
| free(c); | |
| free(slug); | |
| 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) { | |
| return; | |
| } | |
| // 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) { | |
| if (!slug) { | |
| return -1; | |
| } | |
| // Additional byte is for the slash | |
| size_t len = strlen(output_dir) + strlen(slug) + 2; | |
| // Generate a path | |
| char *path = malloc(len); | |
| if (!path) { | |
| return -1; | |
| } | |
| 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(const Fiche_Settings *s, uint8_t *data, char *slug) { | |
| char *file_name = "index.txt"; | |
| // Additional 2 bytes are for 2 slashes | |
| size_t len = strlen(s->output_dir_path) + strlen(slug) + strlen(file_name) + 3; | |
| // Generate a path | |
| char *path = malloc(len); | |
| if (!path) { | |
| return -1; | |
| } | |
| snprintf(path, len, "%s%s%s%s%s", s->output_dir_path, "/", slug, "/", file_name); | |
| // Attempt file saving | |
| FILE *f = fopen(path, "w"); | |
| if (!f) { | |
| free(path); | |
| return -1; | |
| } | |
| // Null-terminate buffer if not null terminated already | |
| data[s->buffer_len - 1] = 0; | |
| if ( fprintf(f, "%s", data) < 0 ) { | |
| fclose(f); | |
| free(path); | |
| return -1; | |
| } | |
| fclose(f); | |
| free(path); | |
| return 0; | |
| } |