fileshare/main.c

526 lines
14 KiB
C

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <netdb.h>
#include <sys/socket.h>
#include <linux/limits.h>
#include <microhttpd.h>
#ifdef HAVE_MAGIC
#include <magic.h>
#endif
#include "logger.h"
#include "templates.h"
#include "dirlisting.h"
#include "favicon.h"
#include "util.h"
#define DEFAULT_PORT 8888
struct ConnectionState {
char cleanedURL[PATH_MAX];
char localFileName[PATH_MAX];
struct stat targetStat;
};
struct RequestRange {
off_t start;
off_t length;
};
char *shareRoot;
struct MHD_Response *error403Response;
struct MHD_Response *error404Response;
struct MHD_Response *error500Response;
struct MHD_Response *faviconResponse;
#ifdef HAVE_MAGIC
magic_t magicCookie;
#endif
int running = 1;
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);
}
int parse_range(const char *range, off_t total_len, struct RequestRange *result) {
char *numstr;
char *dashptr;
int dashpos;
off_t num;
LOG(LVL_DEBUG, "Requested Range is %s", range);
if(range == NULL) {
result->start = 0;
result->length = total_len;
return 0;
}
numstr = strchr(range, '=');
if(numstr == NULL) {
return -1;
}
numstr += 1;
LOG(LVL_DUMP, "Numeric part of range: %s", numstr);
dashptr = strchr(numstr, '-');
if(dashptr == NULL) {
// no '-' found
return -1;
}
dashpos = dashptr - numstr;
LOG(LVL_DUMP, "Dash found at position: %i", dashpos);
if(dashpos == 0) {
// example: bytes=-500
// -> letzte 500 bytes ausgeben
if(sscanf(dashptr+1, "%li", &num) != 1) {
return -1;
}
// total = 1000, num = 100 -> start=500, length=500
result->start = total_len - num - 1;
result->length = num;
} else {
// examples: bytes=100-200; bytes=300-
// parse the first number
if(sscanf(numstr, "%li", &num) != 1) {
return -1;
}
result->start = num;
if(numstr[dashpos + 1] == '\0') {
// everything from num to the end
result->length = total_len - num;
return 0;
}
// a complete range was given -> parse the second number
if(sscanf(numstr+dashpos+1, "%li", &num) != 1) {
return -1;
}
if(num < result->start) {
return -1;
}
result->length = num - result->start + 1;
}
return 0;
}
int serv_regular_file(struct MHD_Connection *connection, struct ConnectionState *connstate) {
struct MHD_Response *response;
struct RequestRange range;
const char *rangestr;
char buf[256];
int ret;
LOG(LVL_DEBUG, "Serving %s as a regular file.", connstate->localFileName);
rangestr = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "Range");
if(0 != parse_range(rangestr, connstate->targetStat.st_size, &range)) {
LOG(LVL_ERR, "Cannot parse range header.");
return MHD_queue_response(connection, MHD_HTTP_INTERNAL_SERVER_ERROR, error500Response);
}
LOG(LVL_DUMP, "Requested range: start=%lu, length=%lu", range.start, range.length);
// 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_at_offset(
range.length,
fd,
range.start);
// build content range header
sprintf(buf, "bytes %li-%li/%li", range.start, range.start+range.length-1, connstate->targetStat.st_size);
MHD_add_response_header(response, MHD_HTTP_HEADER_CONTENT_RANGE, buf);
LOG(LVL_DUMP, "Content-Range: %s", buf);
#ifdef HAVE_MAGIC
// if libmagic is available, determine the correct MIME type for the file
const char *mimeType = magic_file(magicCookie, connstate->localFileName);
LOG(LVL_DEBUG, "MIME type for %s: %s", connstate->localFileName, mimeType);
MHD_add_response_header(response, MHD_HTTP_HEADER_CONTENT_TYPE, mimeType);
#endif
ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
MHD_destroy_response(response);
return ret;
}
int serv_directory(struct MHD_Connection *connection, struct ConnectionState *connstate) {
struct MHD_Response *response;
int ret;
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;
}
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;
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, "%s %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!?
}
// serv the favicon, if requested
if(strcmp(url, "/favicon.png") == 0) {
LOG(LVL_INFO, "Serving FavIcon request.");
return MHD_queue_response(connection, MHD_HTTP_OK, faviconResponse);
}
// 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)) {
return serv_regular_file(connection, connstate);
} else if(S_ISDIR(connstate->targetStat.st_mode)) {
return serv_directory(connection, connstate);
} 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);
}
// signal handler for SIGTERM, SIGINT, etc.
// sets the flag for a clean shutdown
void sig_shutdown_handler(int sig) {
LOG(LVL_DEBUG, "Handling signal: %i", sig);
running = 0;
}
void init_signal_handlers(void) {
struct sigaction sa;
sa.sa_handler = sig_shutdown_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_restorer = NULL;
if(sigaction(SIGTERM, &sa, NULL) == -1) {
LOG(LVL_ERR, "sigaction [SIGTERM] failed: %s", strerror(errno));
}
if(sigaction(SIGINT, &sa, NULL) == -1) {
LOG(LVL_ERR, "sigaction [SIGINT] failed: %s", strerror(errno));
}
sa.sa_handler = SIG_IGN;
if(sigaction(SIGPIPE, &sa, NULL) == -1) {
LOG(LVL_ERR, "sigaction [SIGPIPE] failed: %s", strerror(errno));
}
}
void print_urls(int port) {
struct ifaddrs *ifaddr, *ifa;
int family, s;
char host[INET6_ADDRSTRLEN];
if(getifaddrs(&ifaddr) == -1) {
LOG(LVL_ERR, "getifaddrs failed: %s", strerror(errno));
return;
}
for(ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) {
if(ifa->ifa_addr == NULL) {
continue;
}
family = ifa->ifa_addr->sa_family;
if(family == AF_INET || family == AF_INET6) {
s = getnameinfo(ifa->ifa_addr,
(family == AF_INET) ? sizeof(struct sockaddr_in)
: sizeof(struct sockaddr_in6),
host,
INET6_ADDRSTRLEN,
NULL, 0,
NI_NUMERICHOST);
if(s != 0) {
LOG(LVL_ERR, "getnameinfo failed: %s", gai_strerror(s));
continue;
}
if(family == AF_INET) {
LOG(LVL_INFO, "%-8s: http://%s:%i", ifa->ifa_name, host, port);
} else { // IPv6
LOG(LVL_INFO, "%-8s: http://[%s]:%i", ifa->ifa_name, host, port);
}
}
}
freeifaddrs(ifaddr);
}
int main(int argc, char ** argv) {
struct MHD_Daemon *d, *d6;
struct stat sBuf;
int port;
logger_init();
#ifndef DEBUG
// don't show debug output in release build
logger_set_verbosity(LVL_INFO);
#endif
if (argc < 2) {
printf("%s <dir> [<port>]\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;
}
#ifdef HAVE_MAGIC
// initialize libmagic
magicCookie = magic_open(MAGIC_SYMLINK | MAGIC_MIME_TYPE);
if(!magicCookie) {
LOG(LVL_ERR, "Cannot allocate magic cookie");
} else {
// load the default database
if(magic_load(magicCookie, NULL) == -1) {
LOG(LVL_ERR, "Cannot load default magic database: %s", magic_error(magicCookie));
magic_close(magicCookie);
magicCookie = 0;
} else {
LOG(LVL_INFO, "libmagic initialized successfully.");
}
}
#endif
// setup the signal handlers
init_signal_handlers();
// 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");
// static response for favicon
faviconResponse = create_favicon_response();
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_ERR, "Cannot start up libmicrohttpd listening on IPv4.");
}
d6 = MHD_start_daemon(MHD_USE_THREAD_PER_CONNECTION | MHD_USE_IPv6,
port,
NULL,
NULL,
&connection_handler,
NULL,
MHD_OPTION_NOTIFY_COMPLETED,
&request_completed, NULL,
MHD_OPTION_END);
if (d6 == NULL) {
LOG(LVL_ERR, "Cannot start up libmicrohttpd listening on IPv6.");
}
if(d == NULL && d6 == NULL) {
LOG(LVL_FATAL, "Both IPv4 and IPv6 servers failed to start. Terminating.");
return EXIT_FAILURE;
}
LOG(LVL_INFO, "Startup successful. Here are the IP addresses for this computer:");
print_urls(port);
while(running) {
sleep(60);
}
LOG(LVL_INFO, "Shutting down...");
MHD_stop_daemon(d);
MHD_stop_daemon(d6);
MHD_destroy_response(error403Response);
MHD_destroy_response(error404Response);
MHD_destroy_response(error500Response);
MHD_destroy_response(faviconResponse);
#ifdef HAVE_MAGIC
magic_close(magicCookie);
#endif
LOG(LVL_INFO, "Thanks for using fileshare.");
logger_shutdown();
return EXIT_SUCCESS;
}