From 704b92c394845c8913ab89c5dd8f6703e216c444 Mon Sep 17 00:00:00 2001 From: Nikolay Govorov Date: Wed, 26 Aug 2026 01:50:29 +0100 Subject: Replace cgit ondisk html cache to cgi im-memory cache --- Dockerfile | 2 +- README.md | 1 - cgit/Makefile | 3 - cgit/cache.c | 479 ------------------------- cgit/cache.h | 41 --- cgit/cgit.c | 177 +-------- cgit/cgit.h | 13 +- cgit/cmd.c | 11 +- cgit/ui-shared.c | 3 +- charts/gilti/README.md | 6 +- charts/gilti/templates/configmap.yaml | 8 +- charts/gilti/templates/deployment.yaml | 5 - charts/gilti/values.schema.json | 4 +- charts/gilti/values.yaml | 3 +- config/cgitrc | 8 +- crates/gilti/src/cgi.rs | 276 +++++++++++++- crates/gilti/src/main.rs | 67 +++- scripts/entrypoint.sh | 3 +- tests/chart.sh | 10 + tests/smoke.sh | 20 +- 20 files changed, 386 insertions(+), 754 deletions(-) delete mode 100644 cgit/cache.c delete mode 100644 cgit/cache.h diff --git a/Dockerfile b/Dockerfile index d6b8f37..8c9c369 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,7 @@ RUN apk add --no-cache \ adduser -S -D -u 10000 -G git -h /var/lib/gilti/git -s /bin/sh git && \ passwd -d git && \ install -d -m 0750 -o git -g git \ - /var/lib/gilti/git /var/lib/gilti/git/repositories /var/cache/cgit && \ + /var/lib/gilti/git /var/lib/gilti/git/repositories && \ install -d -m 0700 -o root -g root /var/lib/gilti/ssh && \ install -d -m 0755 -o root -g root /run/gilti && \ install -d -m 0750 -o git -g git /run/gilti/http && \ diff --git a/README.md b/README.md index 3aae8bf..91b7b2a 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,6 @@ docker run --rm \ --cap-add SETGID --cap-add SETUID --cap-add SYS_CHROOT \ --tmpfs /run:rw,nosuid,nodev,noexec,size=32m \ --tmpfs /tmp:rw,nosuid,nodev,noexec,size=256m \ - --tmpfs /var/cache/cgit:rw,nosuid,nodev,noexec,size=1g \ -p 8080:8080 -p 2222:2222 \ -v gilti-state:/var/lib/gilti \ -v "$PWD/authorized_keys:/etc/gilti/authorized_keys:ro" \ diff --git a/cgit/Makefile b/cgit/Makefile index cee94d5..402983d 100644 --- a/cgit/Makefile +++ b/cgit/Makefile @@ -10,16 +10,13 @@ CGIT_PREFIX = ../ CGIT_VERSION = v1.3.1 CGIT_SCRIPT_NAME = gilti-cgit CGIT_CONFIG = /etc/cgitrc -CACHE_ROOT = /var/cache/cgit CGIT_CFLAGS += -DCGIT_CONFIG='"$(CGIT_CONFIG)"' CGIT_CFLAGS += -DCGIT_SCRIPT_NAME='"$(CGIT_SCRIPT_NAME)"' -CGIT_CFLAGS += -DCGIT_CACHE_ROOT='"$(CACHE_ROOT)"' CGIT_CFLAGS += -DCGIT_VERSION='"$(CGIT_VERSION)"' CGIT_CFLAGS += -DNO_LUA ifeq ($(uname_S),Linux) - CGIT_CFLAGS += -DHAVE_LINUX_SENDFILE CGIT_LIBS += -ldl endif diff --git a/cgit/cache.c b/cgit/cache.c deleted file mode 100644 index b026b2f..0000000 --- a/cgit/cache.c +++ /dev/null @@ -1,479 +0,0 @@ -/* SPDX-FileCopyrightText: cgit Development Team - * SPDX-License-Identifier: GPL-2.0-only - */ - -/* cache.c: cache management - * - * Copyright (C) 2006-2014 cgit Development Team - * - * Licensed under GNU General Public License v2 - * (see COPYING for full license text) - * - * - * The cache is just a directory structure where each file is a cache slot, - * and each filename is based on the hash of some key (e.g. the cgit url). - * Each file contains the full key followed by the cached content for that - * key. - * - */ - -#include "cgit.h" -#include "cache.h" -#include "html.h" -#ifdef HAVE_LINUX_SENDFILE -#include -#endif - -#define CACHE_BUFSIZE (1024 * 4) - -struct cache_slot { - const char *key; - size_t keylen; - int ttl; - cache_fill_fn fn; - int cache_fd; - int lock_fd; - int stdout_fd; - const char *cache_name; - const char *lock_name; - int match; - struct stat cache_st; - int bufsize; - char buf[CACHE_BUFSIZE]; -}; - -/* Open an existing cache slot and fill the cache buffer with - * (part of) the content of the cache file. Return 0 on success - * and errno otherwise. - */ -static int open_slot(struct cache_slot *slot) -{ - char *bufz; - ssize_t bufkeylen = -1; - - slot->cache_fd = open(slot->cache_name, O_RDONLY); - if (slot->cache_fd == -1) - return errno; - - if (fstat(slot->cache_fd, &slot->cache_st)) - return errno; - - slot->bufsize = xread(slot->cache_fd, slot->buf, sizeof(slot->buf)); - if (slot->bufsize < 0) - return errno; - - bufz = memchr(slot->buf, 0, slot->bufsize); - if (bufz) - bufkeylen = bufz - slot->buf; - - if (slot->key) - slot->match = bufkeylen == slot->keylen && - !memcmp(slot->key, slot->buf, bufkeylen + 1); - - return 0; -} - -/* Close the active cache slot */ -static int close_slot(struct cache_slot *slot) -{ - int err = 0; - if (slot->cache_fd > 0) { - if (close(slot->cache_fd)) - err = errno; - else - slot->cache_fd = -1; - } - return err; -} - -/* Print the content of the active cache slot (but skip the key). */ -static int print_slot(struct cache_slot *slot) -{ - off_t off; -#ifdef HAVE_LINUX_SENDFILE - off_t size; -#endif - - off = slot->keylen + 1; - -#ifdef HAVE_LINUX_SENDFILE - size = slot->cache_st.st_size; - - do { - ssize_t ret; - ret = sendfile(STDOUT_FILENO, slot->cache_fd, &off, size - off); - if (ret < 0) { - if (errno == EAGAIN || errno == EINTR) - continue; - /* Fall back to read/write on EINVAL or ENOSYS */ - if (errno == EINVAL || errno == ENOSYS) - break; - return errno; - } - if (off == size) - return 0; - } while (1); -#endif - - if (lseek(slot->cache_fd, off, SEEK_SET) != off) - return errno; - - do { - ssize_t ret; - ret = xread(slot->cache_fd, slot->buf, sizeof(slot->buf)); - if (ret < 0) - return errno; - if (ret == 0) - return 0; - if (write_in_full(STDOUT_FILENO, slot->buf, ret) < 0) - return errno; - } while (1); -} - -/* Check if the slot has expired */ -static int is_expired(struct cache_slot *slot) -{ - if (slot->ttl < 0) - return 0; - else - return slot->cache_st.st_mtime + slot->ttl * 60 < time(NULL); -} - -/* Check if the slot has been modified since we opened it. - * NB: If stat() fails, we pretend the file is modified. - */ -static int is_modified(struct cache_slot *slot) -{ - struct stat st; - - if (stat(slot->cache_name, &st)) - return 1; - return (st.st_ino != slot->cache_st.st_ino || - st.st_mtime != slot->cache_st.st_mtime || - st.st_size != slot->cache_st.st_size); -} - -/* Close an open lockfile */ -static int close_lock(struct cache_slot *slot) -{ - int err = 0; - if (slot->lock_fd > 0) { - if (close(slot->lock_fd)) - err = errno; - else - slot->lock_fd = -1; - } - return err; -} - -/* Create a lockfile used to store the generated content for a cache - * slot, and write the slot key + \0 into it. - * Returns 0 on success and errno otherwise. - */ -static int lock_slot(struct cache_slot *slot) -{ - struct flock lock = { - .l_type = F_WRLCK, - .l_whence = SEEK_SET, - .l_start = 0, - .l_len = 0, - }; - - slot->lock_fd = open(slot->lock_name, O_RDWR | O_CREAT, - S_IRUSR | S_IWUSR); - if (slot->lock_fd == -1) - return errno; - if (fcntl(slot->lock_fd, F_SETLK, &lock) < 0) { - int saved_errno = errno; - close(slot->lock_fd); - slot->lock_fd = -1; - return saved_errno; - } - if (ftruncate(slot->lock_fd, 0) < 0) - return errno; - if (xwrite(slot->lock_fd, slot->key, slot->keylen + 1) < 0) - return errno; - return 0; -} - -/* Release the current lockfile. If `replace_old_slot` is set the - * lockfile replaces the old cache slot, otherwise the lockfile is - * just deleted. - */ -static int unlock_slot(struct cache_slot *slot, int replace_old_slot) -{ - int err; - - if (replace_old_slot) - err = rename(slot->lock_name, slot->cache_name); - else - err = unlink(slot->lock_name); - - /* Restore stdout and close the temporary FD. */ - if (slot->stdout_fd >= 0) { - dup2(slot->stdout_fd, STDOUT_FILENO); - close(slot->stdout_fd); - slot->stdout_fd = -1; - } - - if (err) - return errno; - - return 0; -} - -/* Generate the content for the current cache slot by redirecting - * stdout to the lock-fd and invoking the callback function - */ -static int fill_slot(struct cache_slot *slot) -{ - /* Preserve stdout */ - slot->stdout_fd = dup(STDOUT_FILENO); - if (slot->stdout_fd == -1) - return errno; - - /* Redirect stdout to lockfile */ - if (dup2(slot->lock_fd, STDOUT_FILENO) == -1) - return errno; - - /* Generate cache content */ - slot->fn(); - - /* Make sure any buffered data is flushed to the file */ - if (fflush(stdout)) - return errno; - - /* update stat info */ - if (fstat(slot->lock_fd, &slot->cache_st)) - return errno; - - return 0; -} - -/* Crude implementation of 32-bit FNV-1 hash algorithm, - * see http://www.isthe.com/chongo/tech/comp/fnv/ for details - * about the magic numbers. - */ -#define FNV_OFFSET 0x811c9dc5 -#define FNV_PRIME 0x01000193 - -unsigned long hash_str(const char *str) -{ - unsigned long h = FNV_OFFSET; - unsigned char *s = (unsigned char *)str; - - if (!s) - return h; - - while (*s) { - h *= FNV_PRIME; - h ^= *s++; - } - return h; -} - -static int process_slot(struct cache_slot *slot) -{ - int err; - - err = open_slot(slot); - if (!err && slot->match) { - if (is_expired(slot)) { - if (!lock_slot(slot)) { - /* If the cachefile has been replaced between - * `open_slot` and `lock_slot`, we'll just - * serve the stale content from the original - * cachefile. This way we avoid pruning the - * newly generated slot. The same code-path - * is chosen if fill_slot() fails for some - * reason. - * - * TODO? check if the new slot contains the - * same key as the old one, since we would - * prefer to serve the newest content. - * This will require us to open yet another - * file-descriptor and read and compare the - * key from the new file, so for now we're - * lazy and just ignore the new file. - */ - if (is_modified(slot) || fill_slot(slot)) { - unlock_slot(slot, 0); - close_lock(slot); - } else { - close_slot(slot); - unlock_slot(slot, 1); - slot->cache_fd = slot->lock_fd; - } - } - } - if ((err = print_slot(slot)) != 0) { - cache_log("[cgit] error printing cache %s: %s (%d)\n", - slot->cache_name, - strerror(err), - err); - } - close_slot(slot); - return err; - } - - /* If the cache slot does not exist (or its key doesn't match the - * current key), lets try to create a new cache slot for this - * request. If this fails (for whatever reason), lets just generate - * the content without caching it and fool the caller to believe - * everything worked out (but print a warning on stdout). - */ - - close_slot(slot); - if ((err = lock_slot(slot)) != 0) { - cache_log("[cgit] Unable to lock slot %s: %s (%d)\n", - slot->lock_name, strerror(err), err); - slot->fn(); - return 0; - } - - if ((err = fill_slot(slot)) != 0) { - cache_log("[cgit] Unable to fill slot %s: %s (%d)\n", - slot->lock_name, strerror(err), err); - unlock_slot(slot, 0); - close_lock(slot); - slot->fn(); - return 0; - } - // We've got a valid cache slot in the lock file, which - // is about to replace the old cache slot. But if we - // release the lockfile and then try to open the new cache - // slot, we might get a race condition with a concurrent - // writer for the same cache slot (with a different key). - // Lets avoid such a race by just printing the content of - // the lock file. - slot->cache_fd = slot->lock_fd; - unlock_slot(slot, 1); - if ((err = print_slot(slot)) != 0) { - cache_log("[cgit] error printing cache %s: %s (%d)\n", - slot->cache_name, - strerror(err), - err); - } - close_slot(slot); - return err; -} - -/* Print cached content to stdout, generate the content if necessary. */ -int cache_process(int size, const char *path, const char *key, int ttl, - cache_fill_fn fn) -{ - unsigned long hash; - int i; - struct strbuf filename = STRBUF_INIT; - struct strbuf lockname = STRBUF_INIT; - struct cache_slot slot; - int result; - - /* If the cache is disabled, just generate the content */ - if (size <= 0 || ttl == 0) { - fn(); - return 0; - } - - /* Verify input, calculate filenames */ - if (!path) { - cache_log("[cgit] Cache path not specified, caching is disabled\n"); - fn(); - return 0; - } - if (!key) - key = ""; - hash = hash_str(key) % size; - strbuf_addstr(&filename, path); - strbuf_ensure_end(&filename, '/'); - for (i = 0; i < 8; i++) { - strbuf_addf(&filename, "%x", (unsigned char)(hash & 0xf)); - hash >>= 4; - } - strbuf_addbuf(&lockname, &filename); - strbuf_addstr(&lockname, ".lock"); - slot.fn = fn; - slot.ttl = ttl; - slot.stdout_fd = -1; - slot.cache_name = filename.buf; - slot.lock_name = lockname.buf; - slot.key = key; - slot.keylen = strlen(key); - result = process_slot(&slot); - - strbuf_release(&filename); - strbuf_release(&lockname); - return result; -} - -/* Return a strftime formatted date/time - * NB: the result from this function is to shared memory - */ -static char *sprintftime(const char *format, time_t time) -{ - static char buf[64]; - struct tm tm; - - if (!time) - return NULL; - gmtime_r(&time, &tm); - strftime(buf, sizeof(buf)-1, format, &tm); - return buf; -} - -int cache_ls(const char *path) -{ - DIR *dir; - struct dirent *ent; - int err = 0; - struct cache_slot slot = { NULL }; - struct strbuf fullname = STRBUF_INIT; - size_t prefixlen; - - if (!path) { - cache_log("[cgit] cache path not specified\n"); - return -1; - } - dir = opendir(path); - if (!dir) { - err = errno; - cache_log("[cgit] unable to open path %s: %s (%d)\n", - path, strerror(err), err); - return err; - } - strbuf_addstr(&fullname, path); - strbuf_ensure_end(&fullname, '/'); - prefixlen = fullname.len; - while ((ent = readdir(dir)) != NULL) { - if (strlen(ent->d_name) != 8) - continue; - strbuf_setlen(&fullname, prefixlen); - strbuf_addstr(&fullname, ent->d_name); - slot.cache_name = fullname.buf; - if ((err = open_slot(&slot)) != 0) { - cache_log("[cgit] unable to open path %s: %s (%d)\n", - fullname.buf, strerror(err), err); - continue; - } - htmlf("%s %s %10"PRIuMAX" %s\n", - fullname.buf, - sprintftime("%Y-%m-%d %H:%M:%S", - slot.cache_st.st_mtime), - (uintmax_t)slot.cache_st.st_size, - slot.buf); - close_slot(&slot); - } - closedir(dir); - strbuf_release(&fullname); - return 0; -} - -/* Print a message to stdout */ -void cache_log(const char *format, ...) -{ - va_list args; - va_start(args, format); - vfprintf(stderr, format, args); - va_end(args); -} - diff --git a/cgit/cache.h b/cgit/cache.h deleted file mode 100644 index 469ca71..0000000 --- a/cgit/cache.h +++ /dev/null @@ -1,41 +0,0 @@ -/* SPDX-FileCopyrightText: cgit Development Team - * SPDX-License-Identifier: GPL-2.0-only - */ - -/* - * Since git has it's own cache.h which we include, - * lets test on CGIT_CACHE_H to avoid confusion - */ - -#ifndef CGIT_CACHE_H -#define CGIT_CACHE_H - -typedef void (*cache_fill_fn)(void); - - -/* Print cached content to stdout, generate the content if necessary. - * - * Parameters - * size max number of cache files - * path directory used to store cache files - * key the key used to lookup cache files - * ttl max cache time in seconds for this key - * fn content generator function for this key - * - * Return value - * 0 indicates success, everything else is an error - */ -extern int cache_process(int size, const char *path, const char *key, int ttl, - cache_fill_fn fn); - - -/* List info about all cache entries on stdout */ -extern int cache_ls(const char *path); - -/* Print a message to stdout */ -__attribute__((format (printf,1,2))) -extern void cache_log(const char *format, ...); - -extern unsigned long hash_str(const char *str); - -#endif /* CGIT_CACHE_H */ diff --git a/cgit/cgit.c b/cgit/cgit.c index ee6d2f8..1573ca6 100644 --- a/cgit/cgit.c +++ b/cgit/cgit.c @@ -1,4 +1,5 @@ /* SPDX-FileCopyrightText: cgit Development Team + * SPDX-FileCopyrightText: 2026 Nikolay Govorov * SPDX-License-Identifier: GPL-2.0-only */ @@ -13,7 +14,6 @@ #define USE_THE_REPOSITORY_VARIABLE #include "cgit.h" -#include "cache.h" #include "cmd.h" #include "configfile.h" #include "html.h" @@ -43,8 +43,6 @@ static void add_mimetype(const char *name, const char *value) item->util = xstrdup(value); } -static void process_cached_repolist(const char *path); - void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *value) { const char *path; @@ -207,24 +205,6 @@ static void config_cb(const char *name, const char *value) ctx.cfg.enable_git_config = atoi(value); else if (!strcmp(name, "max-stats")) ctx.cfg.max_stats = cgit_find_stats_period(value, NULL); - else if (!strcmp(name, "cache-size")) - ctx.cfg.cache_size = atoi(value); - else if (!strcmp(name, "cache-root")) - ctx.cfg.cache_root = strdup_first_line(expand_macros(value)); - else if (!strcmp(name, "cache-root-ttl")) - ctx.cfg.cache_root_ttl = atoi(value); - else if (!strcmp(name, "cache-repo-ttl")) - ctx.cfg.cache_repo_ttl = atoi(value); - else if (!strcmp(name, "cache-scanrc-ttl")) - ctx.cfg.cache_scanrc_ttl = atoi(value); - else if (!strcmp(name, "cache-static-ttl")) - ctx.cfg.cache_static_ttl = atoi(value); - else if (!strcmp(name, "cache-dynamic-ttl")) - ctx.cfg.cache_dynamic_ttl = atoi(value); - else if (!strcmp(name, "cache-about-ttl")) - ctx.cfg.cache_about_ttl = atoi(value); - else if (!strcmp(name, "cache-snapshot-ttl")) - ctx.cfg.cache_snapshot_ttl = atoi(value); else if (!strcmp(name, "case-sensitive-sort")) ctx.cfg.case_sensitive_sort = atoi(value); else if (!strcmp(name, "about-filter")) @@ -256,9 +236,7 @@ static void config_cb(const char *name, const char *value) else if (!strcmp(name, "project-list")) ctx.cfg.project_list = strdup_first_line(expand_macros(value)); else if (!strcmp(name, "scan-path")) - if (ctx.cfg.cache_size) - process_cached_repolist(expand_macros(value)); - else if (ctx.cfg.project_list) + if (ctx.cfg.project_list) scan_projects(expand_macros(value), ctx.cfg.project_list); else @@ -334,7 +312,6 @@ static void querystring_cb(const char *name, const char *value) ctx.qry.search = xstrdup(value); } else if (!strcmp(name, "h")) { ctx.qry.head = xstrdup(value); - ctx.qry.has_symref = 1; } else if (!strcmp(name, "id")) { ctx.qry.oid = xstrdup(value); ctx.qry.has_oid = 1; @@ -375,16 +352,6 @@ static void prepare_context(void) { memset(&ctx, 0, sizeof(ctx)); ctx.cfg.agefile = "info/web/last-modified"; - ctx.cfg.cache_size = 0; - ctx.cfg.cache_max_create_time = 5; - ctx.cfg.cache_root = CGIT_CACHE_ROOT; - ctx.cfg.cache_about_ttl = 15; - ctx.cfg.cache_snapshot_ttl = 5; - ctx.cfg.cache_repo_ttl = 5; - ctx.cfg.cache_root_ttl = 5; - ctx.cfg.cache_scanrc_ttl = 15; - ctx.cfg.cache_dynamic_ttl = 5; - ctx.cfg.cache_static_ttl = -1; ctx.cfg.case_sensitive_sort = 1; ctx.cfg.branch_sort = 0; ctx.cfg.commit_sort = 0; @@ -437,7 +404,6 @@ static void prepare_context(void) ctx.page.filename = NULL; ctx.page.size = 0; ctx.page.modified = time(NULL); - ctx.page.expires = ctx.page.modified; ctx.page.etag = NULL; string_list_init_dup(&ctx.cfg.mimetypes); if (ctx.env.script_name) @@ -876,88 +842,6 @@ static void print_repolist(FILE *f, struct cgit_repolist *list, int start) print_repo(f, &list->repos[i]); } -/* Scan 'path' for git repositories, save the resulting repolist in 'cached_rc' - * and return 0 on success. - */ -static int generate_cached_repolist(const char *path, const char *cached_rc) -{ - struct strbuf locked_rc = STRBUF_INIT; - int result = 0; - int idx; - FILE *f; - - strbuf_addf(&locked_rc, "%s.lock", cached_rc); - f = fopen(locked_rc.buf, "wx"); - if (!f) { - /* Inform about the error unless the lockfile already existed, - * since that only means we've got concurrent requests. - */ - result = errno; - if (result != EEXIST) - fprintf(stderr, "[cgit] Error opening %s: %s (%d)\n", - locked_rc.buf, strerror(result), result); - goto out; - } - idx = cgit_repolist.count; - if (ctx.cfg.project_list) - scan_projects(path, ctx.cfg.project_list); - else - scan_tree(path); - print_repolist(f, &cgit_repolist, idx); - if (rename(locked_rc.buf, cached_rc)) - fprintf(stderr, "[cgit] Error renaming %s to %s: %s (%d)\n", - locked_rc.buf, cached_rc, strerror(errno), errno); - fclose(f); -out: - strbuf_release(&locked_rc); - return result; -} - -static void process_cached_repolist(const char *path) -{ - struct stat st; - struct strbuf cached_rc = STRBUF_INIT; - time_t age; - unsigned long hash; - - hash = hash_str(path); - if (ctx.cfg.project_list) - hash += hash_str(ctx.cfg.project_list); - strbuf_addf(&cached_rc, "%s/rc-%8lx", ctx.cfg.cache_root, hash); - - if (stat(cached_rc.buf, &st)) { - /* Nothing is cached, we need to scan without forking. And - * if we fail to generate a cached repolist, we need to - * invoke scan_tree manually. - */ - if (generate_cached_repolist(path, cached_rc.buf)) { - if (ctx.cfg.project_list) - scan_projects(path, ctx.cfg.project_list); - else - scan_tree(path); - } - goto out; - } - - parse_configfile(cached_rc.buf, config_cb); - - /* If the cached configfile hasn't expired, lets exit now */ - age = time(NULL) - st.st_mtime; - if (age <= (ctx.cfg.cache_scanrc_ttl * 60)) - goto out; - - /* The cached repolist has been parsed, but it was old. So lets - * rescan the specified path and generate a new cached repolist - * in a child-process to avoid latency for the current request. - */ - if (fork()) - goto out; - - exit(generate_cached_repolist(path, cached_rc.buf)); -out: - strbuf_release(&cached_rc); -} - static void cgit_parse_args(int argc, const char **argv) { int i; @@ -973,18 +857,10 @@ static void cgit_parse_args(int argc, const char **argv) printf("[+] "); #endif printf("Lua scripting\n"); -#ifndef HAVE_LINUX_SENDFILE - printf("[-] "); -#else - printf("[+] "); -#endif - printf("Linux sendfile() usage\n"); exit(0); } - if (skip_prefix(argv[i], "--cache=", &arg)) { - ctx.cfg.cache_root = xstrdup(arg); - } else if (!strcmp(argv[i], "--nohttp")) { + if (!strcmp(argv[i], "--nohttp")) { ctx.env.no_http = "1"; } else if (skip_prefix(argv[i], "--query=", &arg)) { ctx.qry.raw = xstrdup(arg); @@ -994,7 +870,6 @@ static void cgit_parse_args(int argc, const char **argv) ctx.qry.page = xstrdup(arg); } else if (skip_prefix(argv[i], "--head=", &arg)) { ctx.qry.head = xstrdup(arg); - ctx.qry.has_symref = 1; } else if (skip_prefix(argv[i], "--oid=", &arg)) { ctx.qry.oid = xstrdup(arg); ctx.qry.has_oid = 1; @@ -1026,29 +901,6 @@ static void cgit_parse_args(int argc, const char **argv) } } -static int calc_ttl(void) -{ - if (!ctx.repo) - return ctx.cfg.cache_root_ttl; - - if (!ctx.qry.page) - return ctx.cfg.cache_repo_ttl; - - if (!strcmp(ctx.qry.page, "about")) - return ctx.cfg.cache_about_ttl; - - if (!strcmp(ctx.qry.page, "snapshot")) - return ctx.cfg.cache_snapshot_ttl; - - if (ctx.qry.has_oid) - return ctx.cfg.cache_static_ttl; - - if (ctx.qry.has_symref) - return ctx.cfg.cache_dynamic_ttl; - - return ctx.cfg.cache_repo_ttl; -} - static NORETURN void cgit_die_routine(const char *msg, va_list params) { cgit_vprint_error_page(400, "Bad request", msg, params); @@ -1058,7 +910,6 @@ static NORETURN void cgit_die_routine(const char *msg, va_list params) int cmd_main(int argc, const char **argv) { const char *path; - int err, ttl; cgit_init_filters(); atexit(cgit_cleanup_filters); @@ -1081,10 +932,9 @@ int cmd_main(int argc, const char **argv) if (!ctx.cfg.virtual_root && ctx.cfg.script_name) ctx.cfg.virtual_root = ensure_end(ctx.cfg.script_name, '/'); - /* If no url parameter is specified on the querystring, lets - * use PATH_INFO as url. This allows cgit to work with virtual - * urls without the need for rewriterules in the webserver (as - * long as PATH_INFO is included in the cache lookup key). + /* If no url parameter is specified on the querystring, use PATH_INFO + * as url. This allows cgit to work with virtual urls without the need + * for rewriterules in the webserver. */ path = ctx.env.path_info; if (!ctx.qry.url && path) { @@ -1105,18 +955,7 @@ int cmd_main(int argc, const char **argv) * auth_filter. If there is an auth_filter, the filter decides. */ authenticate_cookie(); - ttl = calc_ttl(); - if (ttl < 0) - ctx.page.expires += 10 * 365 * 24 * 60 * 60; /* 10 years */ - else - ctx.page.expires += ttl * 60; - if (!ctx.env.authenticated || (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))) - ctx.cfg.cache_size = 0; - err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, - ctx.qry.raw, ttl, process_request); + process_request(); cgit_cleanup_filters(); - if (err) - cgit_print_error("Error processing page: %s (%d)", - strerror(err), err); - return err; + return 0; } diff --git a/cgit/cgit.h b/cgit/cgit.h index 5d4a668..10c6246 100644 --- a/cgit/cgit.h +++ b/cgit/cgit.h @@ -1,4 +1,5 @@ /* SPDX-FileCopyrightText: cgit Development Team + * SPDX-FileCopyrightText: 2026 Nikolay Govorov * SPDX-License-Identifier: GPL-2.0-only */ @@ -170,7 +171,6 @@ struct reflist { }; struct cgit_query { - int has_symref; int has_oid; int has_difftype; char *raw; @@ -199,7 +199,6 @@ struct cgit_query { struct cgit_config { char *agefile; - char *cache_root; char *clone_prefix; char *clone_url; char *favicon; @@ -222,15 +221,6 @@ struct cgit_config { char *repository_sort; char *virtual_root; /* Always ends with '/'. */ char *strict_export; - int cache_size; - int cache_dynamic_ttl; - int cache_max_create_time; - int cache_repo_ttl; - int cache_root_ttl; - int cache_scanrc_ttl; - int cache_static_ttl; - int cache_about_ttl; - int cache_snapshot_ttl; int case_sensitive_sort; int embedded; int enable_filter_overrides; @@ -282,7 +272,6 @@ struct cgit_config { struct cgit_page { time_t modified; - time_t expires; size_t size; const char *mimetype; const char *charset; diff --git a/cgit/cmd.c b/cgit/cmd.c index 2eaeb16..13892a9 100644 --- a/cgit/cmd.c +++ b/cgit/cmd.c @@ -1,4 +1,5 @@ /* SPDX-FileCopyrightText: cgit Development Team + * SPDX-FileCopyrightText: 2026 Nikolay Govorov * SPDX-License-Identifier: GPL-2.0-only */ @@ -12,7 +13,6 @@ #include "cgit.h" #include "cmd.h" -#include "cache.h" #include "ui-shared.h" #include "ui-atom.h" #include "ui-blame.h" @@ -109,14 +109,6 @@ static void log_fn(void) ctx.repo->commit_sort); } -static void ls_cache_fn(void) -{ - ctx.page.mimetype = "text/plain"; - ctx.page.filename = "ls-cache.txt"; - cgit_print_http_headers(); - cache_ls(ctx.cfg.cache_root); -} - static void objects_fn(void) { cgit_clone_objects(); @@ -183,7 +175,6 @@ struct cgit_cmd *cgit_get_cmd(void) def_cmd(diff, 1, 1, 0), def_cmd(info, 1, 0, 1), def_cmd(log, 1, 1, 0), - def_cmd(ls_cache, 0, 0, 0), def_cmd(objects, 1, 0, 1), def_cmd(patch, 1, 1, 0), def_cmd(plain, 1, 0, 0), diff --git a/cgit/ui-shared.c b/cgit/ui-shared.c index 1c57f3c..976f55b 100644 --- a/cgit/ui-shared.c +++ b/cgit/ui-shared.c @@ -1,4 +1,5 @@ /* SPDX-FileCopyrightText: cgit Development Team + * SPDX-FileCopyrightText: 2026 Nikolay Govorov * SPDX-License-Identifier: GPL-2.0-only */ @@ -749,7 +750,6 @@ void cgit_print_http_headers(void) if (!ctx.env.authenticated) html("Cache-Control: no-cache, no-store\n"); htmlf("Last-Modified: %s\n", http_date(ctx.page.modified)); - htmlf("Expires: %s\n", http_date(ctx.page.expires)); if (ctx.page.etag) htmlf("ETag: \"%s\"\n", ctx.page.etag); html("\n"); @@ -900,7 +900,6 @@ void cgit_print_error_page(int code, const char *msg, const char *fmt, ...) void cgit_vprint_error_page(int code, const char *msg, const char *fmt, va_list ap) { - ctx.page.expires = ctx.cfg.cache_dynamic_ttl; ctx.page.status = code; ctx.page.statusmsg = msg; cgit_print_layout_start(); diff --git a/charts/gilti/README.md b/charts/gilti/README.md index 35a3903..14a31f4 100644 --- a/charts/gilti/README.md +++ b/charts/gilti/README.md @@ -78,5 +78,7 @@ The Rust HTTP gateway and its cgit children run without privileges. OpenSSH intentionally keeps a root master so it can enter the `git` account (UID/GID 10000). The chart drops all capabilities and restores only `CHOWN`, `DAC_OVERRIDE`, `FOWNER`, `SETGID`, `SETUID`, and `SYS_CHROOT`. The root -filesystem is read-only; state, cache, `/run`, and `/tmp` are explicit writable -mounts. +filesystem is read-only; state, `/run`, and `/tmp` are explicit writable +mounts. CGI responses use an in-memory LRU cache bounded to 64 entries and +4 MiB; configure its lifetime with `cgit.cache` in seconds, or set it to zero +to disable caching. diff --git a/charts/gilti/templates/configmap.yaml b/charts/gilti/templates/configmap.yaml index 9f73a90..1376076 100644 --- a/charts/gilti/templates/configmap.yaml +++ b/charts/gilti/templates/configmap.yaml @@ -25,12 +25,8 @@ data: css=/cgit.css logo=/cgit.png favicon=/favicon.ico - cache-root=/var/cache/cgit - cache-size=1000 - cache-dynamic-ttl=5 - cache-repo-ttl=5 - cache-root-ttl=5 - cache-scanrc-ttl=1 + # Gilti consumes this setting before invoking cgit. + cache={{ .Values.cgit.cache }} enable-http-clone=0 enable-index-owner=1 enable-index-links=1 diff --git a/charts/gilti/templates/deployment.yaml b/charts/gilti/templates/deployment.yaml index ad3c40c..19eab1b 100644 --- a/charts/gilti/templates/deployment.yaml +++ b/charts/gilti/templates/deployment.yaml @@ -85,8 +85,6 @@ spec: readOnly: true - name: run mountPath: /run - - name: cache - mountPath: /var/cache/cgit - name: tmp mountPath: /tmp volumes: @@ -103,9 +101,6 @@ spec: - name: run emptyDir: sizeLimit: {{ .Values.runtime.runSizeLimit }} - - name: cache - emptyDir: - sizeLimit: {{ .Values.runtime.cacheSizeLimit }} - name: tmp emptyDir: sizeLimit: {{ .Values.runtime.tmpSizeLimit }} diff --git a/charts/gilti/values.schema.json b/charts/gilti/values.schema.json index 100a06c..c15674a 100644 --- a/charts/gilti/values.schema.json +++ b/charts/gilti/values.schema.json @@ -27,7 +27,8 @@ "properties": { "rootTitle": { "type": "string", "pattern": "^[^\\r\\n]*$" }, "rootDescription": { "type": "string", "pattern": "^[^\\r\\n]*$" }, - "clonePrefix": { "type": "string", "pattern": "^[^\\r\\n]*$" } + "clonePrefix": { "type": "string", "pattern": "^[^\\r\\n]*$" }, + "cache": { "type": "integer", "minimum": 0, "maximum": 3600 } } }, "persistence": { @@ -78,7 +79,6 @@ "type": "object", "properties": { "runSizeLimit": { "type": "string", "minLength": 1 }, - "cacheSizeLimit": { "type": "string", "minLength": 1 }, "tmpSizeLimit": { "type": "string", "minLength": 1 } } }, diff --git a/charts/gilti/values.yaml b/charts/gilti/values.yaml index 553e3e9..03c480b 100644 --- a/charts/gilti/values.yaml +++ b/charts/gilti/values.yaml @@ -21,6 +21,8 @@ cgit: rootTitle: Gilti rootDescription: A tiny Git server clonePrefix: "" + # In-memory CGI response lifetime in seconds; zero disables the cache. + cache: 5 persistence: enabled: true @@ -77,7 +79,6 @@ resources: runtime: runSizeLimit: 32Mi - cacheSizeLimit: 1Gi tmpSizeLimit: 256Mi nodeSelector: {} diff --git a/config/cgitrc b/config/cgitrc index b3926f6..7e3a7d7 100644 --- a/config/cgitrc +++ b/config/cgitrc @@ -9,12 +9,8 @@ css=/cgit.css logo=/cgit.png favicon=/favicon.ico -cache-root=/var/cache/cgit -cache-size=1000 -cache-dynamic-ttl=5 -cache-repo-ttl=5 -cache-root-ttl=5 -cache-scanrc-ttl=1 +# In-memory CGI response lifetime in seconds; zero disables the cache. +cache=5 enable-http-clone=0 enable-index-owner=1 diff --git a/crates/gilti/src/cgi.rs b/crates/gilti/src/cgi.rs index 57800df..ffcc081 100644 --- a/crates/gilti/src/cgi.rs +++ b/crates/gilti/src/cgi.rs @@ -3,6 +3,188 @@ //! HTTP-to-CGI Tower service. +const CACHE_MAX_ENTRIES: usize = 64; +const CACHE_MAX_BYTES: usize = 4 * 1024 * 1024; + +fn has_cache_directive(headers: &axum::http::HeaderMap, expected: &[&str]) -> bool { + headers + .get_all(axum::http::header::CACHE_CONTROL) + .iter() + .filter_map(|value| value.to_str().ok()) + .flat_map(|value| value.split(',')) + .map(str::trim) + .any(|value| { + let name = value.split_once('=').map_or(value, |(name, _)| name); + expected + .iter() + .any(|expected| name.eq_ignore_ascii_case(expected)) + }) +} + +#[derive(Clone, Eq, Hash, PartialEq)] +struct CacheKey { + uri: String, + host: Option>, + cookie: Option>, + referer: Option>, +} + +impl CacheKey { + fn from_request(parts: &axum::http::request::Parts, body: &[u8]) -> Option { + if parts.method != axum::http::Method::GET + || !body.is_empty() + || parts + .headers + .contains_key(axum::http::header::AUTHORIZATION) + || parts.headers.contains_key(axum::http::header::RANGE) + || has_cache_directive(&parts.headers, &["no-cache", "no-store"]) + || parts + .headers + .get(axum::http::header::PRAGMA) + .and_then(|value| value.to_str().ok()) + .is_some_and(|value| value.eq_ignore_ascii_case("no-cache")) + { + return None; + } + let header = |name| { + parts + .headers + .get(name) + .map(|value| value.as_bytes().to_vec()) + }; + Some(Self { + uri: parts.uri.to_string(), + host: header(axum::http::header::HOST), + cookie: header(axum::http::header::COOKIE), + referer: header(axum::http::header::REFERER), + }) + } +} + +#[derive(Clone)] +struct CachedResponse { + status: axum::http::StatusCode, + headers: axum::http::HeaderMap, + body: Vec, +} + +impl CachedResponse { + fn is_cacheable(&self) -> bool { + if self.status != axum::http::StatusCode::OK + || self.headers.contains_key(axum::http::header::SET_COOKIE) + || self.headers.contains_key(axum::http::header::VARY) + { + return false; + } + !has_cache_directive(&self.headers, &["no-cache", "no-store", "private"]) + } + + fn into_response(self) -> std::io::Result> { + let mut response = axum::http::Response::builder() + .status(self.status) + .body(axum::body::Body::from(self.body)) + .map_err(std::io::Error::other)?; + *response.headers_mut() = self.headers; + Ok(response) + } +} + +struct CacheEntry { + response: CachedResponse, + expires: std::time::Instant, +} + +#[derive(Default)] +struct CacheState { + entries: std::collections::VecDeque<(CacheKey, CacheEntry)>, + bytes: usize, +} + +impl CacheState { + fn prune_expired(&mut self, now: std::time::Instant) { + let mut bytes = 0; + self.entries.retain(|(_, entry)| { + if entry.expires <= now { + false + } else { + bytes += entry.response.body.len(); + true + } + }); + self.bytes = bytes; + } +} + +#[derive(Clone)] +struct ResponseCache { + ttl: std::time::Duration, + state: std::sync::Arc>, +} + +impl ResponseCache { + fn new(ttl: std::time::Duration) -> Self { + Self { + ttl, + state: std::sync::Arc::new(std::sync::Mutex::new(CacheState::default())), + } + } + + fn get(&self, key: &CacheKey) -> Option { + self.get_at(key, std::time::Instant::now()) + } + + fn get_at(&self, key: &CacheKey, now: std::time::Instant) -> Option { + let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner()); + state.prune_expired(now); + let index = state + .entries + .iter() + .position(|(entry_key, _)| entry_key == key)?; + let entry = state.entries.remove(index)?; + let response = entry.1.response.clone(); + state.entries.push_back(entry); + Some(response) + } + + fn insert(&self, key: CacheKey, response: CachedResponse) { + self.insert_at(key, response, std::time::Instant::now()); + } + + fn insert_at(&self, key: CacheKey, response: CachedResponse, now: std::time::Instant) { + let size = response.body.len(); + if !response.is_cacheable() || size > CACHE_MAX_BYTES { + return; + } + + let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner()); + state.prune_expired(now); + let previous = state + .entries + .iter() + .position(|(entry_key, _)| entry_key == &key) + .and_then(|index| state.entries.remove(index)); + if let Some(previous) = previous { + state.bytes -= previous.1.response.body.len(); + } + while state.entries.len() >= CACHE_MAX_ENTRIES + || state.bytes.saturating_add(size) > CACHE_MAX_BYTES + { + let Some(previous) = state.entries.pop_front() else { + break; + }; + state.bytes -= previous.1.response.body.len(); + } + state.bytes += size; + state.entries.push_back(( + key, + CacheEntry { + response, + expires: now + self.ttl, + }, + )); + } +} + #[derive(Clone, Copy)] pub struct RemoteAddr(pub std::net::SocketAddr); @@ -12,6 +194,7 @@ pub struct Cgi { current_dir: std::path::PathBuf, environment: Vec<(std::ffi::OsString, std::ffi::OsString)>, server_addr: std::net::SocketAddr, + cache: Option, } impl Cgi { @@ -25,7 +208,15 @@ impl Cgi { current_dir: current_dir.into(), environment: Vec::new(), server_addr, + cache: None, + } + } + + pub fn cache(mut self, ttl: std::time::Duration) -> Self { + if !ttl.is_zero() { + self.cache = Some(ResponseCache::new(ttl)); } + self } pub fn env( @@ -49,6 +240,15 @@ impl Cgi { let body = axum::body::to_bytes(body, 1024 * 1024) .await .map_err(std::io::Error::other)?; + let cache_key = CacheKey::from_request(&parts, &body); + if let Some(response) = self + .cache + .as_ref() + .zip(cache_key.as_ref()) + .and_then(|(cache, key)| cache.get(key)) + { + return response.into_response(); + } let path = percent_encoding::percent_decode_str(parts.uri.path()).collect::>(); if path.contains(&0) { return Err(invalid("request path contains a null byte")); @@ -133,12 +333,15 @@ impl Cgi { } let (status, headers, body) = parse_response(&output.stdout)?; - let mut response = axum::http::Response::builder() - .status(status) - .body(axum::body::Body::from(body.to_vec())) - .map_err(std::io::Error::other)?; - *response.headers_mut() = headers; - Ok(response) + let response = CachedResponse { + status, + headers, + body: body.to_vec(), + }; + if let (Some(cache), Some(key)) = (&self.cache, cache_key) { + cache.insert(key, response.clone()); + } + response.into_response() } } @@ -212,6 +415,67 @@ fn invalid(message: &'static str) -> std::io::Error { #[cfg(test)] mod tests { + fn cache_key(uri: impl Into) -> super::CacheKey { + super::CacheKey { + uri: uri.into(), + host: None, + cookie: None, + referer: None, + } + } + + fn cached_response(body: impl Into>) -> super::CachedResponse { + super::CachedResponse { + status: axum::http::StatusCode::OK, + headers: axum::http::HeaderMap::new(), + body: body.into(), + } + } + + #[test] + fn memory_cache_expires_entries() { + let cache = super::ResponseCache::new(std::time::Duration::from_secs(5)); + let now = std::time::Instant::now(); + cache.insert_at(cache_key("/repo/"), cached_response(b"first".to_vec()), now); + assert_eq!( + cache.get_at(&cache_key("/repo/"), now).unwrap().body, + b"first" + ); + assert!( + cache + .get_at( + &cache_key("/repo/"), + now + std::time::Duration::from_secs(5) + ) + .is_none() + ); + } + + #[test] + fn memory_cache_is_bounded_and_honors_response_directives() { + let cache = super::ResponseCache::new(std::time::Duration::from_secs(5)); + let now = std::time::Instant::now(); + for index in 0..super::CACHE_MAX_ENTRIES { + cache.insert_at( + cache_key(format!("/{index}")), + cached_response(vec![index as u8]), + now, + ); + } + assert!(cache.get_at(&cache_key("/0"), now).is_some()); + cache.insert_at(cache_key("/new"), cached_response(b"new".to_vec()), now); + assert!(cache.get_at(&cache_key("/1"), now).is_none()); + assert!(cache.get_at(&cache_key("/0"), now).is_some()); + + let mut private = cached_response(b"private".to_vec()); + private.headers.insert( + axum::http::header::CACHE_CONTROL, + axum::http::HeaderValue::from_static("private"), + ); + cache.insert_at(cache_key("/private"), private, now); + assert!(cache.get_at(&cache_key("/private"), now).is_none()); + } + #[test] fn parses_cgi_response() { let (status, headers, body) = diff --git a/crates/gilti/src/main.rs b/crates/gilti/src/main.rs index 8a81e5b..591b88e 100644 --- a/crates/gilti/src/main.rs +++ b/crates/gilti/src/main.rs @@ -4,6 +4,7 @@ mod cgi; const DEFAULT_LISTEN_ADDR: &str = "0.0.0.0:8080"; +const MAX_CACHE_SECONDS: u64 = 3600; const CGIT: &str = "/usr/local/bin/gilti-cgit"; const CGIT_CONFIG: &str = "/etc/cgitrc"; @@ -22,22 +23,59 @@ struct AppState { struct CgitConfig { path: std::path::PathBuf, + cache: std::time::Duration, } impl CgitConfig { fn create() -> std::io::Result { - let contents = std::fs::read(CGIT_CONFIG)?; + let contents = std::fs::read_to_string(CGIT_CONFIG)?; + let (contents, cache) = prepare_cgit_config(&contents)?; let path = std::path::PathBuf::from(format!("{RUN_DIR}/cgitrc.{}", std::process::id())); let mut options = std::fs::OpenOptions::new(); options.write(true).create_new(true); std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600); let mut file = options.open(&path)?; - let config = Self { path }; - std::io::Write::write_all(&mut file, &contents)?; + let config = Self { path, cache }; + std::io::Write::write_all(&mut file, contents.as_bytes())?; Ok(config) } } +fn prepare_cgit_config(contents: &str) -> std::io::Result<(String, std::time::Duration)> { + let mut output = String::with_capacity(contents.len()); + let mut cache = None; + + for line in contents.split_inclusive('\n') { + let value = line + .trim_end_matches(['\r', '\n']) + .trim_start() + .strip_prefix("cache="); + let Some(value) = value else { + output.push_str(line); + continue; + }; + if cache.is_some() { + return Err(invalid_config("cache is configured more than once")); + } + let seconds = value + .trim() + .parse::() + .map_err(|_| invalid_config("cache must be an integer number of seconds"))?; + if seconds > MAX_CACHE_SECONDS { + return Err(invalid_config(format!( + "cache must not exceed {MAX_CACHE_SECONDS} seconds" + ))); + } + cache = Some(std::time::Duration::from_secs(seconds)); + } + + Ok((output, cache.unwrap_or_default())) +} + +fn invalid_config(message: impl Into) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::InvalidData, message.into()) +} + impl Drop for CgitConfig { fn drop(&mut self) { match std::fs::remove_file(&self.path) { @@ -55,14 +93,15 @@ async fn main() -> Result<(), Box> { .unwrap_or_else(|_| DEFAULT_LISTEN_ADDR.to_owned()) .parse::()?; check_files()?; + let cgit_config = CgitConfig::create()?; if std::env::args().nth(1).as_deref() == Some("--check") { return Ok(()); } - let cgit_config = CgitConfig::create()?; let state = AppState { cgit: cgi::Cgi::new(CGIT, GIT_HOME, listen_addr) + .cache(cgit_config.cache) .env("CGIT_CONFIG", cgit_config.path.as_os_str()) .env("HOME", GIT_HOME) .env("PATH", "/usr/bin:/bin"), @@ -179,3 +218,23 @@ fn plain_response( ) -> axum::response::Response { response(status, "text/plain", message.as_bytes().to_vec()) } + +#[cfg(test)] +mod tests { + #[test] + fn extracts_cache_from_cgit_config() { + let (contents, cache) = + super::prepare_cgit_config("# comment\r\ncache=5\r\nroot-title=Gilti\r\n").unwrap(); + assert_eq!(contents, "# comment\r\nroot-title=Gilti\r\n"); + assert_eq!(cache, std::time::Duration::from_secs(5)); + } + + #[test] + fn cache_is_optional_and_bounded() { + let (_, cache) = super::prepare_cgit_config("root-title=Gilti\n").unwrap(); + assert!(cache.is_zero()); + assert!(super::prepare_cgit_config("cache=1\ncache=2\n").is_err()); + assert!(super::prepare_cgit_config("cache=3601\n").is_err()); + assert!(super::prepare_cgit_config("cache=forever\n").is_err()); + } +} diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh index 1fa909b..3eaa8f9 100755 --- a/scripts/entrypoint.sh +++ b/scripts/entrypoint.sh @@ -10,7 +10,6 @@ state=/var/lib/gilti run_dir=/run/gilti http_run_dir=$run_dir/http ssh_run_dir=$run_dir/ssh -cache_dir=/var/cache/cgit git_home=$state/git repositories=$git_home/repositories @@ -28,7 +27,7 @@ prepare_runtime() { [ ! -L "$path" ] || { log "refusing symlinked state path $path"; exit 1; } done install -d -m 0755 -o root -g root "$state" - install -d -m 0750 -o git -g git "$git_home" "$repositories" "$cache_dir" + install -d -m 0750 -o git -g git "$git_home" "$repositories" install -d -m 0700 -o root -g root "$host_key_dir" install -d -m 0755 -o root -g root "$run_dir" install -d -m 0750 -o git -g git "$http_run_dir" diff --git a/tests/chart.sh b/tests/chart.sh index 356bce3..f0471d4 100755 --- a/tests/chart.sh +++ b/tests/chart.sh @@ -28,6 +28,11 @@ grep -q '^apiVersion: gateway.networking.k8s.io/v1$' "$rendered" grep -q 'helm.sh/resource-policy: keep' "$rendered" grep -q 'ssh-ed25519 AAAAcharttest gilti' "$rendered" grep -q 'mountPath: /etc/gilti/authorized_keys' "$rendered" +grep -q '^ cache=5$' "$rendered" +if grep -q '/var/cache/cgit' "$rendered"; then + echo 'chart still provisions the removed cgit disk cache' >&2 + exit 1 +fi cat >"$injected" <<'EOF' cgit: @@ -44,3 +49,8 @@ if helm template gilti "$chart" --set replicaCount=2 >"$invalid" 2>&1; then echo 'chart accepted unsupported replicaCount=2' >&2 exit 1 fi + +if helm template gilti "$chart" --set cgit.cache=3601 >"$invalid" 2>&1; then + echo 'chart accepted an excessive CGI cache lifetime' >&2 + exit 1 +fi diff --git a/tests/smoke.sh b/tests/smoke.sh index beccc67..f9ed0c7 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -50,7 +50,6 @@ start() { --cap-add SETGID --cap-add SETUID --cap-add SYS_CHROOT \ --tmpfs /run:rw,nosuid,nodev,noexec,size=32m \ --tmpfs /tmp:rw,nosuid,nodev,noexec,size=256m \ - --tmpfs /var/cache/cgit:rw,nosuid,nodev,noexec,size=256m \ --mount "type=volume,src=$volume,dst=/var/lib/gilti" \ --mount "type=bind,src=$authorized_keys,dst=/etc/gilti/authorized_keys,readonly" \ -p "127.0.0.1:$http_port:8080" -p "127.0.0.1:$ssh_port:2222" \ @@ -83,6 +82,14 @@ if "$engine" exec "$name" test -e /usr/share/webapps/cgit/cgit.cgi; then echo 'legacy cgit.cgi is installed' >&2 exit 1 fi +if "$engine" exec "$name" test -e /var/cache/cgit; then + echo 'legacy cgit disk-cache directory exists' >&2 + exit 1 +fi +if "$engine" exec "$name" sh -c 'grep -q "^cache=" /run/gilti/http/cgitrc.*'; then + echo 'Gilti cache parameter leaked into the cgit configuration' >&2 + exit 1 +fi sshd_config=$("$engine" exec "$name" /usr/sbin/sshd -T -f /etc/ssh/sshd_config \ -C user=git,host=localhost,addr=127.0.0.1) for expected in \ @@ -238,7 +245,16 @@ until curl -fsS "http://127.0.0.1:$http_port/" | grep -q 'testing'; do [ "$i" -lt 30 ] || { "$engine" logs "$name" >&2; exit 1; } sleep 1 done -curl -fsS "http://127.0.0.1:$http_port/testing/" >/dev/null +cache_url="http://127.0.0.1:$http_port/testing/" +curl -fsS -D "$work/cache-1.headers" -o /dev/null "$cache_url" +sleep 2 +curl -fsS -D "$work/cache-2.headers" -o /dev/null "$cache_url" +cache_modified_1=$(awk -F ': ' 'tolower($1) == "last-modified" { gsub("\\r", "", $2); print $2 }' "$work/cache-1.headers") +cache_modified_2=$(awk -F ': ' 'tolower($1) == "last-modified" { gsub("\\r", "", $2); print $2 }' "$work/cache-2.headers") +[ -n "$cache_modified_1" ] && [ "$cache_modified_1" = "$cache_modified_2" ] || { + echo 'CGI response was not served from the in-memory cache' >&2 + exit 1 +} # The running sshd uses its startup snapshot, not the mounted source file. cat "$work/admin.pub" >"$authorized_keys" -- Gilti