405 lines
12 KiB
C
405 lines
12 KiB
C
/**
|
|
* sesimos - secure, simple, modern web server
|
|
* @brief File cache implementation
|
|
* @file src/cache_handler.c
|
|
* @author Lorenz Stechauner
|
|
* @date 2020-12-19
|
|
*/
|
|
|
|
#include "logger.h"
|
|
#include "cache_handler.h"
|
|
#include "lib/utils.h"
|
|
#include "lib/compress.h"
|
|
#include "lib/config.h"
|
|
|
|
#include <stdio.h>
|
|
#include <magic.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <openssl/evp.h>
|
|
#include <sys/mman.h>
|
|
#include <fcntl.h>
|
|
#include <pthread.h>
|
|
#include <semaphore.h>
|
|
#include <signal.h>
|
|
|
|
#define CACHE_BUF_SIZE 16
|
|
|
|
|
|
static magic_t magic;
|
|
static pthread_t thread;
|
|
static sem_t sem_free, sem_used, sem_lock;
|
|
volatile sig_atomic_t alive = 1;
|
|
|
|
typedef struct {
|
|
int rd;
|
|
int wr;
|
|
cache_entry_t *msgs[CACHE_BUF_SIZE];
|
|
} buf_t;
|
|
|
|
static buf_t buffer;
|
|
|
|
static int magic_init(void) {
|
|
if ((magic = magic_open(MAGIC_MIME)) == NULL) {
|
|
critical("Unable to open magic cookie");
|
|
return 1;
|
|
}
|
|
|
|
if (magic_load(magic, CACHE_MAGIC_FILE) != 0) {
|
|
critical("Unable to load magic cookie: %s", magic_error(magic));
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void cache_free(void) {
|
|
for (int i = 0; i < CONFIG_MAX_HOST_CONFIG; i++) {
|
|
host_config_t *hc = &config.hosts[i];
|
|
if (hc->type == CONFIG_TYPE_UNSET) break;
|
|
if (hc->type != CONFIG_TYPE_LOCAL) continue;
|
|
|
|
munmap(hc->cache, sizeof(cache_t));
|
|
}
|
|
}
|
|
|
|
static cache_entry_t *cache_get_entry(cache_t *cache, const char *filename) {
|
|
// search entry
|
|
cache_entry_t *entry;
|
|
for (int i = 0; i < CACHE_ENTRIES; i++) {
|
|
entry = &cache->entries[i];
|
|
if (entry->filename[0] == 0) break;
|
|
if (strcmp(entry->filename, filename) == 0) {
|
|
// found
|
|
return entry;
|
|
}
|
|
}
|
|
|
|
// not found
|
|
return NULL;
|
|
}
|
|
|
|
static cache_entry_t *cache_get_new_entry(cache_t *cache) {
|
|
// search empty slot
|
|
cache_entry_t *entry;
|
|
for (int i = 0; i < CACHE_ENTRIES; i++) {
|
|
entry = &cache->entries[i];
|
|
if (entry->filename[0] == 0)
|
|
return entry;
|
|
}
|
|
|
|
// not found
|
|
return NULL;
|
|
}
|
|
|
|
static void cache_process_entry(cache_entry_t *entry) {
|
|
char buf[16384], comp_buf[16384], filename_comp_gz[256], filename_comp_br[256];
|
|
|
|
info("Hashing file %s", entry->filename);
|
|
|
|
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
|
|
EVP_DigestInit(ctx, EVP_sha1());
|
|
FILE *file = fopen(entry->filename, "rb");
|
|
int compress = mime_is_compressible(entry->meta.type);
|
|
|
|
compress_ctx comp_ctx;
|
|
FILE *comp_file_gz = NULL, *comp_file_br = NULL;
|
|
if (compress) {
|
|
sprintf(buf, "%.*s/.sesimos", entry->webroot_len, entry->filename);
|
|
if (mkdir(buf, 0755) != 0 && errno != EEXIST) {
|
|
error("Unable to create directory %s", buf);
|
|
goto comp_err;
|
|
}
|
|
|
|
sprintf(buf, "%.*s/.sesimos/cache", entry->webroot_len, entry->filename);
|
|
if (mkdir(buf, 0700) != 0 && errno != EEXIST) {
|
|
error("Unable to create directory %s", buf);
|
|
goto comp_err;
|
|
}
|
|
errno = 0;
|
|
|
|
char *rel_path = entry->filename + entry->webroot_len + 1;
|
|
for (int j = 0; j < strlen(rel_path); j++) {
|
|
char ch = rel_path[j];
|
|
if (ch == '/') ch = '_';
|
|
buf[j] = ch;
|
|
}
|
|
buf[strlen(rel_path)] = 0;
|
|
|
|
int p_len_gz = snprintf(filename_comp_gz, sizeof(filename_comp_gz),
|
|
"%.*s/.sesimos/cache/%s.gz",
|
|
entry->webroot_len, entry->filename, buf);
|
|
int p_len_br = snprintf(filename_comp_br, sizeof(filename_comp_br),
|
|
"%.*s/.sesimos/cache/%s.br",
|
|
entry->webroot_len, entry->filename, buf);
|
|
if (p_len_gz < 0 || p_len_gz >= sizeof(filename_comp_gz) || p_len_br < 0 || p_len_br >= sizeof(filename_comp_br)) {
|
|
error("Unable to open cached file: File name for compressed file too long");
|
|
goto comp_err;
|
|
}
|
|
|
|
info("Compressing file %s", entry->filename);
|
|
|
|
comp_file_gz = fopen(filename_comp_gz, "wb");
|
|
comp_file_br = fopen(filename_comp_br, "wb");
|
|
if (comp_file_gz == NULL || comp_file_br == NULL) {
|
|
error("Unable to open cached file");
|
|
comp_err:
|
|
compress = 0;
|
|
} else {
|
|
if ((compress_init(&comp_ctx, COMPRESS_GZ | COMPRESS_BR)) != 0) {
|
|
error("Unable to init compression");
|
|
compress = 0;
|
|
fclose(comp_file_gz);
|
|
fclose(comp_file_br);
|
|
}
|
|
}
|
|
}
|
|
|
|
unsigned long read;
|
|
while ((read = fread(buf, 1, sizeof(buf), file)) > 0) {
|
|
EVP_DigestUpdate(ctx, buf, read);
|
|
if (compress) {
|
|
unsigned long avail_in, avail_out;
|
|
avail_in = read;
|
|
do {
|
|
avail_out = sizeof(comp_buf);
|
|
compress_compress_mode(&comp_ctx, COMPRESS_GZ, buf + read - avail_in, &avail_in, comp_buf, &avail_out, feof(file));
|
|
fwrite(comp_buf, 1, sizeof(comp_buf) - avail_out, comp_file_gz);
|
|
} while (avail_in != 0 || avail_out != sizeof(comp_buf));
|
|
avail_in = read;
|
|
do {
|
|
avail_out = sizeof(comp_buf);
|
|
compress_compress_mode(&comp_ctx, COMPRESS_BR, buf + read - avail_in, &avail_in, comp_buf, &avail_out, feof(file));
|
|
fwrite(comp_buf, 1, sizeof(comp_buf) - avail_out, comp_file_br);
|
|
} while (avail_in != 0 || avail_out != sizeof(comp_buf));
|
|
}
|
|
}
|
|
|
|
if (compress) {
|
|
compress_free(&comp_ctx);
|
|
fclose(comp_file_gz);
|
|
fclose(comp_file_br);
|
|
info("Finished compressing file %s", entry->filename);
|
|
strcpy(entry->meta.filename_comp_gz, filename_comp_gz);
|
|
strcpy(entry->meta.filename_comp_br, filename_comp_br);
|
|
} else {
|
|
memset(entry->meta.filename_comp_gz, 0, sizeof(entry->meta.filename_comp_gz));
|
|
memset(entry->meta.filename_comp_br, 0, sizeof(entry->meta.filename_comp_br));
|
|
}
|
|
|
|
unsigned int md_len;
|
|
unsigned char hash[EVP_MAX_MD_SIZE];
|
|
EVP_DigestFinal(ctx, hash, &md_len);
|
|
EVP_MD_CTX_free(ctx);
|
|
|
|
memset(entry->meta.etag, 0, sizeof(entry->meta.etag));
|
|
for (int j = 0; j < md_len; j++) {
|
|
sprintf(entry->meta.etag + j * 2, "%02x", hash[j]);
|
|
}
|
|
fclose(file);
|
|
entry->flags &= !CACHE_DIRTY;
|
|
|
|
info("Finished hashing file %s", entry->filename);
|
|
}
|
|
|
|
static void *cache_thread(void *arg) {
|
|
logger_set_name("cache");
|
|
|
|
while (alive) {
|
|
pthread_testcancel();
|
|
if (sem_wait(&sem_used) != 0) {
|
|
if (errno == EINTR) {
|
|
errno = 0;
|
|
continue;
|
|
} else {
|
|
error("Unable to lock semaphore");
|
|
errno = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
cache_entry_t *entry = buffer.msgs[buffer.wr];
|
|
buffer.wr = (buffer.wr + 1) % CACHE_BUF_SIZE;
|
|
|
|
cache_process_entry(entry);
|
|
|
|
// unlock slot in buffer
|
|
sem_post(&sem_free);
|
|
}
|
|
|
|
cache_free();
|
|
|
|
return NULL;
|
|
}
|
|
|
|
int cache_init(void) {
|
|
char buf[512];
|
|
int ret, fd;
|
|
if ((ret = magic_init()) != 0)
|
|
return ret;
|
|
|
|
for (int i = 0; i < CONFIG_MAX_HOST_CONFIG; i++) {
|
|
host_config_t *hc = &config.hosts[i];
|
|
if (hc->type == CONFIG_TYPE_UNSET) break;
|
|
if (hc->type != CONFIG_TYPE_LOCAL) continue;
|
|
|
|
sprintf(buf, "%s/.sesimos", hc->local.webroot);
|
|
if (mkdir(buf, 0755) != 0 && errno != EEXIST) {
|
|
critical("Unable to create directory %s", buf);
|
|
return 1;
|
|
}
|
|
errno = 0;
|
|
|
|
sprintf(buf, "%s/.sesimos/metadata", hc->local.webroot);
|
|
if ((fd = open(buf, O_CREAT | O_RDWR, 0600)) == -1) {
|
|
critical("Unable to open file %s", buf);
|
|
return 1;
|
|
}
|
|
|
|
if (ftruncate(fd, sizeof(cache_t)) == -1) {
|
|
critical("Unable to truncate file %s", buf);
|
|
return 1;
|
|
}
|
|
|
|
if ((hc->cache = mmap(NULL, sizeof(cache_t), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == NULL) {
|
|
critical("Unable to map file %s", buf);
|
|
close(fd);
|
|
return 1;
|
|
}
|
|
|
|
close(fd);
|
|
errno = 0;
|
|
}
|
|
|
|
// try to initialize all three semaphores
|
|
if (sem_init(&sem_lock, 0, 1) != 0 ||
|
|
sem_init(&sem_free, 0, 1) != 0 ||
|
|
sem_init(&sem_used, 0, 0) != 0)
|
|
{
|
|
critical("Unable to initialize semaphore");
|
|
return -1;
|
|
}
|
|
|
|
// initialize read/write heads
|
|
buffer.rd = 0;
|
|
buffer.wr = 0;
|
|
|
|
pthread_create(&thread, NULL, cache_thread, NULL);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void cache_stop(void) {
|
|
alive = 0;
|
|
pthread_kill(thread, SIGUSR1);
|
|
}
|
|
|
|
int cache_join(void) {
|
|
return pthread_join(thread, NULL);
|
|
}
|
|
|
|
static void cache_mark_entry_dirty(cache_entry_t *entry) {
|
|
if (entry->flags & CACHE_DIRTY)
|
|
return;
|
|
|
|
memset(entry->meta.etag, 0, sizeof(entry->meta.etag));
|
|
memset(entry->meta.filename_comp_gz, 0, sizeof(entry->meta.filename_comp_gz));
|
|
memset(entry->meta.filename_comp_br, 0, sizeof(entry->meta.filename_comp_br));
|
|
entry->flags |= CACHE_DIRTY;
|
|
|
|
try_again_free:
|
|
if (sem_wait(&sem_free) != 0) {
|
|
if (errno == EINTR) {
|
|
errno = 0;
|
|
goto try_again_free;
|
|
} else {
|
|
error("Unable to lock semaphore");
|
|
errno = 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// try to lock buffer
|
|
try_again_lock:
|
|
if (sem_wait(&sem_lock) != 0) {
|
|
if (errno == EINTR) {
|
|
errno = 0;
|
|
goto try_again_lock;
|
|
} else {
|
|
error("Unable to lock semaphore");
|
|
errno = 0;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// write to buffer
|
|
buffer.msgs[buffer.rd] = entry;
|
|
buffer.rd = (buffer.rd + 1) % CACHE_BUF_SIZE;
|
|
|
|
// unlock buffer
|
|
sem_post(&sem_lock);
|
|
|
|
// unlock slot in buffer for logger
|
|
sem_post(&sem_used);
|
|
}
|
|
|
|
static void cache_update_entry(cache_entry_t *entry, const char *filename, const char *webroot) {
|
|
struct stat statbuf;
|
|
stat(filename, &statbuf);
|
|
memcpy(&entry->meta.stat, &statbuf, sizeof(statbuf));
|
|
|
|
entry->webroot_len = (unsigned char) strlen(webroot);
|
|
strcpy(entry->filename, filename);
|
|
|
|
magic_setflags(magic, MAGIC_MIME_TYPE);
|
|
const char *type = magic_file(magic, filename);
|
|
char type_new[URI_TYPE_SIZE];
|
|
sprintf(type_new, "%s", type);
|
|
if (strncmp(type, "text/", 5) == 0) {
|
|
if (strcmp(filename + strlen(filename) - 4, ".css") == 0) {
|
|
sprintf(type_new, "text/css");
|
|
} else if (strcmp(filename + strlen(filename) - 3, ".js") == 0) {
|
|
sprintf(type_new, "application/javascript");
|
|
}
|
|
}
|
|
strcpy(entry->meta.type, type_new);
|
|
|
|
magic_setflags(magic, MAGIC_MIME_ENCODING);
|
|
strcpy(entry->meta.charset, magic_file(magic, filename));
|
|
|
|
cache_mark_entry_dirty(entry);
|
|
}
|
|
|
|
void cache_mark_dirty(cache_t *cache, const char *filename) {
|
|
cache_entry_t *entry = cache_get_entry(cache, filename);
|
|
if (entry) cache_mark_entry_dirty(entry);
|
|
}
|
|
|
|
void cache_init_uri(cache_t *cache, http_uri *uri) {
|
|
if (!uri->filename)
|
|
return;
|
|
|
|
cache_entry_t *entry = cache_get_entry(cache, uri->filename);
|
|
if (!entry) {
|
|
// no entry found -> create new entry
|
|
if ((entry = cache_get_new_entry(cache))) {
|
|
cache_update_entry(entry, uri->filename, uri->webroot);
|
|
uri->meta = &entry->meta;
|
|
} else {
|
|
warning("No empty cache entry slot found");
|
|
}
|
|
} else {
|
|
uri->meta = &entry->meta;
|
|
if (entry->flags & CACHE_DIRTY)
|
|
return;
|
|
|
|
// check, if file has changed
|
|
struct stat statbuf;
|
|
stat(uri->filename, &statbuf);
|
|
if (memcmp(&uri->meta->stat.st_mtime, &statbuf.st_mtime, sizeof(statbuf.st_mtime)) != 0) {
|
|
// modify time has changed
|
|
cache_update_entry(entry, uri->filename, uri->webroot);
|
|
}
|
|
}
|
|
}
|