commit 6cd65091ff3c0faec778730b3c3f1477bc8dad4c Author: Thomas Kolb Date: Thu Jan 17 21:13:15 2013 +0100 Initial commit - Working version with the following features: - Directory listing and navigation - Download files - HTTP error pages (403, 404, 500) - Verbose logging diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef932ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.o +*.swp +fileshare +core +tags +testdir diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cbc84cd --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +CC=gcc +#CFLAGS+=-O2 -Wall -march=native -pedantic -std=c99 -D_POSIX_C_SOURCE=20120607L -D_XOPEN_SOURCE $(LUA_CFLAGS) +#CFLAGS+=-O2 -Wall -march=native -pedantic -std=c99 -D_POSIX_C_SOURCE=20120607L +CFLAGS+=-g -Wall -pedantic -std=c99 -D_POSIX_C_SOURCE=20120607L +LIBS=-lmicrohttpd + +TARGET=fileshare +SOURCE=main.c logger.c dirlisting.c +DEPS=logger.h templates.h dirlisting.h + +OBJ=$(patsubst %.c, %.o, $(SOURCE)) + +$(TARGET): $(OBJ) $(DEPS) + $(CC) -o $(TARGET) $(OBJ) $(LIBS) + +%.o: %.c $(DEPS) + $(CC) -c $(CFLAGS) -o $@ $< $(INCLUDES) + +doc: + doxygen doxygen.conf + +clean: + rm -f $(TARGET) + rm -f $(OBJ) diff --git a/dirlisting.c b/dirlisting.c new file mode 100644 index 0000000..7c16a65 --- /dev/null +++ b/dirlisting.c @@ -0,0 +1,269 @@ +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include + +#include "logger.h" +#include "templates.h" + +#define FLG_USED 1 +#define FLG_ISDIR 2 + +struct Entry { + char *name; + size_t size; + uint8_t flags; +}; + +const char* format_size(size_t s) { + static char buf[32]; + const char *prefixes = " kMGTPEZY"; + float fs = s; + + int divisions = 0; + + while(fs > 1024) { + fs /= 1024.0f; + divisions++; + } + + if(divisions == 0) { + sprintf(buf, "%lu B", s); + } else { + sprintf(buf, "%.2f %ciB", fs, prefixes[divisions]); + } + + return buf; +} + +static int compare_entries(const void *sp1,const void *sp2 ) { + const struct Entry *e1 = sp1; + const struct Entry *e2 = sp2; + + return( strcasecmp(e1->name, e2->name) ); +} + +char* gen_html(const char *url, struct Entry *entries, size_t numentries) { + char *result; + size_t allocated; + size_t i; + char buf[256]; + char fullpath[PATH_MAX]; + + // allocate a buffer for the result string + allocated = 64 * numentries + strlen(HEADER1 HEADER2 FOOTER) + 4096; + LOG(LVL_DEBUG, "initially allocating result string - size: %lu bytes", allocated * sizeof(char)); + result = malloc(allocated * sizeof(char)); + result[0] = '\0'; + + // this is still safe + strcat(result, HEADER1); + + sprintf(buf, "Directory listing for %s", url); + strcat(result, buf); + + strcat(result, HEADER2); + + sprintf(buf, "

Directory listing for %s

", url); + strcat(result, buf); + + int isrootdir = strcmp(url, "/") == 0; + + // Begin Navigation + strcat(result, "

Navigation: "); + + // tokenize the URL: init + char *token, *urlclone; + + urlclone = strdup(url); + token = strtok(urlclone, "/"); + strcpy(fullpath, "/"); + + if(isrootdir) { + strcat(result, "/"); + } else { + strcat(result, "/"); + + // create a link for each token in URL + do { + LOG(LVL_DUMP, "Found token of %s: %s", url, token); + + strcat(fullpath, token); + strcat(fullpath, "/"); + + sprintf(buf, " %s /", fullpath, token); + strcat(result, buf); + } + while((token = strtok(NULL, "/")) != NULL); + } + + free(urlclone); + + strcat(result, "

"); + + // begin listing subdirs + strcat(result, "

Sub-directories

