diff options
Diffstat (limited to '')
| -rw-r--r-- | source/cgit.c | 177 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1 file changed, 125 insertions, 52 deletions
diff --git a/source/cgit.c b/source/cgit.c index 5b26137..00ce97f 100644 --- a/source/cgit.c +++ b/source/cgit.c @@ -46,10 +46,8 @@ // 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. - */ +// 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; @@ -76,13 +74,16 @@ static void isolate_git_environment(void) /* * 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. + * 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) { - // The error page abandons any cache fill in progress, so the message - // reaches the visitor rather than the cache file stdout points at. - cgit_vprint_error_page(400, "Bad Request", msg, 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); } @@ -154,7 +155,8 @@ static void prepare_context(void) 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.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; @@ -189,6 +191,7 @@ static void print_version(void) 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); } @@ -213,7 +216,8 @@ static void print_repo(FILE *f, struct cgit_repo *repo) fprintf(f, "repo.url=%s\n", repo->url); fprintf(f, "repo.name=%s\n", repo->name); - fprintf(f, "repo.path=%s\n", repo->path); + 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) @@ -318,7 +322,10 @@ static void parse_args(int argc, const char **argv) 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)) { + } 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. @@ -358,7 +365,7 @@ static int generate_cached_repolist(const char *path, const char *cached_rc) fd = open(locked_rc.buf, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); if (fd == -1) { err = errno; - fprintf(stderr, "[cgit] Error opening %s: %s (%d)\n", locked_rc.buf, strerror(err), err); + 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) { @@ -368,7 +375,7 @@ static int generate_cached_repolist(const char *path, const char *cached_rc) // on every request and does deserve one. err = errno; if (err != EACCES && err != EAGAIN) - fprintf(stderr, "[cgit] Error locking %s: %s (%d)\n", locked_rc.buf, strerror(err), err); + fprintf(stderr, "[cgit] Unable to lock %s: %s (%d)\n", locked_rc.buf, strerror(err), err); close(fd); goto out; } @@ -390,7 +397,7 @@ static int generate_cached_repolist(const char *path, const char *cached_rc) // 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] Error writing %s: %s (%d)\n", locked_rc.buf, strerror(err), err); + fprintf(stderr, "[cgit] Unable to write %s: %s (%d)\n", locked_rc.buf, strerror(err), err); unlink(locked_rc.buf); close(fd); goto out; @@ -406,14 +413,14 @@ static int generate_cached_repolist(const char *path, const char *cached_rc) // that stops wherever the buffer happened to end. if (fflush(f) || ferror(f)) { err = errno; - fprintf(stderr, "[cgit] Error writing %s: %s (%d)\n", locked_rc.buf, strerror(err), err); + 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] Error renaming %s to %s: %s (%d)\n", + fprintf(stderr, "[cgit] Unable to rename %s to %s: %s (%d)\n", locked_rc.buf, cached_rc, strerror(err), err); unlink(locked_rc.buf); } @@ -425,7 +432,9 @@ out: return err; } -// A cached repolist is itself a config file, so these two call each other. +/* + * 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) @@ -539,8 +548,8 @@ static void apply_config(const char *name, const char *value) 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-filters")) - ctx.cfg.trust_scan_filters = atoi(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")) @@ -644,6 +653,9 @@ static void apply_config(const char *name, const char *value) 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")) @@ -721,7 +733,7 @@ static void apply_query_param(const char *name, const char *value) if (!value) value = ""; - if (!strcmp(name,"r")) { + if (!strcmp(name, "r")) { ctx.qry.repo = xstrdup(value); ctx.repo = cgit_get_repoinfo(value); } else if (!strcmp(name, "p")) { @@ -814,10 +826,12 @@ static void authenticate_post(void) len = ctx.env.content_length; if (len > MAX_AUTHENTICATION_POST_BYTES) len = MAX_AUTHENTICATION_POST_BYTES; - if ((got = read(STDIN_FILENO, buffer, len)) < 0) - die_errno("Could not read POST from stdin"); - if (write(STDOUT_FILENO, buffer, got) < 0) - die_errno("Could not write POST to stdout"); + // 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); } @@ -856,13 +870,42 @@ static int is_full_oid(const char *rev) if (len != GIT_SHA1_HEXSZ && len != GIT_SHA256_HEXSZ) return 0; for (; *rev; rev++) { - if (!isxdigit(*rev)) + if (!isxdigit((unsigned char)*rev)) return 0; } return 1; } -// Every cache-*-ttl setting is written in minutes, and so is this. +/* + * 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) { if (!ctx.repo) @@ -879,6 +922,7 @@ static int calc_ttl(void) // 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)) ) @@ -917,14 +961,18 @@ static void build_cache_key(struct strbuf *key) free(hosturl); } -// Returning non-zero ends git's walk, so the search stops at the first hit. +/* + * 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; - if (!match->first) + // 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; } @@ -996,14 +1044,18 @@ static void parse_readme(const char *readme, char **filename, char **ref, struct static void choose_readme(struct cgit_repo *repo) { int found; - char *filename, *ref; + char *filename = NULL, *ref = NULL; + struct string_list *list; struct string_list_item *entry; - if (!repo->readme.nr) + // 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, &repo->readme) { + for_each_string_list_item(entry, list) { parse_readme(entry->string, &filename, &ref, repo); if (!filename) { free(ref); @@ -1028,11 +1080,19 @@ static void choose_readme(struct cgit_repo *repo) string_list_append(&repo->readme, filename)->util = ref; } -static void prepare_repo_env(int *nongit) +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) @@ -1043,14 +1103,12 @@ static void prepare_repo_env(int *nongit) * 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) +static int prepare_repo_cmd(int nongit, int err, int clone) { struct object_id oid; - int err; if (nongit) { const char *name = ctx.repo->name; - err = errno; ctx.page.title = cgit_fmtalloc("%s - %s", ctx.cfg.root_title, "config error"); ctx.repo = NULL; cgit_print_error_page( @@ -1070,6 +1128,12 @@ static int prepare_repo_cmd(int nongit) } 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 <head> carries // <link rel='vcs-git'> and those clone urls expand macros such @@ -1083,13 +1147,22 @@ static int prepare_repo_cmd(int nongit) return 1; } - if (repo_get_oid(the_repository, ctx.qry.head, &oid)) { + // 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); @@ -1099,7 +1172,7 @@ static int prepare_repo_cmd(int nongit) static void process_request(void) { const struct cgit_cmd *cmd; - int nongit = 0; + int nongit = 0, err = 0; // An unauthenticated request is answered with the filter's own body // whatever page it asked for. @@ -1115,16 +1188,14 @@ static void process_request(void) } if (ctx.repo) - prepare_repo_env(&nongit); + prepare_repo_env(&nongit, &err); cmd = cgit_get_cmd(); - if (!cmd) { - ctx.page.title = "cgit error"; - cgit_print_error_page(404, "Not Found", "Invalid request"); - return; - } - - if (!ctx.cfg.enable_http_clone && cmd->is_clone) { + 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; @@ -1137,7 +1208,7 @@ static void process_request(void) ctx.qry.vpath = cmd->want_vpath ? ctx.qry.path : NULL; - if (ctx.repo && prepare_repo_cmd(nongit)) + if (ctx.repo && prepare_repo_cmd(nongit, err, cmd->is_clone)) return; cmd->fn(); @@ -1205,13 +1276,11 @@ void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *valu 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") && value != NULL) { - if (repo->readme.items == ctx.cfg.readme.items) - memset(&repo->readme, 0, sizeof(repo->readme)); + else if (!strcmp(name, "readme")) string_list_append(&repo->readme, cgit_strdup_first_line(value)); - } else if (!strcmp(name, "logo") && value != NULL) + else if (!strcmp(name, "logo")) repo->logo = cgit_strdup_first_line(value); - else if (!strcmp(name, "logo-link") && value != NULL) + else if (!strcmp(name, "logo-link")) repo->logo_link = cgit_strdup_first_line(value); else if (!strcmp(name, "hide")) repo->hide = atoi(value); @@ -1238,6 +1307,9 @@ int cmd_main(int argc, const char **argv) 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 @@ -1271,8 +1343,9 @@ int cmd_main(int argc, const char **argv) char *path_and_query = cgit_fmtalloc("%s?%s", path, ctx.qry.raw); free(ctx.qry.raw); ctx.qry.raw = path_and_query; - } else + } else { ctx.qry.raw = xstrdup(ctx.qry.url); + } cgit_parse_url(ctx.qry.url); } |
