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 <sha1.h>
34 #include <sha2.h>
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <string.h>
38 #include <unistd.h>
40 #include "got_error.h"
41 #include "got_object.h"
42 #include "got_reference.h"
43 #include "got_repository.h"
44 #include "got_path.h"
45 #include "got_cancel.h"
46 #include "got_worktree.h"
47 #include "got_diff.h"
48 #include "got_commit_graph.h"
49 #include "got_blame.h"
50 #include "got_privsep.h"
52 #include "gotwebd.h"
53 #include "tmpl.h"
55 static const struct querystring_keys querystring_keys[] = {
56 { "action", ACTION },
57 { "commit", COMMIT },
58 { "file", RFILE },
59 { "folder", FOLDER },
60 { "headref", HEADREF },
61 { "index_page", INDEX_PAGE },
62 { "path", PATH },
63 };
65 static const struct action_keys action_keys[] = {
66 { "blame", BLAME },
67 { "blob", BLOB },
68 { "blobraw", BLOBRAW },
69 { "briefs", BRIEFS },
70 { "commits", COMMITS },
71 { "diff", DIFF },
72 { "error", ERR },
73 { "index", INDEX },
74 { "patch", PATCH },
75 { "summary", SUMMARY },
76 { "tag", TAG },
77 { "tags", TAGS },
78 { "tree", TREE },
79 { "rss", RSS },
80 };
82 static const struct got_error *gotweb_init_querystring(struct querystring **);
83 static const struct got_error *gotweb_parse_querystring(struct querystring **,
84 char *);
85 static const struct got_error *gotweb_assign_querystring(struct querystring **,
86 char *, char *);
87 static int gotweb_render_index(struct template *);
88 static const struct got_error *gotweb_init_repo_dir(struct repo_dir **,
89 const char *);
90 static const struct got_error *gotweb_load_got_path(struct request *c,
91 struct repo_dir *);
92 static const struct got_error *gotweb_get_repo_description(char **,
93 struct server *, const char *, int);
94 static const struct got_error *gotweb_get_clone_url(char **, struct server *,
95 const char *, int);
97 static void gotweb_free_querystring(struct querystring *);
98 static void gotweb_free_repo_dir(struct repo_dir *);
100 struct server *gotweb_get_server(const char *);
102 static int
103 gotweb_reply(struct request *c, int status, const char *ctype,
104 struct gotweb_url *location)
106 const char *csp;
108 if (status != 200 && tp_writef(c->tp, "Status: %d\r\n", status) == -1)
109 return -1;
111 if (location) {
112 if (tp_writes(c->tp, "Location: ") == -1 ||
113 gotweb_render_url(c, location) == -1 ||
114 tp_writes(c->tp, "\r\n") == -1)
115 return -1;
118 csp = "Content-Security-Policy: default-src 'self'; "
119 "script-src 'none'; object-src 'none';\r\n";
120 if (tp_writes(c->tp, csp) == -1)
121 return -1;
123 if (ctype && tp_writef(c->tp, "Content-Type: %s\r\n", ctype) == -1)
124 return -1;
126 return tp_writes(c->tp, "\r\n");
129 static int
130 gotweb_reply_file(struct request *c, const char *ctype, const char *file,
131 const char *suffix)
133 int r;
135 r = tp_writef(c->tp, "Content-Disposition: attachment; "
136 "filename=%s%s\r\n", file, suffix ? suffix : "");
137 if (r == -1)
138 return -1;
139 return gotweb_reply(c, 200, ctype, NULL);
142 void
143 gotweb_process_request(struct request *c)
145 const struct got_error *error = NULL;
146 struct server *srv = NULL;
147 struct querystring *qs = NULL;
148 struct repo_dir *repo_dir = NULL;
149 const char *rss_ctype = "application/rss+xml;charset=utf-8";
150 const uint8_t *buf;
151 size_t len;
152 int r, binary = 0;
154 /* init the transport */
155 error = gotweb_init_transport(&c->t);
156 if (error) {
157 log_warnx("%s: %s", __func__, error->msg);
158 return;
160 /* don't process any further if client disconnected */
161 if (c->sock->client_status == CLIENT_DISCONNECT)
162 return;
163 /* get the gotwebd server */
164 srv = gotweb_get_server(c->server_name);
165 if (srv == NULL) {
166 log_warnx("%s: error server is NULL", __func__);
167 goto err;
169 c->srv = srv;
170 /* parse our querystring */
171 error = gotweb_init_querystring(&qs);
172 if (error) {
173 log_warnx("%s: %s", __func__, error->msg);
174 goto err;
176 c->t->qs = qs;
177 error = gotweb_parse_querystring(&qs, c->querystring);
178 if (error) {
179 log_warnx("%s: %s", __func__, error->msg);
180 goto err;
183 /*
184 * certain actions require a commit id in the querystring. this stops
185 * bad actors from exploiting this by manually manipulating the
186 * querystring.
187 */
189 if (qs->action == BLAME || qs->action == BLOB ||
190 qs->action == BLOBRAW || qs->action == DIFF ||
191 qs->action == PATCH) {
192 if (qs->commit == NULL) {
193 error = got_error(GOT_ERR_BAD_QUERYSTRING);
194 goto err;
198 if (qs->action != INDEX) {
199 error = gotweb_init_repo_dir(&repo_dir, qs->path);
200 if (error)
201 goto err;
202 error = gotweb_load_got_path(c, repo_dir);
203 c->t->repo_dir = repo_dir;
204 if (error && error->code != GOT_ERR_LONELY_PACKIDX)
205 goto err;
208 if (qs->action == BLOBRAW || qs->action == BLOB) {
209 if (qs->folder == NULL || qs->file == NULL) {
210 error = got_error(GOT_ERR_BAD_QUERYSTRING);
211 goto err;
214 error = got_get_repo_commits(c, 1);
215 if (error)
216 goto err;
218 error = got_open_blob_for_output(&c->t->blob, &c->t->fd,
219 &binary, c, qs->folder, qs->file, qs->commit);
220 if (error)
221 goto err;
224 switch (qs->action) {
225 case BLAME:
226 if (qs->folder == NULL || qs->file == NULL) {
227 error = got_error(GOT_ERR_BAD_QUERYSTRING);
228 goto err;
230 error = got_get_repo_commits(c, 1);
231 if (error) {
232 log_warnx("%s: %s", __func__, error->msg);
233 goto err;
235 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
236 return;
237 gotweb_render_page(c->tp, gotweb_render_blame);
238 return;
239 case BLOB:
240 if (binary) {
241 struct gotweb_url url = {
242 .index_page = -1,
243 .action = BLOBRAW,
244 .path = qs->path,
245 .commit = qs->commit,
246 .folder = qs->folder,
247 .file = qs->file,
248 };
250 gotweb_reply(c, 302, NULL, &url);
251 return;
254 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
255 return;
256 gotweb_render_page(c->tp, gotweb_render_blob);
257 return;
258 case BLOBRAW:
259 if (binary)
260 r = gotweb_reply_file(c, "application/octet-stream",
261 qs->file, NULL);
262 else
263 r = gotweb_reply(c, 200, "text/plain", NULL);
264 if (r == -1)
265 return;
266 if (template_flush(c->tp) == -1)
267 return;
269 for (;;) {
270 error = got_object_blob_read_block(&len, c->t->blob);
271 if (error)
272 break;
273 if (len == 0)
274 break;
275 buf = got_object_blob_get_read_buf(c->t->blob);
276 if (fcgi_write(c, buf, len) == -1)
277 break;
279 return;
280 case BRIEFS:
281 error = got_get_repo_commits(c, srv->max_commits_display);
282 if (error)
283 goto err;
284 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
285 return;
286 gotweb_render_page(c->tp, gotweb_render_briefs);
287 return;
288 case COMMITS:
289 error = got_get_repo_commits(c, srv->max_commits_display);
290 if (error) {
291 log_warnx("%s: %s", __func__, error->msg);
292 goto err;
294 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
295 return;
296 gotweb_render_page(c->tp, gotweb_render_commits);
297 return;
298 case DIFF:
299 error = got_get_repo_commits(c, 1);
300 if (error) {
301 log_warnx("%s: %s", __func__, error->msg);
302 goto err;
304 error = got_open_diff_for_output(&c->t->fp, c);
305 if (error) {
306 log_warnx("%s: %s", __func__, error->msg);
307 goto err;
309 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
310 return;
311 gotweb_render_page(c->tp, gotweb_render_diff);
312 return;
313 case INDEX:
314 c->t->nrepos = scandir(srv->repos_path, &c->t->repos, NULL,
315 alphasort);
316 if (c->t->nrepos == -1) {
317 c->t->repos = NULL;
318 error = got_error_from_errno2("scandir",
319 srv->repos_path);
320 goto err;
322 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
323 return;
324 gotweb_render_page(c->tp, gotweb_render_index);
325 return;
326 case PATCH:
327 error = got_get_repo_commits(c, 1);
328 if (error) {
329 log_warnx("%s: %s", __func__, error->msg);
330 goto err;
332 error = got_open_diff_for_output(&c->t->fp, c);
333 if (error) {
334 log_warnx("%s: %s", __func__, error->msg);
335 goto err;
337 if (gotweb_reply(c, 200, "text/plain", NULL) == -1)
338 return;
339 gotweb_render_patch(c->tp);
340 return;
341 case RSS:
342 error = got_get_repo_tags(c, D_MAXSLCOMMDISP);
343 if (error)
344 goto err;
345 if (gotweb_reply_file(c, rss_ctype, repo_dir->name, ".rss")
346 == -1)
347 return;
348 gotweb_render_rss(c->tp);
349 return;
350 case SUMMARY:
351 error = got_ref_list(&c->t->refs, c->t->repo, "refs/heads",
352 got_ref_cmp_by_name, NULL);
353 if (error) {
354 log_warnx("%s: got_ref_list: %s", __func__,
355 error->msg);
356 goto err;
358 error = got_get_repo_commits(c, srv->summary_commits_display);
359 if (error)
360 goto err;
361 qs->action = TAGS;
362 error = got_get_repo_tags(c, srv->summary_tags_display);
363 if (error) {
364 log_warnx("%s: got_get_repo_tags: %s", __func__,
365 error->msg);
366 goto err;
368 qs->action = SUMMARY;
369 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
370 return;
371 gotweb_render_page(c->tp, gotweb_render_summary);
372 return;
373 case TAG:
374 error = got_get_repo_tags(c, 1);
375 if (error) {
376 log_warnx("%s: %s", __func__, error->msg);
377 goto err;
379 if (c->t->tag_count == 0) {
380 error = got_error_msg(GOT_ERR_BAD_OBJ_ID,
381 "bad commit id");
382 goto err;
384 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
385 return;
386 gotweb_render_page(c->tp, gotweb_render_tag);
387 return;
388 case TAGS:
389 error = got_get_repo_tags(c, srv->max_commits_display);
390 if (error) {
391 log_warnx("%s: %s", __func__, error->msg);
392 goto err;
394 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
395 return;
396 gotweb_render_page(c->tp, gotweb_render_tags);
397 return;
398 case TREE:
399 error = got_get_repo_commits(c, 1);
400 if (error) {
401 log_warnx("%s: %s", __func__, error->msg);
402 goto err;
404 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
405 return;
406 gotweb_render_page(c->tp, gotweb_render_tree);
407 return;
408 case ERR:
409 default:
410 error = got_error(GOT_ERR_BAD_QUERYSTRING);
413 err:
414 c->t->error = error;
415 if (gotweb_reply(c, 400, "text/html", NULL) == -1)
416 return;
417 gotweb_render_page(c->tp, gotweb_render_error);
420 struct server *
421 gotweb_get_server(const char *server_name)
423 struct server *srv;
425 /* check against the server name first */
426 if (*server_name != '\0')
427 TAILQ_FOREACH(srv, &gotwebd_env->servers, entry)
428 if (strcmp(srv->name, server_name) == 0)
429 return srv;
431 /* otherwise, use the first server */
432 return TAILQ_FIRST(&gotwebd_env->servers);
433 };
435 const struct got_error *
436 gotweb_init_transport(struct transport **t)
438 const struct got_error *error = NULL;
440 *t = calloc(1, sizeof(**t));
441 if (*t == NULL)
442 return got_error_from_errno2(__func__, "calloc");
444 TAILQ_INIT(&(*t)->repo_commits);
445 TAILQ_INIT(&(*t)->repo_tags);
446 TAILQ_INIT(&(*t)->refs);
448 (*t)->fd = -1;
450 return error;
453 static const struct got_error *
454 gotweb_init_querystring(struct querystring **qs)
456 const struct got_error *error = NULL;
458 *qs = calloc(1, sizeof(**qs));
459 if (*qs == NULL)
460 return got_error_from_errno2(__func__, "calloc");
462 (*qs)->headref = strdup("HEAD");
463 if ((*qs)->headref == NULL) {
464 free(*qs);
465 *qs = NULL;
466 return got_error_from_errno2(__func__, "strdup");
469 (*qs)->action = INDEX;
471 return error;
474 static const struct got_error *
475 gotweb_parse_querystring(struct querystring **qs, char *qst)
477 const struct got_error *error = NULL;
478 char *tok1 = NULL, *tok1_pair = NULL, *tok1_end = NULL;
479 char *tok2 = NULL, *tok2_pair = NULL, *tok2_end = NULL;
481 if (qst == NULL)
482 return error;
484 tok1 = strdup(qst);
485 if (tok1 == NULL)
486 return got_error_from_errno2(__func__, "strdup");
488 tok1_pair = tok1;
489 tok1_end = tok1;
491 while (tok1_pair != NULL) {
492 strsep(&tok1_end, "&");
494 tok2 = strdup(tok1_pair);
495 if (tok2 == NULL) {
496 free(tok1);
497 return got_error_from_errno2(__func__, "strdup");
500 tok2_pair = tok2;
501 tok2_end = tok2;
503 while (tok2_pair != NULL) {
504 strsep(&tok2_end, "=");
505 if (tok2_end) {
506 error = gotweb_assign_querystring(qs, tok2_pair,
507 tok2_end);
508 if (error)
509 goto err;
511 tok2_pair = tok2_end;
513 free(tok2);
514 tok1_pair = tok1_end;
516 free(tok1);
517 return error;
518 err:
519 free(tok2);
520 free(tok1);
521 return error;
524 /*
525 * Adapted from usr.sbin/httpd/httpd.c url_decode.
526 */
527 static const struct got_error *
528 gotweb_urldecode(char *url)
530 char *p, *q;
531 char hex[3];
532 unsigned long x;
534 hex[2] = '\0';
535 p = q = url;
537 while (*p != '\0') {
538 switch (*p) {
539 case '%':
540 /* Encoding character is followed by two hex chars */
541 if (!isxdigit((unsigned char)p[1]) ||
542 !isxdigit((unsigned char)p[2]) ||
543 (p[1] == '0' && p[2] == '0'))
544 return got_error(GOT_ERR_BAD_QUERYSTRING);
546 hex[0] = p[1];
547 hex[1] = p[2];
549 /*
550 * We don't have to validate "hex" because it is
551 * guaranteed to include two hex chars followed by nul.
552 */
553 x = strtoul(hex, NULL, 16);
554 *q = (char)x;
555 p += 2;
556 break;
557 default:
558 *q = *p;
559 break;
561 p++;
562 q++;
564 *q = '\0';
566 return NULL;
569 static const struct got_error *
570 gotweb_assign_querystring(struct querystring **qs, char *key, char *value)
572 const struct got_error *error = NULL;
573 const char *errstr;
574 int a_cnt, el_cnt;
576 error = gotweb_urldecode(value);
577 if (error)
578 return error;
580 for (el_cnt = 0; el_cnt < nitems(querystring_keys); el_cnt++) {
581 if (strcmp(key, querystring_keys[el_cnt].name) != 0)
582 continue;
584 switch (querystring_keys[el_cnt].element) {
585 case ACTION:
586 for (a_cnt = 0; a_cnt < nitems(action_keys); a_cnt++) {
587 if (strcmp(value, action_keys[a_cnt].name) != 0)
588 continue;
589 else if (strcmp(value,
590 action_keys[a_cnt].name) == 0){
591 (*qs)->action =
592 action_keys[a_cnt].action;
593 goto qa_found;
596 (*qs)->action = ERR;
597 qa_found:
598 break;
599 case COMMIT:
600 (*qs)->commit = strdup(value);
601 if ((*qs)->commit == NULL) {
602 error = got_error_from_errno2(__func__,
603 "strdup");
604 goto done;
606 break;
607 case RFILE:
608 (*qs)->file = strdup(value);
609 if ((*qs)->file == NULL) {
610 error = got_error_from_errno2(__func__,
611 "strdup");
612 goto done;
614 break;
615 case FOLDER:
616 (*qs)->folder = strdup(value);
617 if ((*qs)->folder == NULL) {
618 error = got_error_from_errno2(__func__,
619 "strdup");
620 goto done;
622 break;
623 case HEADREF:
624 free((*qs)->headref);
625 (*qs)->headref = strdup(value);
626 if ((*qs)->headref == NULL) {
627 error = got_error_from_errno2(__func__,
628 "strdup");
629 goto done;
631 break;
632 case INDEX_PAGE:
633 if (*value == '\0')
634 break;
635 (*qs)->index_page = strtonum(value, INT64_MIN,
636 INT64_MAX, &errstr);
637 if (errstr) {
638 error = got_error_from_errno3(__func__,
639 "strtonum", errstr);
640 goto done;
642 if ((*qs)->index_page < 0)
643 (*qs)->index_page = 0;
644 break;
645 case PATH:
646 (*qs)->path = strdup(value);
647 if ((*qs)->path == NULL) {
648 error = got_error_from_errno2(__func__,
649 "strdup");
650 goto done;
652 break;
655 /* entry found */
656 break;
658 done:
659 return error;
662 void
663 gotweb_free_repo_tag(struct repo_tag *rt)
665 if (rt != NULL) {
666 free(rt->commit_id);
667 free(rt->tag_name);
668 free(rt->tag_commit);
669 free(rt->commit_msg);
670 free(rt->tagger);
672 free(rt);
675 void
676 gotweb_free_repo_commit(struct repo_commit *rc)
678 if (rc != NULL) {
679 free(rc->path);
680 free(rc->refs_str);
681 free(rc->commit_id);
682 free(rc->parent_id);
683 free(rc->tree_id);
684 free(rc->author);
685 free(rc->committer);
686 free(rc->commit_msg);
688 free(rc);
691 static void
692 gotweb_free_querystring(struct querystring *qs)
694 if (qs != NULL) {
695 free(qs->commit);
696 free(qs->file);
697 free(qs->folder);
698 free(qs->headref);
699 free(qs->path);
701 free(qs);
704 static void
705 gotweb_free_repo_dir(struct repo_dir *repo_dir)
707 if (repo_dir != NULL) {
708 free(repo_dir->name);
709 free(repo_dir->owner);
710 free(repo_dir->description);
711 free(repo_dir->url);
712 free(repo_dir->path);
714 free(repo_dir);
717 void
718 gotweb_free_transport(struct transport *t)
720 const struct got_error *err;
721 struct repo_commit *rc = NULL, *trc = NULL;
722 struct repo_tag *rt = NULL, *trt = NULL;
723 int i;
725 got_ref_list_free(&t->refs);
726 TAILQ_FOREACH_SAFE(rc, &t->repo_commits, entry, trc) {
727 TAILQ_REMOVE(&t->repo_commits, rc, entry);
728 gotweb_free_repo_commit(rc);
730 TAILQ_FOREACH_SAFE(rt, &t->repo_tags, entry, trt) {
731 TAILQ_REMOVE(&t->repo_tags, rt, entry);
732 gotweb_free_repo_tag(rt);
734 gotweb_free_repo_dir(t->repo_dir);
735 gotweb_free_querystring(t->qs);
736 free(t->more_id);
737 free(t->tags_more_id);
738 if (t->blob)
739 got_object_blob_close(t->blob);
740 if (t->fp) {
741 err = got_gotweb_closefile(t->fp);
742 if (err)
743 log_warnx("%s: got_gotweb_closefile failure: %s",
744 __func__, err->msg);
746 if (t->fd != -1 && close(t->fd) == -1)
747 log_warn("%s: close", __func__);
748 if (t->repos) {
749 for (i = 0; i < t->nrepos; ++i)
750 free(t->repos[i]);
751 free(t->repos);
753 if (t->repo)
754 got_repo_close(t->repo);
755 free(t);
758 void
759 gotweb_index_navs(struct request *c, struct gotweb_url *prev, int *have_prev,
760 struct gotweb_url *next, int *have_next)
762 struct transport *t = c->t;
763 struct querystring *qs = t->qs;
764 struct server *srv = c->srv;
766 *have_prev = *have_next = 0;
768 if (qs->index_page > 0) {
769 *have_prev = 1;
770 *prev = (struct gotweb_url){
771 .action = -1,
772 .index_page = qs->index_page - 1,
773 };
775 if (t->next_disp == srv->max_repos_display &&
776 t->repos_total != (qs->index_page + 1) *
777 srv->max_repos_display) {
778 *have_next = 1;
779 *next = (struct gotweb_url){
780 .action = -1,
781 .index_page = qs->index_page + 1,
782 };
786 static int
787 gotweb_render_index(struct template *tp)
789 const struct got_error *error = NULL;
790 struct request *c = tp->tp_arg;
791 struct server *srv = c->srv;
792 struct transport *t = c->t;
793 struct querystring *qs = t->qs;
794 struct repo_dir *repo_dir = NULL;
795 struct dirent **sd_dent = t->repos;
796 unsigned int d_i, d_disp = 0;
797 unsigned int d_skipped = 0;
798 int type, r;
800 if (gotweb_render_repo_table_hdr(c->tp) == -1)
801 return -1;
803 for (d_i = 0; d_i < t->nrepos; d_i++) {
804 if (strcmp(sd_dent[d_i]->d_name, ".") == 0 ||
805 strcmp(sd_dent[d_i]->d_name, "..") == 0) {
806 d_skipped++;
807 continue;
810 error = got_path_dirent_type(&type, srv->repos_path,
811 sd_dent[d_i]);
812 if (error)
813 continue;
814 if (type != DT_DIR) {
815 d_skipped++;
816 continue;
819 if (qs->index_page > 0 && (qs->index_page *
820 srv->max_repos_display) > t->prev_disp) {
821 t->prev_disp++;
822 continue;
825 error = gotweb_init_repo_dir(&repo_dir, sd_dent[d_i]->d_name);
826 if (error)
827 continue;
829 error = gotweb_load_got_path(c, repo_dir);
830 if (error && error->code != GOT_ERR_LONELY_PACKIDX) {
831 if (error->code != GOT_ERR_NOT_GIT_REPO)
832 log_warnx("%s: %s: %s", __func__,
833 sd_dent[d_i]->d_name, error->msg);
834 gotweb_free_repo_dir(repo_dir);
835 repo_dir = NULL;
836 d_skipped++;
837 continue;
840 d_disp++;
841 t->prev_disp++;
843 r = gotweb_render_repo_fragment(c->tp, repo_dir);
844 gotweb_free_repo_dir(repo_dir);
845 repo_dir = NULL;
846 got_repo_close(t->repo);
847 t->repo = NULL;
848 if (r == -1)
849 return -1;
851 t->next_disp++;
852 if (d_disp == srv->max_repos_display)
853 break;
855 t->repos_total = t->nrepos - d_skipped;
857 if (srv->max_repos_display == 0 ||
858 t->repos_total <= srv->max_repos_display)
859 return 0;
861 if (gotweb_render_navs(c->tp) == -1)
862 return -1;
864 return 0;
867 static inline int
868 should_urlencode(int c)
870 if (c <= ' ' || c >= 127)
871 return 1;
873 switch (c) {
874 /* gen-delim */
875 case ':':
876 case '/':
877 case '?':
878 case '#':
879 case '[':
880 case ']':
881 case '@':
882 /* sub-delims */
883 case '!':
884 case '$':
885 case '&':
886 case '\'':
887 case '(':
888 case ')':
889 case '*':
890 case '+':
891 case ',':
892 case ';':
893 case '=':
894 /* needed because the URLs are embedded into the HTML */
895 case '\"':
896 return 1;
897 default:
898 return 0;
902 static char *
903 gotweb_urlencode(const char *str)
905 const char *s;
906 char *escaped;
907 size_t i, len;
908 int a, b;
910 len = 0;
911 for (s = str; *s; ++s) {
912 len++;
913 if (should_urlencode(*s))
914 len += 2;
917 escaped = calloc(1, len + 1);
918 if (escaped == NULL)
919 return NULL;
921 i = 0;
922 for (s = str; *s; ++s) {
923 if (should_urlencode(*s)) {
924 a = (*s & 0xF0) >> 4;
925 b = (*s & 0x0F);
927 escaped[i++] = '%';
928 escaped[i++] = a <= 9 ? ('0' + a) : ('7' + a);
929 escaped[i++] = b <= 9 ? ('0' + b) : ('7' + b);
930 } else
931 escaped[i++] = *s;
934 return escaped;
937 const char *
938 gotweb_action_name(int action)
940 switch (action) {
941 case BLAME:
942 return "blame";
943 case BLOB:
944 return "blob";
945 case BLOBRAW:
946 return "blobraw";
947 case BRIEFS:
948 return "briefs";
949 case COMMITS:
950 return "commits";
951 case DIFF:
952 return "diff";
953 case ERR:
954 return "err";
955 case INDEX:
956 return "index";
957 case PATCH:
958 return "patch";
959 case SUMMARY:
960 return "summary";
961 case TAG:
962 return "tag";
963 case TAGS:
964 return "tags";
965 case TREE:
966 return "tree";
967 case RSS:
968 return "rss";
969 default:
970 return NULL;
974 int
975 gotweb_render_url(struct request *c, struct gotweb_url *url)
977 const char *sep = "?", *action;
978 char *tmp;
979 int r;
981 action = gotweb_action_name(url->action);
982 if (action != NULL) {
983 if (tp_writef(c->tp, "?action=%s", action) == -1)
984 return -1;
985 sep = "&";
988 if (url->commit) {
989 if (tp_writef(c->tp, "%scommit=%s", sep, url->commit) == -1)
990 return -1;
991 sep = "&";
994 if (url->previd) {
995 if (tp_writef(c->tp, "%sprevid=%s", sep, url->previd) == -1)
996 return -1;
997 sep = "&";
1000 if (url->prevset) {
1001 if (tp_writef(c->tp, "%sprevset=%s", sep, url->prevset) == -1)
1002 return -1;
1003 sep = "&";
1006 if (url->file) {
1007 tmp = gotweb_urlencode(url->file);
1008 if (tmp == NULL)
1009 return -1;
1010 r = tp_writef(c->tp, "%sfile=%s", sep, tmp);
1011 free(tmp);
1012 if (r == -1)
1013 return -1;
1014 sep = "&";
1017 if (url->folder) {
1018 tmp = gotweb_urlencode(url->folder);
1019 if (tmp == NULL)
1020 return -1;
1021 r = tp_writef(c->tp, "%sfolder=%s", sep, tmp);
1022 free(tmp);
1023 if (r == -1)
1024 return -1;
1025 sep = "&";
1028 if (url->headref) {
1029 tmp = gotweb_urlencode(url->headref);
1030 if (tmp == NULL)
1031 return -1;
1032 r = tp_writef(c->tp, "%sheadref=%s", sep, url->headref);
1033 free(tmp);
1034 if (r == -1)
1035 return -1;
1036 sep = "&";
1039 if (url->index_page != -1) {
1040 if (tp_writef(c->tp, "%sindex_page=%d", sep,
1041 url->index_page) == -1)
1042 return -1;
1043 sep = "&";
1046 if (url->path) {
1047 tmp = gotweb_urlencode(url->path);
1048 if (tmp == NULL)
1049 return -1;
1050 r = tp_writef(c->tp, "%spath=%s", sep, tmp);
1051 free(tmp);
1052 if (r == -1)
1053 return -1;
1054 sep = "&";
1057 return 0;
1060 int
1061 gotweb_render_absolute_url(struct request *c, struct gotweb_url *url)
1063 struct template *tp = c->tp;
1064 const char *proto = c->https ? "https" : "http";
1066 if (tp_writes(tp, proto) == -1 ||
1067 tp_writes(tp, "://") == -1 ||
1068 tp_htmlescape(tp, c->server_name) == -1 ||
1069 tp_htmlescape(tp, c->document_uri) == -1)
1070 return -1;
1072 return gotweb_render_url(c, url);
1075 static const struct got_error *
1076 gotweb_load_got_path(struct request *c, struct repo_dir *repo_dir)
1078 const struct got_error *error = NULL;
1079 struct socket *sock = c->sock;
1080 struct server *srv = c->srv;
1081 struct transport *t = c->t;
1082 DIR *dt;
1083 char *dir_test;
1085 if (asprintf(&dir_test, "%s/%s/%s", srv->repos_path, repo_dir->name,
1086 GOTWEB_GIT_DIR) == -1)
1087 return got_error_from_errno("asprintf");
1089 dt = opendir(dir_test);
1090 if (dt == NULL) {
1091 free(dir_test);
1092 if (asprintf(&dir_test, "%s/%s", srv->repos_path,
1093 repo_dir->name) == -1)
1094 return got_error_from_errno("asprintf");
1095 dt = opendir(dir_test);
1096 if (dt == NULL) {
1097 free(dir_test);
1098 return got_error_path(repo_dir->name,
1099 GOT_ERR_NOT_GIT_REPO);
1103 repo_dir->path = dir_test;
1104 dir_test = NULL;
1106 if (srv->respect_exportok &&
1107 faccessat(dirfd(dt), "git-daemon-export-ok", F_OK, 0) == -1) {
1108 error = got_error_path(repo_dir->name, GOT_ERR_NOT_GIT_REPO);
1109 goto err;
1112 error = got_repo_open(&t->repo, repo_dir->path, NULL, sock->pack_fds);
1113 if (error)
1114 goto err;
1115 error = gotweb_get_repo_description(&repo_dir->description, srv,
1116 repo_dir->path, dirfd(dt));
1117 if (error)
1118 goto err;
1119 error = got_get_repo_owner(&repo_dir->owner, c);
1120 if (error)
1121 goto err;
1122 if (srv->show_repo_age) {
1123 error = got_get_repo_age(&repo_dir->age, c, NULL);
1124 if (error)
1125 goto err;
1127 error = gotweb_get_clone_url(&repo_dir->url, srv, repo_dir->path,
1128 dirfd(dt));
1129 err:
1130 free(dir_test);
1131 if (dt != NULL && closedir(dt) == EOF && error == NULL)
1132 error = got_error_from_errno("closedir");
1133 if (error && t->repo) {
1134 got_repo_close(t->repo);
1135 t->repo = NULL;
1137 return error;
1140 static const struct got_error *
1141 gotweb_init_repo_dir(struct repo_dir **repo_dir, const char *dir)
1143 const struct got_error *error;
1145 *repo_dir = calloc(1, sizeof(**repo_dir));
1146 if (*repo_dir == NULL)
1147 return got_error_from_errno("calloc");
1149 if (asprintf(&(*repo_dir)->name, "%s", dir) == -1) {
1150 error = got_error_from_errno("asprintf");
1151 free(*repo_dir);
1152 *repo_dir = NULL;
1153 return error;
1155 (*repo_dir)->owner = NULL;
1156 (*repo_dir)->description = NULL;
1157 (*repo_dir)->url = NULL;
1158 (*repo_dir)->path = NULL;
1160 return NULL;
1163 static const struct got_error *
1164 gotweb_get_repo_description(char **description, struct server *srv,
1165 const char *dirpath, int dir)
1167 const struct got_error *error = NULL;
1168 struct stat sb;
1169 int fd = -1;
1170 off_t len;
1172 *description = NULL;
1173 if (srv->show_repo_description == 0)
1174 return NULL;
1176 fd = openat(dir, "description", O_RDONLY);
1177 if (fd == -1) {
1178 if (errno != ENOENT && errno != EACCES) {
1179 error = got_error_from_errno_fmt("openat %s/%s",
1180 dirpath, "description");
1182 goto done;
1185 if (fstat(fd, &sb) == -1) {
1186 error = got_error_from_errno_fmt("fstat %s/%s",
1187 dirpath, "description");
1188 goto done;
1191 len = sb.st_size;
1192 if (len > GOTWEBD_MAXDESCRSZ - 1)
1193 len = GOTWEBD_MAXDESCRSZ - 1;
1195 *description = calloc(len + 1, sizeof(**description));
1196 if (*description == NULL) {
1197 error = got_error_from_errno("calloc");
1198 goto done;
1201 if (read(fd, *description, len) == -1)
1202 error = got_error_from_errno("read");
1203 done:
1204 if (fd != -1 && close(fd) == -1 && error == NULL)
1205 error = got_error_from_errno("close");
1206 return error;
1209 static const struct got_error *
1210 gotweb_get_clone_url(char **url, struct server *srv, const char *dirpath,
1211 int dir)
1213 const struct got_error *error = NULL;
1214 struct stat sb;
1215 int fd = -1;
1216 off_t len;
1218 *url = NULL;
1219 if (srv->show_repo_cloneurl == 0)
1220 return NULL;
1222 fd = openat(dir, "cloneurl", O_RDONLY);
1223 if (fd == -1) {
1224 if (errno != ENOENT && errno != EACCES) {
1225 error = got_error_from_errno_fmt("openat %s/%s",
1226 dirpath, "cloneurl");
1228 goto done;
1231 if (fstat(fd, &sb) == -1) {
1232 error = got_error_from_errno_fmt("fstat %s/%s",
1233 dirpath, "cloneurl");
1234 goto done;
1237 len = sb.st_size;
1238 if (len > GOTWEBD_MAXCLONEURLSZ - 1)
1239 len = GOTWEBD_MAXCLONEURLSZ - 1;
1241 *url = calloc(len + 1, sizeof(**url));
1242 if (*url == NULL) {
1243 error = got_error_from_errno("calloc");
1244 goto done;
1247 if (read(fd, *url, len) == -1)
1248 error = got_error_from_errno("read");
1249 done:
1250 if (fd != -1 && close(fd) == -1 && error == NULL)
1251 error = got_error_from_errno("close");
1252 return error;
1255 int
1256 gotweb_render_age(struct template *tp, time_t committer_time)
1258 struct request *c = tp->tp_arg;
1259 long long diff_time;
1260 const char *years = "years ago", *months = "months ago";
1261 const char *weeks = "weeks ago", *days = "days ago";
1262 const char *hours = "hours ago", *minutes = "minutes ago";
1263 const char *seconds = "seconds ago", *now = "right now";
1265 diff_time = time(NULL) - committer_time;
1266 if (diff_time > 60 * 60 * 24 * 365 * 2) {
1267 if (tp_writef(c->tp, "%lld %s",
1268 (diff_time / 60 / 60 / 24 / 365), years) == -1)
1269 return -1;
1270 } else if (diff_time > 60 * 60 * 24 * (365 / 12) * 2) {
1271 if (tp_writef(c->tp, "%lld %s",
1272 (diff_time / 60 / 60 / 24 / (365 / 12)),
1273 months) == -1)
1274 return -1;
1275 } else if (diff_time > 60 * 60 * 24 * 7 * 2) {
1276 if (tp_writef(c->tp, "%lld %s",
1277 (diff_time / 60 / 60 / 24 / 7), weeks) == -1)
1278 return -1;
1279 } else if (diff_time > 60 * 60 * 24 * 2) {
1280 if (tp_writef(c->tp, "%lld %s",
1281 (diff_time / 60 / 60 / 24), days) == -1)
1282 return -1;
1283 } else if (diff_time > 60 * 60 * 2) {
1284 if (tp_writef(c->tp, "%lld %s",
1285 (diff_time / 60 / 60), hours) == -1)
1286 return -1;
1287 } else if (diff_time > 60 * 2) {
1288 if (tp_writef(c->tp, "%lld %s", (diff_time / 60),
1289 minutes) == -1)
1290 return -1;
1291 } else if (diff_time > 2) {
1292 if (tp_writef(c->tp, "%lld %s", diff_time,
1293 seconds) == -1)
1294 return -1;
1295 } else {
1296 if (tp_writes(tp, now) == -1)
1297 return -1;
1299 return 0;