aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat
-rw-r--r--Dockerfile2+1 −1
-rw-r--r--README.md1+0 −1
-rw-r--r--cgit/Makefile3+0 −3
-rw-r--r--cgit/cache.c479+0 −479
-rw-r--r--cgit/cache.h41+0 −41
-rw-r--r--cgit/cgit.c177+8 −169
-rw-r--r--cgit/cgit.h13+1 −12
-rw-r--r--cgit/cmd.c11+1 −10
-rw-r--r--cgit/ui-shared.c3+1 −2
-rw-r--r--charts/gilti/README.md6+4 −2
-rw-r--r--charts/gilti/templates/configmap.yaml8+2 −6
-rw-r--r--charts/gilti/templates/deployment.yaml5+0 −5
-rw-r--r--charts/gilti/values.schema.json4+2 −2
-rw-r--r--charts/gilti/values.yaml3+2 −1
-rw-r--r--config/cgitrc8+2 −6
-rw-r--r--crates/gilti/src/cgi.rs276+270 −6
-rw-r--r--crates/gilti/src/main.rs67+63 −4
-rwxr-xr-xscripts/entrypoint.sh3+1 −2
-rwxr-xr-xtests/chart.sh10+10 −0
-rwxr-xr-xtests/smoke.sh20+18 −2
20 files changed, 386 insertions, 754 deletions
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
--- a/cgit/cache.c
+++ /dev/null
@@ -1,479 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * SPDX-License-Identifier: GPL-2.0-only
- */
-
-/* cache.c: cache management
- *
- * Copyright (C) 2006-2014 cgit Development Team <cgit@lists.zx2c4.com>
- *
- * 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 <sys/sendfile.h>
-#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
--- a/cgit/cache.h
+++ /dev/null
@@ -1,41 +0,0 @@
-/* SPDX-FileCopyrightText: cgit Development Team <cgit@lists.zx2c4.com>
- * 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 <cgit@lists.zx2c4.com>
+ * 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 <cgit@lists.zx2c4.com>
+ * 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 <cgit@lists.zx2c4.com>
+ * 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 <cgit@lists.zx2c4.com>
+ * 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<Vec<u8>>,
+ cookie: Option<Vec<u8>>,
+ referer: Option<Vec<u8>>,
+}
+
+impl CacheKey {
+ fn from_request(parts: &axum::http::request::Parts, body: &[u8]) -> Option<Self> {
+ 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<u8>,
+}
+
+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<axum::http::Response<axum::body::Body>> {
+ 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<std::sync::Mutex<CacheState>>,
+}
+
+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<CachedResponse> {
+ self.get_at(key, std::time::Instant::now())
+ }
+
+ fn get_at(&self, key: &CacheKey, now: std::time::Instant) -> Option<CachedResponse> {
+ 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<ResponseCache>,
}
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::<Vec<_>>();
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<String>) -> super::CacheKey {
+ super::CacheKey {
+ uri: uri.into(),
+ host: None,
+ cookie: None,
+ referer: None,
+ }
+ }
+
+ fn cached_response(body: impl Into<Vec<u8>>) -> 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<Self> {
- 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::<u64>()
+ .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<String>) -> 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<dyn std::error::Error>> {
.unwrap_or_else(|_| DEFAULT_LISTEN_ADDR.to_owned())
.parse::<std::net::SocketAddr>()?;
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"