/* * The dumb HTTP transport, which lets a client clone by fetching plain files * rather than by talking to a server side helper. It answers the requests * such a client makes, the ref listing under info, the loose objects and pack * files under objects, and HEAD, each written as raw bytes rather than as a * page. The two listings a clone starts from are built per request, so a * repository serves without anyone having run git update-server-info over it. */ #define USE_THE_REPOSITORY_VARIABLE #include "cgit.h" #include "html.h" #include "ui-clone.h" #include "ui-shared.h" /* * A client reading this listing expects an annotated tag to be followed by a * second line, marked with ^{}, naming the object the tag peels to, the * format git update-server-info writes into info/refs. */ static int print_ref(const struct reference *ref, void *cb_data UNUSED) { struct object *obj; obj = parse_object(the_repository, ref->oid); if (!obj) return 0; htmlf("%s\t%s\n", oid_to_hex(ref->oid), ref->name); if (obj->type == OBJ_TAG) { obj = deref_tag(the_repository, obj, ref->name, 0); if (!obj) return 0; htmlf("%s\t%s^{}\n", oid_to_hex(&obj->oid), ref->name); } return 0; } /* * A path ending in a slash is handed back whole, where git's own * pack_basename would return the empty string. */ static const char *last_path_component(const char *path) { const char *slash = strrchr(path, '/'); if (slash && slash[1] != '\0') return slash + 1; return path; } /* * Only the packs this repository holds itself are listed, because one * borrowed from an alternate is not reachable below this URL and a client * told about it would come back for a file that is not there. */ static void print_pack_info(void) { struct odb_source *source; ctx.page.mimetype = "text/plain"; ctx.page.filename = "objects/info/packs"; cgit_print_http_headers(); odb_reprepare(the_repository->objects); for (source = the_repository->objects->sources; source; source = source->next) { struct odb_source_files *files = odb_source_files_downcast(source); struct packfile_list_entry *entry; // Asked for through the accessor rather than read off the list, // because a pack the multi-pack-index already covers joins that // list only when the accessor loads it, and reading the field // directly leaves those packs unfindable. for (entry = packfile_store_get_packs(files->packed); entry; entry = entry->next) { struct packed_git *pack = entry->pack; if (pack->pack_local) htmlf("P %s\n", last_path_component(pack->pack_name)); } } } static size_t hex_span(const char *s) { return strspn(s, "0123456789abcdef"); } /* * A dumb client asks for three shapes of file below objects: a loose object * at xx/rest-of-hex, a pack or its index at pack/pack-hex.pack or .idx, and * info/http-alternates. Nothing else is served, so no spelling of a path can * reach a file outside those, the alternates file among them. */ static int object_path_is_served(const char *path) { const size_t hexsz = the_hash_algo->hexsz; const char *rest; if (!strcmp(path, "info/http-alternates")) return 1; if (skip_prefix(path, "pack/pack-", &rest)) { size_t n = hex_span(rest); return n == hexsz && (!strcmp(rest + n, ".pack") || !strcmp(rest + n, ".idx")); } return hex_span(path) == 2 && path[2] == '/' && hex_span(path + 3) == hexsz - 2 && path[hexsz + 1] == '\0'; } /* * The file is read in batches the size of the output buffer, so each one * goes to the client in a single write, and its size is sent ahead so the * client can tell a cut-off transfer from a complete one. */ static void send_file(const char *path) { static char buf[HTML_BATCH]; struct stat st; ssize_t got; int fd; fd = open(path, O_RDONLY); if (fd < 0) { switch (errno) { case ENOENT: cgit_print_error_page(404, "Not Found", "Not found"); break; case EACCES: cgit_print_error_page(403, "Forbidden", "Forbidden"); break; default: cgit_print_error_page(400, "Bad Request", "Bad request"); } return; } // open succeeds on a directory on most systems and reads nothing from // it. if (fstat(fd, &st) || !S_ISREG(st.st_mode)) { close(fd); cgit_print_error_page(404, "Not Found", "Not found"); return; } ctx.page.mimetype = "application/octet-stream"; // Offer the file under its path inside the repository, so the layout // of the server's disk stays out of the download name. ctx.page.filename = path; skip_prefix(path, ctx.repo->path, &ctx.page.filename); skip_prefix(ctx.page.filename, "/", &ctx.page.filename); ctx.page.size = st.st_size; cgit_print_http_headers(); while ((got = xread(fd, buf, sizeof(buf))) > 0) html_raw(buf, got); close(fd); } void cgit_clone_info(void) { if (!ctx.qry.path || strcmp(ctx.qry.path, "refs")) { cgit_print_error_page(400, "Bad Request", "Bad request"); return; } ctx.page.mimetype = "text/plain"; ctx.page.filename = "info/refs"; cgit_print_http_headers(); refs_for_each_ref(get_main_ref_store(the_repository), print_ref, NULL); } void cgit_clone_objects(void) { char *path; if (!ctx.qry.path) goto err; if (!strcmp(ctx.qry.path, "info/packs")) { print_pack_info(); return; } if (!object_path_is_served(ctx.qry.path)) { cgit_print_error_page(404, "Not Found", "Not found"); return; } path = repo_git_path(the_repository, "objects/%s", ctx.qry.path); send_file(path); free(path); return; err: cgit_print_error_page(400, "Bad Request", "Bad request"); } /* * HEAD is answered from the ref store, the way git's own http-backend answers * it, rather than read off the disk, where a reftable repository keeps only a * placeholder. The target need not exist yet, so an empty repository still * names the branch a first push will create. */ void cgit_clone_head(void) { struct strbuf head = STRBUF_INIT; struct object_id oid; const char *target; int flags; target = refs_resolve_ref_unsafe(get_main_ref_store(the_repository), "HEAD", 0, &oid, &flags); if (!target) { cgit_print_error_page(404, "Not Found", "Not found"); return; } if (flags & REF_ISSYMREF) strbuf_addf(&head, "ref: %s\n", target); else strbuf_addf(&head, "%s\n", oid_to_hex(&oid)); ctx.page.mimetype = "text/plain"; ctx.page.filename = "HEAD"; ctx.page.size = head.len; cgit_print_http_headers(); html_raw(head.buf, head.len); strbuf_release(&head); }