fix a typo in CHANGES, spotted by teajaygray@rap.social
[got-portable.git] / gotwebd / gotweb.c
blob53cbc91e069a115f4cf2a397a5f0747d68241108
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>
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.
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.
20 #include "got_compat.h"
22 #include <net/if.h>
23 #include <netinet/in.h>
24 #include <sys/queue.h>
25 #include <sys/stat.h>
26 #include <sys/types.h>
28 #include <ctype.h>
29 #include <dirent.h>
30 #include <errno.h>
31 #include <event.h>
32 #include <fcntl.h>
33 #include <imsg.h>
34 #include <stdio.h>
35 #include <stdlib.h>
36 #include <string.h>
37 #include <unistd.h>
39 #include "got_error.h"
40 #include "got_object.h"
41 #include "got_reference.h"
42 #include "got_repository.h"
43 #include "got_path.h"
44 #include "got_cancel.h"
45 #include "got_worktree.h"
46 #include "got_diff.h"
47 #include "got_commit_graph.h"
48 #include "got_blame.h"
49 #include "got_privsep.h"
51 #include "gotwebd.h"
52 #include "log.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 },
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 },
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_load_got_path(struct repo_dir **,
89 const char *, struct request *);
90 static const struct got_error *gotweb_load_file(char **, const char *,
91 const char *, int);
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 struct repo_commit *commit;
150 const char *rss_ctype = "application/rss+xml;charset=utf-8";
151 const uint8_t *buf;
152 size_t len;
153 int r, binary = 0;
155 /* init the transport */
156 error = gotweb_init_transport(&c->t);
157 if (error) {
158 log_warnx("%s: %s", __func__, error->msg);
159 return;
161 /* get the gotwebd server */
162 srv = gotweb_get_server(c->server_name);
163 if (srv == NULL) {
164 log_warnx("%s: error server is NULL", __func__);
165 goto err;
167 c->srv = srv;
168 /* parse our querystring */
169 error = gotweb_init_querystring(&qs);
170 if (error) {
171 log_warnx("%s: %s", __func__, error->msg);
172 goto err;
174 c->t->qs = qs;
175 error = gotweb_parse_querystring(qs, c->querystring);
176 if (error) {
177 log_warnx("%s: %s", __func__, error->msg);
178 goto err;
182 * certain actions require a commit id in the querystring. this stops
183 * bad actors from exploiting this by manually manipulating the
184 * querystring.
187 if (qs->action == BLAME || qs->action == BLOB ||
188 qs->action == BLOBRAW || qs->action == DIFF ||
189 qs->action == PATCH) {
190 if (qs->commit == NULL) {
191 error = got_error(GOT_ERR_BAD_QUERYSTRING);
192 goto err;
196 if (qs->action != INDEX) {
197 error = gotweb_load_got_path(&repo_dir, qs->path, c);
198 c->t->repo_dir = repo_dir;
199 if (error)
200 goto err;
203 if (qs->action == BLOBRAW || qs->action == BLOB) {
204 if (qs->folder == NULL || qs->file == NULL) {
205 error = got_error(GOT_ERR_BAD_QUERYSTRING);
206 goto err;
209 error = got_get_repo_commits(c, 1);
210 if (error)
211 goto err;
213 error = got_open_blob_for_output(&c->t->blob, &c->t->fd,
214 &binary, c, qs->folder, qs->file, qs->commit);
215 if (error)
216 goto err;
219 switch (qs->action) {
220 case BLAME:
221 if (qs->folder == NULL || qs->file == NULL) {
222 error = got_error(GOT_ERR_BAD_QUERYSTRING);
223 goto err;
225 error = got_get_repo_commits(c, 1);
226 if (error) {
227 log_warnx("%s: %s", __func__, error->msg);
228 goto err;
230 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
231 return;
232 gotweb_render_page(c->tp, gotweb_render_blame);
233 return;
234 case BLOB:
235 if (binary) {
236 struct gotweb_url url = {
237 .index_page = -1,
238 .action = BLOBRAW,
239 .path = qs->path,
240 .commit = qs->commit,
241 .folder = qs->folder,
242 .file = qs->file,
245 gotweb_reply(c, 302, NULL, &url);
246 return;
249 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
250 return;
251 gotweb_render_page(c->tp, gotweb_render_blob);
252 return;
253 case BLOBRAW:
254 if (binary)
255 r = gotweb_reply_file(c, "application/octet-stream",
256 qs->file, NULL);
257 else
258 r = gotweb_reply(c, 200, "text/plain", NULL);
259 if (r == -1)
260 return;
261 if (template_flush(c->tp) == -1)
262 return;
264 for (;;) {
265 error = got_object_blob_read_block(&len, c->t->blob);
266 if (error)
267 break;
268 if (len == 0)
269 break;
270 buf = got_object_blob_get_read_buf(c->t->blob);
271 if (fcgi_write(c, buf, len) == -1)
272 break;
274 return;
275 case BRIEFS:
276 error = got_get_repo_commits(c, srv->max_commits_display);
277 if (error)
278 goto err;
279 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
280 return;
281 gotweb_render_page(c->tp, gotweb_render_briefs);
282 return;
283 case COMMITS:
284 error = got_get_repo_commits(c, srv->max_commits_display);
285 if (error) {
286 log_warnx("%s: %s", __func__, error->msg);
287 goto err;
289 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
290 return;
291 gotweb_render_page(c->tp, gotweb_render_commits);
292 return;
293 case DIFF:
294 error = got_get_repo_commits(c, 1);
295 if (error) {
296 log_warnx("%s: %s", __func__, error->msg);
297 goto err;
299 error = got_open_diff_for_output(&c->t->fp, c);
300 if (error) {
301 log_warnx("%s: %s", __func__, error->msg);
302 goto err;
304 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
305 return;
306 gotweb_render_page(c->tp, gotweb_render_diff);
307 return;
308 case INDEX:
309 c->t->nrepos = scandir(srv->repos_path, &c->t->repos, NULL,
310 alphasort);
311 if (c->t->nrepos == -1) {
312 c->t->repos = NULL;
313 error = got_error_from_errno2("scandir",
314 srv->repos_path);
315 goto err;
317 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
318 return;
319 gotweb_render_page(c->tp, gotweb_render_index);
320 return;
321 case PATCH:
322 error = got_get_repo_commits(c, 1);
323 if (error) {
324 log_warnx("%s: %s", __func__, error->msg);
325 goto err;
327 error = got_open_diff_for_output(&c->t->fp, c);
328 if (error) {
329 log_warnx("%s: %s", __func__, error->msg);
330 goto err;
332 if (gotweb_reply(c, 200, "text/plain", NULL) == -1)
333 return;
334 gotweb_render_patch(c->tp);
335 return;
336 case RSS:
337 error = got_get_repo_tags(c, D_MAXSLCOMMDISP);
338 if (error)
339 goto err;
340 if (gotweb_reply_file(c, rss_ctype, repo_dir->name, ".rss")
341 == -1)
342 return;
343 gotweb_render_rss(c->tp);
344 return;
345 case SUMMARY:
346 error = got_ref_list(&c->t->refs, c->t->repo, "refs/heads",
347 got_ref_cmp_by_name, NULL);
348 if (error) {
349 log_warnx("%s: got_ref_list: %s", __func__,
350 error->msg);
351 goto err;
353 error = got_get_repo_commits(c, srv->summary_commits_display);
354 if (error)
355 goto err;
356 qs->action = TAGS;
357 error = got_get_repo_tags(c, srv->summary_tags_display);
358 if (error) {
359 log_warnx("%s: got_get_repo_tags: %s", __func__,
360 error->msg);
361 goto err;
363 qs->action = SUMMARY;
364 commit = TAILQ_FIRST(&c->t->repo_commits);
365 if (commit && qs->commit == NULL) {
366 qs->commit = strdup(commit->commit_id);
367 if (qs->commit == NULL) {
368 error = got_error_from_errno("strdup");
369 log_warn("%s: strdup", __func__);
370 goto err;
373 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
374 return;
375 gotweb_render_page(c->tp, gotweb_render_summary);
376 return;
377 case TAG:
378 error = got_get_repo_tags(c, 1);
379 if (error) {
380 log_warnx("%s: %s", __func__, error->msg);
381 goto err;
383 if (TAILQ_EMPTY(&c->t->repo_tags)) {
384 error = got_error_msg(GOT_ERR_BAD_OBJ_ID,
385 "bad commit id");
386 goto err;
388 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
389 return;
390 gotweb_render_page(c->tp, gotweb_render_tag);
391 return;
392 case TAGS:
393 error = got_get_repo_tags(c, srv->max_commits_display);
394 if (error) {
395 log_warnx("%s: %s", __func__, error->msg);
396 goto err;
398 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
399 return;
400 gotweb_render_page(c->tp, gotweb_render_tags);
401 return;
402 case TREE:
403 error = got_get_repo_commits(c, 1);
404 if (error) {
405 log_warnx("%s: %s", __func__, error->msg);
406 goto err;
408 if (gotweb_reply(c, 200, "text/html", NULL) == -1)
409 return;
410 gotweb_render_page(c->tp, gotweb_render_tree);
411 return;
412 case ERR:
413 default:
414 error = got_error(GOT_ERR_BAD_QUERYSTRING);
417 err:
418 c->t->error = error;
419 if (gotweb_reply(c, 400, "text/html", NULL) == -1)
420 return;
421 gotweb_render_page(c->tp, gotweb_render_error);
424 struct server *
425 gotweb_get_server(const char *server_name)
427 struct server *srv;
429 /* check against the server name first */
430 if (*server_name != '\0')
431 TAILQ_FOREACH(srv, &gotwebd_env->servers, entry)
432 if (strcmp(srv->name, server_name) == 0)
433 return srv;
435 /* otherwise, use the first server */
436 return TAILQ_FIRST(&gotwebd_env->servers);
439 const struct got_error *
440 gotweb_init_transport(struct transport **t)
442 const struct got_error *error = NULL;
444 *t = calloc(1, sizeof(**t));
445 if (*t == NULL)
446 return got_error_from_errno2(__func__, "calloc");
448 TAILQ_INIT(&(*t)->repo_commits);
449 TAILQ_INIT(&(*t)->repo_tags);
450 TAILQ_INIT(&(*t)->refs);
452 (*t)->fd = -1;
454 return error;
457 static const struct got_error *
458 gotweb_init_querystring(struct querystring **qs)
460 const struct got_error *error = NULL;
462 *qs = calloc(1, sizeof(**qs));
463 if (*qs == NULL)
464 return got_error_from_errno2(__func__, "calloc");
466 (*qs)->headref = strdup("HEAD");
467 if ((*qs)->headref == NULL) {
468 free(*qs);
469 *qs = NULL;
470 return got_error_from_errno2(__func__, "strdup");
473 (*qs)->action = INDEX;
475 return error;
478 static const struct got_error *
479 gotweb_parse_querystring(struct querystring *qs, char *qst)
481 const struct got_error *error = NULL;
482 char *tok1 = NULL, *tok1_pair = NULL, *tok1_end = NULL;
483 char *tok2 = NULL, *tok2_pair = NULL, *tok2_end = NULL;
485 if (qst == NULL)
486 return error;
488 tok1 = strdup(qst);
489 if (tok1 == NULL)
490 return got_error_from_errno2(__func__, "strdup");
492 tok1_pair = tok1;
493 tok1_end = tok1;
495 while (tok1_pair != NULL) {
496 strsep(&tok1_end, "&");
498 tok2 = strdup(tok1_pair);
499 if (tok2 == NULL) {
500 free(tok1);
501 return got_error_from_errno2(__func__, "strdup");
504 tok2_pair = tok2;
505 tok2_end = tok2;
507 while (tok2_pair != NULL) {
508 strsep(&tok2_end, "=");
509 if (tok2_end) {
510 error = gotweb_assign_querystring(qs, tok2_pair,
511 tok2_end);
512 if (error)
513 goto err;
515 tok2_pair = tok2_end;
517 free(tok2);
518 tok1_pair = tok1_end;
520 free(tok1);
521 return error;
522 err:
523 free(tok2);
524 free(tok1);
525 return error;
529 * Adapted from usr.sbin/httpd/httpd.c url_decode.
531 static const struct got_error *
532 gotweb_urldecode(char *url)
534 char *p, *q;
535 char hex[3];
536 unsigned long x;
538 hex[2] = '\0';
539 p = q = url;
541 while (*p != '\0') {
542 switch (*p) {
543 case '%':
544 /* Encoding character is followed by two hex chars */
545 if (!isxdigit((unsigned char)p[1]) ||
546 !isxdigit((unsigned char)p[2]) ||
547 (p[1] == '0' && p[2] == '0'))
548 return got_error(GOT_ERR_BAD_QUERYSTRING);
550 hex[0] = p[1];
551 hex[1] = p[2];
554 * We don't have to validate "hex" because it is
555 * guaranteed to include two hex chars followed by nul.
557 x = strtoul(hex, NULL, 16);
558 *q = (char)x;
559 p += 2;
560 break;
561 default:
562 *q = *p;
563 break;
565 p++;
566 q++;
568 *q = '\0';
570 return NULL;
573 static const struct got_error *
574 gotweb_assign_querystring(struct querystring *qs, char *key, char *value)
576 const struct got_error *error = NULL;
577 const char *errstr;
578 int a_cnt, el_cnt;
580 error = gotweb_urldecode(value);
581 if (error)
582 return error;
584 for (el_cnt = 0; el_cnt < nitems(querystring_keys); el_cnt++) {
585 if (strcmp(key, querystring_keys[el_cnt].name) != 0)
586 continue;
588 switch (querystring_keys[el_cnt].element) {
589 case ACTION:
590 for (a_cnt = 0; a_cnt < nitems(action_keys); a_cnt++) {
591 if (strcmp(value, action_keys[a_cnt].name) != 0)
592 continue;
593 qs->action = action_keys[a_cnt].action;
594 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,
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,
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_load_got_path(&repo_dir, sd_dent[d_i]->d_name,
827 if (error) {
828 if (error->code != GOT_ERR_NOT_GIT_REPO)
829 log_warnx("%s: %s: %s", __func__,
830 sd_dent[d_i]->d_name, error->msg);
831 gotweb_free_repo_dir(repo_dir);
832 repo_dir = NULL;
833 d_skipped++;
834 continue;
837 d_disp++;
838 t->prev_disp++;
840 r = gotweb_render_repo_fragment(c->tp, repo_dir);
841 gotweb_free_repo_dir(repo_dir);
842 repo_dir = NULL;
843 got_repo_close(t->repo);
844 t->repo = NULL;
845 if (r == -1)
846 return -1;
848 t->next_disp++;
849 if (d_disp == srv->max_repos_display)
850 break;
852 t->repos_total = t->nrepos - d_skipped;
854 if (srv->max_repos_display == 0 ||
855 t->repos_total <= srv->max_repos_display)
856 return 0;
858 if (gotweb_render_navs(c->tp) == -1)
859 return -1;
861 return 0;
864 static inline int
865 should_urlencode(int c)
867 if (c <= ' ' || c >= 127)
868 return 1;
870 switch (c) {
871 /* gen-delim */
872 case ':':
873 case '/':
874 case '?':
875 case '#':
876 case '[':
877 case ']':
878 case '@':
879 /* sub-delims */
880 case '!':
881 case '$':
882 case '&':
883 case '\'':
884 case '(':
885 case ')':
886 case '*':
887 case '+':
888 case ',':
889 case ';':
890 case '=':
891 /* needed because the URLs are embedded into the HTML */
892 case '\"':
893 return 1;
894 default:
895 return 0;
899 static char *
900 gotweb_urlencode(const char *str)
902 const char *s;
903 char *escaped;
904 size_t i, len;
905 int a, b;
907 len = 0;
908 for (s = str; *s; ++s) {
909 len++;
910 if (should_urlencode(*s))
911 len += 2;
914 escaped = calloc(1, len + 1);
915 if (escaped == NULL)
916 return NULL;
918 i = 0;
919 for (s = str; *s; ++s) {
920 if (should_urlencode(*s)) {
921 a = (*s & 0xF0) >> 4;
922 b = (*s & 0x0F);
924 escaped[i++] = '%';
925 escaped[i++] = a <= 9 ? ('0' + a) : ('7' + a);
926 escaped[i++] = b <= 9 ? ('0' + b) : ('7' + b);
927 } else
928 escaped[i++] = *s;
931 return escaped;
934 const char *
935 gotweb_action_name(int action)
937 switch (action) {
938 case BLAME:
939 return "blame";
940 case BLOB:
941 return "blob";
942 case BLOBRAW:
943 return "blobraw";
944 case BRIEFS:
945 return "briefs";
946 case COMMITS:
947 return "commits";
948 case DIFF:
949 return "diff";
950 case ERR:
951 return "err";
952 case INDEX:
953 return "index";
954 case PATCH:
955 return "patch";
956 case SUMMARY:
957 return "summary";
958 case TAG:
959 return "tag";
960 case TAGS:
961 return "tags";
962 case TREE:
963 return "tree";
964 case RSS:
965 return "rss";
966 default:
967 return NULL;
972 gotweb_render_url(struct request *c, struct gotweb_url *url)
974 const char *sep = "?", *action;
975 char *tmp;
976 int r;
978 action = gotweb_action_name(url->action);
979 if (action != NULL) {
980 if (tp_writef(c->tp, "?action=%s", action) == -1)
981 return -1;
982 sep = "&";
985 if (url->commit) {
986 if (tp_writef(c->tp, "%scommit=%s", sep, url->commit) == -1)
987 return -1;
988 sep = "&";
991 if (url->file) {
992 tmp = gotweb_urlencode(url->file);
993 if (tmp == NULL)
994 return -1;
995 r = tp_writef(c->tp, "%sfile=%s", sep, tmp);
996 free(tmp);
997 if (r == -1)
998 return -1;
999 sep = "&";
1002 if (url->folder) {
1003 tmp = gotweb_urlencode(url->folder);
1004 if (tmp == NULL)
1005 return -1;
1006 r = tp_writef(c->tp, "%sfolder=%s", sep, tmp);
1007 free(tmp);
1008 if (r == -1)
1009 return -1;
1010 sep = "&";
1013 if (url->headref) {
1014 tmp = gotweb_urlencode(url->headref);
1015 if (tmp == NULL)
1016 return -1;
1017 r = tp_writef(c->tp, "%sheadref=%s", sep, url->headref);
1018 free(tmp);
1019 if (r == -1)
1020 return -1;
1021 sep = "&";
1024 if (url->index_page != -1) {
1025 if (tp_writef(c->tp, "%sindex_page=%d", sep,
1026 url->index_page) == -1)
1027 return -1;
1028 sep = "&";
1031 if (url->path) {
1032 tmp = gotweb_urlencode(url->path);
1033 if (tmp == NULL)
1034 return -1;
1035 r = tp_writef(c->tp, "%spath=%s", sep, tmp);
1036 free(tmp);
1037 if (r == -1)
1038 return -1;
1039 sep = "&";
1042 return 0;
1046 gotweb_render_absolute_url(struct request *c, struct gotweb_url *url)
1048 struct template *tp = c->tp;
1049 const char *proto = c->https ? "https" : "http";
1051 if (tp_writes(tp, proto) == -1 ||
1052 tp_writes(tp, "://") == -1 ||
1053 tp_htmlescape(tp, c->server_name) == -1 ||
1054 tp_htmlescape(tp, c->document_uri) == -1)
1055 return -1;
1057 return gotweb_render_url(c, url);
1060 static const struct got_error *
1061 gotweb_load_got_path(struct repo_dir **rp, const char *dir,
1062 struct request *c)
1064 const struct got_error *error = NULL;
1065 struct server *srv = c->srv;
1066 struct transport *t = c->t;
1067 struct repo_dir *repo_dir;
1068 DIR *dt;
1069 char *dir_test;
1071 *rp = calloc(1, sizeof(**rp));
1072 if (*rp == NULL)
1073 return got_error_from_errno("calloc");
1074 repo_dir = *rp;
1076 if (asprintf(&dir_test, "%s/%s/%s", srv->repos_path, dir,
1077 GOTWEB_GIT_DIR) == -1)
1078 return got_error_from_errno("asprintf");
1080 dt = opendir(dir_test);
1081 if (dt == NULL) {
1082 free(dir_test);
1083 if (asprintf(&dir_test, "%s/%s", srv->repos_path, dir) == -1)
1084 return got_error_from_errno("asprintf");
1085 dt = opendir(dir_test);
1086 if (dt == NULL) {
1087 free(dir_test);
1088 if (asprintf(&dir_test, "%s/%s%s", srv->repos_path,
1089 dir, GOTWEB_GIT_DIR) == -1)
1090 return got_error_from_errno("asprintf");
1091 dt = opendir(dir_test);
1092 if (dt == NULL) {
1093 free(dir_test);
1094 return got_error_path(dir,
1095 GOT_ERR_NOT_GIT_REPO);
1100 repo_dir->path = dir_test;
1101 dir_test = NULL;
1103 repo_dir->name = strdup(repo_dir->path + strlen(srv->repos_path) + 1);
1104 if (repo_dir->name == NULL) {
1105 error = got_error_from_errno("strdup");
1106 goto err;
1109 if (srv->respect_exportok &&
1110 faccessat(dirfd(dt), "git-daemon-export-ok", F_OK, 0) == -1) {
1111 error = got_error_path(repo_dir->name, GOT_ERR_NOT_GIT_REPO);
1112 goto err;
1115 error = got_repo_open(&t->repo, repo_dir->path, NULL,
1116 gotwebd_env->pack_fds);
1117 if (error)
1118 goto err;
1119 error = gotweb_get_repo_description(&repo_dir->description, srv,
1120 repo_dir->path, dirfd(dt));
1121 if (error)
1122 goto err;
1123 if (srv->show_repo_owner) {
1124 error = gotweb_load_file(&repo_dir->owner, repo_dir->path,
1125 "owner", dirfd(dt));
1126 if (error)
1127 goto err;
1128 if (repo_dir->owner == NULL) {
1129 error = got_get_repo_owner(&repo_dir->owner, c);
1130 if (error)
1131 goto err;
1134 if (srv->show_repo_age) {
1135 error = got_get_repo_age(&repo_dir->age, c, NULL);
1136 if (error)
1137 goto err;
1139 error = gotweb_get_clone_url(&repo_dir->url, srv, repo_dir->path,
1140 dirfd(dt));
1141 err:
1142 free(dir_test);
1143 if (dt != NULL && closedir(dt) == EOF && error == NULL)
1144 error = got_error_from_errno("closedir");
1145 if (error && t->repo) {
1146 got_repo_close(t->repo);
1147 t->repo = NULL;
1149 return error;
1152 static const struct got_error *
1153 gotweb_load_file(char **str, const char *dir, const char *file, int dirfd)
1155 const struct got_error *error = NULL;
1156 struct stat sb;
1157 off_t len;
1158 int fd;
1160 *str = NULL;
1162 fd = openat(dirfd, file, O_RDONLY);
1163 if (fd == -1) {
1164 if (errno == ENOENT || errno == EACCES)
1165 return NULL;
1166 return got_error_from_errno_fmt("openat %s/%s", dir, file);
1169 if (fstat(fd, &sb) == -1) {
1170 error = got_error_from_errno_fmt("fstat %s/%s", dir, file);
1171 goto done;
1174 len = sb.st_size;
1175 if (len > GOTWEBD_MAXDESCRSZ - 1)
1176 len = GOTWEBD_MAXDESCRSZ - 1;
1178 *str = calloc(len + 1, 1);
1179 if (*str == NULL) {
1180 error = got_error_from_errno("calloc");
1181 goto done;
1184 if (read(fd, *str, len) == -1)
1185 error = got_error_from_errno("read");
1186 done:
1187 if (fd != -1 && close(fd) == -1 && error == NULL)
1188 error = got_error_from_errno("close");
1189 return error;
1192 static const struct got_error *
1193 gotweb_get_repo_description(char **description, struct server *srv,
1194 const char *dirpath, int dir)
1196 *description = NULL;
1197 if (srv->show_repo_description == 0)
1198 return NULL;
1200 return gotweb_load_file(description, dirpath, "description", dir);
1203 static const struct got_error *
1204 gotweb_get_clone_url(char **url, struct server *srv, const char *dirpath,
1205 int dir)
1207 *url = NULL;
1208 if (srv->show_repo_cloneurl == 0)
1209 return NULL;
1211 return gotweb_load_file(url, dirpath, "cloneurl", dir);
1215 gotweb_render_age(struct template *tp, time_t committer_time)
1217 struct request *c = tp->tp_arg;
1218 long long diff_time;
1219 const char *years = "years ago", *months = "months ago";
1220 const char *weeks = "weeks ago", *days = "days ago";
1221 const char *hours = "hours ago", *minutes = "minutes ago";
1222 const char *seconds = "seconds ago", *now = "right now";
1224 diff_time = time(NULL) - committer_time;
1225 if (diff_time > 60 * 60 * 24 * 365 * 2) {
1226 if (tp_writef(c->tp, "%lld %s",
1227 (diff_time / 60 / 60 / 24 / 365), years) == -1)
1228 return -1;
1229 } else if (diff_time > 60 * 60 * 24 * (365 / 12) * 2) {
1230 if (tp_writef(c->tp, "%lld %s",
1231 (diff_time / 60 / 60 / 24 / (365 / 12)),
1232 months) == -1)
1233 return -1;
1234 } else if (diff_time > 60 * 60 * 24 * 7 * 2) {
1235 if (tp_writef(c->tp, "%lld %s",
1236 (diff_time / 60 / 60 / 24 / 7), weeks) == -1)
1237 return -1;
1238 } else if (diff_time > 60 * 60 * 24 * 2) {
1239 if (tp_writef(c->tp, "%lld %s",
1240 (diff_time / 60 / 60 / 24), days) == -1)
1241 return -1;
1242 } else if (diff_time > 60 * 60 * 2) {
1243 if (tp_writef(c->tp, "%lld %s",
1244 (diff_time / 60 / 60), hours) == -1)
1245 return -1;
1246 } else if (diff_time > 60 * 2) {
1247 if (tp_writef(c->tp, "%lld %s", (diff_time / 60),
1248 minutes) == -1)
1249 return -1;
1250 } else if (diff_time > 2) {
1251 if (tp_writef(c->tp, "%lld %s", diff_time,
1252 seconds) == -1)
1253 return -1;
1254 } else {
1255 if (tp_writes(tp, now) == -1)
1256 return -1;
1258 return 0;