"); + strcat(result, "
    "); + + for(i = 0; i < numentries; i++) { + // skip all non-directories + if(!(entries[i].flags & FLG_ISDIR)) + continue; + + if(isrootdir) { + sprintf(buf, "
  • %s/
  • \n", + entries[i].name, entries[i].name); + } else { + sprintf(buf, "
  • %s/
  • \n", + url, entries[i].name, entries[i].name); + } + strcat(result, buf); + } + + strcat(result, "
"); + + // begin listing files + strcat(result, "

Files

"); + strcat(result, "
    "); + + // here the listing entries are generated + for(i = 0; i < numentries; i++) { + size_t entrylen = 32 + 2 * strlen(entries[i].name); + size_t resultlen = strlen(result); + + // skip all directories + if(entries[i].flags & FLG_ISDIR) + continue; + + if(allocated < resultlen + entrylen) { + allocated *= 2; + LOG(LVL_DEBUG, "reallocating result string - new size: %lu bytes", allocated * sizeof(char)); + char *tmpresult = realloc(result, allocated * sizeof(char)); + if(!tmpresult) { + LOG(LVL_ERR, "realloc failed for result string: %s", strerror(errno)); + break; + } + + result = tmpresult; + } + + if(isrootdir) { + sprintf(buf, "
  • %s [%s]
  • \n", + entries[i].name, entries[i].name, format_size(entries[i].size)); + } else { + sprintf(buf, "
  • %s [%s]
  • \n", + url, entries[i].name, entries[i].name, format_size(entries[i].size)); + } + strcat(result, buf); + } + + strcat(result, "
