/* * The entry point every request passes through. It reads cgitrc and the query * string into the global ctx, works out how long the answer may be cached, * and then hands over to the cache, which either serves a copy it already has * or calls back here to render one. Rendering means authenticating the * request, resolving the repository and the branch it names, and dispatching * to the page handler cmd.c maps the page name to. Run from a shell rather * than from a web server the same binary instead prints what it was built * with, or scans a directory tree and writes out the repositories it found in * cgitrc form. */ #define USE_THE_REPOSITORY_VARIABLE #include "cache.h" #include "cgit.h" #include "cmd.h" #include "config.h" #include "filter.h" #include "html.h" #include "parsing.h" #include "scan-tree.h" #include "shared.h" #include "ui-blob.h" #include "ui-diff.h" #include "ui-empty.h" #include "ui-shared.h" #include "ui-snapshot.h" #include "ui-stats.h" // Deliberately small. The whole body is read at once and handed to the auth // filter, so a filter never has to defend itself against a huge or a // dribbled-out POST. #define MAX_AUTHENTICATION_POST_BYTES 4096 // The filter bar matches the search against every item a page lists, so this // bounds both the work one request can ask for and the cache key the query // lands in. #define MAX_SEARCH_LEN 512 // Furthest into a listing a request may ask to start. A walk has to step over // every row it skips, so this bounds the work an offset alone can buy. #define MAX_QUERY_OFFSET 100000 // cgit knows fewer than eight archive formats, so this stands in for every bit // a snapshots mask can carry. #define ALL_SNAPSHOT_FORMATS 0xFF // The first branch found is the fallback, so a repository whose default branch // does not exist still has something to show. struct refmatch { char *wanted; char *first; int found; }; const char *cgit_version = CGIT_VERSION; /* * Isolate git from the calling user's configuration, which could otherwise * break once HOME is unset below. Called from cmd_main rather than from a * constructor attribute, because git-compat-util.h defines __attribute__ * away on a compiler without it, which would leave this silently unrun. */ static void isolate_git_environment(void) { setenv("GIT_CONFIG_NOSYSTEM", "1", 1); setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1); setenv("GIT_ATTR_NOSYSTEM", "1", 1); unsetenv("HOME"); unsetenv("XDG_CONFIG_HOME"); } /* * Turn a die from anywhere inside git into a rendered page, since a CGI that * produced no output leaves the visitor with whatever the web server makes of * it. Git's message names files and objects on the server, so it goes to the * log and the visitor gets a fixed page. */ static NORETURN void die_routine(const char *msg, va_list params) { fputs("[cgit] ", stderr); vfprintf(stderr, msg, params); fputc('\n', stderr); cgit_abort_filters(); cgit_print_error_page(500, "Internal Server Error", "Unable to complete the request"); exit(0); } 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_root = CGIT_CACHE_ROOT; ctx.cfg.cache_about_ttl = 15; ctx.cfg.cache_snapshot_ttl = 5; ctx.cfg.cache_summary_ttl = 5; ctx.cfg.cache_index_ttl = 5; ctx.cfg.cache_scan_ttl = 15; ctx.cfg.cache_dynamic_ttl = 5; ctx.cfg.cache_static_ttl = -1; // Counted in kilobytes, so sixty-four megabytes, past which a response // is served but not kept. ctx.cfg.cache_max_slot_size = 64 * 1024; ctx.cfg.case_sensitive_sort = 1; ctx.cfg.branch_sort = 0; ctx.cfg.commit_sort = 0; ctx.cfg.logo = "/cgit.png"; ctx.cfg.favicon = "/favicon.ico"; ctx.cfg.local_time = 0; ctx.cfg.date_mode = date_mode_from_type(DATE_SHORT); ctx.cfg.enable_header = 1; ctx.cfg.enable_mailmap = 1; ctx.cfg.enable_plain_email = 1; ctx.cfg.enable_relative_dates = 1; ctx.cfg.enable_http_clone = 1; ctx.cfg.enable_index_owner = 1; ctx.cfg.enable_tree_linenumbers = 1; ctx.cfg.enable_git_config = 0; ctx.cfg.max_repo_count = 50; ctx.cfg.max_commit_count = 50; ctx.cfg.max_patch_count = 50; ctx.cfg.max_diff_files = 200; ctx.cfg.max_diff_lines = 1000; ctx.cfg.max_msg_len = 80; ctx.cfg.max_ref_count = 200; ctx.cfg.max_repodesc_len = 80; // Counted in kilobytes, so ten megabytes, which bounds the memory one // request can be made to allocate for a blob. ctx.cfg.max_blob_size = 10 * 1024; ctx.cfg.max_stats = 0; ctx.cfg.project_list = NULL; ctx.cfg.renamelimit = -1; ctx.cfg.remove_suffix = 0; ctx.cfg.robots = "index, nofollow"; ctx.cfg.root_title = "Git repository browser"; ctx.cfg.root_desc = "a fast webinterface for the git dscm"; ctx.cfg.scan_hidden_path = 0; ctx.cfg.script_name = CGIT_SCRIPT_NAME; ctx.cfg.section = ""; ctx.cfg.repository_sort = "name"; ctx.cfg.section_sort = 0; ctx.cfg.summary_branches = 10; ctx.cfg.summary_log = 10; ctx.cfg.summary_tags = 10; ctx.cfg.max_atom_items = 10; ctx.cfg.difftype = DIFF_UNIFIED; ctx.env.cgit_config = getenv("CGIT_CONFIG"); ctx.env.http_host = getenv("HTTP_HOST"); ctx.env.https = getenv("HTTPS"); ctx.env.no_http = getenv("NO_HTTP"); ctx.env.path_info = getenv("PATH_INFO"); ctx.env.query_string = getenv("QUERY_STRING"); ctx.env.request_method = getenv("REQUEST_METHOD"); ctx.env.script_name = getenv("SCRIPT_NAME"); ctx.env.server_name = getenv("SERVER_NAME"); ctx.env.server_port = getenv("SERVER_PORT"); ctx.env.http_cookie = getenv("HTTP_COOKIE"); ctx.env.http_referer = getenv("HTTP_REFERER"); ctx.env.content_length = getenv("CONTENT_LENGTH") ? strtoul(getenv("CONTENT_LENGTH"), NULL, 10) : 0; ctx.env.authenticated = 0; ctx.page.mimetype = "text/html"; ctx.page.charset = PAGE_ENCODING; ctx.page.filename = NULL; ctx.page.size = 0; string_list_init_dup(&ctx.cfg.mimetypes); if (ctx.env.script_name) ctx.cfg.script_name = xstrdup(ctx.env.script_name); if (ctx.env.query_string) ctx.qry.raw = xstrdup(ctx.env.query_string); if (!ctx.env.cgit_config) ctx.env.cgit_config = CGIT_CONFIG; } static void print_version(void) { printf("CGit %s | https://github.com/brycekwon/cgit\n\nCompiled in features:\n", CGIT_VERSION); #ifdef NO_LUA printf("[-] "); #else printf("[+] "); #endif printf("Lua scripting\n"); #ifndef HAVE_LINUX_SENDFILE printf("[-] "); #else printf("[+] "); #endif printf("Linux sendfile() usage\n"); } static int cmp_repos(const void *a, const void *b) { const struct cgit_repo *repo_a = a, *repo_b = b; return strcmp(repo_a->url, repo_b->url); } static char *build_snapshot_setting(int mask) { const struct cgit_snapshot_format *format; struct strbuf result = STRBUF_INIT; for (format = cgit_snapshot_formats; format->suffix; format++) { if (cgit_snapshot_format_bit(format) & mask) { if (result.len) strbuf_addch(&result, ' '); strbuf_addstr(&result, format->suffix); } } return strbuf_detach(&result, NULL); } static void print_repo(FILE *f, struct cgit_repo *repo) { struct string_list_item *item; fprintf(f, "repo.url=%s\n", repo->url); fprintf(f, "repo.name=%s\n", repo->name); if (repo->path) fprintf(f, "repo.path=%s\n", repo->path); if (repo->owner) fprintf(f, "repo.owner=%s\n", repo->owner); if (repo->desc) fprintf(f, "repo.desc=%s\n", repo->desc); for_each_string_list_item(item, &repo->readme) { if (item->util) fprintf(f, "repo.readme=%s:%s\n", (char *)item->util, item->string); else fprintf(f, "repo.readme=%s\n", item->string); } if (repo->defbranch) fprintf(f, "repo.defbranch=%s\n", repo->defbranch); if (repo->head_content) fprintf(f, "repo.head-content=%s\n", repo->head_content); if (repo->module_link) fprintf(f, "repo.module-link=%s\n", repo->module_link); if (repo->section) fprintf(f, "repo.section=%s\n", repo->section); if (repo->clone_url) fprintf(f, "repo.clone-url=%s\n", repo->clone_url); fprintf(f, "repo.enable-blame=%d\n", repo->enable_blame); fprintf(f, "repo.enable-commit-graph=%d\n", repo->enable_commit_graph); fprintf(f, "repo.enable-follow-links=%d\n", repo->enable_follow_links); fprintf(f, "repo.enable-gitmodules-links=%d\n", repo->enable_gitmodules_links); fprintf(f, "repo.enable-log-filecount=%d\n", repo->enable_log_filecount); fprintf(f, "repo.enable-log-linecount=%d\n", repo->enable_log_linecount); fprintf(f, "repo.enable-mailmap=%d\n", repo->enable_mailmap); if (repo->about_filter && repo->about_filter != ctx.cfg.about_filter) cgit_fprintf_filter(repo->about_filter, f, "repo.about-filter="); if (repo->commit_filter && repo->commit_filter != ctx.cfg.commit_filter) cgit_fprintf_filter(repo->commit_filter, f, "repo.commit-filter="); if (repo->source_filter && repo->source_filter != ctx.cfg.source_filter) cgit_fprintf_filter(repo->source_filter, f, "repo.source-filter="); if (repo->email_filter && repo->email_filter != ctx.cfg.email_filter) cgit_fprintf_filter(repo->email_filter, f, "repo.email-filter="); if (repo->trailer_filter && repo->trailer_filter != ctx.cfg.trailer_filter) cgit_fprintf_filter(repo->trailer_filter, f, "repo.trailer-filter="); if (repo->snapshots != ctx.cfg.snapshots) { char *formats = build_snapshot_setting(repo->snapshots); fprintf(f, "repo.snapshots=%s\n", formats ? formats : ""); free(formats); } if (repo->snapshot_prefix) fprintf(f, "repo.snapshot-prefix=%s\n", repo->snapshot_prefix); if (repo->max_stats != ctx.cfg.max_stats) fprintf(f, "repo.max-stats=%s\n", cgit_find_stats_periodname(repo->max_stats)); if (repo->logo) fprintf(f, "repo.logo=%s\n", repo->logo); if (repo->logo_link) fprintf(f, "repo.logo-link=%s\n", repo->logo_link); fprintf(f, "repo.enable-remote-branches=%d\n", repo->enable_remote_branches); fprintf(f, "repo.enable-subject-links=%d\n", repo->enable_subject_links); fprintf(f, "repo.enable-commit-trailers=%d\n", repo->enable_commit_trailers); fprintf(f, "repo.enable-html-serving=%d\n", repo->enable_html_serving); if (repo->branch_sort == 1) fprintf(f, "repo.branch-sort=age\n"); if (repo->commit_sort) { if (repo->commit_sort == 1) fprintf(f, "repo.commit-sort=date\n"); else if (repo->commit_sort == 2) fprintf(f, "repo.commit-sort=topo\n"); } fprintf(f, "repo.hide=%d\n", repo->hide); fprintf(f, "repo.ignore=%d\n", repo->ignore); fprintf(f, "\n"); } static void print_repolist(FILE *f, struct cgit_repolist *list, int start) { int i; for (i = start; i < list->count; i++) print_repo(f, &list->repos[i]); } static void parse_args(int argc, const char **argv) { int i; const char *arg; int scanned = 0; for (i = 1; i < argc; i++) { if (!strcmp(argv[i], "--version")) { print_version(); exit(0); } if (skip_prefix(argv[i], "--cache=", &arg)) { ctx.cfg.cache_root = xstrdup(arg); } else if (!strcmp(argv[i], "--nohttp")) { ctx.env.no_http = "1"; } else if (skip_prefix(argv[i], "--query=", &arg)) { ctx.qry.raw = xstrdup(arg); } else if (skip_prefix(argv[i], "--repo=", &arg)) { ctx.qry.repo = xstrdup(arg); } else if (skip_prefix(argv[i], "--page=", &arg)) { 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; } else if (skip_prefix(argv[i], "--ofs=", &arg)) { ctx.qry.ofs = atoi(arg); } else if ( skip_prefix(argv[i], "--scan-tree=", &arg) || skip_prefix(argv[i], "--scan-path=", &arg) ) { // A repository's snapshots setting is masked with the // global one, and cgitrc has not been read here, so an // empty mask would discard what the repository set. ctx.cfg.snapshots = ALL_SNAPSHOT_FORMATS; scanned++; scan_tree(arg); } } if (scanned) { qsort(cgit_repolist.repos, cgit_repolist.count, sizeof(struct cgit_repo), cmp_repos); print_repolist(stdout, &cgit_repolist, 0); exit(0); } } /* * The lock is a fcntl lock rather than the mere existence of the lock file, * because a lock the kernel drops with its process cannot outlive a scan * killed mid-run. A leftover file would count as a scan forever in progress. */ static int generate_cached_repolist(const char *path, const char *cached_rc) { struct strbuf locked_rc = STRBUF_INIT; struct flock lock = { .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = 0, .l_len = 0, }; struct stat held, named; int err = 0; int fd; int first; FILE *f; strbuf_addf(&locked_rc, "%s.lock", cached_rc); fd = open(locked_rc.buf, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); if (fd == -1) { err = errno; fprintf(stderr, "[cgit] Unable to open %s: %s (%d)\n", locked_rc.buf, strerror(err), err); goto out; } if (fcntl(fd, F_SETLK, &lock) < 0) { // A lock held elsewhere only means concurrent requests, which // is not worth a line in the server log. Any other failure, a // filesystem without lock support say, silently costs a scan // on every request and does deserve one. err = errno; if (err != EACCES && err != EAGAIN) fprintf(stderr, "[cgit] Unable to lock %s: %s (%d)\n", locked_rc.buf, strerror(err), err); close(fd); goto out; } // The lock landed on whatever inode the path named at open. A holder // finishing in between renames that inode into place as the live // list, and once the path is confirmed to still name this file that // rename can no longer happen, because it takes the lock held here. if ( fstat(fd, &held) || stat(locked_rc.buf, &named) || held.st_ino != named.st_ino || held.st_dev != named.st_dev ) { err = EAGAIN; close(fd); goto out; } // A run that died before its rename leaves the lock file behind, so // start from empty now that nobody else can be writing it. if (ftruncate(fd, 0) < 0 || !(f = fdopen(fd, "w"))) { err = errno; fprintf(stderr, "[cgit] Unable to write %s: %s (%d)\n", locked_rc.buf, strerror(err), err); unlink(locked_rc.buf); close(fd); goto out; } first = cgit_repolist.count; if (ctx.cfg.project_list) scan_projects(path, ctx.cfg.project_list); else scan_tree(path); print_repolist(f, &cgit_repolist, first); // Flushed before the rename, because print_repolist writes through // stdio and a rename of the file would otherwise publish a repolist // that stops wherever the buffer happened to end. if (fflush(f) || ferror(f)) { err = errno; fprintf(stderr, "[cgit] Unable to write %s: %s (%d)\n", locked_rc.buf, strerror(err), err); unlink(locked_rc.buf); fclose(f); goto out; } if (rename(locked_rc.buf, cached_rc)) { err = errno; fprintf(stderr, "[cgit] Unable to rename %s to %s: %s (%d)\n", locked_rc.buf, cached_rc, strerror(err), err); unlink(locked_rc.buf); } // Closed after the rename so the lock is held until the fresh list is // in place, and nobody can truncate the file being renamed. fclose(f); out: strbuf_release(&locked_rc); return err; } /* * A cached repolist is itself a config file, so these two call each other. */ static void apply_config(const char *name, const char *value); static void process_cached_repolist(const char *path) { struct stat st; struct strbuf cached_rc = STRBUF_INIT; time_t age; unsigned long hash; int devnull; hash = cache_hash_str(path); if (ctx.cfg.project_list) hash += cache_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 yet, so this request scans in its own // process, leaving no copy behind when it cannot take the lock. 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; } config_file_parse(cached_rc.buf, apply_config); age = time(NULL) - st.st_mtime; if (age <= (ctx.cfg.cache_scan_ttl * 60)) goto out; // The list just parsed is stale but usable, so a child rebuilds it // while this request answers from what it already has. if (fork()) goto out; // The child inherits the request's descriptors, and the web server // reads stdout until every holder is gone, so left in place they // would keep the visitor waiting on the scan and let its output // land on the response. devnull = open("/dev/null", O_RDWR); if (devnull >= 0) { dup2(devnull, STDIN_FILENO); dup2(devnull, STDOUT_FILENO); dup2(devnull, STDERR_FILENO); if (devnull > STDERR_FILENO) close(devnull); } // _exit rather than exit, so the handlers the request registered do // not run a second time in the child. _exit(generate_cached_repolist(path, cached_rc.buf)); out: strbuf_release(&cached_rc); } static void add_mimetype(const char *name, const char *value) { struct string_list_item *item; item = string_list_insert(&ctx.cfg.mimetypes, name); item->util = xstrdup(value); } static void apply_config(const char *name, const char *value) { const char *arg; if (!strcmp(name, "section")) ctx.cfg.section = cgit_strdup_first_line(value); else if (!strcmp(name, "repo.url")) ctx.repo = cgit_add_repo(value); else if (ctx.repo && !strcmp(name, "repo.path")) ctx.repo->path = cgit_trim_end(value, '/'); else if (ctx.repo && skip_prefix(name, "repo.", &arg)) cgit_repo_config(ctx.repo, arg, value); else if (!strcmp(name, "readme")) string_list_append(&ctx.cfg.readme, cgit_strdup_first_line(value)); else if (!strcmp(name, "root-title")) ctx.cfg.root_title = cgit_strdup_first_line(value); else if (!strcmp(name, "root-desc")) ctx.cfg.root_desc = cgit_strdup_first_line(value); else if (!strcmp(name, "root-readme")) ctx.cfg.root_readme = cgit_strdup_first_line(value); else if (!strcmp(name, "css")) string_list_append(&ctx.cfg.css, cgit_strdup_first_line(value)); else if (!strcmp(name, "js")) string_list_append(&ctx.cfg.js, cgit_strdup_first_line(value)); else if (!strcmp(name, "favicon")) ctx.cfg.favicon = cgit_strdup_first_line(value); else if (!strcmp(name, "footer")) ctx.cfg.footer = cgit_strdup_first_line(value); else if (!strcmp(name, "head-include")) ctx.cfg.head_include = cgit_strdup_first_line(value); else if (!strcmp(name, "header")) ctx.cfg.header = cgit_strdup_first_line(value); else if (!strcmp(name, "logo")) ctx.cfg.logo = cgit_strdup_first_line(value); else if (!strcmp(name, "logo-link")) ctx.cfg.logo_link = cgit_strdup_first_line(value); else if (!strcmp(name, "module-link")) ctx.cfg.module_link = cgit_strdup_first_line(value); else if (!strcmp(name, "strict-export")) ctx.cfg.strict_export = cgit_strdup_first_line(value); else if (!strcmp(name, "virtual-root")) ctx.cfg.virtual_root = cgit_ensure_end(value, '/'); else if (!strcmp(name, "enable-plain-email")) ctx.cfg.enable_plain_email = atoi(value); else if (!strcmp(name, "enable-header")) ctx.cfg.enable_header = atoi(value); else if (!strcmp(name, "snapshots")) ctx.cfg.snapshots = cgit_parse_snapshots_mask(value); else if (!strcmp(name, "trust-scan-config")) ctx.cfg.trust_scan_config = atoi(value); else if (!strcmp(name, "enable-follow-links")) ctx.cfg.enable_follow_links = atoi(value); else if (!strcmp(name, "enable-http-clone")) ctx.cfg.enable_http_clone = atoi(value); else if (!strcmp(name, "enable-index-links")) ctx.cfg.enable_index_links = atoi(value); else if (!strcmp(name, "enable-index-owner")) ctx.cfg.enable_index_owner = atoi(value); else if (!strcmp(name, "enable-blame")) ctx.cfg.enable_blame = atoi(value); else if (!strcmp(name, "enable-commit-graph")) ctx.cfg.enable_commit_graph = atoi(value); else if (!strcmp(name, "enable-gitmodules-links")) ctx.cfg.enable_gitmodules_links = atoi(value); else if (!strcmp(name, "enable-log-filecount")) ctx.cfg.enable_log_filecount = atoi(value); else if (!strcmp(name, "enable-log-linecount")) ctx.cfg.enable_log_linecount = atoi(value); else if (!strcmp(name, "enable-mailmap")) ctx.cfg.enable_mailmap = atoi(value); else if (!strcmp(name, "enable-relative-dates")) ctx.cfg.enable_relative_dates = atoi(value); else if (!strcmp(name, "enable-remote-branches")) ctx.cfg.enable_remote_branches = atoi(value); else if (!strcmp(name, "enable-subject-links")) ctx.cfg.enable_subject_links = atoi(value); else if (!strcmp(name, "enable-commit-trailers")) ctx.cfg.enable_commit_trailers = atoi(value); else if (!strcmp(name, "enable-html-serving")) ctx.cfg.enable_html_serving = atoi(value); else if (!strcmp(name, "enable-tree-linenumbers")) ctx.cfg.enable_tree_linenumbers = atoi(value); else if (!strcmp(name, "enable-tree-group-dirs")) ctx.cfg.enable_tree_group_dirs = atoi(value); else if (!strcmp(name, "enable-git-config")) ctx.cfg.enable_git_config = atoi(value); else if (!strcmp(name, "enable-cache-list")) ctx.cfg.enable_cache_list = 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 = cgit_strdup_first_line(cgit_expand_macros(value)); else if (!strcmp(name, "cache-index-ttl")) ctx.cfg.cache_index_ttl = atoi(value); else if (!strcmp(name, "cache-summary-ttl")) ctx.cfg.cache_summary_ttl = atoi(value); else if (!strcmp(name, "cache-scan-ttl")) ctx.cfg.cache_scan_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, "cache-max-slot-size")) ctx.cfg.cache_max_slot_size = atoi(value); else if (!strcmp(name, "case-sensitive-sort")) ctx.cfg.case_sensitive_sort = atoi(value); else if (!strcmp(name, "about-filter")) ctx.cfg.about_filter = cgit_new_filter(value, ABOUT); else if (!strcmp(name, "commit-filter")) ctx.cfg.commit_filter = cgit_new_filter(value, COMMIT); else if (!strcmp(name, "email-filter")) ctx.cfg.email_filter = cgit_new_filter(value, EMAIL); else if (!strcmp(name, "trailer-filter")) ctx.cfg.trailer_filter = cgit_new_filter(value, TRAILER); else if (!strcmp(name, "auth-filter")) ctx.cfg.auth_filter = cgit_new_filter(value, AUTH); else if (!strcmp(name, "embedded")) ctx.cfg.embedded = atoi(value); else if (!strcmp(name, "max-atom-items")) ctx.cfg.max_atom_items = atoi(value); else if (!strcmp(name, "max-message-length")) ctx.cfg.max_msg_len = atoi(value); else if (!strcmp(name, "max-repodesc-length")) ctx.cfg.max_repodesc_len = atoi(value); else if (!strcmp(name, "max-blob-size")) ctx.cfg.max_blob_size = atoi(value); else if (!strcmp(name, "max-diff-files")) ctx.cfg.max_diff_files = atoi(value); else if (!strcmp(name, "max-diff-lines")) ctx.cfg.max_diff_lines = atoi(value); else if (!strcmp(name, "max-ref-count")) ctx.cfg.max_ref_count = atoi(value); else if (!strcmp(name, "max-repo-count")) { ctx.cfg.max_repo_count = atoi(value); if (ctx.cfg.max_repo_count <= 0) ctx.cfg.max_repo_count = INT_MAX; } else if (!strcmp(name, "max-commit-count")) ctx.cfg.max_commit_count = atoi(value); else if (!strcmp(name, "max-patch-count")) ctx.cfg.max_patch_count = atoi(value); else if (!strcmp(name, "project-list")) ctx.cfg.project_list = cgit_strdup_first_line(cgit_expand_macros(value)); else if (!strcmp(name, "scan-path")) { if (ctx.cfg.cache_size) process_cached_repolist(cgit_expand_macros(value)); else if (ctx.cfg.project_list) scan_projects(cgit_expand_macros(value), ctx.cfg.project_list); else scan_tree(cgit_expand_macros(value)); // The scan grows the repository array, so a record taken before // it may have moved, and a repo key after it belongs to nothing. ctx.repo = NULL; } else if (!strcmp(name, "scan-hidden-path")) ctx.cfg.scan_hidden_path = atoi(value); else if (!strcmp(name, "section-from-path")) ctx.cfg.section_from_path = atoi(value); else if (!strcmp(name, "repository-sort")) ctx.cfg.repository_sort = cgit_strdup_first_line(value); else if (!strcmp(name, "section-sort")) ctx.cfg.section_sort = atoi(value); else if (!strcmp(name, "source-filter")) ctx.cfg.source_filter = cgit_new_filter(value, SOURCE); else if (!strcmp(name, "summary-log")) ctx.cfg.summary_log = atoi(value); else if (!strcmp(name, "summary-branches")) ctx.cfg.summary_branches = atoi(value); else if (!strcmp(name, "summary-tags")) ctx.cfg.summary_tags = atoi(value); else if (!strcmp(name, "side-by-side-diffs")) ctx.cfg.difftype = atoi(value) ? DIFF_SSDIFF : DIFF_UNIFIED; else if (!strcmp(name, "age-file")) ctx.cfg.agefile = cgit_strdup_first_line(value); else if (!strcmp(name, "mimetype-file")) ctx.cfg.mimetype_file = cgit_strdup_first_line(value); else if (!strcmp(name, "rename-limit")) ctx.cfg.renamelimit = atoi(value); else if (!strcmp(name, "remove-suffix")) ctx.cfg.remove_suffix = atoi(value); else if (!strcmp(name, "robots")) ctx.cfg.robots = cgit_strdup_first_line(value); else if (!strcmp(name, "clone-prefix")) ctx.cfg.clone_prefix = cgit_strdup_first_line(value); else if (!strcmp(name, "clone-url")) ctx.cfg.clone_url = cgit_strdup_first_line(value); else if (!strcmp(name, "local-time")) ctx.cfg.local_time = atoi(value); else if (!strcmp(name, "date-format")) cgit_parse_date_format(value, &ctx.cfg.date_mode); else if (!strcmp(name, "commit-sort")) { if (!strcmp(value, "date")) ctx.cfg.commit_sort = 1; if (!strcmp(value, "topo")) ctx.cfg.commit_sort = 2; } else if (!strcmp(name, "branch-sort")) { if (!strcmp(value, "age")) ctx.cfg.branch_sort = 1; if (!strcmp(value, "name")) ctx.cfg.branch_sort = 0; } else if (skip_prefix(name, "mimetype.", &arg)) add_mimetype(arg, value); else if (!strcmp(name, "include")) config_file_parse(cgit_expand_macros(value), apply_config); else if (skip_prefix(name, "repo.", &arg)) fprintf(stderr, "[cgit] Ignoring %s before any repo.url\n", name); else fprintf(stderr, "[cgit] Unknown config key: %s\n", name); } /* * Read a whole number a request supplied, clamped into the range the caller * accepts. strtol rather than atoi, because atoi is undefined on overflow * and every value here arrives straight from the query string. */ static int query_int(const char *value, int min, int max) { long number = strtol(value, NULL, 10); if (number < min) return min; if (number > max) return max; return number; } static void apply_query_param(const char *name, const char *value) { if (!value) value = ""; if (!strcmp(name, "r")) { ctx.qry.repo = xstrdup(value); ctx.repo = cgit_get_repoinfo(value); } else if (!strcmp(name, "p")) { ctx.qry.page = xstrdup(value); } else if (!strcmp(name, "url")) { // Every leading slash goes, not just one, so a value like // //example.com cannot survive as /example.com and make the // virtual root join a scheme-relative link to another host. while (*value == '/') value++; ctx.qry.url = xstrdup(value); cgit_parse_url(value); } else if (!strcmp(name, "qt")) { ctx.qry.grep = xstrdup(value); } else if (!strcmp(name, "q")) { if (strlen(value) > MAX_SEARCH_LEN) ctx.qry.search = xstrndup(value, MAX_SEARCH_LEN); else 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; } else if (!strcmp(name, "id2")) { ctx.qry.oid2 = xstrdup(value); ctx.qry.has_oid = 1; } else if (!strcmp(name, "ofs")) { // Bounded above so a crafted value cannot force a walk over // the whole history. The floor is -1 rather than 0 because // the stats page submits -1 for all authors. ctx.qry.ofs = query_int(value, -1, MAX_QUERY_OFFSET); } else if (!strcmp(name, "path")) { ctx.qry.path = cgit_trim_end(value, '/'); } else if (!strcmp(name, "s")) { ctx.qry.sort = xstrdup(value); } else if (!strcmp(name, "showmsg")) { ctx.qry.showmsg = query_int(value, INT_MIN, INT_MAX); } else if (!strcmp(name, "period")) { ctx.qry.period = xstrdup(value); } else if (!strcmp(name, "dt")) { ctx.qry.difftype = query_int(value, INT_MIN, INT_MAX); ctx.qry.has_difftype = 1; } else if (!strcmp(name, "ss")) { // No longer generated, but old links still carry it. ctx.qry.difftype = query_int(value, INT_MIN, INT_MAX) ? DIFF_SSDIFF : DIFF_UNIFIED; ctx.qry.has_difftype = 1; } else if (!strcmp(name, "all")) { ctx.qry.show_all = query_int(value, INT_MIN, INT_MAX); } else if (!strcmp(name, "context")) { // Context lines are not counted against max-diff-lines, so an // unbounded width turns a whole blob into context and renders // it in full however small the change was. ctx.qry.context = query_int(value, 0, MAX_DIFF_CONTEXT_LINES); } else if (!strcmp(name, "ignorews")) { ctx.qry.ignorews = query_int(value, INT_MIN, INT_MAX); } else if (!strcmp(name, "follow")) { ctx.qry.follow = query_int(value, INT_MIN, INT_MAX); } } static void open_auth_filter(const char *action) { cgit_open_filter(ctx.cfg.auth_filter, action, ctx.env.http_cookie ? ctx.env.http_cookie : "", ctx.env.request_method ? ctx.env.request_method : "", ctx.env.query_string ? ctx.env.query_string : "", ctx.env.http_referer ? ctx.env.http_referer : "", ctx.env.path_info ? ctx.env.path_info : "", ctx.env.http_host ? ctx.env.http_host : "", ctx.env.https ? ctx.env.https : "", ctx.qry.repo ? ctx.qry.repo : "", ctx.qry.page ? ctx.qry.page : "", cgit_currentfullurl(), cgit_loginurl()); } /* * The filter answers the login POST itself, writing the status line and every * header, so nothing here prints any and the process ends before this returns. */ static void authenticate_post(void) { char buffer[MAX_AUTHENTICATION_POST_BYTES]; size_t len; ssize_t got; open_auth_filter("authenticate-post"); len = ctx.env.content_length; if (len > MAX_AUTHENTICATION_POST_BYTES) len = MAX_AUTHENTICATION_POST_BYTES; // The body can arrive in more than one write, so read until it is // all there or the server closes the stream. if ((got = read_in_full(STDIN_FILENO, buffer, len)) < 0) die_errno("Unable to read the POST body"); if (write_in_full(STDOUT_FILENO, buffer, got) < 0) die_errno("Unable to pass the POST body to the auth filter"); cgit_close_filter(ctx.cfg.auth_filter); exit(0); } static void authenticate_cookie(void) { if (!ctx.cfg.auth_filter) { ctx.env.authenticated = 1; return; } if ( ctx.env.request_method && ctx.qry.page && !ctx.repo && !strcmp(ctx.env.request_method, "POST") && !strcmp(ctx.qry.page, "login") ) { authenticate_post(); return; } open_auth_filter("authenticate-cookie"); ctx.env.authenticated = cgit_close_filter(ctx.cfg.auth_filter); } /* * Only a full object id names content that can never change. The id * parameter accepts anything git can resolve, so a page pinned to a ref * name or abbreviation must not be cached under the static ttl. */ static int is_full_oid(const char *rev) { size_t len = strlen(rev); if (len != GIT_SHA1_HEXSZ && len != GIT_SHA256_HEXSZ) return 0; for (; *rev; rev++) { if (!isxdigit((unsigned char)*rev)) return 0; } return 1; } /* * Only these pages render the same bytes for the same id for ever. A log or * refs page named with an id still lists branches and tags, which move. */ static int page_is_static(void) { static const char *const pages[] = { "blame", "blob", "commit", "diff", "patch", "plain", "rawdiff", "snapshot", "tag", "tree", }; size_t i; for (i = 0; i < ARRAY_SIZE(pages); i++) { if (!strcmp(ctx.qry.page, pages[i])) return 1; } return 0; } /* * Every cache-*-ttl setting is written in minutes, and so is this. */ static int calc_ttl(void) { const struct cgit_cmd *cmd; if (!ctx.repo) return ctx.cfg.cache_index_ttl; if (!ctx.qry.page) return ctx.cfg.cache_summary_ttl; // The dumb transport serves files that already sit on the disk, so a // copy in a slot would cost that disk twice and gain nothing. cmd = cgit_find_cmd(ctx.qry.page); if (cmd && cmd->is_clone) return 0; if (!strcmp(ctx.qry.page, "about")) return ctx.cfg.cache_about_ttl; // Checked ahead of the snapshot ttl, because a tarball pinned to an // object id can never come out differently and is the most expensive // page to rebuild. if ( ctx.qry.has_oid && page_is_static() && (!ctx.qry.oid || is_full_oid(ctx.qry.oid)) && (!ctx.qry.oid2 || is_full_oid(ctx.qry.oid2)) ) return ctx.cfg.cache_static_ttl; if (!strcmp(ctx.qry.page, "snapshot")) return ctx.cfg.cache_snapshot_ttl; if (ctx.qry.has_symref) return ctx.cfg.cache_dynamic_ttl; return ctx.cfg.cache_summary_ttl; } /* * The scheme and host are folded in because the page's absolute urls, its * clone urls and atom links, are built from them, so a spoofed Host must * not poison the page served on the real one. */ static void build_cache_key(struct strbuf *key) { char *hosturl = cgit_hosturl(); const char *parts[] = { cgit_httpscheme(), hosturl ? hosturl : "", ctx.env.path_info ? ctx.env.path_info : "", ctx.env.query_string ? ctx.env.query_string : "", }; size_t i; // Each part is written behind its own length so no value can make two // requests spell one key, and the path and query come from the // environment because cgit's rebuilt query string folds them together. for (i = 0; i < ARRAY_SIZE(parts); i++) strbuf_addf(key, "%zu|%s", strlen(parts[i]), parts[i]); free(hosturl); } /* * Returning non-zero ends git's walk, so the search stops at the first hit. */ static int find_current_ref(const struct reference *ref, void *data) { struct refmatch *match = data; if (!strcmp(ref->name, match->wanted)) match->found = 1; // The fallback has to pass the check every request makes on its head, // or one branch named with a leading dash takes the repository offline. if (!match->first && ref->name[0] != '-') match->first = xstrdup(ref->name); return match->found; } static char *find_default_branch(struct cgit_repo *repo) { struct refmatch match; char *ref; match.wanted = repo->defbranch; match.first = NULL; match.found = 0; refs_for_each_branch_ref(get_main_ref_store(the_repository), find_current_ref, &match); if (match.found) ref = match.wanted; else ref = match.first; if (ref) ref = xstrdup(ref); free(match.first); return ref; } static char *guess_defbranch(void) { const char *ref, *refname; struct object_id oid; ref = refs_resolve_ref_unsafe(get_main_ref_store(the_repository), "HEAD", 0, &oid, NULL); if (!ref || !skip_prefix(ref, "refs/heads/", &refname)) return "master"; return xstrdup(refname); } /* * Split one readme setting into the file it names and the ref that file is * read from, leaving the ref NULL for a file on disk. The caller frees both. */ static void parse_readme(const char *readme, char **filename, char **ref, struct cgit_repo *repo) { const char *colon; *filename = NULL; *ref = NULL; if (!readme || !readme[0]) return; // A colon separates a ref from a path, so a setting carrying one names // a file tracked in the repository rather than one on disk. colon = strchr(readme, ':'); if (colon && strlen(colon) > 1) { if (colon == readme && ctx.qry.head) *ref = xstrdup(ctx.qry.head); else if (colon == readme && repo->defbranch) *ref = xstrdup(repo->defbranch); else *ref = xstrndup(readme, colon - readme); readme = colon + 1; } if (!(*ref) && readme[0] != '/') *filename = cgit_fmtalloc("%s/%s", repo->path, readme); else *filename = xstrdup(readme); } static void choose_readme(struct cgit_repo *repo) { int found; char *filename = NULL, *ref = NULL; struct string_list *list; struct string_list_item *entry; // A repository without readme settings of its own follows the global // ones, which it must not free. list = repo->readme.nr ? &repo->readme : &ctx.cfg.readme; if (!list->nr) return; found = 0; for_each_string_list_item(entry, list) { parse_readme(entry->string, &filename, &ref, repo); if (!filename) { free(ref); continue; } if (ref) { if (cgit_ref_path_exists(filename, ref, 1)) { found = 1; break; } } else if (!access(filename, R_OK)) { found = 1; break; } free(filename); free(ref); } repo->readme.strdup_strings = 1; string_list_clear(&repo->readme, 0); repo->readme.strdup_strings = 0; if (found) string_list_append(&repo->readme, filename)->util = ref; } static void prepare_repo_env(int *nongit, int *err) { // A repository configured without a path has nothing to open. if (!ctx.repo->path) { *nongit = 1; *err = 0; return; } setenv("GIT_DIR", ctx.repo->path, 1); errno = 0; setup_git_directory_gently(the_repository, nongit); *err = errno; // Notes come out of the object store, which a repository that failed // to open has none of. if (!*nongit) load_display_notes(NULL); } /* * Returns non-zero once it has written a complete response of its own, in * which case the caller must not render a page over the top of it. */ static int prepare_repo_cmd(int nongit, int err, int clone) { struct object_id oid; if (nongit) { const char *name = ctx.repo->name; ctx.page.title = cgit_fmtalloc("%s - %s", ctx.cfg.root_title, "config error"); ctx.repo = NULL; cgit_print_error_page( 404, "Not Found", "Failed to open %s: %s", name, err ? strerror(err) : "Not a valid git repository" ); return 1; } ctx.page.title = cgit_fmtalloc("%s - %s", ctx.repo->name, ctx.repo->desc); if (!ctx.repo->defbranch) ctx.repo->defbranch = guess_defbranch(); if (!ctx.qry.head) { ctx.qry.nohead = 1; ctx.qry.head = find_default_branch(ctx.repo); } if (!ctx.qry.head) { // The dumb transport serves refs and objects off the disk and // asks nothing of the head, and an empty repository clones. if (clone) { cgit_prepare_repo_env(ctx.repo); return 0; } ctx.empty_repo = 1; // Before the document starts, since the
carries // and those clone urls expand macros such // as $CGIT_REPO_URL out of this environment. cgit_prepare_repo_env(ctx.repo); cgit_print_http_headers(); cgit_print_docstart(); cgit_print_pageheader(); cgit_print_empty_repo(); cgit_print_docend(); return 1; } // Every revision the request names is checked before git sees it, // see cgit_valid_rev, and the head has to resolve as well. if (!cgit_valid_rev(ctx.qry.head) || repo_get_oid(the_repository, ctx.qry.head, &oid)) { char *old_head = ctx.qry.head; ctx.qry.head = xstrdup(ctx.repo->defbranch); cgit_print_error_page(404, "Not Found", "Invalid branch: %s", old_head); free(old_head); return 1; } if ( (ctx.qry.oid && !cgit_valid_rev(ctx.qry.oid)) || (ctx.qry.oid2 && !cgit_valid_rev(ctx.qry.oid2)) ) { cgit_print_error_page(400, "Bad Request", "Invalid revision"); return 1; } string_list_sort(&ctx.repo->submodules); cgit_prepare_repo_env(ctx.repo); choose_readme(ctx.repo); return 0; } static void process_request(void) { const struct cgit_cmd *cmd; int nongit = 0, err = 0; // An unauthenticated request is answered with the filter's own body // whatever page it asked for. if (!ctx.env.authenticated) { ctx.page.title = "Authentication Required"; cgit_print_http_headers(); cgit_print_docstart(); cgit_print_pageheader(); open_auth_filter("body"); cgit_close_filter(ctx.cfg.auth_filter); cgit_print_docend(); return; } if (ctx.repo) prepare_repo_env(&nongit, &err); cmd = cgit_get_cmd(); if (!cmd || (!ctx.cfg.enable_http_clone && cmd->is_clone)) { // The error page carries the repository's own header, whose // branch switcher needs the head resolved first. if (ctx.repo && prepare_repo_cmd(nongit, err, 0)) return; ctx.page.title = "cgit error"; cgit_print_error_page(404, "Not Found", "Invalid request"); return; } if (cmd->want_repo && !ctx.repo) { cgit_print_error_page(400, "Bad Request", "No repository selected"); return; } ctx.qry.vpath = cmd->want_vpath ? ctx.qry.path : NULL; if (ctx.repo && prepare_repo_cmd(nongit, err, cmd->is_clone)) return; cmd->fn(); } void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *value) { const char *path; struct string_list_item *item; if (!strcmp(name, "name")) repo->name = cgit_strdup_first_line(value); else if (!strcmp(name, "clone-url")) repo->clone_url = cgit_strdup_first_line(value); else if (!strcmp(name, "desc")) repo->desc = cgit_strdup_first_line(value); else if (!strcmp(name, "owner")) repo->owner = cgit_strdup_first_line(value); else if (!strcmp(name, "defbranch")) repo->defbranch = cgit_strdup_first_line(value); else if (!strcmp(name, "head-content")) repo->head_content = cgit_strdup_first_line(value); else if (!strcmp(name, "snapshots")) repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); else if (!strcmp(name, "enable-blame")) repo->enable_blame = atoi(value); else if (!strcmp(name, "enable-commit-graph")) repo->enable_commit_graph = atoi(value); else if (!strcmp(name, "enable-follow-links")) repo->enable_follow_links = atoi(value); else if (!strcmp(name, "enable-gitmodules-links")) repo->enable_gitmodules_links = atoi(value); else if (!strcmp(name, "enable-log-filecount")) repo->enable_log_filecount = atoi(value); else if (!strcmp(name, "enable-log-linecount")) repo->enable_log_linecount = atoi(value); else if (!strcmp(name, "enable-mailmap")) repo->enable_mailmap = atoi(value); else if (!strcmp(name, "enable-remote-branches")) repo->enable_remote_branches = atoi(value); else if (!strcmp(name, "enable-subject-links")) repo->enable_subject_links = atoi(value); else if (!strcmp(name, "enable-commit-trailers")) repo->enable_commit_trailers = atoi(value); else if (!strcmp(name, "enable-html-serving")) repo->enable_html_serving = atoi(value); else if (!strcmp(name, "branch-sort")) { if (!strcmp(value, "age")) repo->branch_sort = 1; if (!strcmp(value, "name")) repo->branch_sort = 0; } else if (!strcmp(name, "commit-sort")) { if (!strcmp(value, "date")) repo->commit_sort = 1; if (!strcmp(value, "topo")) repo->commit_sort = 2; } else if (!strcmp(name, "max-stats")) repo->max_stats = cgit_find_stats_period(value, NULL); else if (!strcmp(name, "module-link")) repo->module_link = cgit_strdup_first_line(value); else if (skip_prefix(name, "module-link.", &path)) { item = string_list_append(&repo->submodules, cgit_strdup_first_line(path)); item->util = cgit_strdup_first_line(value); } else if (!strcmp(name, "section")) repo->section = cgit_strdup_first_line(value); else if (!strcmp(name, "snapshot-prefix")) repo->snapshot_prefix = cgit_strdup_first_line(value); else if (!strcmp(name, "readme")) string_list_append(&repo->readme, cgit_strdup_first_line(value)); else if (!strcmp(name, "logo")) repo->logo = cgit_strdup_first_line(value); else if (!strcmp(name, "logo-link")) repo->logo_link = cgit_strdup_first_line(value); else if (!strcmp(name, "hide")) repo->hide = atoi(value); else if (!strcmp(name, "ignore")) repo->ignore = atoi(value); else if (!strcmp(name, "about-filter")) repo->about_filter = cgit_new_filter(value, ABOUT); else if (!strcmp(name, "commit-filter")) repo->commit_filter = cgit_new_filter(value, COMMIT); else if (!strcmp(name, "source-filter")) repo->source_filter = cgit_new_filter(value, SOURCE); else if (!strcmp(name, "email-filter")) repo->email_filter = cgit_new_filter(value, EMAIL); else if (!strcmp(name, "trailer-filter")) repo->trailer_filter = cgit_new_filter(value, TRAILER); else fprintf(stderr, "[cgit] Unknown repo config key: %s\n", name); } int cmd_main(int argc, const char **argv) { struct strbuf cache_key = STRBUF_INIT; const char *path; size_t max_bytes = 0; int err, ttl; isolate_git_environment(); // Ignored, a filter that exits early makes the write fail with EPIPE // and the error page reach the visitor, instead of ending cgit. signal(SIGPIPE, SIG_IGN); cgit_init_filters(); atexit(cgit_cleanup_filters); // Registered second so it runs first, since exit is reached from error // paths and from the HEAD shortcut with a page still buffered. atexit(html_flush); set_die_routine(die_routine); prepare_context(); cgit_repolist.length = 0; cgit_repolist.count = 0; cgit_repolist.repos = NULL; parse_args(argc, argv); config_file_parse(cgit_expand_macros(ctx.env.cgit_config), apply_config); ctx.repo = NULL; http_parse_querystring(ctx.qry.raw, apply_query_param); if (!ctx.cfg.virtual_root && ctx.cfg.script_name) ctx.cfg.virtual_root = cgit_ensure_end(ctx.cfg.script_name, '/'); // Falling back to PATH_INFO lets cgit serve virtual urls without a // rewrite rule in the web server, and folding it into the raw query // string keeps it part of the cache key. path = ctx.env.path_info; if (!ctx.qry.url && path) { // Stripped like the url parameter and for the same reason. while (*path == '/') path++; ctx.qry.url = xstrdup(path); if (ctx.qry.raw) { char *path_and_query = cgit_fmtalloc("%s?%s", path, ctx.qry.raw); free(ctx.qry.raw); ctx.qry.raw = path_and_query; } else { ctx.qry.raw = xstrdup(ctx.qry.url); } cgit_parse_url(ctx.qry.url); } authenticate_cookie(); ttl = calc_ttl(); // An unauthenticated request gets a body meant for one visitor, and a // HEAD request stops after the headers. if (!ctx.env.authenticated || (ctx.env.request_method && !strcmp(ctx.env.request_method, "HEAD"))) ctx.cfg.cache_size = 0; // Written in kilobytes in cgitrc, where zero and below lift the bound. if (ctx.cfg.cache_max_slot_size > 0) max_bytes = (size_t)ctx.cfg.cache_max_slot_size * 1024; build_cache_key(&cache_key); err = cache_process(ctx.cfg.cache_size, ctx.cfg.cache_root, cache_key.buf, ttl, max_bytes, process_request); strbuf_release(&cache_key); cgit_cleanup_filters(); // A slot that could not be sent because the client went away is not // worth an error page nobody will read. if (err && err != EPIPE) cgit_print_error("Error processing page: %s (%d)", strerror(err), err); return err; }