From 6cd65091ff3c0faec778730b3c3f1477bc8dad4c Mon Sep 17 00:00:00 2001 From: Thomas Kolb Date: Thu, 17 Jan 2013 21:13:15 +0100 Subject: [PATCH] Initial commit - Working version with the following features: - Directory listing and navigation - Download files - HTTP error pages (403, 404, 500) - Verbose logging --- .gitignore | 6 ++ Makefile | 24 +++++ dirlisting.c | 269 +++++++++++++++++++++++++++++++++++++++++++++++++++ dirlisting.h | 6 ++ logger.c | 151 +++++++++++++++++++++++++++++ logger.h | 27 ++++++ main.c | 269 +++++++++++++++++++++++++++++++++++++++++++++++++++ templates.h | 79 +++++++++++++++ 8 files changed, 831 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 dirlisting.c create mode 100644 dirlisting.h create mode 100644 logger.c create mode 100644 logger.h create mode 100644 main.c create mode 100644 templates.h 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