"); + strcat(result, FOOTER); + + return result; +} + +char* create_dirlisting(const char *url, const char *localpath) { + char *result; + struct Entry *entries; + + size_t numentries, allocated; + size_t i; + + DIR *dir; + struct dirent *de; + + struct stat s; + + char fullpath[PATH_MAX]; + + if((dir = opendir(localpath)) == NULL) { + LOG(LVL_ERR, "Cannot opendir %s: %s", localpath, strerror(errno)); + return NULL; + } + + // allocate some entry pointers + allocated = 100; + numentries = 0; + + entries = malloc(allocated * sizeof(struct Entry)); + if(!entries) { + LOG(LVL_ERR, "malloc failed for directory entries: %s", strerror(errno)); + return NULL; + } + + // initialize all entry strings with NULL + for(i = 0; i < allocated; i++) { + entries[i].flags = 0; + } + + while((de = readdir(dir)) != NULL) { + if(de->d_name[0] == '.') { + continue; + } + + // check if entries array is full + if(numentries == allocated) { + // double the size of the entries array + LOG(LVL_DEBUG, "reallocating entry list - new size: %lu bytes", 2 * allocated * sizeof(struct Entry)); + struct Entry *tmpentries = realloc(entries, 2 * allocated * sizeof(struct Entry)); + if(!tmpentries) { + LOG(LVL_ERR, "realloc failed for directory entries: %s", strerror(errno)); + free(entries); + return NULL; + } + + entries = tmpentries; + + // initialize the new entries + for(i = allocated; i < 2*allocated; i++) { + entries[i].flags = 0; + } + + allocated *= 2; + } + + snprintf(fullpath, PATH_MAX, "%s/%s", localpath, de->d_name); + + if(stat(fullpath, &s) == -1) { + LOG(LVL_WARN, "Cannot stat %s while listing directories: %s", + fullpath, strerror(errno)); + + // stat failed -> ignore this entry + continue; + } + + entries[numentries].name = strdup(de->d_name); + entries[numentries].flags |= FLG_USED; + entries[numentries].size = s.st_size; + + if(S_ISDIR(s.st_mode)) { + entries[numentries].flags |= FLG_ISDIR; + } + + numentries++; + } + + closedir(dir); + + qsort(entries, numentries, sizeof(struct Entry), compare_entries); + + result = gen_html(url, entries, numentries); + + for(i = 0; i < numentries; i++) { + if(entries[i].flags & FLG_USED) free(entries[i].name); + } + free(entries); + + return result; +} + diff --git a/dirlisting.h b/dirlisting.h new file mode 100644 index 0000000..942ed01 --- /dev/null +++ b/dirlisting.h @@ -0,0 +1,6 @@ +#ifndef DIRLISTING_H +#define DIRLISTING_H + +char* create_dirlisting(const char *url, const char *localpath); + +#endif // DIRLISTING_H diff --git a/logger.c b/logger.c new file mode 100644 index 0000000..7a7aeb0 --- /dev/null +++ b/logger.c @@ -0,0 +1,151 @@ +#include +#include + +#include +#include + +#include "logger.h" + +// define constants +const char *LOGGER_STR_FATAL = "FATAL"; +const char *LOGGER_STR_ERR = "ERROR"; +const char *LOGGER_STR_WARN = "WARN "; +const char *LOGGER_STR_INFO = "INFO "; +const char *LOGGER_STR_DEBUG = "DEBUG"; +const char *LOGGER_STR_DUMP = "DUMP "; + +const char *LOGGER_COLOR_FATAL = "\033[1;31m"; +const char *LOGGER_COLOR_ERR = "\033[1;31m"; +const char *LOGGER_COLOR_WARN = "\033[1;33m"; +const char *LOGGER_COLOR_INFO = "\033[1;32m"; +const char *LOGGER_COLOR_DEBUG = "\033[1m"; +const char *LOGGER_COLOR_DUMP = "\033[1;30m"; +const char *LOGGER_COLOR_NONE = "\033[0m"; + +// global variables +sem_t logger_semaphore; +int logger_verbosity; +int logger_use_colors; + +void logger_init(void) { + // Initialize the semaphore + sem_init(&logger_semaphore, 0, 1); + + logger_verbosity = 2147483647; + logger_use_colors = 1; +} + +void logger_shutdown(void) { + sem_destroy(&logger_semaphore); +} + +void logger_enable_colors(int enable) { + logger_use_colors = enable; +} + +void logger_set_verbosity(int verbosity) { + logger_verbosity = verbosity; +} + +void logger_debug_message(const char *prefix, const char *fmt, va_list ap) { + /* Guess we need no more than 100 bytes. */ + int n, size = 100; + char *p, *np; + va_list internal_ap; + + if ((p = (char*)malloc(size)) == NULL) { + fprintf(stderr, "[%s] FATAL: Cannot allocate string buffer while processing arguments.\n", LOGGER_STR_ERR); + return; + } + + while (1) { + /* Try to print in the allocated space. */ + va_copy(internal_ap, ap); + n = vsnprintf(p, size, fmt, internal_ap); + va_end(internal_ap); + + /* If that worked, return the string. */ + if (n > -1 && n < size) + break; + + /* Else try again with more space. */ + if (n > -1) /* glibc 2.1 */ + size = n+1; /* precisely what is needed */ + else /* glibc 2.0 */ + size *= 2; /* twice the old size */ + + if ((np = (char*)realloc (p, size)) == NULL) { + free(p); + fprintf(stderr, "[%s] FATAL: Cannot reallocate string buffer while processing arguments.\n", LOGGER_STR_ERR); + return; + } else { + p = np; + } + } + + sem_wait(&logger_semaphore); + fprintf(stderr, "%s %s\n", prefix, p); + sem_post(&logger_semaphore); + + free(p); +} + +void logger_log(int level, const char *format, ...) { + va_list argptr; + + char timebuf[32]; + char timebuf2[32]; + + char prefixbuf[64]; + const char *prefixcolor = "", *prefixtext = ""; + + if(level > logger_verbosity) + return; + + struct timeval tv; + gettimeofday(&tv, NULL); + strftime(timebuf, 32, "%Y-%M-%d %H:%M:%S.%%03d", localtime(&(tv.tv_sec))); + snprintf(timebuf2, 32, timebuf, tv.tv_usec/1000); + + if(level >= LVL_DUMP) { + if(logger_use_colors) + prefixcolor = LOGGER_COLOR_DUMP; + + prefixtext = LOGGER_STR_DUMP; + } else if(level >= LVL_DEBUG) { + if(logger_use_colors) + prefixcolor = LOGGER_COLOR_DEBUG; + + prefixtext = LOGGER_STR_DEBUG; + } else if(level >= LVL_INFO) { + if(logger_use_colors) + prefixcolor = LOGGER_COLOR_INFO; + + prefixtext = LOGGER_STR_INFO; + } else if(level >= LVL_WARN) { + if(logger_use_colors) + prefixcolor = LOGGER_COLOR_WARN; + + prefixtext = LOGGER_STR_WARN; + } else if(level >= LVL_ERR) { + if(logger_use_colors) + prefixcolor = LOGGER_COLOR_ERR; + + prefixtext = LOGGER_STR_ERR; + } else { + if(logger_use_colors) + prefixcolor = LOGGER_COLOR_FATAL; + + prefixtext = LOGGER_STR_FATAL; + } + + if(logger_use_colors) { + sprintf(prefixbuf, "%s [%s%s%s]", timebuf2, prefixcolor, prefixtext, LOGGER_COLOR_NONE); + } else { + sprintf(prefixbuf, "%s [%s]", timebuf2, prefixtext); + } + + va_start(argptr, format); + logger_debug_message(prefixbuf, format, argptr); + va_end(argptr); +} diff --git a/logger.h b/logger.h new file mode 100644 index 0000000..4264b37 --- /dev/null +++ b/logger.h @@ -0,0 +1,27 @@ +#ifndef LOGGER_H +#define LOGGER_H + +#include +#include + +static const int LVL_FATAL = 0; /*!< Fatal message level */ +static const int LVL_ERR = 5; /*!< Error message level */ +static const int LVL_WARN = 50; /*!< Warning message level */ +static const int LVL_INFO = 100; /*!< Information message level */ +static const int LVL_DEBUG = 200; /*!< Debug message level */ +static const int LVL_DUMP = 500; /*!< Dump message level */ + +extern sem_t logger_semaphore; +extern int logger_verbosity; +extern int logger_use_colors; + +void logger_init(void); +void logger_shutdown(void); +void logger_enable_colors(int enable); +void logger_set_verbosity(int verbosity); + +void logger_log(int level, const char *format, ...); + +#define LOG(level, ...) logger_log(level, __VA_ARGS__) + +#endif // LOGGER_H diff --git a/main.c b/main.c new file mode 100644 index 0000000..14ac652 --- /dev/null +++ b/main.c @@ -0,0 +1,269 @@ +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +#include + +#include "logger.h" +#include "templates.h" +#include "dirlisting.h" + +#define DEFAULT_PORT 8888 + +struct ConnectionState { + char cleanedURL[PATH_MAX]; + char localFileName[PATH_MAX]; + struct stat targetStat; +}; + +char *shareRoot; +struct MHD_Response *error403Response; +struct MHD_Response *error404Response; +struct MHD_Response *error500Response; + +int running = 1; + +void remove_trailing_slash(char *str) { + size_t offset = strlen(str)-1; + if((offset != 0) && (str[offset] == '/')) { + str[offset] = '\0'; + } +} + +void request_completed(void *cls, + struct MHD_Connection * connection, + void **ptr, + enum MHD_RequestTerminationCode toe) { + struct ConnectionState *connstate = *ptr; + + // TODO: show IP+Port in log + LOG(LVL_DEBUG, "Freeing state for request of %s", connstate->localFileName); + + free(connstate); +} + +static int connection_handler(void * cls, + struct MHD_Connection *connection, + const char *url, + const char *method, + const char *version, + const char *upload_data, + size_t *upload_data_size, + void **ptr) { + struct ConnectionState *connstate; + + struct MHD_Response *response; + int ret; + + if (0 != strcmp(method, "GET")) { + LOG(LVL_WARN, "Unexpected method: %s.", method); + return MHD_NO; + } + + if (*ptr == NULL) { + // This is the first time this connection is seen. + // Initialize the connection state + connstate = malloc(sizeof(struct ConnectionState)); + *ptr = connstate; + + // set the local file name + strncpy(connstate->localFileName, shareRoot, PATH_MAX); + strncat(connstate->localFileName, url, + PATH_MAX - strlen(connstate->localFileName)); + + remove_trailing_slash(connstate->localFileName); + + strncpy(connstate->cleanedURL, url, PATH_MAX); + remove_trailing_slash(connstate->cleanedURL); + + LOG(LVL_INFO, "New %s request for %s (local: %s)", + method, url, connstate->localFileName); + + return MHD_YES; + } else { + connstate = *ptr; + } + + if (0 != *upload_data_size) { + return MHD_NO; // upload data in a GET!? + } + + // check if url contains a "/.." sequence. If so, block the request as it is + // trying to access the parent directory (and may be an attempt to leave the + // shared space. + if(NULL != strstr(url, "/..")) { + LOG(LVL_WARN, "User is trying to access %s, which may be an attempt to leave the shared space.", + url); + + return MHD_queue_response(connection, MHD_HTTP_FORBIDDEN, error403Response); + } + + + // check properties of the target file/dir + if(stat(connstate->localFileName, &(connstate->targetStat)) == -1) { + LOG(LVL_ERR, "Cannot stat %s: %s", + connstate->localFileName, strerror(errno)); + + return MHD_queue_response(connection, MHD_HTTP_NOT_FOUND, error404Response); + } + + if(S_ISREG(connstate->targetStat.st_mode)) { + LOG(LVL_DEBUG, "Serving %s as a regular file.", connstate->localFileName); + + // open a file descriptor to the file + int fd = open(connstate->localFileName, O_RDONLY); + + if(fd == -1) { + LOG(LVL_ERR, "Cannot open %s: %s", + connstate->localFileName, strerror(errno)); + + return MHD_queue_response(connection, MHD_HTTP_FORBIDDEN, error403Response); + } + + // no error so far -> serve the file + response = MHD_create_response_from_fd( + connstate->targetStat.st_size, + fd); + + ret = MHD_queue_response(connection, MHD_HTTP_OK, response); + MHD_destroy_response(response); + return ret; + } else if(S_ISDIR(connstate->targetStat.st_mode)) { + LOG(LVL_DEBUG, "Generating directory listing for %s.", connstate->localFileName); + + char *result = create_dirlisting(connstate->cleanedURL, connstate->localFileName); + if(!result) { + LOG(LVL_ERR, "Failed to generate dirlisting for %s.", connstate->localFileName); + return MHD_queue_response(connection, MHD_HTTP_INTERNAL_SERVER_ERROR, error500Response); + } + + response = MHD_create_response_from_buffer( + strlen(result), result, MHD_RESPMEM_MUST_FREE); + + MHD_add_response_header(response, MHD_HTTP_HEADER_CONTENT_TYPE, "text/html"); + + ret = MHD_queue_response(connection, MHD_HTTP_OK, response); + MHD_destroy_response(response); + return ret; + } else { + LOG(LVL_WARN, + "%s is neither a directory nor a regular file. Don't allow the access.", + connstate->localFileName); + + return MHD_queue_response(connection, MHD_HTTP_FORBIDDEN, error403Response); + } + + // generate a 500 if request was not handled properly + return MHD_queue_response( + connection, MHD_HTTP_INTERNAL_SERVER_ERROR, error500Response); +} + +int main(int argc, char ** argv) { + struct MHD_Daemon *d; + struct stat sBuf; + + int port; + + logger_init(); + + if (argc < 2) { + printf("%s []\n", argv[0]); + return 1; + } + + // check if shareRoot is an existing directory + shareRoot = argv[1]; + if(stat(shareRoot, &sBuf) == -1) { + LOG(LVL_FATAL, "Cannot stat %s: %s", shareRoot, strerror(errno)); + return EXIT_FAILURE; + } + + if(!S_ISDIR(sBuf.st_mode)) { + LOG(LVL_FATAL, "%s is not a directory!", shareRoot); + return EXIT_FAILURE; + } + + // remove trailing / from shareRoot + remove_trailing_slash(shareRoot); + + LOG(LVL_INFO, "Shared directory is: %s", shareRoot); + + if(argc >= 3) { + // a port was given on the command line, try to parse it + if(sscanf(argv[2], "%i", &port) != 1) { + LOG(LVL_FATAL, "%s is not a valid port number!", argv[2]); + return EXIT_FAILURE; + } + + if(port < 1 || port > 65535) { + LOG(LVL_FATAL, "Port %i is out of range [1-65535]!", port); + return EXIT_FAILURE; + } + + LOG(LVL_INFO, "Using port: %i", port); + } else { + LOG(LVL_INFO, "Using default port: %i", DEFAULT_PORT); + port = DEFAULT_PORT; + } + + // create the static response for error pages + error403Response = MHD_create_response_from_data( + strlen(ERROR_403), + (void*) ERROR_403, + MHD_NO, + MHD_NO); + + error404Response = MHD_create_response_from_data( + strlen(ERROR_404), + (void*) ERROR_404, + MHD_NO, + MHD_NO); + + error500Response = MHD_create_response_from_data( + strlen(ERROR_500), + (void*) ERROR_500, + MHD_NO, + MHD_NO); + + MHD_add_response_header(error403Response, MHD_HTTP_HEADER_CONTENT_TYPE, "text/html"); + MHD_add_response_header(error404Response, MHD_HTTP_HEADER_CONTENT_TYPE, "text/html"); + MHD_add_response_header(error500Response, MHD_HTTP_HEADER_CONTENT_TYPE, "text/html"); + + d = MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION, + port, + NULL, + NULL, + &connection_handler, + NULL, + MHD_OPTION_NOTIFY_COMPLETED, + &request_completed, NULL, + MHD_OPTION_END); + if (d == NULL) { + LOG(LVL_FATAL, "Cannot start up libmicrohttpd."); + return EXIT_FAILURE; + } + + while(running) { + getc(stdin); + } + + LOG(LVL_INFO, "Shutting down..."); + + MHD_stop_daemon(d); + + MHD_destroy_response(error403Response); + MHD_destroy_response(error404Response); + MHD_destroy_response(error500Response); + + logger_shutdown(); + return EXIT_SUCCESS; +} diff --git a/templates.h b/templates.h new file mode 100644 index 0000000..13398d1 --- /dev/null +++ b/templates.h @@ -0,0 +1,79 @@ +#ifndef TEMPLATES_H +#define TEMPLATES_H + +// generic definitions +#define HEADER1 \ + "" \ + "" \ + " " + +#define HEADER2 \ + " " \ + " " \ + " " + +#define FOOTER \ + " " \ + "" + + +// error page definitions +#define ERROR_404 \ + HEADER1 \ + " Error 404*i - File is imaginary" \ + HEADER2 \ + "

Error 404*i - File is imaginary

" \ + "

This page cannot be displayed because it is imaginary.

" \ + "

To view this page in the real world, turn your screen by π/2 .

" \ + "
" \
+  "            ^ Im\n" \
+  "            |\n" \
+  "            | +j\n" \
+  "        __--+--__\n" \
+  "      _-    |    -_\n" \
+  "     /      X      \\\n" \
+  "    |       |       |\n" \
+  "----+-------+-------+-----> Re\n" \
+  " -1 |       |    +1 |\n" \
+  "     \\      |      /\n" \
+  "      --__  |  __--\n" \
+  "          --+-- \n" \
+  "            | -j\n" \
+  "            |\n" \
+  "		
" \ + FOOTER + +#define ERROR_500 \ + HEADER1 \ + " Error 500 - Internal Server Error" \ + HEADER2 \ + "

Error 500 - Internal Server Error

" \ + "
" \
+  " _______________________________________ \n" \
+  "/ Oops. Something went wrong.           \\\n" \
+  "| Please contact the administrator and  |\n" \
+  "\\ tell him how to reproduce this error. /\n" \
+  " --------------------------------------- \n" \
+  "       \\   \\_______\n" \
+  " v__v   \\  \\   O   )\n" \
+  " (oo)      ||----w |\n" \
+  " (__)      ||     ||  \\/\\\n" \
+  "		
" \ + FOOTER + +#define ERROR_403 \ + HEADER1 \ + " Error 403 - Forbidden" \ + HEADER2 \ + "

Error 403 - Forbidden

" \ + "

You are not allowed to access this file or directory.

" \ + "

This error is shown under the following conditions:

" \ + "

    " \ + "
  • You are trying to access a file that the server isn't allowed to read
  • " \ + "
  • The target filesystem entry is not a regular file or directory
  • " \ + "
  • The URL contains \"/..\", which is blocked for security reasons
  • " \ + "
" \ + FOOTER + +#endif // TEMPLATES_H