Blob


1 /*
2 * Copyright (c) 2016, 2019, 2020-2022 Tracey Emery <tracey@traceyemery.net>
3 * Copyright (c) 2015 Mike Larkin <mlarkin@openbsd.org>
4 * Copyright (c) 2014 Reyk Floeter <reyk@openbsd.org>
5 * Copyright (c) 2013 David Gwynne <dlg@openbsd.org>
6 * Copyright (c) 2013 Florian Obser <florian@openbsd.org>
7 *
8 * Permission to use, copy, modify, and distribute this software for any
9 * purpose with or without fee is hereby granted, provided that the above
10 * copyright notice and this permission notice appear in all copies.
11 *
12 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
13 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
14 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
15 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
16 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
17 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
18 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 */
21 #include <net/if.h>
22 #include <netinet/in.h>
23 #include <sys/queue.h>
24 #include <sys/stat.h>
25 #include <sys/types.h>
27 #include <ctype.h>
28 #include <dirent.h>
29 #include <errno.h>
30 #include <event.h>
31 #include <fcntl.h>
32 #include <imsg.h>
33 #include <stdio.h>
34 #include <stdlib.h>
35 #include <string.h>
36 #include <unistd.h>
38 #include "got_error.h"
39 #include "got_object.h"
40 #include "got_reference.h"
41 #include "got_repository.h"
42 #include "got_path.h"
43 #include "got_cancel.h"
44 #include "got_worktree.h"
45 #include "got_diff.h"
46 #include "got_commit_graph.h"
47 #include "got_blame.h"
48 #include "got_privsep.h"
50 #include "got_compat.h"
52 #include "proc.h"
53 #include "gotwebd.h"
54 #include "tmpl.h"
56 static const struct querystring_keys querystring_keys[] = {
57 { "action", ACTION },
58 { "commit", COMMIT },
59 { "file", RFILE },
60 { "folder", FOLDER },
61 { "headref", HEADREF },
62 { "index_page", INDEX_PAGE },
63 { "path", PATH },
64 { "page", PAGE },
65 };
67 static const struct action_keys action_keys[] = {
68 { "blame", BLAME },
69 { "blob", BLOB },
70 { "blobraw", BLOBRAW },
71 { "briefs", BRIEFS },
72 { "commits", COMMITS },
73 { "diff", DIFF },
74 { "error", ERR },
75 { "index", INDEX },
76 { "summary", SUMMARY },
77 { "tag", TAG },
78 { "tags", TAGS },
79 { "tree", TREE },
80 { "rss", RSS },
81 };
83 static const struct got_error *gotweb_init_querystring(struct querystring **);
84 static const struct got_error *gotweb_parse_querystring(struct querystring **,
85 char *);
86 static const struct got_error *gotweb_assign_querystring(struct querystring **,
87 char *, char *);
88 static const struct got_error *gotweb_render_index(struct request *);
89 static const struct got_error *gotweb_init_repo_dir(struct repo_dir **,
90 const char *);
91 static const struct got_error *gotweb_load_got_path(struct request *c,
92 struct repo_dir *);
93 static const struct got_error *gotweb_get_repo_description(char **,
94 struct server *, const char *, int);
95 static const struct got_error *gotweb_get_clone_url(char **, struct server *,
96 const char *, int);
98 static void gotweb_free_querystring(struct querystring *);
99 static void gotweb_free_repo_dir(struct repo_dir *);
101 struct server *gotweb_get_server(uint8_t *, uint8_t *);
103 void
104 gotweb_process_request(struct request *c)
106 const struct got_error *error = NULL, *error2 = NULL;
107 struct got_blob_object *blob = NULL;
108 struct server *srv = NULL;
109 struct querystring *qs = NULL;
110 struct repo_dir *repo_dir = NULL;
111 struct got_reflist_head refs;
112 FILE *fp = NULL;
113 uint8_t err[] = "gotwebd experienced an error: ";
114 int r, html = 0, fd = -1;
116 TAILQ_INIT(&refs);
118 /* init the transport */
119 error = gotweb_init_transport(&c->t);
120 if (error) {
121 log_warnx("%s: %s", __func__, error->msg);
122 return;
124 /* don't process any further if client disconnected */
125 if (c->sock->client_status == CLIENT_DISCONNECT)
126 return;
127 /* get the gotwebd server */
128 srv = gotweb_get_server(c->server_name, c->http_host);
129 if (srv == NULL) {
130 log_warnx("%s: error server is NULL", __func__);
131 goto err;
133 c->srv = srv;
134 /* parse our querystring */
135 error = gotweb_init_querystring(&qs);
136 if (error) {
137 log_warnx("%s: %s", __func__, error->msg);
138 goto err;
140 c->t->qs = qs;
141 error = gotweb_parse_querystring(&qs, c->querystring);
142 if (error) {
143 log_warnx("%s: %s", __func__, error->msg);
144 goto err;
147 /*
148 * certain actions require a commit id in the querystring. this stops
149 * bad actors from exploiting this by manually manipulating the
150 * querystring.
151 */
153 if (qs->action == BLAME || qs->action == BLOB ||
154 qs->action == BLOBRAW || qs->action == DIFF) {
155 if (qs->commit == NULL) {
156 error2 = got_error(GOT_ERR_QUERYSTRING);
157 goto render;
161 if (qs->action != INDEX) {
162 error = gotweb_init_repo_dir(&repo_dir, qs->path);
163 if (error)
164 goto done;
165 error = gotweb_load_got_path(c, repo_dir);
166 c->t->repo_dir = repo_dir;
167 if (error && error->code != GOT_ERR_LONELY_PACKIDX)
168 goto err;
171 if (qs->action == BLOBRAW) {
172 const uint8_t *buf;
173 size_t len;
174 int binary;
176 error = got_get_repo_commits(c, 1);
177 if (error)
178 goto done;
180 error2 = got_open_blob_for_output(&blob, &fd, &binary, c);
181 if (error2)
182 goto render;
184 if (binary)
185 error = gotweb_render_content_type_file(c,
186 "application/octet-stream", qs->file, NULL);
187 else
188 error = gotweb_render_content_type(c, "text/plain");
190 if (error) {
191 log_warnx("%s: %s", __func__, error->msg);
192 goto done;
195 for (;;) {
196 error = got_object_blob_read_block(&len, blob);
197 if (error)
198 goto done;
199 if (len == 0)
200 break;
201 buf = got_object_blob_get_read_buf(blob);
202 if (fcgi_gen_binary_response(c, buf, len) == -1)
203 goto done;
206 goto done;
209 if (qs->action == BLOB) {
210 int binary;
211 struct gotweb_url url = {
212 .index_page = -1,
213 .page = -1,
214 .action = BLOBRAW,
215 .path = qs->path,
216 .commit = qs->commit,
217 .folder = qs->folder,
218 .file = qs->file,
219 };
221 error = got_get_repo_commits(c, 1);
222 if (error)
223 goto done;
225 error2 = got_open_blob_for_output(&blob, &fd, &binary, c);
226 if (error2)
227 goto render;
228 if (binary) {
229 fcgi_puts(c->tp, "Status: 302\r\n");
230 fcgi_puts(c->tp, "Location: ");
231 gotweb_render_url(c, &url);
232 fcgi_puts(c->tp, "\r\n\r\n");
233 goto done;
237 if (qs->action == RSS) {
238 error = gotweb_render_content_type_file(c,
239 "application/rss+xml;charset=utf-8",
240 repo_dir->name, ".rss");
241 if (error) {
242 log_warnx("%s: %s", __func__, error->msg);
243 goto err;
246 error = got_get_repo_tags(c, D_MAXSLCOMMDISP);
247 if (error) {
248 log_warnx("%s: %s", __func__, error->msg);
249 goto err;
251 if (gotweb_render_rss(c->tp) == -1)
252 goto err;
253 goto done;
256 render:
257 error = gotweb_render_content_type(c, "text/html");
258 if (error) {
259 log_warnx("%s: %s", __func__, error->msg);
260 goto err;
262 html = 1;
264 if (gotweb_render_header(c->tp) == -1)
265 goto err;
267 if (error2) {
268 error = error2;
269 goto err;
272 switch(qs->action) {
273 case BLAME:
274 error = got_get_repo_commits(c, 1);
275 if (error) {
276 log_warnx("%s: %s", __func__, error->msg);
277 goto err;
279 if (gotweb_render_blame(c->tp) == -1)
280 goto done;
281 break;
282 case BLOB:
283 if (gotweb_render_blob(c->tp, blob) == -1)
284 goto err;
285 break;
286 case BRIEFS:
287 if (gotweb_render_briefs(c->tp) == -1)
288 goto err;
289 break;
290 case COMMITS:
291 error = got_get_repo_commits(c, srv->max_commits_display);
292 if (error) {
293 log_warnx("%s: %s", __func__, error->msg);
294 goto err;
296 if (gotweb_render_commits(c->tp) == -1)
297 goto err;
298 break;
299 case DIFF:
300 error = got_get_repo_commits(c, 1);
301 if (error) {
302 log_warnx("%s: %s", __func__, error->msg);
303 goto err;
305 error = got_open_diff_for_output(&fp, &fd, c);
306 if (error) {
307 log_warnx("%s: %s", __func__, error->msg);
308 goto err;
310 if (gotweb_render_diff(c->tp, fp) == -1)
311 goto err;
312 break;
313 case INDEX:
314 error = gotweb_render_index(c);
315 if (error) {
316 log_warnx("%s: %s", __func__, error->msg);
317 goto err;
319 break;
320 case SUMMARY:
321 error = got_ref_list(&refs, c->t->repo, "refs/heads",
322 got_ref_cmp_by_name, NULL);
323 if (error) {
324 log_warnx("%s: got_ref_list: %s", __func__,
325 error->msg);
326 goto err;
328 qs->action = TAGS;
329 error = got_get_repo_tags(c, D_MAXSLCOMMDISP);
330 if (error) {
331 log_warnx("%s: got_get_repo_tags: %s", __func__,
332 error->msg);
333 goto err;
335 qs->action = SUMMARY;
336 if (gotweb_render_summary(c->tp, &refs) == -1)
337 goto done;
338 break;
339 case TAG:
340 error = got_get_repo_tags(c, 1);
341 if (error) {
342 log_warnx("%s: %s", __func__, error->msg);
343 goto err;
345 if (c->t->tag_count == 0) {
346 error = got_error_msg(GOT_ERR_BAD_OBJ_ID,
347 "bad commit id");
348 goto err;
350 if (gotweb_render_tag(c->tp) == -1)
351 goto done;
352 break;
353 case TAGS:
354 error = got_get_repo_tags(c, srv->max_commits_display);
355 if (error) {
356 log_warnx("%s: %s", __func__, error->msg);
357 goto err;
359 if (gotweb_render_tags(c->tp) == -1)
360 goto done;
361 break;
362 case TREE:
363 error = got_get_repo_commits(c, 1);
364 if (error) {
365 log_warnx("%s: %s", __func__, error->msg);
366 goto err;
368 if (gotweb_render_tree(c->tp) == -1)
369 goto err;
370 break;
371 case ERR:
372 default:
373 r = fcgi_printf(c, "<div id='err_content'>%s</div>\n",
374 "Erorr: Bad Querystring");
375 if (r == -1)
376 goto err;
377 break;
380 goto done;
381 err:
382 if (html && fcgi_printf(c, "<div id='err_content'>") == -1)
383 return;
384 if (fcgi_printf(c, "\n%s", err) == -1)
385 return;
386 if (error) {
387 if (fcgi_printf(c, "%s", error->msg) == -1)
388 return;
389 } else {
390 if (fcgi_printf(c, "see daemon logs for details") == -1)
391 return;
393 if (html && fcgi_printf(c, "</div>\n") == -1)
394 return;
395 done:
396 if (blob)
397 got_object_blob_close(blob);
398 if (fp) {
399 error = got_gotweb_flushfile(fp, fd);
400 if (error)
401 log_warnx("%s: got_gotweb_flushfile failure: %s",
402 __func__, error->msg);
403 fd = -1;
405 if (fd != -1)
406 close(fd);
407 if (html && srv != NULL)
408 gotweb_render_footer(c->tp);
410 got_ref_list_free(&refs);
413 struct server *
414 gotweb_get_server(uint8_t *server_name, uint8_t *subdomain)
416 struct server *srv = NULL;
418 /* check against the server name first */
419 if (strlen(server_name) > 0)
420 TAILQ_FOREACH(srv, &gotwebd_env->servers, entry)
421 if (strcmp(srv->name, server_name) == 0)
422 goto done;
424 /* check against subdomain second */
425 if (strlen(subdomain) > 0)
426 TAILQ_FOREACH(srv, &gotwebd_env->servers, entry)
427 if (strcmp(srv->name, subdomain) == 0)
428 goto done;
430 /* if those fail, send first server */
431 TAILQ_FOREACH(srv, &gotwebd_env->servers, entry)
432 if (srv != NULL)
433 break;
434 done:
435 return srv;
436 };
438 const struct got_error *
439 gotweb_init_transport(struct transport **t)
441 const struct got_error *error = NULL;
443 *t = calloc(1, sizeof(**t));
444 if (*t == NULL)
445 return got_error_from_errno2("%s: calloc", __func__);
447 TAILQ_INIT(&(*t)->repo_commits);
448 TAILQ_INIT(&(*t)->repo_tags);
450 (*t)->repo = NULL;
451 (*t)->repo_dir = NULL;
452 (*t)->qs = NULL;
453 (*t)->next_id = NULL;
454 (*t)->prev_id = NULL;
455 (*t)->next_disp = 0;
456 (*t)->prev_disp = 0;
458 return error;
461 static const struct got_error *
462 gotweb_init_querystring(struct querystring **qs)
464 const struct got_error *error = NULL;
466 *qs = calloc(1, sizeof(**qs));
467 if (*qs == NULL)
468 return got_error_from_errno2("%s: calloc", __func__);
470 (*qs)->headref = strdup("HEAD");
471 if ((*qs)->headref == NULL) {
472 free(*qs);
473 *qs = NULL;
474 return got_error_from_errno2("%s: strdup", __func__);
477 (*qs)->action = INDEX;
478 (*qs)->commit = NULL;
479 (*qs)->file = NULL;
480 (*qs)->folder = NULL;
481 (*qs)->index_page = 0;
482 (*qs)->path = NULL;
484 return error;
487 static const struct got_error *
488 gotweb_parse_querystring(struct querystring **qs, char *qst)
490 const struct got_error *error = NULL;
491 char *tok1 = NULL, *tok1_pair = NULL, *tok1_end = NULL;
492 char *tok2 = NULL, *tok2_pair = NULL, *tok2_end = NULL;
494 if (qst == NULL)
495 return error;
497 tok1 = strdup(qst);
498 if (tok1 == NULL)
499 return got_error_from_errno2("%s: strdup", __func__);
501 tok1_pair = tok1;
502 tok1_end = tok1;
504 while (tok1_pair != NULL) {
505 strsep(&tok1_end, "&");
507 tok2 = strdup(tok1_pair);
508 if (tok2 == NULL) {
509 free(tok1);
510 return got_error_from_errno2("%s: strdup", __func__);
513 tok2_pair = tok2;
514 tok2_end = tok2;
516 while (tok2_pair != NULL) {
517 strsep(&tok2_end, "=");
518 if (tok2_end) {
519 error = gotweb_assign_querystring(qs, tok2_pair,
520 tok2_end);
521 if (error)
522 goto err;
524 tok2_pair = tok2_end;
526 free(tok2);
527 tok1_pair = tok1_end;
529 free(tok1);
530 return error;
531 err:
532 free(tok2);
533 free(tok1);
534 return error;
537 /*
538 * Adapted from usr.sbin/httpd/httpd.c url_decode.
539 */
540 static const struct got_error *
541 gotweb_urldecode(char *url)
543 char *p, *q;
544 char hex[3];
545 unsigned long x;
547 hex[2] = '\0';
548 p = q = url;
550 while (*p != '\0') {
551 switch (*p) {
552 case '%':
553 /* Encoding character is followed by two hex chars */
554 if (!isxdigit((unsigned char)p[1]) ||
555 !isxdigit((unsigned char)p[2]) ||
556 (p[1] == '0' && p[2] == '0'))
557 return got_error(GOT_ERR_BAD_QUERYSTRING);
559 hex[0] = p[1];
560 hex[1] = p[2];
562 /*
563 * We don't have to validate "hex" because it is
564 * guaranteed to include two hex chars followed by nul.
565 */
566 x = strtoul(hex, NULL, 16);
567 *q = (char)x;
568 p += 2;
569 break;
570 default:
571 *q = *p;
572 break;
574 p++;
575 q++;
577 *q = '\0';
579 return NULL;
582 static const struct got_error *
583 gotweb_assign_querystring(struct querystring **qs, char *key, char *value)
585 const struct got_error *error = NULL;
586 const char *errstr;
587 int a_cnt, el_cnt;
589 error = gotweb_urldecode(value);
590 if (error)
591 return error;
593 for (el_cnt = 0; el_cnt < QSELEM__MAX; el_cnt++) {
594 if (strcmp(key, querystring_keys[el_cnt].name) != 0)
595 continue;
597 switch (querystring_keys[el_cnt].element) {
598 case ACTION:
599 for (a_cnt = 0; a_cnt < ACTIONS__MAX; a_cnt++) {
600 if (strcmp(value, action_keys[a_cnt].name) != 0)
601 continue;
602 else if (strcmp(value,
603 action_keys[a_cnt].name) == 0){
604 (*qs)->action =
605 action_keys[a_cnt].action;
606 goto qa_found;
609 (*qs)->action = ERR;
610 qa_found:
611 break;
612 case COMMIT:
613 (*qs)->commit = strdup(value);
614 if ((*qs)->commit == NULL) {
615 error = got_error_from_errno2("%s: strdup",
616 __func__);
617 goto done;
619 break;
620 case RFILE:
621 (*qs)->file = strdup(value);
622 if ((*qs)->file == NULL) {
623 error = got_error_from_errno2("%s: strdup",
624 __func__);
625 goto done;
627 break;
628 case FOLDER:
629 (*qs)->folder = strdup(value);
630 if ((*qs)->folder == NULL) {
631 error = got_error_from_errno2("%s: strdup",
632 __func__);
633 goto done;
635 break;
636 case HEADREF:
637 free((*qs)->headref);
638 (*qs)->headref = strdup(value);
639 if ((*qs)->headref == NULL) {
640 error = got_error_from_errno2("%s: strdup",
641 __func__);
642 goto done;
644 break;
645 case INDEX_PAGE:
646 if (strlen(value) == 0)
647 break;
648 (*qs)->index_page = strtonum(value, INT64_MIN,
649 INT64_MAX, &errstr);
650 if (errstr) {
651 error = got_error_from_errno3("%s: strtonum %s",
652 __func__, errstr);
653 goto done;
655 if ((*qs)->index_page < 0)
656 (*qs)->index_page = 0;
657 break;
658 case PATH:
659 (*qs)->path = strdup(value);
660 if ((*qs)->path == NULL) {
661 error = got_error_from_errno2("%s: strdup",
662 __func__);
663 goto done;
665 break;
666 case PAGE:
667 if (strlen(value) == 0)
668 break;
669 (*qs)->page = strtonum(value, INT64_MIN,
670 INT64_MAX, &errstr);
671 if (errstr) {
672 error = got_error_from_errno3("%s: strtonum %s",
673 __func__, errstr);
674 goto done;
676 if ((*qs)->page < 0)
677 (*qs)->page = 0;
678 break;
679 default:
680 break;
683 done:
684 return error;
687 void
688 gotweb_free_repo_tag(struct repo_tag *rt)
690 if (rt != NULL) {
691 free(rt->commit_id);
692 free(rt->tag_name);
693 free(rt->tag_commit);
694 free(rt->commit_msg);
695 free(rt->tagger);
697 free(rt);
700 void
701 gotweb_free_repo_commit(struct repo_commit *rc)
703 if (rc != NULL) {
704 free(rc->path);
705 free(rc->refs_str);
706 free(rc->commit_id);
707 free(rc->parent_id);
708 free(rc->tree_id);
709 free(rc->author);
710 free(rc->committer);
711 free(rc->commit_msg);
713 free(rc);
716 static void
717 gotweb_free_querystring(struct querystring *qs)
719 if (qs != NULL) {
720 free(qs->commit);
721 free(qs->file);
722 free(qs->folder);
723 free(qs->headref);
724 free(qs->path);
726 free(qs);
729 static void
730 gotweb_free_repo_dir(struct repo_dir *repo_dir)
732 if (repo_dir != NULL) {
733 free(repo_dir->name);
734 free(repo_dir->owner);
735 free(repo_dir->description);
736 free(repo_dir->url);
737 free(repo_dir->age);
738 free(repo_dir->path);
740 free(repo_dir);
743 void
744 gotweb_free_transport(struct transport *t)
746 struct repo_commit *rc = NULL, *trc = NULL;
747 struct repo_tag *rt = NULL, *trt = NULL;
749 TAILQ_FOREACH_SAFE(rc, &t->repo_commits, entry, trc) {
750 TAILQ_REMOVE(&t->repo_commits, rc, entry);
751 gotweb_free_repo_commit(rc);
753 TAILQ_FOREACH_SAFE(rt, &t->repo_tags, entry, trt) {
754 TAILQ_REMOVE(&t->repo_tags, rt, entry);
755 gotweb_free_repo_tag(rt);
757 gotweb_free_repo_dir(t->repo_dir);
758 gotweb_free_querystring(t->qs);
759 free(t->next_id);
760 free(t->prev_id);
761 free(t);
764 const struct got_error *
765 gotweb_render_content_type(struct request *c, const char *type)
767 const char *csp = "default-src 'self'; script-src 'none'; "
768 "object-src 'none';";
770 fcgi_printf(c,
771 "Content-Security-Policy: %s\r\n"
772 "Content-Type: %s\r\n\r\n",
773 csp, type);
774 return NULL;
777 const struct got_error *
778 gotweb_render_content_type_file(struct request *c, const char *type,
779 const char *file, const char *suffix)
781 fcgi_printf(c, "Content-type: %s\r\n"
782 "Content-disposition: attachment; filename=%s%s\r\n\r\n",
783 type, file, suffix ? suffix : "");
784 return NULL;
787 void
788 gotweb_get_navs(struct request *c, struct gotweb_url *prev, int *have_prev,
789 struct gotweb_url *next, int *have_next)
791 struct transport *t = c->t;
792 struct querystring *qs = t->qs;
793 struct server *srv = c->srv;
795 *have_prev = *have_next = 0;
797 switch(qs->action) {
798 case INDEX:
799 if (qs->index_page > 0) {
800 *have_prev = 1;
801 *prev = (struct gotweb_url){
802 .action = -1,
803 .index_page = qs->index_page - 1,
804 .page = -1,
805 };
807 if (t->next_disp == srv->max_repos_display &&
808 t->repos_total != (qs->index_page + 1) *
809 srv->max_repos_display) {
810 *have_next = 1;
811 *next = (struct gotweb_url){
812 .action = -1,
813 .index_page = qs->index_page + 1,
814 .page = -1,
815 };
817 break;
818 case BRIEFS:
819 if (t->prev_id && qs->commit != NULL &&
820 strcmp(qs->commit, t->prev_id) != 0) {
821 *have_prev = 1;
822 *prev = (struct gotweb_url){
823 .action = BRIEFS,
824 .index_page = -1,
825 .page = qs->page - 1,
826 .path = qs->path,
827 .commit = t->prev_id,
828 .headref = qs->headref,
829 };
831 if (t->next_id) {
832 *have_next = 1;
833 *next = (struct gotweb_url){
834 .action = BRIEFS,
835 .index_page = -1,
836 .page = qs->page + 1,
837 .path = qs->path,
838 .commit = t->next_id,
839 .headref = qs->headref,
840 };
842 break;
843 case COMMITS:
844 if (t->prev_id && qs->commit != NULL &&
845 strcmp(qs->commit, t->prev_id) != 0) {
846 *have_prev = 1;
847 *prev = (struct gotweb_url){
848 .action = COMMITS,
849 .index_page = -1,
850 .page = qs->page - 1,
851 .path = qs->path,
852 .commit = t->prev_id,
853 .headref = qs->headref,
854 .folder = qs->folder,
855 .file = qs->file,
856 };
858 if (t->next_id) {
859 *have_next = 1;
860 *next = (struct gotweb_url){
861 .action = COMMITS,
862 .index_page = -1,
863 .page = qs->page + 1,
864 .path = qs->path,
865 .commit = t->next_id,
866 .headref = qs->headref,
867 .folder = qs->folder,
868 .file = qs->file,
869 };
871 break;
872 case TAGS:
873 if (t->prev_id && qs->commit != NULL &&
874 strcmp(qs->commit, t->prev_id) != 0) {
875 *have_prev = 1;
876 *prev = (struct gotweb_url){
877 .action = TAGS,
878 .index_page = -1,
879 .page = qs->page - 1,
880 .path = qs->path,
881 .commit = t->prev_id,
882 .headref = qs->headref,
883 };
885 if (t->next_id) {
886 *have_next = 1;
887 *next = (struct gotweb_url){
888 .action = TAGS,
889 .index_page = -1,
890 .page = qs->page + 1,
891 .path = qs->path,
892 .commit = t->next_id,
893 .headref = qs->headref,
894 };
896 break;
900 static const struct got_error *
901 gotweb_render_index(struct request *c)
903 const struct got_error *error = NULL;
904 struct server *srv = c->srv;
905 struct transport *t = c->t;
906 struct querystring *qs = t->qs;
907 struct repo_dir *repo_dir = NULL;
908 DIR *d;
909 struct dirent **sd_dent = NULL;
910 unsigned int d_cnt, d_i, d_disp = 0;
911 unsigned int d_skipped = 0;
912 int type;
914 d = opendir(srv->repos_path);
915 if (d == NULL) {
916 error = got_error_from_errno2("opendir", srv->repos_path);
917 return error;
920 d_cnt = scandir(srv->repos_path, &sd_dent, NULL, alphasort);
921 if (d_cnt == -1) {
922 sd_dent = NULL;
923 error = got_error_from_errno2("scandir", srv->repos_path);
924 goto done;
927 if (gotweb_render_repo_table_hdr(c->tp) == -1)
928 goto done;
930 for (d_i = 0; d_i < d_cnt; d_i++) {
931 if (srv->max_repos > 0 && t->prev_disp == srv->max_repos)
932 break;
934 if (strcmp(sd_dent[d_i]->d_name, ".") == 0 ||
935 strcmp(sd_dent[d_i]->d_name, "..") == 0) {
936 d_skipped++;
937 continue;
940 error = got_path_dirent_type(&type, srv->repos_path,
941 sd_dent[d_i]);
942 if (error)
943 goto done;
944 if (type != DT_DIR) {
945 d_skipped++;
946 continue;
949 if (qs->index_page > 0 && (qs->index_page *
950 srv->max_repos_display) > t->prev_disp) {
951 t->prev_disp++;
952 continue;
955 error = gotweb_init_repo_dir(&repo_dir, sd_dent[d_i]->d_name);
956 if (error)
957 goto done;
959 error = gotweb_load_got_path(c, repo_dir);
960 if (error && error->code == GOT_ERR_NOT_GIT_REPO) {
961 error = NULL;
962 gotweb_free_repo_dir(repo_dir);
963 repo_dir = NULL;
964 d_skipped++;
965 continue;
967 if (error && error->code != GOT_ERR_LONELY_PACKIDX)
968 goto done;
970 d_disp++;
971 t->prev_disp++;
973 if (gotweb_render_repo_fragment(c->tp, repo_dir) == -1)
974 goto done;
976 gotweb_free_repo_dir(repo_dir);
977 repo_dir = NULL;
978 t->next_disp++;
979 if (d_disp == srv->max_repos_display)
980 break;
982 t->repos_total = d_cnt - d_skipped;
984 if (srv->max_repos_display == 0)
985 goto done;
986 if (srv->max_repos > 0 && srv->max_repos < srv->max_repos_display)
987 goto done;
988 if (t->repos_total <= srv->max_repos ||
989 t->repos_total <= srv->max_repos_display)
990 goto done;
992 if (gotweb_render_navs(c->tp) == -1)
993 goto done;
994 done:
995 if (sd_dent) {
996 for (d_i = 0; d_i < d_cnt; d_i++)
997 free(sd_dent[d_i]);
998 free(sd_dent);
1000 if (d != NULL && closedir(d) == EOF && error == NULL)
1001 error = got_error_from_errno("closedir");
1002 return error;
1005 static inline int
1006 should_urlencode(int c)
1008 if (c <= ' ' || c >= 127)
1009 return 1;
1011 switch (c) {
1012 /* gen-delim */
1013 case ':':
1014 case '/':
1015 case '?':
1016 case '#':
1017 case '[':
1018 case ']':
1019 case '@':
1020 /* sub-delims */
1021 case '!':
1022 case '$':
1023 case '&':
1024 case '\'':
1025 case '(':
1026 case ')':
1027 case '*':
1028 case '+':
1029 case ',':
1030 case ';':
1031 case '=':
1032 /* needed because the URLs are embedded into the HTML */
1033 case '\"':
1034 return 1;
1035 default:
1036 return 0;
1040 static char *
1041 gotweb_urlencode(const char *str)
1043 const char *s;
1044 char *escaped;
1045 size_t i, len;
1046 int a, b;
1048 len = 0;
1049 for (s = str; *s; ++s) {
1050 len++;
1051 if (should_urlencode(*s))
1052 len += 2;
1055 escaped = calloc(1, len + 1);
1056 if (escaped == NULL)
1057 return NULL;
1059 i = 0;
1060 for (s = str; *s; ++s) {
1061 if (should_urlencode(*s)) {
1062 a = (*s & 0xF0) >> 4;
1063 b = (*s & 0x0F);
1065 escaped[i++] = '%';
1066 escaped[i++] = a <= 9 ? ('0' + a) : ('7' + a);
1067 escaped[i++] = b <= 9 ? ('0' + b) : ('7' + b);
1068 } else
1069 escaped[i++] = *s;
1072 return escaped;
1075 const char *
1076 gotweb_action_name(int action)
1078 switch (action) {
1079 case BLAME:
1080 return "blame";
1081 case BLOB:
1082 return "blob";
1083 case BLOBRAW:
1084 return "blobraw";
1085 case BRIEFS:
1086 return "briefs";
1087 case COMMITS:
1088 return "commits";
1089 case DIFF:
1090 return "diff";
1091 case ERR:
1092 return "err";
1093 case INDEX:
1094 return "index";
1095 case SUMMARY:
1096 return "summary";
1097 case TAG:
1098 return "tag";
1099 case TAGS:
1100 return "tags";
1101 case TREE:
1102 return "tree";
1103 case RSS:
1104 return "rss";
1105 default:
1106 return NULL;
1110 int
1111 gotweb_render_url(struct request *c, struct gotweb_url *url)
1113 const char *sep = "?", *action;
1114 char *tmp;
1115 int r;
1117 action = gotweb_action_name(url->action);
1118 if (action != NULL) {
1119 if (fcgi_printf(c, "?action=%s", action) == -1)
1120 return -1;
1121 sep = "&";
1124 if (url->commit) {
1125 if (fcgi_printf(c, "%scommit=%s", sep, url->commit) == -1)
1126 return -1;
1127 sep = "&";
1130 if (url->previd) {
1131 if (fcgi_printf(c, "%sprevid=%s", sep, url->previd) == -1)
1132 return -1;
1133 sep = "&";
1136 if (url->prevset) {
1137 if (fcgi_printf(c, "%sprevset=%s", sep, url->prevset) == -1)
1138 return -1;
1139 sep = "&";
1142 if (url->file) {
1143 tmp = gotweb_urlencode(url->file);
1144 if (tmp == NULL)
1145 return -1;
1146 r = fcgi_printf(c, "%sfile=%s", sep, tmp);
1147 free(tmp);
1148 if (r == -1)
1149 return -1;
1150 sep = "&";
1153 if (url->folder) {
1154 tmp = gotweb_urlencode(url->folder);
1155 if (tmp == NULL)
1156 return -1;
1157 r = fcgi_printf(c, "%sfolder=%s", sep, tmp);
1158 free(tmp);
1159 if (r == -1)
1160 return -1;
1161 sep = "&";
1164 if (url->headref) {
1165 tmp = gotweb_urlencode(url->headref);
1166 if (tmp == NULL)
1167 return -1;
1168 r = fcgi_printf(c, "%sheadref=%s", sep, url->headref);
1169 free(tmp);
1170 if (r == -1)
1171 return -1;
1172 sep = "&";
1175 if (url->index_page != -1) {
1176 if (fcgi_printf(c, "%sindex_page=%d", sep,
1177 url->index_page) == -1)
1178 return -1;
1179 sep = "&";
1182 if (url->path) {
1183 tmp = gotweb_urlencode(url->path);
1184 if (tmp == NULL)
1185 return -1;
1186 r = fcgi_printf(c, "%spath=%s", sep, tmp);
1187 free(tmp);
1188 if (r == -1)
1189 return -1;
1190 sep = "&";
1193 if (url->page != -1) {
1194 if (fcgi_printf(c, "%spage=%d", sep, url->page) == -1)
1195 return -1;
1196 sep = "&";
1199 return 0;
1202 int
1203 gotweb_render_absolute_url(struct request *c, struct gotweb_url *url)
1205 struct template *tp = c->tp;
1206 const char *proto = c->https ? "https" : "http";
1208 if (fcgi_puts(tp, proto) == -1 ||
1209 fcgi_puts(tp, "://") == -1 ||
1210 tp_htmlescape(tp, c->server_name) == -1 ||
1211 tp_htmlescape(tp, c->document_uri) == -1)
1212 return -1;
1214 return gotweb_render_url(c, url);
1217 static struct got_repository *
1218 find_cached_repo(struct server *srv, const char *path)
1220 int i;
1222 for (i = 0; i < srv->ncached_repos; i++) {
1223 if (strcmp(srv->cached_repos[i].path, path) == 0)
1224 return srv->cached_repos[i].repo;
1227 return NULL;
1230 static const struct got_error *
1231 cache_repo(struct got_repository **new, struct server *srv,
1232 struct repo_dir *repo_dir, struct socket *sock)
1234 const struct got_error *error = NULL;
1235 struct got_repository *repo;
1236 struct cached_repo *cr;
1237 int evicted = 0;
1239 if (srv->ncached_repos >= GOTWEBD_REPO_CACHESIZE) {
1240 cr = &srv->cached_repos[srv->ncached_repos - 1];
1241 error = got_repo_close(cr->repo);
1242 memset(cr, 0, sizeof(*cr));
1243 srv->ncached_repos--;
1244 if (error)
1245 return error;
1246 memmove(&srv->cached_repos[1], &srv->cached_repos[0],
1247 srv->ncached_repos * sizeof(srv->cached_repos[0]));
1248 cr = &srv->cached_repos[0];
1249 evicted = 1;
1250 } else {
1251 cr = &srv->cached_repos[srv->ncached_repos];
1254 error = got_repo_open(&repo, repo_dir->path, NULL, sock->pack_fds);
1255 if (error) {
1256 if (evicted) {
1257 memmove(&srv->cached_repos[0], &srv->cached_repos[1],
1258 srv->ncached_repos * sizeof(srv->cached_repos[0]));
1260 return error;
1263 if (strlcpy(cr->path, repo_dir->path, sizeof(cr->path))
1264 >= sizeof(cr->path)) {
1265 if (evicted) {
1266 memmove(&srv->cached_repos[0], &srv->cached_repos[1],
1267 srv->ncached_repos * sizeof(srv->cached_repos[0]));
1269 return got_error(GOT_ERR_NO_SPACE);
1272 cr->repo = repo;
1273 srv->ncached_repos++;
1274 *new = repo;
1275 return NULL;
1278 static const struct got_error *
1279 gotweb_load_got_path(struct request *c, struct repo_dir *repo_dir)
1281 const struct got_error *error = NULL;
1282 struct socket *sock = c->sock;
1283 struct server *srv = c->srv;
1284 struct transport *t = c->t;
1285 struct got_repository *repo = NULL;
1286 DIR *dt;
1287 char *dir_test;
1289 if (asprintf(&dir_test, "%s/%s/%s", srv->repos_path, repo_dir->name,
1290 GOTWEB_GIT_DIR) == -1)
1291 return got_error_from_errno("asprintf");
1293 dt = opendir(dir_test);
1294 if (dt == NULL) {
1295 free(dir_test);
1296 } else {
1297 repo_dir->path = dir_test;
1298 dir_test = NULL;
1299 goto done;
1302 if (asprintf(&dir_test, "%s/%s", srv->repos_path,
1303 repo_dir->name) == -1)
1304 return got_error_from_errno("asprintf");
1306 dt = opendir(dir_test);
1307 if (dt == NULL) {
1308 error = got_error_path(repo_dir->name, GOT_ERR_NOT_GIT_REPO);
1309 goto err;
1310 } else {
1311 repo_dir->path = dir_test;
1312 dir_test = NULL;
1315 done:
1316 if (srv->respect_exportok &&
1317 faccessat(dirfd(dt), "git-daemon-export-ok", F_OK, 0) == -1) {
1318 error = got_error_path(repo_dir->name, GOT_ERR_NOT_GIT_REPO);
1319 goto err;
1322 repo = find_cached_repo(srv, repo_dir->path);
1323 if (repo == NULL) {
1324 error = cache_repo(&repo, srv, repo_dir, sock);
1325 if (error)
1326 goto err;
1328 t->repo = repo;
1329 error = gotweb_get_repo_description(&repo_dir->description, srv,
1330 repo_dir->path, dirfd(dt));
1331 if (error)
1332 goto err;
1333 error = got_get_repo_owner(&repo_dir->owner, c);
1334 if (error)
1335 goto err;
1336 error = got_get_repo_age(&repo_dir->age, c, NULL, TM_DIFF);
1337 if (error)
1338 goto err;
1339 error = gotweb_get_clone_url(&repo_dir->url, srv, repo_dir->path,
1340 dirfd(dt));
1341 err:
1342 free(dir_test);
1343 if (dt != NULL && closedir(dt) == EOF && error == NULL)
1344 error = got_error_from_errno("closedir");
1345 return error;
1348 static const struct got_error *
1349 gotweb_init_repo_dir(struct repo_dir **repo_dir, const char *dir)
1351 const struct got_error *error;
1353 *repo_dir = calloc(1, sizeof(**repo_dir));
1354 if (*repo_dir == NULL)
1355 return got_error_from_errno("calloc");
1357 if (asprintf(&(*repo_dir)->name, "%s", dir) == -1) {
1358 error = got_error_from_errno("asprintf");
1359 free(*repo_dir);
1360 *repo_dir = NULL;
1361 return error;
1363 (*repo_dir)->owner = NULL;
1364 (*repo_dir)->description = NULL;
1365 (*repo_dir)->url = NULL;
1366 (*repo_dir)->age = NULL;
1367 (*repo_dir)->path = NULL;
1369 return NULL;
1372 static const struct got_error *
1373 gotweb_get_repo_description(char **description, struct server *srv,
1374 const char *dirpath, int dir)
1376 const struct got_error *error = NULL;
1377 struct stat sb;
1378 int fd = -1;
1379 off_t len;
1381 *description = NULL;
1382 if (srv->show_repo_description == 0)
1383 return NULL;
1385 fd = openat(dir, "description", O_RDONLY);
1386 if (fd == -1) {
1387 if (errno != ENOENT && errno != EACCES) {
1388 error = got_error_from_errno_fmt("openat %s/%s",
1389 dirpath, "description");
1391 goto done;
1394 if (fstat(fd, &sb) == -1) {
1395 error = got_error_from_errno_fmt("fstat %s/%s",
1396 dirpath, "description");
1397 goto done;
1400 len = sb.st_size;
1401 if (len > GOTWEBD_MAXDESCRSZ - 1)
1402 len = GOTWEBD_MAXDESCRSZ - 1;
1404 *description = calloc(len + 1, sizeof(**description));
1405 if (*description == NULL) {
1406 error = got_error_from_errno("calloc");
1407 goto done;
1410 if (read(fd, *description, len) == -1)
1411 error = got_error_from_errno("read");
1412 done:
1413 if (fd != -1 && close(fd) == -1 && error == NULL)
1414 error = got_error_from_errno("close");
1415 return error;
1418 static const struct got_error *
1419 gotweb_get_clone_url(char **url, struct server *srv, const char *dirpath,
1420 int dir)
1422 const struct got_error *error = NULL;
1423 struct stat sb;
1424 int fd = -1;
1425 off_t len;
1427 *url = NULL;
1428 if (srv->show_repo_cloneurl == 0)
1429 return NULL;
1431 fd = openat(dir, "cloneurl", O_RDONLY);
1432 if (fd == -1) {
1433 if (errno != ENOENT && errno != EACCES) {
1434 error = got_error_from_errno_fmt("openat %s/%s",
1435 dirpath, "cloneurl");
1437 goto done;
1440 if (fstat(fd, &sb) == -1) {
1441 error = got_error_from_errno_fmt("fstat %s/%s",
1442 dirpath, "cloneurl");
1443 goto done;
1446 len = sb.st_size;
1447 if (len > GOTWEBD_MAXCLONEURLSZ - 1)
1448 len = GOTWEBD_MAXCLONEURLSZ - 1;
1450 *url = calloc(len + 1, sizeof(**url));
1451 if (*url == NULL) {
1452 error = got_error_from_errno("calloc");
1453 goto done;
1456 if (read(fd, *url, len) == -1)
1457 error = got_error_from_errno("read");
1458 done:
1459 if (fd != -1 && close(fd) == -1 && error == NULL)
1460 error = got_error_from_errno("close");
1461 return error;
1464 const struct got_error *
1465 gotweb_get_time_str(char **repo_age, time_t committer_time, int ref_tm)
1467 struct tm tm;
1468 long long diff_time;
1469 const char *years = "years ago", *months = "months ago";
1470 const char *weeks = "weeks ago", *days = "days ago";
1471 const char *hours = "hours ago", *minutes = "minutes ago";
1472 const char *seconds = "seconds ago", *now = "right now";
1473 char *s;
1474 char datebuf[64];
1475 size_t r;
1477 *repo_age = NULL;
1479 switch (ref_tm) {
1480 case TM_DIFF:
1481 diff_time = time(NULL) - committer_time;
1482 if (diff_time > 60 * 60 * 24 * 365 * 2) {
1483 if (asprintf(repo_age, "%lld %s",
1484 (diff_time / 60 / 60 / 24 / 365), years) == -1)
1485 return got_error_from_errno("asprintf");
1486 } else if (diff_time > 60 * 60 * 24 * (365 / 12) * 2) {
1487 if (asprintf(repo_age, "%lld %s",
1488 (diff_time / 60 / 60 / 24 / (365 / 12)),
1489 months) == -1)
1490 return got_error_from_errno("asprintf");
1491 } else if (diff_time > 60 * 60 * 24 * 7 * 2) {
1492 if (asprintf(repo_age, "%lld %s",
1493 (diff_time / 60 / 60 / 24 / 7), weeks) == -1)
1494 return got_error_from_errno("asprintf");
1495 } else if (diff_time > 60 * 60 * 24 * 2) {
1496 if (asprintf(repo_age, "%lld %s",
1497 (diff_time / 60 / 60 / 24), days) == -1)
1498 return got_error_from_errno("asprintf");
1499 } else if (diff_time > 60 * 60 * 2) {
1500 if (asprintf(repo_age, "%lld %s",
1501 (diff_time / 60 / 60), hours) == -1)
1502 return got_error_from_errno("asprintf");
1503 } else if (diff_time > 60 * 2) {
1504 if (asprintf(repo_age, "%lld %s", (diff_time / 60),
1505 minutes) == -1)
1506 return got_error_from_errno("asprintf");
1507 } else if (diff_time > 2) {
1508 if (asprintf(repo_age, "%lld %s", diff_time,
1509 seconds) == -1)
1510 return got_error_from_errno("asprintf");
1511 } else {
1512 if (asprintf(repo_age, "%s", now) == -1)
1513 return got_error_from_errno("asprintf");
1515 break;
1516 case TM_LONG:
1517 if (gmtime_r(&committer_time, &tm) == NULL)
1518 return got_error_from_errno("gmtime_r");
1520 s = asctime_r(&tm, datebuf);
1521 if (s == NULL)
1522 return got_error_from_errno("asctime_r");
1524 if (asprintf(repo_age, "%s UTC", datebuf) == -1)
1525 return got_error_from_errno("asprintf");
1526 break;
1527 case TM_RFC822:
1528 if (gmtime_r(&committer_time, &tm) == NULL)
1529 return got_error_from_errno("gmtime_r");
1531 r = strftime(datebuf, sizeof(datebuf),
1532 "%a, %d %b %Y %H:%M:%S GMT", &tm);
1533 if (r == 0)
1534 return got_error(GOT_ERR_NO_SPACE);
1536 *repo_age = strdup(datebuf);
1537 if (*repo_age == NULL)
1538 return got_error_from_errno("asprintf");
1539 break;
1541 return NULL;