commit a596b9579655b53f1549498aff25ae1d2c9dfc81 from: Tracey Emery date: Thu Jul 14 13:30:52 2022 UTC import gotwebd thread fcgi response to client for rendering in browser as data is returned fix potential problem with a stuck loop if the client is hammering the server with random clicks and stop/restarts render our index! WOOHOO! small var refactoring. fcgi.c to handle all clean-up, various error clean-up remove output used to trace down got bug temporarily stop overloading a socket, but a better solution needs to be found return on fcgi_gen_response, so we can track if a client is writable or not this stops page creation when the client is unavailable remove old comments enable profile building, although, i don't think this works thoroughly in a priv/proc daemon catch more errors correctly count repos remove temp logger we don't need to start our responder thread so early. move it to fcgi.c and start when we start processing html kill the unneeded thread, stop queueing responses, and just write to clients immediately clean up some memory leaks and dead stores rework querystring so an error can be displayed instead of showing the index on querystring error get framework in place for the rest of the content add server struct to response struct bo last commit get back a usable gotweb. not sure what i was thinking yesterday properly move our structs around this time remember index page for sitelink, fix leak unused var is annoying, so stop it for now. don't forget to change this! style briefs nearly completed. finish briefs output add briefs to summary cleanup some html properly retrieve next and previous commit ids for list navigation follow naddy's stailq macro change we will never have a previous link on the summary page goto correct label, so we get a previous link on the last page of briefs don't wrap short line simplify got_get_repo_commits code start rendering a diff start rendering a diff this was by accident finish diff output functions cleanup prepare for fd request that was a stupid idea, just flush the priv_fd bo that too. that won't work eith with append in mkstemp that isn't going to work actually zero out the priv_fd missed seek to beginning of file was overwriting first line of diff fsync our fd as well add link to repo path by sitelink and add back verbose fcgi debugging that was removed add modest write heuristics to fcgi_send_response fix dead assignments and XXX comment where a leak is happening that I can't find right now there was no leak. stsp is brilliant and knew it was the cache growing prevent double-free, render prettier err output if we can remove unused variables correctly fix double-free fix gotwebd to build with main's changes after rebase fix double-free don't error on index if pack files missing and fixup some error handling render commits finish up tag briefs and start the tag page finish up tag page unbreak TAGS and SUMMARY actions grab the correct tag from the queue unbreak TAGS and SUMMARY actions again update some error handling clean up unneeded code and start tree output render tree render branches remove tags from summary if there aren't any fix tree div structure and start blob render render blob render blame fix tree href in briefs clean up some css add headref to querystrings load correct commit for tree and diff fixup some error output update some copyright dates add full SNI support rm debug line found by Lucas6023, notified via IRC. thanks!! fix tree fix crash when querystring is manipulated to not have a commit id in certain instances. also break a stuck while loop on client error. fix for new got_object_id_by_path arguments rebase and fix prep for multiple fds per socket, instead of just one fix overlooked shift/reduce conflicts backout priv_fds as a list. after discussion with stsp, an array and length are the better direction prepare array of fds to pass into got functions make a new set of pack fds, which will be passed to got_repo_open work with new pack_fds in got_repo_open give output when no tags exist escape html in blame output change files listed in tree view to show blob, file commits, and blame, instead of blob, blob, blame. idea from mp4 on irc. this is way more handy. stop populating the queue from the headref and figure out previous commit id while iterating. this should reduce some overhead. actually purge our sockets instead of not using the function start work with new blob rm volatile use new diff change func names no more temp files increase blame number line width set content-type to text/plain so firefox won't download files rm test infra for now account for -Wwrite-strings fix for sigs and algorithm choice clean up some leaks and other mistakes commit - 84bc347c349f09dc692e8d760ba9af3d21b0f5af commit + a596b9579655b53f1549498aff25ae1d2c9dfc81 blob - 8a7d489ac1ca0b7fd0cf744dfdea4257efcda1c9 blob + 8b6c6977676250da107793984432e332c86e1251 --- Makefile +++ Makefile @@ -7,7 +7,7 @@ SUBDIR += regress .endif .if make(clean) || make(obj) || make(release) -SUBDIR += gotweb +SUBDIR += gotweb gotwebd .endif .if make(tags) || make(cleandir) @@ -39,4 +39,10 @@ web: web-install: ${MAKE} -C gotweb install +webd: + ${MAKE} -C gotwebd + +webd-install: + ${MAKE} -C gotwebd install + .include blob - /dev/null blob + 4c0096efb52bb79e4a8f7e5f09c1b1d953009636 (mode 644) --- /dev/null +++ gotwebd/Makefile @@ -0,0 +1,55 @@ +.PATH:${.CURDIR}/../lib + +SUBDIR = libexec + +.include "../got-version.mk" +.include "Makefile.inc" + +PROG = gotwebd +SRCS = config.c sockets.c log.c gotwebd.c parse.y proc.c \ + fcgi.c gotweb.c got_operations.c +SRCS += blame.c commit_graph.c delta.c diff.c \ + diffreg.c error.c fileindex.c object.c object_cache.c \ + object_idset.c object_parse.c opentemp.c path.c pack.c \ + privsep.c reference.c repository.c sha1.c worktree.c \ + utf8.c inflate.c buf.c rcsutil.c diff3.c \ + lockfile.c deflate.c object_create.c delta_cache.c \ + gotconfig.c diff_main.c diff_atomize_text.c diff_myers.c \ + diff_output.c diff_output_plain.c diff_output_unidiff.c \ + diff_output_edscript.c diff_patience.c bloom.c murmurhash2.c \ + worktree_open.c patch.c sigs.c date.c + +MAN = ${PROG}.conf.5 ${PROG}.8 + +CPPFLAGS += -I${.CURDIR}/../include -I${.CURDIR}/../lib -I${.CURDIR} +LDADD += -lz -levent -lutil -lm +YFLAGS = +DPADD = ${LIBEVENT} ${LIBUTIL} +#CFLAGS += -DGOT_NO_OBJ_CACHE + +.if ${GOT_RELEASE} != "Yes" +NOMAN = Yes +.endif + +.if defined(PROFILE) +CPPFLAGS += -DPROFILE +DEBUG = -O0 -pg -g -static +.else +DEBUG = -O0 -g +.endif + +realinstall: + if [ ! -d ${DESTDIR}${PUB_REPOS_DIR}/. ]; then \ + ${INSTALL} -d -o root -g daemon -m 755 ${DESTDIR}${PUB_REPOS_DIR}; \ + fi + ${INSTALL} -c -o root -g daemon -m 0755 ${PROG} ${BINDIR}/${PROG} + if [ ! -d ${DESTDIR}${HTTPD_DIR}/. ]; then \ + ${INSTALL} -d -o root -g daemon -m 755 ${DESTDIR}${HTTPD_DIR}; \ + fi + if [ ! -d ${DESTDIR}${PROG_DIR}/. ]; then \ + ${INSTALL} -d -o root -g daemon -m 755 ${DESTDIR}${PROG_DIR}; \ + fi + ${INSTALL} -c -o ${WWWUSR} -g ${WWWGRP} -m 0755 \ + ${.CURDIR}/files/htdocs/${PROG}/* ${DESTDIR}${PROG_DIR} + +.include blob - /dev/null blob + 8e5361495585907a0f12661cad8b8b9fbfb0b503 (mode 644) --- /dev/null +++ gotwebd/Makefile.inc @@ -0,0 +1,11 @@ +LDADD += -lz -lutil +PREFIX ?= /usr/local +BINDIR ?= ${PREFIX}/sbin +CHROOT_DIR ?= /var/www +GOTWEB_DIR = /bin/gotwebd +LIBEXECDIR = ${GOTWEB_DIR}/libexec +LIBEXEC_DIR = ${CHROOT_DIR}${LIBEXECDIR} +HTTPD_DIR = ${CHROOT_DIR}/htdocs +PROG_DIR = ${HTTPD_DIR}/${PROG} +WWWUSR ?= www +WWWGRP ?= www blob - /dev/null blob + 4a8874a87e47d8824f253f466514c16f8ce8c881 (mode 644) --- /dev/null +++ gotwebd/config.c @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2020-2021 Tracey Emery + * Copyright (c) 2015 Reyk Floeter + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "got_opentemp.h" + +#include "proc.h" +#include "gotwebd.h" + +int +config_init(struct gotwebd *env) +{ + struct privsep *ps = env->gotwebd_ps; + unsigned int what; + + /* Global configuration. */ + if (privsep_process == PROC_GOTWEBD) + env->prefork_gotwebd = GOTWEBD_NUMPROC; + + ps->ps_what[PROC_GOTWEBD] = CONFIG_ALL; + ps->ps_what[PROC_SOCKS] = CONFIG_SOCKS; + + /* Other configuration. */ + what = ps->ps_what[privsep_process]; + if (what & CONFIG_SOCKS) { + env->server_cnt = 0; + env->servers = calloc(1, sizeof(*env->servers)); + if (env->servers == NULL) + fatalx("%s: calloc", __func__); + env->sockets = calloc(1, sizeof(*env->sockets)); + if (env->sockets == NULL) + fatalx("%s: calloc", __func__); + TAILQ_INIT(env->servers); + TAILQ_INIT(env->sockets); + } + return 0; +} + +int +config_getcfg(struct gotwebd *env, struct imsg *imsg) +{ + /* nothing to do but tell gotwebd configuration is done */ + if (privsep_process != PROC_GOTWEBD) + proc_compose(env->gotwebd_ps, PROC_GOTWEBD, + IMSG_CFG_DONE, NULL, 0); + + return 0; +} + +int +config_setserver(struct gotwebd *env, struct server *srv) +{ + struct server ssrv; + struct privsep *ps = env->gotwebd_ps; + + memcpy(&ssrv, srv, sizeof(ssrv)); + proc_compose(ps, PROC_SOCKS, IMSG_CFG_SRV, &ssrv, sizeof(ssrv)); + return 0; +} + +int +config_getserver(struct gotwebd *env, struct imsg *imsg) +{ + struct server *srv; + uint8_t *p = imsg->data; + + IMSG_SIZE_CHECK(imsg, &srv); + + srv = calloc(1, sizeof(*srv)); + if (srv == NULL) + fatalx("%s: calloc", __func__); + memcpy(srv, p, sizeof(*srv)); + + if (IMSG_DATA_SIZE(imsg) != sizeof(*srv)) { + log_debug("%s: imsg size error", __func__); + free(srv); + return 1; + } + + /* log server info */ + log_debug("%s: server=%s fcgi_socket=%s unix_socket=%s", __func__, + srv->name, srv->fcgi_socket ? "yes" : "no", srv->unix_socket ? + "yes" : "no"); + + TAILQ_INSERT_TAIL(env->servers, srv, entry); + + return 0; +} + +int +config_setsock(struct gotwebd *env, struct socket *sock) +{ + struct privsep *ps = env->gotwebd_ps; + struct socket_conf s; + int id; + int fd = -1, n, m; + struct iovec iov[6]; + size_t c; + unsigned int what; + + /* open listening sockets */ + if (sockets_privinit(env, sock) == -1) + return -1; + + for (id = 0; id < PROC_MAX; id++) { + what = ps->ps_what[id]; + + if ((what & CONFIG_SOCKS) == 0 || id == privsep_process) + continue; + + memcpy(&s, &sock->conf, sizeof(s)); + + c = 0; + iov[c].iov_base = &s; + iov[c++].iov_len = sizeof(s); + + if (id == PROC_SOCKS) { + /* XXX imsg code will close the fd after 1st call */ + n = -1; + proc_range(ps, id, &n, &m); + for (n = 0; n < m; n++) { + if (sock->fd == -1) + fd = -1; + else if ((fd = dup(sock->fd)) == -1) + return 1; + if (proc_composev_imsg(ps, id, n, IMSG_CFG_SOCK, + -1, fd, iov, c) != 0) { + log_warn("%s: failed to compose " + "IMSG_CFG_SOCK imsg", + __func__); + return 1; + } + if (proc_flush_imsg(ps, id, n) == -1) { + log_warn("%s: failed to flush " + "IMSG_CFG_SOCK imsg", + __func__); + return 1; + } + } + } + } + + /* Close socket early to prevent fd exhaustion in gotwebd. */ + if (sock->fd != -1) { + close(sock->fd); + sock->fd = -1; + } + + return 0; +} + +int +config_getsock(struct gotwebd *env, struct imsg *imsg) +{ + struct socket *sock = NULL; + struct socket_conf sock_conf; + uint8_t *p = imsg->data; + int i; + + IMSG_SIZE_CHECK(imsg, &sock_conf); + memcpy(&sock_conf, p, sizeof(sock_conf)); + + if (IMSG_DATA_SIZE(imsg) != sizeof(sock_conf)) { + log_debug("%s: imsg size error", __func__); + return 1; + } + + /* create a new socket */ + if ((sock = calloc(1, sizeof(*sock))) == NULL) { + if (imsg->fd != -1) + close(imsg->fd); + return 1; + } + + memcpy(&sock->conf, &sock_conf, sizeof(sock->conf)); + sock->fd = imsg->fd; + + TAILQ_INSERT_TAIL(env->sockets, sock, entry); + + for (i = 0; i < PRIV_FDS__MAX; i++) + sock->priv_fd[i] = -1; + + for (i = 0; i < GOT_PACK_NUM_TEMPFILES; i++) + sock->pack_fds[i] = -1; + + /* log new socket info */ + log_debug("%s: name=%s id=%d server=%s child_id=%d parent_id=%d " + "type=%s ipv4=%d ipv6=%d socket_path=%s", + __func__, sock->conf.name, sock->conf.id, sock->conf.srv_name, + sock->conf.child_id, sock->conf.parent_id, sock->conf.type ? + "fcgi" : "unix", sock->conf.ipv4, sock->conf.ipv6, + strlen(sock->conf.unix_socket_name) ? + sock->conf.unix_socket_name : "none"); + + return 0; +} + +int +config_setfd(struct gotwebd *env, struct socket *sock) +{ + struct privsep *ps = env->gotwebd_ps; + int id, s; + int fd = -1, n, m, j; + struct iovec iov[6]; + size_t c; + unsigned int what; + + log_debug("%s: Allocating %d file descriptors", + __func__, PRIV_FDS__MAX + GOT_PACK_NUM_TEMPFILES); + + for (j = 0; j < PRIV_FDS__MAX + GOT_PACK_NUM_TEMPFILES; j++) { + for (id = 0; id < PROC_MAX; id++) { + what = ps->ps_what[id]; + + if ((what & CONFIG_SOCKS) == 0 || id == privsep_process) + continue; + + s = sock->conf.id; + c = 0; + iov[c].iov_base = &s; + iov[c++].iov_len = sizeof(s); + + if (id == PROC_SOCKS) { + /* + * XXX imsg code will close the fd + * after 1st call + */ + n = -1; + proc_range(ps, id, &n, &m); + for (n = 0; n < m; n++) { + fd = got_opentempfd(); + if (fd == -1) + return 1; + if (proc_composev_imsg(ps, id, n, + IMSG_CFG_FD, -1, fd, iov, c) != 0) { + log_warn("%s: failed to compose " + "IMSG_CFG_FD imsg", + __func__); + return 1; + } + if (proc_flush_imsg(ps, id, n) == -1) { + log_warn("%s: failed to flush " + "IMSG_CFG_FD imsg", + __func__); + return 1; + } + } + } + } + + /* Close fd early to prevent fd exhaustion in gotwebd. */ + if (fd != -1) + close(fd); + } + return 0; +} + +int +config_getfd(struct gotwebd *env, struct imsg *imsg) +{ + struct socket *sock; + uint8_t *p = imsg->data; + int sock_id, match = 0, i; + + IMSG_SIZE_CHECK(imsg, &sock_id); + memcpy(&sock_id, p, sizeof(sock_id)); + + TAILQ_FOREACH(sock, env->sockets, entry) { + for (i = 0; i < (GOT_PACK_NUM_TEMPFILES + PRIV_FDS__MAX); i++) { + if (i < PRIV_FDS__MAX && sock->priv_fd[i] == -1) { + log_debug("%s: assigning socket %d priv_fd %d", + __func__, sock_id, imsg->fd); + sock->priv_fd[i] = imsg->fd; + match = 1; + break; + } + if (sock->pack_fds[i - PRIV_FDS__MAX] == -1) { + log_debug("%s: assigning socket %d pack_fd %d", + __func__, sock_id, imsg->fd); + sock->pack_fds[i - PRIV_FDS__MAX] = imsg->fd; + match = 1; + break; + } + } + } + + if (match) + return 0; + else + return 1; +} blob - /dev/null blob + 1581cf938674fc59b27476119bfcc54b5b4417b5 (mode 644) --- /dev/null +++ gotwebd/fcgi.c @@ -0,0 +1,511 @@ +/* + * Copyright (c) 2020-2022 Tracey Emery + * Copyright (c) 2013 David Gwynne + * Copyright (c) 2013 Florian Obser + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "got_error.h" + +#include "proc.h" +#include "gotwebd.h" + +size_t fcgi_parse_record(uint8_t *, size_t, struct request *); +void fcgi_parse_begin_request(uint8_t *, uint16_t, struct request *, + uint16_t); +void fcgi_parse_params(uint8_t *, uint16_t, struct request *, uint16_t); +void fcgi_send_response(struct request *, struct fcgi_response *); + +void dump_fcgi_record_header(const char *, struct fcgi_record_header *); +void dump_fcgi_begin_request_body(const char *, + struct fcgi_begin_request_body *); +void dump_fcgi_end_request_body(const char *, + struct fcgi_end_request_body *); + +extern int cgi_inflight; +extern volatile int client_cnt; + +void +fcgi_request(int fd, short events, void *arg) +{ + struct request *c = arg; + ssize_t n; + size_t parsed = 0; + + n = read(fd, c->buf + c->buf_pos + c->buf_len, + FCGI_RECORD_SIZE - c->buf_pos-c->buf_len); + + switch (n) { + case -1: + switch (errno) { + case EINTR: + case EAGAIN: + return; + default: + goto fail; + } + break; + + case 0: + log_debug("closed connection"); + goto fail; + default: + break; + } + + c->buf_len += n; + + /* + * Parse the records as they are received. Per the FastCGI + * specification, the server need only receive the FastCGI + * parameter records in full; it is free to begin execution + * at that point, which is what happens here. + */ + do { + parsed = fcgi_parse_record(c->buf + c->buf_pos, c->buf_len, c); + if (parsed != 0) { + c->buf_pos += parsed; + c->buf_len -= parsed; + } + } while (parsed > 0 && c->buf_len > 0); + + /* Make space for further reads */ + if (parsed != 0) + if (c->buf_len > 0) { + bcopy(c->buf + c->buf_pos, c->buf, c->buf_len); + c->buf_pos = 0; + } + return; +fail: + fcgi_cleanup_request(c); +} + +size_t +fcgi_parse_record(uint8_t *buf, size_t n, struct request *c) +{ + struct fcgi_record_header *h; + + if (n < sizeof(struct fcgi_record_header)) + return 0; + + h = (struct fcgi_record_header*) buf; + + dump_fcgi_record("", h); + + if (n < sizeof(struct fcgi_record_header) + ntohs(h->content_len) + + h->padding_len) + return 0; + + if (h->version != 1) + log_warn("wrong version"); + + switch (h->type) { + case FCGI_BEGIN_REQUEST: + fcgi_parse_begin_request(buf + + sizeof(struct fcgi_record_header), + ntohs(h->content_len), c, ntohs(h->id)); + break; + case FCGI_PARAMS: + fcgi_parse_params(buf + sizeof(struct fcgi_record_header), + ntohs(h->content_len), c, ntohs(h->id)); + break; + case FCGI_STDIN: + case FCGI_ABORT_REQUEST: + fcgi_create_end_record(c); + fcgi_cleanup_request(c); + return 0; + default: + log_warn("unimplemented type %d", h->type); + break; + } + + return (sizeof(struct fcgi_record_header) + ntohs(h->content_len) + + h->padding_len); +} + +void +fcgi_parse_begin_request(uint8_t *buf, uint16_t n, + struct request *c, uint16_t id) +{ + /* XXX -- FCGI_CANT_MPX_CONN */ + if (c->request_started) { + log_warn("unexpected FCGI_BEGIN_REQUEST, ignoring"); + return; + } + + if (n != sizeof(struct fcgi_begin_request_body)) { + log_warn("wrong size %d != %lu", n, + sizeof(struct fcgi_begin_request_body)); + return; + } + + c->request_started = 1; + + c->id = id; + SLIST_INIT(&c->env); + c->env_count = 0; +} + +void +fcgi_parse_params(uint8_t *buf, uint16_t n, struct request *c, uint16_t id) +{ + struct env_val *env_entry; + uint32_t name_len, val_len; + uint8_t *sd, *dr_buf; + + if (!c->request_started) { + log_warn("FCGI_PARAMS without FCGI_BEGIN_REQUEST, ignoring"); + return; + } + + if (c->id != id) { + log_warn("unexpected id, ignoring"); + return; + } + + if (n == 0) { + gotweb_process_request(c); + return; + } + + while (n > 0) { + if (buf[0] >> 7 == 0) { + name_len = buf[0]; + n--; + buf++; + } else { + if (n > 3) { + name_len = ((buf[0] & 0x7f) << 24) + + (buf[1] << 16) + (buf[2] << 8) + buf[3]; + n -= 4; + buf += 4; + } else + return; + } + + if (n > 0) { + if (buf[0] >> 7 == 0) { + val_len = buf[0]; + n--; + buf++; + } else { + if (n > 3) { + val_len = ((buf[0] & 0x7f) << 24) + + (buf[1] << 16) + (buf[2] << 8) + + buf[3]; + n -= 4; + buf += 4; + } else + return; + } + } else + return; + + if (n < name_len + val_len) + return; + + if ((env_entry = malloc(sizeof(struct env_val))) == NULL) { + log_warn("cannot malloc env_entry"); + return; + } + + if ((env_entry->val = calloc(sizeof(char), name_len + val_len + + 2)) == NULL) { + log_warn("cannot allocate env_entry->val"); + free(env_entry); + return; + } + + bcopy(buf, env_entry->val, name_len); + buf += name_len; + n -= name_len; + + env_entry->val[name_len] = '\0'; + if (val_len < MAX_QUERYSTRING && strcmp(env_entry->val, + "QUERY_STRING") == 0 && c->querystring[0] == '\0') { + bcopy(buf, c->querystring, val_len); + c->querystring[val_len] = '\0'; + } + if (val_len < GOTWEBD_MAXTEXT && strcmp(env_entry->val, + "HTTP_HOST") == 0 && c->http_host[0] == '\0') { + + /* + * lazily get subdomain + * will only get domain if no subdomain exists + * this can still work if gotweb server name is the same + */ + sd = strchr(buf, '.'); + if (sd) + *sd = '\0'; + + bcopy(buf, c->http_host, val_len); + c->http_host[val_len] = '\0'; + } + if (val_len < MAX_DOCUMENT_ROOT && strcmp(env_entry->val, + "DOCUMENT_ROOT") == 0 && c->document_root[0] == '\0') { + + /* drop first char, as it's always / */ + dr_buf = &buf[1]; + + bcopy(dr_buf, c->document_root, val_len - 1); + c->document_root[val_len] = '\0'; + } + if (val_len < MAX_SERVER_NAME && strcmp(env_entry->val, + "SERVER_NAME") == 0 && c->server_name[0] == '\0') { + /* drop first char, as it's always / */ + + bcopy(buf, c->server_name, val_len); + c->server_name[val_len] = '\0'; + } + env_entry->val[name_len] = '='; + + bcopy(buf, (env_entry->val) + name_len + 1, val_len); + buf += val_len; + n -= val_len; + + SLIST_INSERT_HEAD(&c->env, env_entry, entry); + log_debug("env[%d], %s", c->env_count, env_entry->val); + c->env_count++; + } +} + +void +fcgi_timeout(int fd, short events, void *arg) +{ + fcgi_cleanup_request((struct request*) arg); +} + +int +fcgi_gen_binary_response(struct request *c, const uint8_t *data, int len) +{ + struct fcgi_response *resp; + struct fcgi_record_header *header; + ssize_t n = 0; + int i; + + if (c->sock->client_status == CLIENT_DISCONNECT) + return -1; + + if (data == NULL) + return 0; + + if ((resp = calloc(1, sizeof(struct fcgi_response))) == NULL) { + log_warn("%s: cannot calloc fcgi_response", __func__); + return -1; + } + + header = (struct fcgi_record_header*) resp->data; + header->version = 1; + header->type = FCGI_STDOUT; + header->id = htons(c->id); + header->padding_len = 0; + header->reserved = 0; + + for (i = 0; i < len; i++) { + resp->data[i+8] = data[i]; + n++; + } + + header->content_len = htons(n); + resp->data_pos = 0; + resp->data_len = n + sizeof(struct fcgi_record_header); + fcgi_send_response(c, resp); + + return 0; +} + +int +fcgi_gen_response(struct request *c, const char *data) +{ + struct fcgi_response *resp; + struct fcgi_record_header *header; + ssize_t n = 0; + int i; + + if (c->sock->client_status == CLIENT_DISCONNECT) + return -1; + + if (data == NULL) + return 0; + + if (strlen(data) == 0) + return 0; + + if ((resp = calloc(1, sizeof(struct fcgi_response))) == NULL) { + log_warn("%s: cannot calloc fcgi_response", __func__); + return -1; + } + + header = (struct fcgi_record_header*) resp->data; + header->version = 1; + header->type = FCGI_STDOUT; + header->id = htons(c->id); + header->padding_len = 0; + header->reserved = 0; + + for (i = 0; i < strlen(data); i++) { + resp->data[i+8] = data[i]; + n++; + } + + header->content_len = htons(n); + resp->data_pos = 0; + resp->data_len = n + sizeof(struct fcgi_record_header); + fcgi_send_response(c, resp); + + return 0; +} + +void +fcgi_send_response(struct request *c, struct fcgi_response *resp) +{ + struct fcgi_record_header *header; + struct timespec ts; + size_t padded_len; + int err = 0, th = 2000; + + ts.tv_sec = 0; + ts.tv_nsec = 50; + + header = (struct fcgi_record_header*)resp->data; + + /* The FastCGI spec suggests to align the output buffer */ + padded_len = FCGI_ALIGN(resp->data_len); + if (padded_len > resp->data_len) { + /* There should always be FCGI_PADDING_SIZE bytes left */ + if (padded_len > FCGI_RECORD_SIZE) + log_warn("response too long"); + header->padding_len = padded_len - resp->data_len; + resp->data_len = padded_len; + } + + dump_fcgi_record("resp ", header); + + /* + * XXX: add some simple write heuristics here + * On slower VMs, spotty connections, etc., we don't want to go right to + * disconnect. Let's at least try to write the data a few times before + * giving up. + */ + while ((write(c->fd, resp->data + resp->data_pos, + resp->data_len)) == -1) { + nanosleep(&ts, NULL); + err++; + if (err == th) { + c->sock->client_status = CLIENT_DISCONNECT; + break; + } + } + + free(resp); +} + +void +fcgi_create_end_record(struct request *c) +{ + struct fcgi_response *resp; + struct fcgi_record_header *header; + struct fcgi_end_request_body *end_request; + + if ((resp = calloc(1, sizeof(struct fcgi_response))) == NULL) { + log_warn("cannot calloc fcgi_response"); + return; + } + header = (struct fcgi_record_header*) resp->data; + header->version = 1; + header->type = FCGI_END_REQUEST; + header->id = htons(c->id); + header->content_len = htons(sizeof(struct + fcgi_end_request_body)); + header->padding_len = 0; + header->reserved = 0; + end_request = (struct fcgi_end_request_body *) (resp->data + + sizeof(struct fcgi_record_header)); + end_request->app_status = htonl(0); /* script_status */ + end_request->protocol_status = FCGI_REQUEST_COMPLETE; + end_request->reserved[0] = 0; + end_request->reserved[1] = 0; + end_request->reserved[2] = 0; + resp->data_pos = 0; + resp->data_len = sizeof(struct fcgi_end_request_body) + + sizeof(struct fcgi_record_header); + fcgi_send_response(c, resp); +} + +void +fcgi_cleanup_request(struct request *c) +{ + cgi_inflight--; + client_cnt--; + + evtimer_del(&c->tmo); + if (event_initialized(&c->ev)) + event_del(&c->ev); + + close(c->fd); + gotweb_free_transport(c->t); + free(c); +} + +void +dump_fcgi_record(const char *p, struct fcgi_record_header *h) +{ + dump_fcgi_record_header(p, h); + + if (h->type == FCGI_BEGIN_REQUEST) + dump_fcgi_begin_request_body(p, + (struct fcgi_begin_request_body *)(h + 1)); + else if (h->type == FCGI_END_REQUEST) + dump_fcgi_end_request_body(p, + (struct fcgi_end_request_body *)(h + 1)); +} + +void +dump_fcgi_record_header(const char* p, struct fcgi_record_header *h) +{ + log_debug("%sversion: %d", p, h->version); + log_debug("%stype: %d", p, h->type); + log_debug("%srequestId: %d", p, ntohs(h->id)); + log_debug("%scontentLength: %d", p, ntohs(h->content_len)); + log_debug("%spaddingLength: %d", p, h->padding_len); + log_debug("%sreserved: %d", p, h->reserved); +} + +void +dump_fcgi_begin_request_body(const char *p, struct fcgi_begin_request_body *b) +{ + log_debug("%srole %d", p, ntohs(b->role)); + log_debug("%sflags %d", p, b->flags); +} + +void +dump_fcgi_end_request_body(const char *p, struct fcgi_end_request_body *b) +{ + log_debug("%sappStatus: %d", p, ntohl(b->app_status)); + log_debug("%sprotocolStatus: %d", p, b->protocol_status); +} blob - /dev/null blob + f841f054bc2941b0cdca7e496ea69621671d6766 (mode 755) Binary files /dev/null and gotwebd/files/htdocs/gotwebd/android-chrome-192x192.png differ blob - /dev/null blob + 653a1510ce933f7fe9fbab2fcd171f04fa0b24cc (mode 755) Binary files /dev/null and gotwebd/files/htdocs/gotwebd/android-chrome-384x384.png differ blob - /dev/null blob + 460aa1299f8e9f37773618bcab2619794416fb49 (mode 755) Binary files /dev/null and gotwebd/files/htdocs/gotwebd/apple-touch-icon.png differ blob - /dev/null blob + b3930d0f047184047cb81d620436d91653438b8b (mode 755) --- /dev/null +++ gotwebd/files/htdocs/gotwebd/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + blob - /dev/null blob + f6c1a7c289faa4a48e03c97e68b1ba7a11dfddd1 (mode 755) Binary files /dev/null and gotwebd/files/htdocs/gotwebd/favicon-16x16.png differ blob - /dev/null blob + 0ceea8c0eabe73e8d12cf106d73c34abb1999cb2 (mode 755) Binary files /dev/null and gotwebd/files/htdocs/gotwebd/favicon-32x32.png differ blob - /dev/null blob + ee414573031ea5b310539196d2530a1e52d49b64 (mode 755) Binary files /dev/null and gotwebd/files/htdocs/gotwebd/favicon.ico differ blob - /dev/null blob + 33933f80ee46217039804bc96672ede12b352b93 (mode 755) Binary files /dev/null and gotwebd/files/htdocs/gotwebd/got.png differ blob - /dev/null blob + 97ace786464b193baf1cd51e54016aea3016e62f (mode 755) Binary files /dev/null and gotwebd/files/htdocs/gotwebd/got_large.png differ blob - /dev/null blob + 4b9d5d8b3a18a88189f1c92d789387f654bd25ac (mode 755) --- /dev/null +++ gotwebd/files/htdocs/gotwebd/gotweb.css @@ -0,0 +1,792 @@ +/* + * Copyright (c) 2019 Jerome Kasper + * Copyright (c) 2019, 2020 Tracey Emery + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* general sections */ + +a { + color: #444444; + text-decoration: none; +} +a:hover { + color: Gold; + text-decoration: none; +} +body { + background-color: #ffffff; + color: #000000; + margin: 0; + padding: 0; + font-family: Arial, sans-serif; +} + +.diff_minus, .diff_submodule { + color: magenta; +} +.diff_plus, .diff_symlink, .diff_author { + color: darkcyan; +} +.diff_chunk_header, .diff_date { + background-color: LightSlateGray; + color: yellow; +} +.diff_meta, .diff_executable, .diff_commit { + color: green; +} +.diff_directory { + color: blue; +} + +.back_white { + background-color: #ffffff; +} +.back_lightgray { + background-color: #d8f3ef; +} + +#logo { + height: 50px; +} +#refs_str { + background-color: #243647; + color: #ffffff; + font-style: italic; +} +#dotted_line { + clear: left; + float: left; + width: 100%; + border-top: 1px dotted #444444; +} +#header { + overflow: auto; + width: 100%; + background-image: linear-gradient(to right, White, LightSlateGray); +} +#header a { + color: #ffffff; + font-size: 1.2em; + text-decoration: none; +} +#header a:hover { + color: Gold; + font-size: 1.2em; + text-decoration: none; +} +#site_path { + clear: left; + float: left; + overflow: auto; + width: 100%; + background-color: #243647; +} +#site_link { + float: left; + width: 40%; + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; + color: #ffffff; + overflow: hidden; +} +#site_link a { + color: #ffffff; + text-decoration: none; +} +#search { + float: right; + padding-right: 10px; + padding-top: 5px; + padding-bottom: 5px; +} +#got_link { + float: left; + padding-bottom: 10px; + padding-top: 10px; +} +#content { + width: 100%; +} +#np_wrapper { + clear: left; + float: left; + width: 100%; + border-bottom: 1px dotted #444444; + background-color: #f5fcfb; + overflow: hidden; +} +#nav_prev { + float: left; + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; + overflow: visible; +} +#nav_next { + padding-right: 10px; + padding-top: 5px; + padding-bottom: 5px; + text-align: right; + overflow: hidden; +} +#navs_wrapper { + clear: left; + float: left; + width: 100%; + background-color: #ced7e0; +} +#navs { + padding-left: 10px; + padding-top: 2px; + padding-bottom: 2px; + font-size: .8em; +} +#site_owner_wrapper { + clear: left; + float: left; + width: 100%; + background-color: LightSlateGray; + color: #ffffff; +} +#site_owner { + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; +} +#description_title { + clear: left; + float: left; + width: 6.5em; + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; +} +#description { + float: left; + width: 72%; + padding-top: 5px; + padding-bottom: 5px; +} +#repo_owner_title { + clear: left; + float: left; + width: 6.5em; + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; +} +#repo_owner { + float: left; + width: 72%; + padding-top: 5px; + padding-bottom: 5px; +} +#last_change_title { + clear: left; + float: left; + width: 6.5em; + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; +} +#last_change { + float: left; + width: 72%; + padding-top: 5px; + padding-bottom: 5px; +} +#cloneurl_title { + clear: left; + float: left; + width: 6.5em; + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; +} +#cloneurl { + float: left; + width: 72%; + padding-top: 5px; + padding-bottom: 5px; + overflow: auto; + white-space: pre-wrap; +} + +#header_commit_title { + clear: left; + float: left; + width: 6.5em; + padding-left: 10px; + padding-top: 2px; + padding-bottom: 2px; +} +#header_commit { + float: left; + width: 72%; + padding-top: 2px; + padding-bottom: 2px; +} +#header_diff_title { + clear: left; + float: left; + width: 6.5em; + padding-left: 10px; + padding-top: 2px; + padding-bottom: 2px; +} +#header_diff { + float: left; + width: 72%; + padding-top: 2px; + padding-bottom: 2px; +} +#header_author_title { + clear: left; + float: left; + width: 6.5em; + padding-left: 10px; + padding-top: 2px; + padding-bottom: 2px; +} +#header_author { + float: left; + width: 72%; + padding-top: 2px; + padding-bottom: 2px; +} +#header_committer_title { + clear: left; + float: left; + width: 6.5em; + padding-left: 10px; + padding-top: 2px; + padding-bottom: 2px; +} +#header_committer { + float: left; + width: 72%; + padding-top: 2px; + padding-bottom: 2px; +} +#header_age_title { + clear: left; + float: left; + width: 6.5em; + padding-left: 10px; + padding-top: 2px; + padding-bottom: 2px; +} +#header_age { + float: left; + width: 72%; + padding-top: 2px; + padding-bottom: 2px; +} +#header_commit_msg_title { + clear: left; + float: left; + width: 6.5em; + padding-left: 10px; + padding-top: 2px; + padding-bottom: 2px; +} +#header_commit_msg { + float: left; + width: 72%; + padding-top: 2px; + padding-bottom: 2px; + white-space: pre-wrap; +} +#header_tree_title { + clear: left; + float: left; + width: 6.5em; + padding-left: 10px; + padding-top: 2px; + padding-bottom: 2px; +} +#header_tree { + float: left; + width: 72%; + padding-top: 2px; + padding-bottom: 2px; +} + +#err_content { + clear: left; + float: left; + width: 100%; + padding-left: 20px; + padding-top: 20px; + padding-bottom: 20px; +} + +#briefs_title_wrapper { + clear: left; + float: left; + width: 100%; + background-color: LightSlateGray; + color: #ffffff; +} +#briefs_title { + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; +} +#briefs_content { + clear: left; + float: left; + width: 100%; +} +#briefs_age { + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; + float: left; + width: 7.5em; + overflow: auto; +} +#briefs_author { + float: left; + padding-top: 5px; + padding-bottom: 5px; + width: 8.5em; + font-style: italic; + overflow: auto; +} +#briefs_log { + float: left; + padding-left: 10px; + padding-right: 10px; + padding-top: 5px; + padding-bottom: 5px; + width: 65%; +} + +#tags_title_wrapper { + clear: left; + float: left; + width: 100%; + background-color: LightSlateGray; + color: #ffffff; +} +#tags_title { + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; +} +#tags_content { + clear: left; + float: left; + width: 100%; +} +#tag_age { + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; + float: left; + width: 7.5em; + overflow: auto; +} +#tags_log { + float: left; + padding-left: 10px; + padding-right: 10px; + padding-top: 5px; + padding-bottom: 5px; + width: 65%; +} + +#tag_header_wrapper { + clear: left; + float: left; + background-color: #f5fcfb; + width: 100%; +} +#tag_header { + float: left; + padding-left: 10px; + padding-top: 5px; + padding-bottom: 2px; + width: 80%; +} +#tag { + float: left; + width: 8.5em; + font-style: italic; + padding-top: 5px; + padding-bottom: 5px; +} +#tag_commit { + clear: left; + float: left; + padding-left: 20px; + padding-bottom: 20px; + white-space: pre-wrap; +} + +#index_header { + clear: left; + float: left; + overflow: auto; + width: 100%; + background-color: Khaki; +} +#index_header_project { + clear: left; + float: left; + width: 20%; + padding: 10px; +} +#index_header_description { + float: left; + width: 30%; + padding: 10px; +} +#index_header_owner { + float: left; + width: 12%; + padding: 10px; +} +#index_header_age { + padding: 10px; + overflow: hidden; +} +#index_wrapper { + clear: left; + float: left; + width: 100%; +} +#index_project { + float: left; + width: 20%; + padding: 10px; + overflow: hidden; +} +#index_project_description { + float: left; + width: 30%; + padding: 10px; + overflow: auto; +} +#index_project_owner { + float: left; + width: 12%; + padding: 10px; + overflow: hidden; +} +#index_project_age { + float: left; + width: 14%; + padding: 10px; + overflow: visible; +} +#index_project a { + color: #444444; + text-decoration: none; +} +#index_project a:hover { + color: SteelBlue; + text-decoration: none; +} +#index_project_navs a { + color: #444444; + text-decoration: none; +} +#index_project_navs a:hover { + color: SteelBlue; + text-decoration: none; +} +#index_next a { + color: #444444; + text-decoration: none; +} +#index_next a:hover { + color: SteelBlue; + text-decoration: none; +} +#index_prev a { + color: #444444; + text-decoration: none; +} +#index_prev a:hover { + color: SteelBlue; + text-decoration: none; +} + +#commits_title_wrapper { + clear: left; + float: left; + width: 100%; + background-color: LightSlateGray; + color: #ffffff; +} +#commits_title { + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; +} +#commits_content { + clear: left; + float: left; + width: 100%; +} +#commits_header_wrapper { + float: left; + background-color: #f5fcfb; + width: 100%; +} +#commits_header { + float: left; + padding-top: 5px; + padding-bottom: 2px; + width: 80%; +} +#commit { + clear: left; + float: left; + padding-left: 20px; + padding-bottom: 20px; + white-space: pre-wrap; +} +#commits_line { + clear: left; + float: left; +} + +#blame_title_wrapper { + clear: left; + float: left; + width: 100%; + background-color: LightSlateGray; + color: #ffffff; +} +#blame_title { + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; +} +#blame_content { + clear: left; + float: left; + width: 100%; +} +#blame_header_wrapper { + float: left; + background-color: #f5fcfb; + width: 100%; +} +#blame_header { + float: left; + padding-left: 10px; + padding-top: 5px; + padding-bottom: 2px; + width: 80%; +} +#blame { + clear: left; + float: left; + margin-left: 20px; + margin-bottom: 20px; + font-family: monospace; + white-space: pre; + overflow: auto; +} +#blame_wrapper { + clear: left; + float: left; + width: 100%; +} +#blame_number { + float: left; + width: 6em; + overflow: hidden; +} +#blame_hash { + float: left; + width: 6em; + overflow: auto; +} +#blame_date { + float: left; + width: 7em; + overflow: auto; +} +#blame_author { + float: left; + width: 6em; + overflow: hidden; +} +#blame_code { + float:left; + width: 50%; + overflow: visible; +} + +#tree_title_wrapper { + clear: left; + float: left; + width: 100%; + background-color: LightSlateGray; + color: #ffffff; +} +#tree_title { + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; +} +#tree_content { + clear: left; + float: left; + width: 100%; +} +#tree_header_wrapper { + clear: left; + float: left; + background-color: #f5fcfb; + width: 100%; +} +#tree_header { + float: left; + padding-left: 10px; + padding-top: 5px; + padding-bottom: 2px; + width: 80%; +} +#tree { + clear: left; + float: left; + margin-left: 20px; + margin-top: 20px; + margin-bottom: 20px; + font-family: monospace; +} +#tree_wrapper { + clear: left; + float: left; + width: 100%; +} +#tree_line { + clear: left; + float: left; + width: 20em; + padding: 1px; +} +#tree_line_blank { + float: left; + padding: 1px; + width: 9.5em; +} +#tree_line_navs { + float: left; + text-align: right; + padding: 1px; +} + +#diff_title_wrapper { + clear: left; + float: left; + width: 100%; + background-color: LightSlateGray; + color: #ffffff; +} +#diff_title { + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; +} +#diff_content { + clear: left; + float: left; + width: 100%; +} +#diff_header_wrapper { + float: left; + background-color: #f5fcfb; + width: 100%; +} +#diff_header { + float: left; + padding-left: 10px; + padding-top: 5px; + padding-bottom: 2px; + width: 80%; +} +#diff { + clear: left; + float: left; + margin-left: 20px; + margin-bottom: 20px; + font-family: monospace; + white-space: pre; +} +#diff_line { + clear: left; + float: left; +} + +#summary_wrapper { + clear: left; + float: left; + width: 100%; + background-color: Khaki; +} + +#branches_title_wrapper { + clear: left; + float: left; + width: 100%; + background-color: LightSlateGray; + color: #ffffff; +} +#branches_title { + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; +} +#branches_content { + clear: left; + float: left; + width: 100%; +} + +#branches_wrapper { + clear: left; + float: left; + width: 100%; +} +#branches_age { + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; + float: left; + width: 7.5em; + overflow: auto; +} +#branches_space { + padding-left: 10px; + padding-top: 5px; + padding-bottom: 5px; + float: left; + width: 8.5em; + overflow: auto; +} +#branch { + float: left; + padding-right: 10px; + padding-top: 5px; + padding-bottom: 5px; +} blob - /dev/null blob + 0c47027971e9e0a5060e23fe73e7cb0399eacea8 (mode 755) Binary files /dev/null and gotwebd/files/htdocs/gotwebd/mstile-150x150.png differ blob - /dev/null blob + 96e67c7c4b7cb9b1b395281fae8d7cffa834a991 (mode 755) --- /dev/null +++ gotwebd/files/htdocs/gotwebd/safari-pinned-tab.svg @@ -0,0 +1,15 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + blob - /dev/null blob + a1553eb86b573da072c732c9aabac5a80968461f (mode 755) --- /dev/null +++ gotwebd/files/htdocs/gotwebd/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-384x384.png", + "sizes": "384x384", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} blob - /dev/null blob + 6437bf550c8e246193623b0bed1c1356161f0b2e (mode 644) --- /dev/null +++ gotwebd/got_operations.c @@ -0,0 +1,1927 @@ +/* + * Copyright (c) 2020-2022 Tracey Emery + * Copyright (c) 2018, 2019 Stefan Sperling + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "got_error.h" +#include "got_object.h" +#include "got_reference.h" +#include "got_repository.h" +#include "got_path.h" +#include "got_cancel.h" +#include "got_diff.h" +#include "got_commit_graph.h" +#include "got_blame.h" +#include "got_privsep.h" + +#include "proc.h" +#include "gotwebd.h" + +static const struct got_error *got_init_repo_commit(struct repo_commit **); +static const struct got_error *got_init_repo_tag(struct repo_tag **); +static const struct got_error *got_get_repo_commit(struct request *, + struct repo_commit *, struct got_commit_object *, struct got_reflist_head *, + struct got_object_id *); +static const struct got_error *got_gotweb_dupfd(int *, int *); +static const struct got_error *got_gotweb_openfile(FILE **, int *, int *); +static const struct got_error *got_gotweb_flushfile(FILE *, int); +static const struct got_error *got_gotweb_blame_cb(void *, int, int, + struct got_commit_object *,struct got_object_id *); + +static int +isbinary(const uint8_t *buf, size_t n) +{ + size_t i; + + for (i = 0; i < n; i++) + if (buf[i] == 0) + return 1; + return 0; +} + + +static const struct got_error * +got_gotweb_flushfile(FILE *f, int fd) +{ + if (fseek(f, 0, SEEK_SET) == -1) + return got_error_from_errno("fseek"); + + if (ftruncate(fd, 0) == -1) + return got_error_from_errno("ftruncate"); + + if (fsync(fd) == -1) + return got_error_from_errno("fsync"); + + if (f && fclose(f) == EOF) + return got_error_from_errno("fclose"); + + if (fd != -1 && close(fd) != -1) + return got_error_from_errno("close"); + + return NULL; +} + +static const struct got_error * +got_gotweb_openfile(FILE **f, int *priv_fd, int *fd) +{ + const struct got_error *error = NULL; + + *fd = dup(*priv_fd); + + if (*fd < 0) + return NULL; + + *f = fdopen(*fd, "w+"); + if (*f == NULL) { + close(*fd); + error = got_error(GOT_ERR_PRIVSEP_NO_FD); + } + + return error; +} + +static const struct got_error * +got_gotweb_dupfd(int *priv_fd, int *fd) +{ + const struct got_error *error = NULL; + + *fd = dup(*priv_fd); + + if (*fd < 0) + return NULL; + + return error; +} + +const struct got_error * +got_get_repo_owner(char **owner, struct request *c, char *dir) +{ + const struct got_error *error = NULL; + struct server *srv = c->srv; + struct transport *t = c->t; + struct got_repository *repo = t->repo; + const char *gitconfig_owner; + + *owner = NULL; + + if (srv->show_repo_owner == 0) + return NULL; + + gitconfig_owner = got_repo_get_gitconfig_owner(repo); + if (gitconfig_owner) { + *owner = strdup(gitconfig_owner); + if (*owner == NULL) + return got_error_from_errno("strdup"); + } + return error; +} + +const struct got_error * +got_get_repo_age(char **repo_age, struct request *c, char *dir, + const char *refname, int ref_tm) +{ + const struct got_error *error = NULL; + struct server *srv = c->srv; + struct transport *t = c->t; + struct got_repository *repo = t->repo; + struct got_commit_object *commit = NULL; + struct got_reflist_head refs; + struct got_reflist_entry *re; + time_t committer_time = 0, cmp_time = 0; + + *repo_age = NULL; + TAILQ_INIT(&refs); + + if (srv->show_repo_age == 0) + return NULL; + + error = got_ref_list(&refs, repo, "refs/heads", + got_ref_cmp_by_name, NULL); + if (error) + goto done; + + /* + * Find the youngest branch tip in the repository, or the age of + * the a specific branch tip if a name was provided by the caller. + */ + TAILQ_FOREACH(re, &refs, entry) { + struct got_object_id *id = NULL; + + if (refname && strcmp(got_ref_get_name(re->ref), refname) != 0) + continue; + + error = got_ref_resolve(&id, repo, re->ref); + if (error) + goto done; + + error = got_object_open_as_commit(&commit, repo, id); + free(id); + if (error) + goto done; + + committer_time = + got_object_commit_get_committer_time(commit); + got_object_commit_close(commit); + if (cmp_time < committer_time) + cmp_time = committer_time; + + if (refname) + break; + } + + if (cmp_time != 0) { + committer_time = cmp_time; + error = gotweb_get_time_str(repo_age, committer_time, ref_tm); + } +done: + got_ref_list_free(&refs); + return error; +} + +static const struct got_error * +got_get_repo_commit(struct request *c, struct repo_commit *repo_commit, + struct got_commit_object *commit, struct got_reflist_head *refs, + struct got_object_id *id) +{ + const struct got_error *error = NULL; + struct got_reflist_entry *re; + struct got_object_id *id2 = NULL; + struct got_object_qid *parent_id; + struct transport *t = c->t; + struct querystring *qs = c->t->qs; + char *commit_msg = NULL, *commit_msg0; + + TAILQ_FOREACH(re, refs, entry) { + char *s; + const char *name; + struct got_tag_object *tag = NULL; + struct got_object_id *ref_id; + int cmp; + + if (got_ref_is_symbolic(re->ref)) + continue; + + name = got_ref_get_name(re->ref); + if (strncmp(name, "refs/", 5) == 0) + name += 5; + if (strncmp(name, "got/", 4) == 0) + continue; + if (strncmp(name, "heads/", 6) == 0) + name += 6; + if (strncmp(name, "remotes/", 8) == 0) { + name += 8; + s = strstr(name, "/" GOT_REF_HEAD); + if (s != NULL && s[strlen(s)] == '\0') + continue; + } + error = got_ref_resolve(&ref_id, t->repo, re->ref); + if (error) + return error; + if (strncmp(name, "tags/", 5) == 0) { + error = got_object_open_as_tag(&tag, t->repo, ref_id); + if (error) { + if (error->code != GOT_ERR_OBJ_TYPE) { + free(ref_id); + continue; + } + /* + * Ref points at something other + * than a tag. + */ + error = NULL; + tag = NULL; + } + } + cmp = got_object_id_cmp(tag ? + got_object_tag_get_object_id(tag) : ref_id, id); + free(ref_id); + if (tag) + got_object_tag_close(tag); + if (cmp != 0) + continue; + s = repo_commit->refs_str; + if (asprintf(&repo_commit->refs_str, "%s%s%s", s ? s : "", + s ? ", " : "", name) == -1) { + error = got_error_from_errno("asprintf"); + free(s); + repo_commit->refs_str = NULL; + return error; + } + free(s); + } + + error = got_object_id_str(&repo_commit->commit_id, id); + if (error) + return error; + + error = got_object_id_str(&repo_commit->tree_id, + got_object_commit_get_tree_id(commit)); + if (error) + return error; + + if (qs->action == DIFF) { + parent_id = STAILQ_FIRST( + got_object_commit_get_parent_ids(commit)); + if (parent_id != NULL) { + id2 = got_object_id_dup(&parent_id->id); + error = got_object_id_str(&repo_commit->parent_id, id2); + if (error) + return error; + free(id2); + } else { + repo_commit->parent_id = strdup("/dev/null"); + if (repo_commit->parent_id == NULL) { + error = got_error_from_errno("strdup"); + return error; + } + } + } + + repo_commit->committer_time = + got_object_commit_get_committer_time(commit); + + repo_commit->author = + strdup(got_object_commit_get_author(commit)); + if (repo_commit->author == NULL) { + error = got_error_from_errno("strdup"); + return error; + } + repo_commit->committer = + strdup(got_object_commit_get_committer(commit)); + if (repo_commit->committer == NULL) { + error = got_error_from_errno("strdup"); + return error; + } + error = got_object_commit_get_logmsg(&commit_msg0, commit); + if (error) + return error; + + commit_msg = commit_msg0; + while (*commit_msg == '\n') + commit_msg++; + + repo_commit->commit_msg = strdup(commit_msg); + if (repo_commit->commit_msg == NULL) + error = got_error_from_errno("strdup"); + free(commit_msg0); + return error; +} + +const struct got_error * +got_get_repo_commits(struct request *c, int limit) +{ + const struct got_error *error = NULL; + struct got_object_id *id = NULL; + struct got_commit_graph *graph = NULL; + struct got_commit_object *commit = NULL; + struct got_reflist_head refs; + struct got_reference *ref; + struct repo_commit *repo_commit = NULL; + struct server *srv = c->srv; + struct transport *t = c->t; + struct got_repository *repo = t->repo; + struct querystring *qs = t->qs; + struct repo_dir *repo_dir = t->repo_dir; + char *in_repo_path = NULL, *repo_path = NULL, *file_path = NULL; + int chk_next = 0, chk_multi = 0, commit_found = 0; + int obj_type, limit_chk = 0; + + TAILQ_INIT(&refs); + + if (qs->file != NULL && strlen(qs->file) > 0) + if (asprintf(&file_path, "%s/%s", qs->folder ? qs->folder : "", + qs->file) == -1) + return got_error_from_errno("asprintf"); + + if (asprintf(&repo_path, "%s/%s", srv->repos_path, + repo_dir->name) == -1) + return got_error_from_errno("asprintf"); + + error = got_init_repo_commit(&repo_commit); + if (error) + return error; + + /* + * XXX: jumping directly to a commit id via + * got_repo_match_object_id_prefix significantly improves performance, + * but does not allow us to create a PREVIOUS button, since commits can + * only be itereated forward. So, we have to match as we iterate from + * the headref. + */ + if (qs->action == BRIEFS || qs->action == COMMITS || + (qs->action == TREE && qs->commit == NULL)) { + error = got_ref_open(&ref, repo, qs->headref, 0); + if (error) + goto done; + + error = got_ref_resolve(&id, repo, ref); + got_ref_close(ref); + if (error) + goto done; + } else if (qs->commit != NULL) { + error = got_ref_open(&ref, repo, qs->commit, 0); + if (error == NULL) { + error = got_ref_resolve(&id, repo, ref); + if (error) + goto done; + error = got_object_get_type(&obj_type, repo, id); + got_ref_close(ref); + if (error) + goto done; + if (obj_type == GOT_OBJ_TYPE_TAG) { + struct got_tag_object *tag; + error = got_object_open_as_tag(&tag, repo, id); + if (error) + goto done; + if (got_object_tag_get_object_type(tag) != + GOT_OBJ_TYPE_COMMIT) { + got_object_tag_close(tag); + error = got_error(GOT_ERR_OBJ_TYPE); + goto done; + } + free(id); + id = got_object_id_dup( + got_object_tag_get_object_id(tag)); + if (id == NULL) + error = got_error_from_errno( + "got_object_id_dup"); + got_object_tag_close(tag); + if (error) + goto done; + } else if (obj_type != GOT_OBJ_TYPE_COMMIT) { + error = got_error(GOT_ERR_OBJ_TYPE); + goto done; + } + } + error = got_repo_match_object_id_prefix(&id, qs->commit, + GOT_OBJ_TYPE_COMMIT, repo); + if (error) + goto done; + } + + error = got_repo_map_path(&in_repo_path, repo, repo_path); + if (error) + goto done; + + error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, NULL); + if (error) + goto done; + + if (qs->file != NULL && strlen(qs->file) > 0) { + error = got_commit_graph_open(&graph, file_path, 0); + if (error) + goto done; + } else { + error = got_commit_graph_open(&graph, in_repo_path, 0); + if (error) + goto done; + } + + error = got_commit_graph_iter_start(graph, id, repo, NULL, NULL); + if (error) + goto done; + + for (;;) { + if (limit_chk == ((limit * qs->page) - (limit - 1)) && + commit_found == 0 && repo_commit->commit_id != NULL) { + t->prev_id = strdup(repo_commit->commit_id); + if (t->prev_id == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + + error = got_commit_graph_iter_next(&id, graph, repo, NULL, + NULL); + if (error) { + if (error->code == GOT_ERR_ITER_COMPLETED) + error = NULL; + goto done; + } + if (id == NULL) + goto done; + + error = got_object_open_as_commit(&commit, repo, id); + if (error) + goto done; + + error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, + NULL); + if (error) + goto done; + + error = got_get_repo_commit(c, repo_commit, commit, + &refs, id); + if (error) + goto done; + + if (qs->commit != NULL && commit_found == 0 && limit != 1) { + if (strcmp(qs->commit, repo_commit->commit_id) == 0) + commit_found = 1; + else if (qs->file != NULL && strlen(qs->file) > 0 && + qs->page == 0) + commit_found = 1; + else { + limit_chk++; + free(id); + id = NULL; + continue; + } + } + + struct repo_commit *new_repo_commit = NULL; + error = got_init_repo_commit(&new_repo_commit); + if (error) + goto done; + + TAILQ_INSERT_TAIL(&t->repo_commits, new_repo_commit, entry); + + error = got_get_repo_commit(c, new_repo_commit, commit, + &refs, id); + if (error) + goto done; + + free(id); + id = NULL; + + if (limit == 1 && chk_multi == 0 && + srv->max_commits_display != 1) + commit_found = 1; + else { + chk_multi = 1; + + /* + * check for one more commit before breaking, + * so we know whether to navigate through briefs + * commits and summary + */ + if (chk_next && (qs->action == BRIEFS || + qs->action == COMMITS || qs->action == SUMMARY)) { + t->next_id = strdup(new_repo_commit->commit_id); + if (t->next_id == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + if (commit) { + got_object_commit_close(commit); + commit = NULL; + } + if (t->next_id == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + TAILQ_REMOVE(&t->repo_commits, new_repo_commit, + entry); + gotweb_free_repo_commit(new_repo_commit); + goto done; + } + } + got_ref_list_free(&refs); + if (error || (limit && --limit == 0)) { + if (commit_found || (qs->file != NULL && + strlen(qs->file) > 0)) + if (chk_multi == 0) + break; + chk_next = 1; + } + if (commit) { + got_object_commit_close(commit); + commit = NULL; + } + } +done: + gotweb_free_repo_commit(repo_commit); + if (commit) + got_object_commit_close(commit); + if (graph) + got_commit_graph_close(graph); + got_ref_list_free(&refs); + free(file_path); + free(repo_path); + free(id); + return error; +} + +const struct got_error * +got_get_repo_tags(struct request *c, int limit) +{ + const struct got_error *error = NULL; + struct got_object_id *id = NULL; + struct got_commit_object *commit = NULL; + struct got_reflist_head refs; + struct got_reference *ref; + struct got_reflist_entry *re; + struct server *srv = c->srv; + struct transport *t = c->t; + struct got_repository *repo = t->repo; + struct querystring *qs = t->qs; + struct repo_dir *repo_dir = t->repo_dir; + struct got_tag_object *tag = NULL; + struct repo_tag *rt = NULL, *trt = NULL; + char *in_repo_path = NULL, *repo_path = NULL, *id_str = NULL; + char *commit_msg = NULL, *commit_msg0 = NULL; + int chk_next = 0, chk_multi = 1, commit_found = 0, c_cnt = 0; + + TAILQ_INIT(&refs); + + if (asprintf(&repo_path, "%s/%s", srv->repos_path, + repo_dir->name) == -1) + return got_error_from_errno("asprintf"); + + if (error) + return error; + + if (qs->commit == NULL && qs->action == TAGS) { + error = got_ref_open(&ref, repo, qs->headref, 0); + if (error) + goto err; + error = got_ref_resolve(&id, repo, ref); + got_ref_close(ref); + if (error) + goto err; + } else if (qs->commit == NULL && qs->action == TAG) { + error = got_error_msg(GOT_ERR_EOF, "commit id missing"); + goto err; + } else { + error = got_repo_match_object_id_prefix(&id, qs->commit, + GOT_OBJ_TYPE_COMMIT, repo); + if (error) + goto err; + } + + if (qs->action != SUMMARY && qs->action != TAGS) { + error = got_object_open_as_commit(&commit, repo, id); + if (error) + goto err; + error = got_object_commit_get_logmsg(&commit_msg0, commit); + if (error) + goto err; + if (commit) { + got_object_commit_close(commit); + commit = NULL; + } + } + + error = got_repo_map_path(&in_repo_path, repo, repo_path); + if (error) + goto err; + + error = got_ref_list(&refs, repo, "refs/tags", got_ref_cmp_tags, + repo); + if (error) + goto err; + + if (limit == 1) + chk_multi = 0; + + /* + * XXX: again, see previous message about caching + */ + + TAILQ_FOREACH(re, &refs, entry) { + struct repo_tag *new_repo_tag = NULL; + error = got_init_repo_tag(&new_repo_tag); + if (error) + goto err; + + TAILQ_INSERT_TAIL(&t->repo_tags, new_repo_tag, entry); + + new_repo_tag->tag_name = strdup(got_ref_get_name(re->ref)); + if (new_repo_tag->tag_name == NULL) { + error = got_error_from_errno("strdup"); + goto err; + } + + error = got_ref_resolve(&id, repo, re->ref); + if (error) + goto done; + + error = got_object_open_as_tag(&tag, repo, id); + if (error) { + if (error->code != GOT_ERR_OBJ_TYPE) { + free(id); + id = NULL; + goto done; + } + /* "lightweight" tag */ + error = got_object_open_as_commit(&commit, repo, id); + if (error) { + free(id); + id = NULL; + goto done; + } + new_repo_tag->tagger = + strdup(got_object_commit_get_committer(commit)); + if (new_repo_tag->tagger == NULL) { + error = got_error_from_errno("strdup"); + goto err; + } + new_repo_tag->tagger_time = + got_object_commit_get_committer_time(commit); + error = got_object_id_str(&id_str, id); + if (error) + goto err; + free(id); + id = NULL; + } else { + free(id); + id = NULL; + new_repo_tag->tagger = + strdup(got_object_tag_get_tagger(tag)); + if (new_repo_tag->tagger == NULL) { + error = got_error_from_errno("strdup"); + goto err; + } + new_repo_tag->tagger_time = + got_object_tag_get_tagger_time(tag); + error = got_object_id_str(&id_str, + got_object_tag_get_object_id(tag)); + if (error) + goto err; + } + + new_repo_tag->commit_id = strdup(id_str); + if (new_repo_tag->commit_id == NULL) + goto err; + + if (commit_found == 0 && qs->commit != NULL && + strncmp(id_str, qs->commit, strlen(id_str)) != 0) + continue; + else + commit_found = 1; + + t->tag_count++; + + /* + * check for one more commit before breaking, + * so we know whether to navigate through briefs + * commits and summary + */ + if (chk_next) { + t->next_id = strdup(new_repo_tag->commit_id); + if (t->next_id == NULL) { + error = got_error_from_errno("strdup"); + goto err; + } + if (commit) { + got_object_commit_close(commit); + commit = NULL; + } + if (t->next_id == NULL) { + error = got_error_from_errno("strdup"); + goto err; + } + TAILQ_REMOVE(&t->repo_tags, new_repo_tag, entry); + gotweb_free_repo_tag(new_repo_tag); + goto done; + } + + if (commit) { + error = got_object_commit_get_logmsg(&new_repo_tag-> + tag_commit, commit); + if (error) + goto done; + got_object_commit_close(commit); + commit = NULL; + } else { + new_repo_tag->tag_commit = + strdup(got_object_tag_get_message(tag)); + if (new_repo_tag->tag_commit == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + + while (*new_repo_tag->tag_commit == '\n') + new_repo_tag->tag_commit++; + + if (qs->action != SUMMARY && qs->action != TAGS) { + commit_msg = commit_msg0; + while (*commit_msg == '\n') + commit_msg++; + + new_repo_tag->commit_msg = strdup(commit_msg); + if (new_repo_tag->commit_msg == NULL) { + error = got_error_from_errno("strdup"); + free(commit_msg0); + goto err; + } + free(commit_msg0); + } + + if (limit && --limit == 0) { + if (chk_multi == 0) + break; + chk_next = 1; + } + free(id); + id = NULL; + } + +done: + /* + * we have tailq populated, so find previous commit id + * for navigation through briefs and commits + */ + if (t->tag_count == 0) { + TAILQ_FOREACH_SAFE(rt, &t->repo_tags, entry, trt) { + TAILQ_REMOVE(&t->repo_tags, rt, entry); + gotweb_free_repo_tag(rt); + } + } + if (t->tag_count > 0 && t->prev_id == NULL && qs->commit != NULL) { + commit_found = 0; + TAILQ_FOREACH_REVERSE(rt, &t->repo_tags, repo_tags_head, + entry) { + if (commit_found == 0 && rt->commit_id != NULL && + strcmp(qs->commit, rt->commit_id) != 0) { + continue; + } else + commit_found = 1; + if (c_cnt == srv->max_commits_display || + rt == TAILQ_FIRST(&t->repo_tags)) { + t->prev_id = strdup(rt->commit_id); + if (t->prev_id == NULL) + error = got_error_from_errno("strdup"); + break; + } + c_cnt++; + } + } +err: + if (commit) + got_object_commit_close(commit); + got_ref_list_free(&refs); + free(repo_path); + free(id); + return error; +} + +const struct got_error * +got_output_repo_tree(struct request *c) +{ + const struct got_error *error = NULL; + struct transport *t = c->t; + struct got_commit_object *commit = NULL; + struct got_repository *repo = t->repo; + struct querystring *qs = t->qs; + struct repo_commit *rc = NULL; + struct got_object_id *tree_id = NULL, *commit_id = NULL; + struct got_reflist_head refs; + struct got_tree_object *tree = NULL; + struct repo_dir *repo_dir = t->repo_dir; + char *id_str = NULL; + char *path = NULL, *in_repo_path = NULL, *build_folder = NULL; + char *modestr = NULL, *name = NULL, *class = NULL; + int nentries, i, class_flip = 0; + + TAILQ_INIT(&refs); + + rc = TAILQ_FIRST(&t->repo_commits); + + if (qs->folder != NULL) { + path = strdup(qs->folder); + if (path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else { + error = got_repo_map_path(&in_repo_path, repo, repo_dir->path); + if (error) + goto done; + free(path); + path = in_repo_path; + } + + error = got_repo_match_object_id(&commit_id, NULL, rc->commit_id, + GOT_OBJ_TYPE_COMMIT, &refs, repo); + if (error) + goto done; + + error = got_object_open_as_commit(&commit, repo, commit_id); + if (error) + goto done; + + error = got_object_id_by_path(&tree_id, repo, commit, path); + if (error) + goto done; + + error = got_object_open_as_tree(&tree, repo, tree_id); + if (error) + goto done; + + nentries = got_object_tree_get_nentries(tree); + + for (i = 0; i < nentries; i++) { + struct got_tree_entry *te; + mode_t mode; + + te = got_object_tree_get_entry(tree, i); + + error = got_object_id_str(&id_str, got_tree_entry_get_id(te)); + if (error) + goto done; + + modestr = strdup(""); + if (modestr == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + mode = got_tree_entry_get_mode(te); + if (got_object_tree_entry_is_submodule(te)) { + free(modestr); + modestr = strdup("$"); + if (modestr == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (S_ISLNK(mode)) { + free(modestr); + modestr = strdup("@"); + if (modestr == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (S_ISDIR(mode)) { + free(modestr); + modestr = strdup("/"); + if (modestr == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (mode & S_IXUSR) { + free(modestr); + modestr = strdup("*"); + if (modestr == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + + if (class_flip == 0) { + class = strdup("back_lightgray"); + if (class == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + class_flip = 1; + } else { + class = strdup("back_white"); + if (class == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + class_flip = 0; + } + + name = strdup(got_tree_entry_get_name(te)); + if (name == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + if (S_ISDIR(mode)) { + if (asprintf(&build_folder, "%s/%s", + qs->folder ? qs->folder : "", + got_tree_entry_get_name(te)) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + + if (fcgi_gen_response(c, + "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, " ") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + } else { + free(name); + name = strdup(got_tree_entry_get_name(te)); + if (name == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + + if (fcgi_gen_response(c, + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + + if (fcgi_gen_response(c, + "") == -1) + goto done; + if (fcgi_gen_response(c, name) == -1) + goto done; + if (fcgi_gen_response(c, modestr) == -1) + goto done; + + if (fcgi_gen_response(c, "") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + + if (fcgi_gen_response(c, + "") == -1) + goto done; + + if (fcgi_gen_response(c, "commits") == -1) + goto done; + if (fcgi_gen_response(c, "\n") == -1) + goto done; + + if (fcgi_gen_response(c, " | \n") == -1) + goto done; + + if (fcgi_gen_response(c, + "") == -1) + goto done; + + if (fcgi_gen_response(c, "blame") == -1) + goto done; + if (fcgi_gen_response(c, "\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + } + free(id_str); + id_str = NULL; + free(build_folder); + build_folder = NULL; + free(name); + name = NULL; + free(modestr); + modestr = NULL; + free(class); + class = NULL; + } +done: + free(id_str); + free(build_folder); + free(modestr); + free(path); + free(name); + free(class); + got_ref_list_free(&refs); + if (commit) + got_object_commit_close(commit); + free(commit_id); + free(tree_id); + return error; +} + +const struct got_error * +got_output_file_blob(struct request *c) +{ + const struct got_error *error = NULL; + struct transport *t = c->t; + struct got_repository *repo = t->repo; + struct querystring *qs = c->t->qs; + struct got_commit_object *commit = NULL; + struct got_object_id *commit_id = NULL; + struct got_reflist_head refs; + struct got_blob_object *blob = NULL; + char *path = NULL, *in_repo_path = NULL; + int obj_type, set_mime = 0, type = 0, fd = -1; + char *buf_output = NULL; + size_t len, hdrlen; + const uint8_t *buf; + + TAILQ_INIT(&refs); + + if (asprintf(&path, "%s%s%s", qs->folder ? qs->folder : "", + qs->folder ? "/" : "", qs->file) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + + error = got_repo_map_path(&in_repo_path, repo, path); + if (error) + goto done; + + error = got_repo_match_object_id(&commit_id, NULL, qs->commit, + GOT_OBJ_TYPE_COMMIT, &refs, repo); + if (error) + goto done; + + error = got_object_open_as_commit(&commit, repo, commit_id); + if (error) + goto done; + + error = got_object_id_by_path(&commit_id, repo, commit, in_repo_path); + if (error) + goto done; + + if (commit_id == NULL) { + error = got_error(GOT_ERR_NO_OBJ); + goto done; + } + + error = got_object_get_type(&obj_type, repo, commit_id); + if (error) + goto done; + + if (obj_type != GOT_OBJ_TYPE_BLOB) { + error = got_error(GOT_ERR_OBJ_TYPE); + goto done; + } + + error = got_gotweb_dupfd(&c->priv_fd[BLOB_FD_1], &fd); + if (error) + goto done; + + error = got_object_open_as_blob(&blob, repo, commit_id, BUF, fd); + if (error) + goto done; + hdrlen = got_object_blob_get_hdrlen(blob); + do { + error = got_object_blob_read_block(&len, blob); + if (error) + goto done; + buf = got_object_blob_get_read_buf(blob); + + /* + * Skip blob object header first time around, + * which also contains a zero byte. + */ + buf += hdrlen; + if (set_mime == 0) { + if (isbinary(buf, len - hdrlen)) { + error = gotweb_render_content_type_file(c, + "application/octet-stream", + qs->file); + if (error) { + log_warnx("%s: %s", __func__, + error->msg); + goto done; + } + type = 0; + } else { + error = gotweb_render_content_type(c, + "text/text"); + if (error) { + log_warnx("%s: %s", __func__, + error->msg); + goto done; + } + type = 1; + } + } + set_mime = 1; + if (type) { + buf_output = calloc(len - hdrlen + 1, + sizeof(*buf_output)); + if (buf_output == NULL) { + error = got_error_from_errno("calloc"); + goto done; + } + memcpy(buf_output, buf, len - hdrlen); + fcgi_gen_response(c, buf_output); + free(buf_output); + buf_output = NULL; + } else + fcgi_gen_binary_response(c, buf, len - hdrlen); + + hdrlen = 0; + } while (len != 0); +done: + if (commit) + got_object_commit_close(commit); + if (fd != -1 && close(fd) == -1 && error == NULL) + error = got_error_from_errno("close"); + if (blob) + got_object_blob_close(blob); + free(buf_output); + free(in_repo_path); + free(commit_id); + free(path); + return error; +} + +struct blame_line { + int annotated; + char *id_str; + char *committer; + char datebuf[11]; /* YYYY-MM-DD + NUL */ +}; + +struct blame_cb_args { + struct blame_line *lines; + int nlines; + int nlines_prec; + int lineno_cur; + off_t *line_offsets; + FILE *f; + struct got_repository *repo; + struct request *c; +}; + +static const struct got_error * +got_gotweb_blame_cb(void *arg, int nlines, int lineno, + struct got_commit_object *commit, struct got_object_id *id) +{ + const struct got_error *err = NULL; + struct blame_cb_args *a = arg; + struct blame_line *bline; + struct request *c = a->c; + struct transport *t = c->t; + struct querystring *qs = t->qs; + struct repo_dir *repo_dir = t->repo_dir; + char *line = NULL, *eline = NULL; + size_t linesize = 0; + off_t offset; + struct tm tm; + time_t committer_time; + + if (nlines != a->nlines || + (lineno != -1 && lineno < 1) || lineno > a->nlines) + return got_error(GOT_ERR_RANGE); + + if (lineno == -1) + return NULL; /* no change in this commit */ + + /* Annotate this line. */ + bline = &a->lines[lineno - 1]; + if (bline->annotated) + return NULL; + err = got_object_id_str(&bline->id_str, id); + if (err) + return err; + + bline->committer = strdup(got_object_commit_get_committer(commit)); + if (bline->committer == NULL) { + err = got_error_from_errno("strdup"); + goto done; + } + + committer_time = got_object_commit_get_committer_time(commit); + if (gmtime_r(&committer_time, &tm) == NULL) + return got_error_from_errno("gmtime_r"); + if (strftime(bline->datebuf, sizeof(bline->datebuf), "%G-%m-%d", + &tm) == 0) { + err = got_error(GOT_ERR_NO_SPACE); + goto done; + } + bline->annotated = 1; + + /* Print lines annotated so far. */ + bline = &a->lines[a->lineno_cur - 1]; + if (!bline->annotated) + goto done; + + offset = a->line_offsets[a->lineno_cur - 1]; + if (fseeko(a->f, offset, SEEK_SET) == -1) { + err = got_error_from_errno("fseeko"); + goto done; + } + + while (bline->annotated) { + int out_buff_size = 100; + char *smallerthan, *at, *nl, *committer; + char out_buff[out_buff_size]; + size_t len; + + if (getline(&line, &linesize, a->f) == -1) { + if (ferror(a->f)) + err = got_error_from_errno("getline"); + break; + } + + committer = bline->committer; + smallerthan = strchr(committer, '<'); + if (smallerthan && smallerthan[1] != '\0') + committer = smallerthan + 1; + at = strchr(committer, '@'); + if (at) + *at = '\0'; + len = strlen(committer); + if (len >= 9) + committer[8] = '\0'; + + nl = strchr(line, '\n'); + if (nl) + *nl = '\0'; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (snprintf(out_buff, strlen(out_buff), "%.*d", a->nlines_prec, + a->lineno_cur) < 0) + goto done; + if (fcgi_gen_response(c, out_buff) == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + + if (fcgi_gen_response(c, "") == -1) + goto done; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, bline->datebuf) == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, committer) == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + err = gotweb_escape_html(&eline, line); + if (err) + goto done; + if (fcgi_gen_response(c, eline) == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + a->lineno_cur++; + bline = &a->lines[a->lineno_cur - 1]; + } +done: + free(line); + free(eline); + return err; +} + +const struct got_error * +got_output_file_blame(struct request *c) +{ + const struct got_error *error = NULL; + struct transport *t = c->t; + struct got_repository *repo = t->repo; + struct querystring *qs = c->t->qs; + struct got_object_id *obj_id = NULL, *commit_id = NULL; + struct got_commit_object *commit = NULL; + struct got_reflist_head refs; + struct got_blob_object *blob = NULL; + char *path = NULL, *in_repo_path = NULL; + struct blame_cb_args bca; + int i, obj_type, fd1 = -1, fd2 = -1, fd3 = -1, fd4 = -1, fd5 = -1; + int fd6 = -1; + off_t filesize; + FILE *f1 = NULL, *f2 = NULL; + + TAILQ_INIT(&refs); + bca.f = NULL; + bca.lines = NULL; + + if (asprintf(&path, "%s%s%s", qs->folder ? qs->folder : "", + qs->folder ? "/" : "", qs->file) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + + error = got_repo_map_path(&in_repo_path, repo, path); + if (error) + goto done; + + error = got_repo_match_object_id(&commit_id, NULL, qs->commit, + GOT_OBJ_TYPE_COMMIT, &refs, repo); + if (error) + goto done; + + error = got_object_open_as_commit(&commit, repo, commit_id); + if (error) + goto done; + + error = got_object_id_by_path(&obj_id, repo, commit, in_repo_path); + if (error) + goto done; + + if (commit_id == NULL) { + error = got_error(GOT_ERR_NO_OBJ); + goto done; + } + + error = got_object_get_type(&obj_type, repo, obj_id); + if (error) + goto done; + + if (obj_type != GOT_OBJ_TYPE_BLOB) { + error = got_error(GOT_ERR_OBJ_TYPE); + goto done; + } + + error = got_gotweb_openfile(&bca.f, &c->priv_fd[BLAME_FD_1], &fd1); + if (error) + goto done; + + error = got_gotweb_dupfd(&c->priv_fd[BLAME_FD_2], &fd2); + if (error) + goto done; + + error = got_object_open_as_blob(&blob, repo, obj_id, BUF, fd2); + if (error) + goto done; + + error = got_object_blob_dump_to_file(&filesize, &bca.nlines, + &bca.line_offsets, bca.f, blob); + if (error || bca.nlines == 0) + goto done; + + /* Don't include \n at EOF in the blame line count. */ + if (bca.line_offsets[bca.nlines - 1] == filesize) + bca.nlines--; + + bca.lines = calloc(bca.nlines, sizeof(*bca.lines)); + if (bca.lines == NULL) { + error = got_error_from_errno("calloc"); + goto done; + } + bca.lineno_cur = 1; + bca.nlines_prec = 0; + i = bca.nlines; + while (i > 0) { + i /= 10; + bca.nlines_prec++; + } + bca.repo = repo; + bca.c = c; + + error = got_gotweb_dupfd(&c->priv_fd[BLAME_FD_3], &fd3); + if (error) + goto done; + + error = got_gotweb_dupfd(&c->priv_fd[BLAME_FD_4], &fd4); + if (error) + goto done; + + error = got_gotweb_openfile(&f1, &c->priv_fd[BLAME_FD_5], &fd5); + if (error) + goto done; + + error = got_gotweb_openfile(&f2, &c->priv_fd[BLAME_FD_6], &fd6); + if (error) + goto done; + + error = got_blame(in_repo_path, commit_id, repo, + GOT_DIFF_ALGORITHM_MYERS, got_gotweb_blame_cb, &bca, NULL, NULL, + fd3, fd4, f1, f2); + + if (blob) { + free(bca.line_offsets); + for (i = 0; i < bca.nlines; i++) { + struct blame_line *bline = &bca.lines[i]; + free(bline->id_str); + free(bline->committer); + } + } +done: + free(bca.lines); + if (fd2 != -1 && close(fd2) == -1 && error == NULL) + error = got_error_from_errno("close"); + if (fd3 != -1 && close(fd3) == -1 && error == NULL) + error = got_error_from_errno("close"); + if (fd4 != -1 && close(fd4) == -1 && error == NULL) + error = got_error_from_errno("close"); + if (bca.f) { + const struct got_error *bca_err = + got_gotweb_flushfile(bca.f, fd1); + if (error == NULL) + error = bca_err; + } + if (f1) { + const struct got_error *f1_err = + got_gotweb_flushfile(f1, fd5); + if (error == NULL) + error = f1_err; + } + if (f2) { + const struct got_error *f2_err = + got_gotweb_flushfile(f2, fd6); + if (error == NULL) + error = f2_err; + } + if (commit) + got_object_commit_close(commit); + if (blob) + got_object_blob_close(blob); + free(in_repo_path); + free(commit_id); + free(path); + return error; +} + +const struct got_error * +got_output_repo_diff(struct request *c) +{ + const struct got_error *error = NULL; + struct transport *t = c->t; + struct got_repository *repo = t->repo; + struct repo_commit *rc = NULL; + struct got_object_id *id1 = NULL, *id2 = NULL; + struct got_reflist_head refs; + FILE *f1 = NULL, *f2 = NULL, *f3 = NULL; + char *label1 = NULL, *label2 = NULL, *line = NULL; + char *newline, *eline = NULL, *color = NULL; + int obj_type, fd1, fd2, fd3, fd4 = -1, fd5 = -1; + size_t linesize = 0; + ssize_t linelen; + int wrlen = 0; + + TAILQ_INIT(&refs); + + error = got_gotweb_openfile(&f1, &c->priv_fd[DIFF_FD_1], &fd1); + if (error) + return error; + + error = got_gotweb_openfile(&f2, &c->priv_fd[DIFF_FD_2], &fd2); + if (error) + return error; + + error = got_gotweb_openfile(&f3, &c->priv_fd[DIFF_FD_3], &fd3); + if (error) + return error; + + rc = TAILQ_FIRST(&t->repo_commits); + + if (rc->parent_id != NULL && + strncmp(rc->parent_id, "/dev/null", 9) != 0) { + error = got_repo_match_object_id(&id1, &label1, + rc->parent_id, GOT_OBJ_TYPE_ANY, + &refs, repo); + if (error) + goto done; + } + + error = got_repo_match_object_id(&id2, &label2, rc->commit_id, + GOT_OBJ_TYPE_ANY, &refs, repo); + if (error) + goto done; + + error = got_object_get_type(&obj_type, repo, id2); + if (error) + goto done; + + error = got_gotweb_dupfd(&c->priv_fd[DIFF_FD_4], &fd4); + if (error) + goto done; + + error = got_gotweb_dupfd(&c->priv_fd[DIFF_FD_5], &fd5); + if (error) + goto done; + + switch (obj_type) { + case GOT_OBJ_TYPE_BLOB: + error = got_diff_objects_as_blobs(NULL, NULL, f1, f2, fd4, fd5, + id1, id2, NULL, NULL, GOT_DIFF_ALGORITHM_MYERS, 3, 0, 0, + repo, f3); + break; + case GOT_OBJ_TYPE_TREE: + error = got_diff_objects_as_trees(NULL, NULL, f1, f2, fd4, fd5, + id1, id2, NULL, "", "", GOT_DIFF_ALGORITHM_MYERS, 3, 0, 0, + repo, f3); + break; + case GOT_OBJ_TYPE_COMMIT: + error = got_diff_objects_as_commits(NULL, NULL, f1, f2, fd4, + fd5, id1, id2, NULL, GOT_DIFF_ALGORITHM_MYERS, 3, 0, 0, + repo, f3); + break; + default: + error = got_error(GOT_ERR_OBJ_TYPE); + } + if (error) + goto done; + + if (fseek(f1, 0, SEEK_SET) == -1) { + error = got_ferror(f1, GOT_ERR_IO); + goto done; + } + + if (fseek(f2, 0, SEEK_SET) == -1) { + error = got_ferror(f2, GOT_ERR_IO); + goto done; + } + + if (fseek(f3, 0, SEEK_SET) == -1) { + error = got_ferror(f3, GOT_ERR_IO); + goto done; + } + + while ((linelen = getline(&line, &linesize, f3)) != -1) { + if (strncmp(line, "-", 1) == 0) { + color = strdup("diff_minus"); + if (color == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (strncmp(line, "+", 1) == 0) { + color = strdup("diff_plus"); + if (color == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (strncmp(line, "@@", 2) == 0) { + color = strdup("diff_chunk_header"); + if (color == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (strncmp(line, "@@", 2) == 0) { + color = strdup("diff_chunk_header"); + if (color == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (strncmp(line, "commit +", 8) == 0) { + color = strdup("diff_meta"); + if (color == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (strncmp(line, "commit -", 8) == 0) { + color = strdup("diff_meta"); + if (color == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (strncmp(line, "blob +", 6) == 0) { + color = strdup("diff_meta"); + if (color == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (strncmp(line, "blob -", 6) == 0) { + color = strdup("diff_meta"); + if (color == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (strncmp(line, "file +", 6) == 0) { + color = strdup("diff_meta"); + if (color == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (strncmp(line, "file -", 6) == 0) { + color = strdup("diff_meta"); + if (color == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (strncmp(line, "from:", 5) == 0) { + color = strdup("diff_author"); + if (color == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (strncmp(line, "via:", 4) == 0) { + color = strdup("diff_author"); + if (color == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (strncmp(line, "date:", 5) == 0) { + color = strdup("diff_date"); + if (color == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + if (fcgi_gen_response(c, "
") == -1) + goto done; + newline = strchr(line, '\n'); + if (newline) + *newline = '\0'; + + error = gotweb_escape_html(&eline, line); + if (error) + goto done; + if (fcgi_gen_response(c, eline) == -1) + goto done; + free(eline); + eline = NULL; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (linelen > 0) + wrlen = wrlen + linelen; + free(color); + color = NULL; + } + if (linelen == -1 && ferror(f3)) + error = got_error_from_errno("getline"); +done: + free(color); + if (fd4 != -1 && close(fd4) == -1 && error == NULL) + error = got_error_from_errno("close"); + if (fd5 != -1 && close(fd5) == -1 && error == NULL) + error = got_error_from_errno("close"); + if (f1) { + const struct got_error *f1_err = + got_gotweb_flushfile(f1, fd1); + if (error == NULL) + error = f1_err; + } + if (f2) { + const struct got_error *f2_err = + got_gotweb_flushfile(f2, fd2); + if (error == NULL) + error = f2_err; + } + if (f3) { + const struct got_error *f3_err = + got_gotweb_flushfile(f3, fd3); + if (error == NULL) + error = f3_err; + } + got_ref_list_free(&refs); + free(line); + free(eline); + free(label1); + free(label2); + free(id1); + free(id2); + return error; +} + +static const struct got_error * +got_init_repo_commit(struct repo_commit **rc) +{ + const struct got_error *error = NULL; + + *rc = calloc(1, sizeof(**rc)); + if (*rc == NULL) + return got_error_from_errno2("%s: calloc", __func__); + + (*rc)->path = NULL; + (*rc)->refs_str = NULL; + (*rc)->commit_id = NULL; + (*rc)->committer = NULL; + (*rc)->author = NULL; + (*rc)->parent_id = NULL; + (*rc)->tree_id = NULL; + (*rc)->commit_msg = NULL; + + return error; +} + +static const struct got_error * +got_init_repo_tag(struct repo_tag **rt) +{ + const struct got_error *error = NULL; + + *rt = calloc(1, sizeof(**rt)); + if (*rt == NULL) + return got_error_from_errno2("%s: calloc", __func__); + + (*rt)->commit_id = NULL; + (*rt)->tag_name = NULL; + (*rt)->tag_commit = NULL; + (*rt)->commit_msg = NULL; + (*rt)->tagger = NULL; + + return error; +} blob - /dev/null blob + 5de75e9906e8db76b4f47ea4e47f97bbff344227 (mode 644) --- /dev/null +++ gotwebd/gotweb.c @@ -0,0 +1,2830 @@ +/* + * Copyright (c) 2016, 2019, 2020-2022 Tracey Emery + * Copyright (c) 2015 Mike Larkin + * Copyright (c) 2013 David Gwynne + * Copyright (c) 2013 Florian Obser + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "got_error.h" +#include "got_object.h" +#include "got_reference.h" +#include "got_repository.h" +#include "got_path.h" +#include "got_cancel.h" +#include "got_worktree.h" +#include "got_diff.h" +#include "got_commit_graph.h" +#include "got_blame.h" +#include "got_privsep.h" + +#include "proc.h" +#include "gotwebd.h" + +enum gotweb_ref_tm { + TM_DIFF, + TM_LONG, +}; + +static const struct querystring_keys querystring_keys[] = { + { "action", ACTION }, + { "commit", COMMIT }, + { "file", RFILE }, + { "folder", FOLDER }, + { "headref", HEADREF }, + { "index_page", INDEX_PAGE }, + { "path", PATH }, + { "page", PAGE }, +}; + +static const struct action_keys action_keys[] = { + { "blame", BLAME }, + { "blob", BLOB }, + { "briefs", BRIEFS }, + { "commits", COMMITS }, + { "diff", DIFF }, + { "error", ERR }, + { "index", INDEX }, + { "summary", SUMMARY }, + { "tag", TAG }, + { "tags", TAGS }, + { "tree", TREE }, +}; + +static const struct got_error *gotweb_init_querystring(struct querystring **); +static const struct got_error *gotweb_parse_querystring(struct querystring **, + char *); +static const struct got_error *gotweb_assign_querystring(struct querystring **, + char *, char *); +static const struct got_error *gotweb_render_header(struct request *); +static const struct got_error *gotweb_render_footer(struct request *); +static const struct got_error *gotweb_render_index(struct request *); +static const struct got_error *gotweb_init_repo_dir(struct repo_dir **, + const char *); +static const struct got_error *gotweb_load_got_path(struct request *c, + struct repo_dir *); +static const struct got_error *gotweb_get_repo_description(char **, + struct server *, char *); +static const struct got_error *gotweb_get_clone_url(char **, struct server *, + char *); +static const struct got_error *gotweb_render_navs(struct request *); +static const struct got_error *gotweb_render_blame(struct request *); +static const struct got_error *gotweb_render_briefs(struct request *); +static const struct got_error *gotweb_render_commits(struct request *); +static const struct got_error *gotweb_render_diff(struct request *); +static const struct got_error *gotweb_render_summary(struct request *); +static const struct got_error *gotweb_render_tag(struct request *); +static const struct got_error *gotweb_render_tags(struct request *); +static const struct got_error *gotweb_render_tree(struct request *); +static const struct got_error *gotweb_render_branches(struct request *); + +static void gotweb_free_querystring(struct querystring *); +static void gotweb_free_repo_dir(struct repo_dir *); + +struct server *gotweb_get_server(uint8_t *, uint8_t *, uint8_t *); + +void +gotweb_process_request(struct request *c) +{ + const struct got_error *error = NULL, *error2 = NULL; + struct server *srv = NULL; + struct querystring *qs = NULL; + struct repo_dir *repo_dir = NULL; + uint8_t err[] = "gotwebd experienced an error: "; + int html = 0; + + /* init the transport */ + error = gotweb_init_transport(&c->t); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + /* don't process any further if client disconnected */ + if (c->sock->client_status == CLIENT_DISCONNECT) + return; + /* get the gotwebd server */ + srv = gotweb_get_server(c->server_name, c->document_root, c->http_host); + if (srv == NULL) { + log_warnx("%s: error server is NULL", __func__); + goto err; + } + c->srv = srv; + /* parse our querystring */ + error = gotweb_init_querystring(&qs); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + c->t->qs = qs; + error = gotweb_parse_querystring(&qs, c->querystring); + if (error) { + gotweb_free_querystring(qs); + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + + /* + * certain actions require a commit id in the querystring. this stops + * bad actors from exploiting this by manually manipulating the + * querystring. + */ + + if (qs->commit == NULL && (qs->action == BLAME || qs->action == BLOB || + qs->action == DIFF)) { + error2 = got_error(GOT_ERR_QUERYSTRING); + goto render; + } + + if (qs->action != INDEX) { + error = gotweb_init_repo_dir(&repo_dir, qs->path); + if (error) + goto done; + error = gotweb_load_got_path(c, repo_dir); + c->t->repo_dir = repo_dir; + if (error && error->code != GOT_ERR_LONELY_PACKIDX) + goto err; + } + + /* render top of page */ + if (qs != NULL && qs->action == BLOB) { + error = got_get_repo_commits(c, 1); + if (error) + goto done; + error = gotweb_render_content_type(c, "text/plain"); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + error = got_output_file_blob(c); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + goto done; + } else { +render: + error = gotweb_render_content_type(c, "text/html"); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + html = 1; + } + + error = gotweb_render_header(c); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + + if (error2) { + error = error2; + goto err; + } + + switch(qs->action) { + case BLAME: + error = gotweb_render_blame(c); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + break; + case BRIEFS: + error = gotweb_render_briefs(c); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + break; + case COMMITS: + error = gotweb_render_commits(c); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + break; + case DIFF: + error = gotweb_render_diff(c); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + break; + case INDEX: + error = gotweb_render_index(c); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + break; + case SUMMARY: + error = gotweb_render_summary(c); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + break; + case TAG: + error = gotweb_render_tag(c); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + break; + case TAGS: + error = gotweb_render_tags(c); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + break; + case TREE: + error = gotweb_render_tree(c); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto err; + } + break; + case ERR: + default: + if (fcgi_gen_response(c, "
") == -1) + goto err; + if (fcgi_gen_response(c, "Error: Bad Querystring\n") == -1) + goto err; + if (fcgi_gen_response(c, "
\n") == -1) + goto err; + break; + } + + goto done; +err: + if (html && fcgi_gen_response(c, "
") == -1) + return; + if (fcgi_gen_response(c, err) == -1) + return; + if (error) { + if (fcgi_gen_response(c, (uint8_t *)error->msg) == -1) + return; + } else { + if (fcgi_gen_response(c, "see daemon logs for details") == -1) + return; + } + if (html && fcgi_gen_response(c, "
\n") == -1) + return; +done: + if (c->t->repo != NULL && qs->action != INDEX) + got_repo_close(c->t->repo); + if (html && srv != NULL) + gotweb_render_footer(c); +} + +struct server * +gotweb_get_server(uint8_t *server_name, uint8_t *document_root, + uint8_t *subdomain) +{ + struct server *srv = NULL; + + /* check against document_root first */ + if (strlen(server_name) > 0) + TAILQ_FOREACH(srv, gotwebd_env->servers, entry) + if (strcmp(srv->name, server_name) == 0) + goto done; + + /* check against document_root second */ + if (strlen(document_root) > 0) + TAILQ_FOREACH(srv, gotwebd_env->servers, entry) + if (strcmp(srv->name, document_root) == 0) + goto done; + + /* check against subdomain third */ + if (strlen(subdomain) > 0) + TAILQ_FOREACH(srv, gotwebd_env->servers, entry) + if (strcmp(srv->name, subdomain) == 0) + goto done; + + /* if those fail, send first server */ + TAILQ_FOREACH(srv, gotwebd_env->servers, entry) + if (srv != NULL) + break; +done: + return srv; +}; + +const struct got_error * +gotweb_init_transport(struct transport **t) +{ + const struct got_error *error = NULL; + + *t = calloc(1, sizeof(**t)); + if (*t == NULL) + return got_error_from_errno2("%s: calloc", __func__); + + TAILQ_INIT(&(*t)->repo_commits); + TAILQ_INIT(&(*t)->repo_tags); + + (*t)->repo = NULL; + (*t)->repo_dir = NULL; + (*t)->qs = NULL; + (*t)->next_id = NULL; + (*t)->prev_id = NULL; + (*t)->next_disp = 0; + (*t)->prev_disp = 0; + + return error; +} + +static const struct got_error * +gotweb_init_querystring(struct querystring **qs) +{ + const struct got_error *error = NULL; + + *qs = calloc(1, sizeof(**qs)); + if (*qs == NULL) + return got_error_from_errno2("%s: calloc", __func__); + + (*qs)->action = INDEX; + (*qs)->commit = NULL; + (*qs)->file = NULL; + (*qs)->folder = NULL; + (*qs)->headref = strdup("HEAD"); + if ((*qs)->headref == NULL) { + return got_error_from_errno2("%s: strdup", __func__); + } + (*qs)->index_page = 0; + (*qs)->index_page_str = NULL; + (*qs)->path = NULL; + + return error; +} + +static const struct got_error * +gotweb_parse_querystring(struct querystring **qs, char *qst) +{ + const struct got_error *error = NULL; + char *tok1 = NULL, *tok1_pair = NULL, *tok1_end = NULL; + char *tok2 = NULL, *tok2_pair = NULL, *tok2_end = NULL; + + if (qst == NULL) + return error; + + tok1 = strdup(qst); + if (tok1 == NULL) + return got_error_from_errno2("%s: strdup", __func__); + + tok1_pair = tok1; + tok1_end = tok1; + + while (tok1_pair != NULL) { + strsep(&tok1_end, "&"); + + tok2 = strdup(tok1_pair); + if (tok2 == NULL) { + free(tok1); + return got_error_from_errno2("%s: strdup", __func__); + } + + tok2_pair = tok2; + tok2_end = tok2; + + while (tok2_pair != NULL) { + strsep(&tok2_end, "="); + if (tok2_end) { + error = gotweb_assign_querystring(qs, tok2_pair, + tok2_end); + if (error) + goto err; + } + tok2_pair = tok2_end; + } + free(tok2); + tok1_pair = tok1_end; + } + free(tok1); + return error; +err: + free(tok2); + free(tok1); + return error; +} + +static const struct got_error * +gotweb_assign_querystring(struct querystring **qs, char *key, char *value) +{ + const struct got_error *error = NULL; + const char *errstr; + int a_cnt, el_cnt; + + for (el_cnt = 0; el_cnt < QSELEM__MAX; el_cnt++) { + if (strcmp(key, querystring_keys[el_cnt].name) != 0) + continue; + + switch (querystring_keys[el_cnt].element) { + case ACTION: + for (a_cnt = 0; a_cnt < ACTIONS__MAX; a_cnt++) { + if (strcmp(value, action_keys[a_cnt].name) != 0) + continue; + else if (strcmp(value, + action_keys[a_cnt].name) == 0){ + (*qs)->action = + action_keys[a_cnt].action; + goto qa_found; + } + } + (*qs)->action = ERR; +qa_found: + break; + case COMMIT: + (*qs)->commit = strdup(value); + if ((*qs)->commit == NULL) { + error = got_error_from_errno2("%s: strdup", + __func__); + goto done; + } + break; + case RFILE: + (*qs)->file = strdup(value); + if ((*qs)->file == NULL) { + error = got_error_from_errno2("%s: strdup", + __func__); + goto done; + } + break; + case FOLDER: + (*qs)->folder = strdup(value); + if ((*qs)->folder == NULL) { + error = got_error_from_errno2("%s: strdup", + __func__); + goto done; + } + break; + case HEADREF: + (*qs)->headref = strdup(value); + if ((*qs)->headref == NULL) { + error = got_error_from_errno2("%s: strdup", + __func__); + goto done; + } + break; + case INDEX_PAGE: + if (strlen(value) == 0) + break; + (*qs)->index_page_str = strdup(value); + if ((*qs)->index_page_str == NULL) { + error = got_error_from_errno2("%s: strdup", + __func__); + goto done; + } + (*qs)->index_page = strtonum(value, INT64_MIN, + INT64_MAX, &errstr); + if (errstr) { + error = got_error_from_errno3("%s: strtonum %s", + __func__, errstr); + goto done; + } + if ((*qs)->index_page < 0) { + (*qs)->index_page = 0; + sprintf((*qs)->index_page_str, "%d", 0); + } + break; + case PATH: + (*qs)->path = strdup(value); + if ((*qs)->path == NULL) { + error = got_error_from_errno2("%s: strdup", + __func__); + goto done; + } + break; + case PAGE: + if (strlen(value) == 0) + break; + (*qs)->page_str = strdup(value); + if ((*qs)->page_str == NULL) { + error = got_error_from_errno2("%s: strdup", + __func__); + goto done; + } + (*qs)->page = strtonum(value, INT64_MIN, + INT64_MAX, &errstr); + if (errstr) { + error = got_error_from_errno3("%s: strtonum %s", + __func__, errstr); + goto done; + } + if ((*qs)->page < 0) { + (*qs)->page = 0; + sprintf((*qs)->page_str, "%d", 0); + } + break; + default: + break; + } + } +done: + return error; +} + +void +gotweb_free_repo_tag(struct repo_tag *rt) +{ + if (rt != NULL) { + free(rt->commit_msg); + free(rt->commit_id); + free(rt->tagger); + } + free(rt); +} + +void +gotweb_free_repo_commit(struct repo_commit *rc) +{ + if (rc != NULL) { + free(rc->path); + free(rc->refs_str); + free(rc->commit_id); + free(rc->parent_id); + free(rc->tree_id); + free(rc->author); + free(rc->committer); + free(rc->commit_msg); + } + free(rc); +} + +static void +gotweb_free_querystring(struct querystring *qs) +{ + if (qs != NULL) { + free(qs->commit); + free(qs->file); + free(qs->folder); + free(qs->headref); + free(qs->index_page_str); + free(qs->path); + free(qs->page_str); + } + free(qs); +} + +static void +gotweb_free_repo_dir(struct repo_dir *repo_dir) +{ + if (repo_dir != NULL) { + free(repo_dir->name); + free(repo_dir->owner); + free(repo_dir->description); + free(repo_dir->url); + free(repo_dir->age); + free(repo_dir->path); + } + free(repo_dir); +} + +void +gotweb_free_transport(struct transport *t) +{ + struct repo_commit *rc = NULL, *trc = NULL; + struct repo_tag *rt = NULL, *trt = NULL; + + TAILQ_FOREACH_SAFE(rc, &t->repo_commits, entry, trc) { + TAILQ_REMOVE(&t->repo_commits, rc, entry); + gotweb_free_repo_commit(rc); + } + TAILQ_FOREACH_SAFE(rt, &t->repo_tags, entry, trt) { + TAILQ_REMOVE(&t->repo_tags, rt, entry); + gotweb_free_repo_tag(rt); + } + gotweb_free_repo_dir(t->repo_dir); + gotweb_free_querystring(t->qs); + if (t != NULL) { + free(t->next_id); + free(t->prev_id); + } + free(t); +} + +const struct got_error * +gotweb_render_content_type(struct request *c, const uint8_t *type) +{ + const struct got_error *error = NULL; + char *h = NULL; + + if (asprintf(&h, "Content-type: %s\r\n\r\n", type) == -1) { + error = got_error_from_errno2("%s: asprintf", __func__); + goto done; + } + + fcgi_gen_response(c, h); +done: + free(h); + + return error; +} + +const struct got_error * +gotweb_render_content_type_file(struct request *c, const uint8_t *type, + char *file) +{ + const struct got_error *error = NULL; + char *h = NULL; + + if (asprintf(&h, "Content-type: %s\r\n" + "Content-disposition: attachment; filename=%s\r\n\r\n", + type, file) == -1) { + error = got_error_from_errno2("%s: asprintf", __func__); + goto done; + } + + fcgi_gen_response(c, h); +done: + free(h); + + return error; +} + +static const struct got_error * +gotweb_render_header(struct request *c) +{ + const struct got_error *error = NULL; + struct server *srv = c->srv; + struct querystring *qs = c->t->qs; + char *title = NULL, *droot = NULL, *css = NULL, *gotlink = NULL; + char *gotimg = NULL, *sitelink = NULL, *summlink = NULL; + + if (strlen(c->document_root) > 0) { + if (asprintf(&droot, "/%s/", c->document_root) == -1) { + error = got_error_from_errno2("%s: asprintf", __func__); + goto done; + } + } else { + if (asprintf(&droot, "/") == -1) { + error = got_error_from_errno2("%s: asprintf", __func__); + goto done; + } + } + + if (asprintf(&title, "%s\n", srv->site_name) == -1) { + error = got_error_from_errno2("%s: asprintf", __func__); + goto done; + } + if (asprintf(&css, + "\n", + droot, srv->custom_css) == -1) { + error = got_error_from_errno2("%s: asprintf", __func__); + goto done; + } + if (asprintf(&gotlink, "", + srv->logo_url) == -1) { + error = got_error_from_errno2("%s: asprintf", __func__); + goto done; + } + if (asprintf(&gotimg, "", + droot, srv->logo) == -1) { + error = got_error_from_errno2("%s: asprintf", __func__); + goto done; + } + if (asprintf(&sitelink, "%s", c->document_root, qs->index_page, + srv->site_link) == -1) { + error = got_error_from_errno2("%s: asprintf", __func__); + goto done; + } + if (asprintf(&summlink, "%s", c->document_root, + qs->index_page, qs->path, qs->path) == -1) { + error = got_error_from_errno2("%s: asprintf", __func__); + goto done; + } + + if (fcgi_gen_response(c, "\n\n") == -1) + goto done; + if (fcgi_gen_response(c, title) == -1) + goto done; + if (fcgi_gen_response(c, "\n") == -1) + goto done; + if (fcgi_gen_response(c, "\n") == -1) + goto done; + if (fcgi_gen_response(c, "\n") == -1) + goto done; + if (fcgi_gen_response(c, + "\n") == -1) + goto done; + if (fcgi_gen_response(c, "\n") == -1) + goto done; + if (fcgi_gen_response(c, + "\n") == -1) + goto done; + if (fcgi_gen_response(c, "\n") == -1) + goto done; + if (fcgi_gen_response(c, "\n") == -1) + goto done; + if (fcgi_gen_response(c, "\n") == -1) + goto done; + if (fcgi_gen_response(c, css) == -1) + goto done; + if (fcgi_gen_response(c, "\n\n
\n") == -1) + goto done; + if (fcgi_gen_response(c, + "\n") == -1) + goto done; + if (fcgi_gen_response(c, + "
\n\n
\n
\n"); +done: + free(title); + free(droot); + free(css); + free(gotlink); + free(gotimg); + free(sitelink); + free(summlink); + + return error; +} + +static const struct got_error * +gotweb_render_footer(struct request *c) +{ + const struct got_error *error = NULL; + struct server *srv = c->srv; + char *siteowner = NULL; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (srv->show_site_owner) { + error = gotweb_escape_html(&siteowner, srv->site_owner); + if (error) + goto done; + if (fcgi_gen_response(c, siteowner) == -1) + goto done; + } else + if (fcgi_gen_response(c, " ") == -1) + goto done; + fcgi_gen_response(c, "
\n
\n
\n\n"); +done: + free(siteowner); + + return error; +} + +static const struct got_error * +gotweb_render_navs(struct request *c) +{ + const struct got_error *error = NULL; + struct transport *t = c->t; + struct querystring *qs = t->qs; + struct server *srv = c->srv; + char *nhref = NULL, *phref = NULL; + int disp = 0; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "\n") == -1) + goto done; + if (fcgi_gen_response(c, "\n"); +done: + free(t->next_id); + t->next_id = NULL; + free(t->prev_id); + t->prev_id = NULL; + free(phref); + free(nhref); + return error; +} + +static const struct got_error * +gotweb_render_index(struct request *c) +{ + const struct got_error *error = NULL; + struct server *srv = c->srv; + struct transport *t = c->t; + struct querystring *qs = t->qs; + struct repo_dir *repo_dir = NULL; + DIR *d; + struct dirent **sd_dent; + char *c_path = NULL; + struct stat st; + unsigned int d_cnt, d_i, d_disp = 0; + + d = opendir(srv->repos_path); + if (d == NULL) { + error = got_error_from_errno2("opendir", srv->repos_path); + return error; + } + + d_cnt = scandir(srv->repos_path, &sd_dent, NULL, alphasort); + if (d_cnt == -1) { + error = got_error_from_errno2("scandir", srv->repos_path); + goto done; + } + + /* get total count of repos */ + for (d_i = 0; d_i < d_cnt; d_i++) { + if (strcmp(sd_dent[d_i]->d_name, ".") == 0 || + strcmp(sd_dent[d_i]->d_name, "..") == 0) + continue; + + if (asprintf(&c_path, "%s/%s", srv->repos_path, + sd_dent[d_i]->d_name) == -1) { + error = got_error_from_errno("asprintf"); + return error; + } + + if (lstat(c_path, &st) == 0 && S_ISDIR(st.st_mode) && + !got_path_dir_is_empty(c_path)) + t->repos_total++; + free(c_path); + c_path = NULL; + } + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, + "
Project
\n") == -1) + goto done; + if (srv->show_repo_description) + if (fcgi_gen_response(c, "
" + "Description
\n") == -1) + goto done; + if (srv->show_repo_owner) + if (fcgi_gen_response(c, "
" + "Owner
\n") == -1) + goto done; + if (srv->show_repo_age) + if (fcgi_gen_response(c, "
" + "Last Change
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + for (d_i = 0; d_i < d_cnt; d_i++) { + if (srv->max_repos > 0 && (d_i - 2) == srv->max_repos) + break; /* account for parent and self */ + + if (strcmp(sd_dent[d_i]->d_name, ".") == 0 || + strcmp(sd_dent[d_i]->d_name, "..") == 0) + continue; + + if (qs->index_page > 0 && (qs->index_page * + srv->max_repos_display) > t->prev_disp) { + t->prev_disp++; + continue; + } + + error = gotweb_init_repo_dir(&repo_dir, sd_dent[d_i]->d_name); + if (error) + goto done; + + error = gotweb_load_got_path(c, repo_dir); + if (error && error->code == GOT_ERR_NOT_GIT_REPO) { + error = NULL; + continue; + } + else if (error && error->code != GOT_ERR_LONELY_PACKIDX) + goto done; + + if (lstat(repo_dir->path, &st) == 0 && + S_ISDIR(st.st_mode) && + !got_path_dir_is_empty(repo_dir->path)) + goto render; + else { + gotweb_free_repo_dir(repo_dir); + repo_dir = NULL; + continue; + } +render: + d_disp++; + t->prev_disp++; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + + if (fcgi_gen_response(c, "") == -1) + goto done; + if (fcgi_gen_response(c, repo_dir->name) == -1) + goto done; + if (fcgi_gen_response(c, "") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (srv->show_repo_description) { + if (fcgi_gen_response(c, + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, repo_dir->description) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + } + + if (srv->show_repo_owner) { + if (fcgi_gen_response(c, + "
") == -1) + goto done; + if (fcgi_gen_response(c, repo_dir->owner) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + } + + if (srv->show_repo_age) { + if (fcgi_gen_response(c, + "
") == -1) + goto done; + if (fcgi_gen_response(c, repo_dir->age) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + } + + if (fcgi_gen_response(c, "\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + gotweb_free_repo_dir(repo_dir); + repo_dir = NULL; + error = got_repo_close(t->repo); + if (error) + goto done; + t->next_disp++; + if (d_disp == srv->max_repos_display) + break; + } + if (srv->max_repos_display == 0) + goto div; + if (srv->max_repos > 0 && srv->max_repos < srv->max_repos_display) + goto div; + if (t->repos_total <= srv->max_repos || + t->repos_total <= srv->max_repos_display) + goto div; + + error = gotweb_render_navs(c); + if (error) + goto done; +div: + fcgi_gen_response(c, "
\n"); +done: + if (d != NULL && closedir(d) == EOF && error == NULL) + error = got_error_from_errno("closedir"); + return error; +} + +static const struct got_error * +gotweb_render_blame(struct request *c) +{ + const struct got_error *error = NULL; + struct transport *t = c->t; + struct repo_commit *rc = NULL; + char *age = NULL; + + error = got_get_repo_commits(c, 1); + if (error) + return error; + + rc = TAILQ_FIRST(&t->repo_commits); + + error = gotweb_get_time_str(&age, rc->committer_time, TM_LONG); + if (error) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
Blame
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Date:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, age ? age : "") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Message:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, rc->commit_msg) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + error = got_output_file_blame(c); + if (error) + goto done; + + fcgi_gen_response(c, "
\n"); +done: + fcgi_gen_response(c, "
\n"); + return error; +} + +static const struct got_error * +gotweb_render_briefs(struct request *c) +{ + const struct got_error *error = NULL; + struct repo_commit *rc = NULL; + struct server *srv = c->srv; + struct transport *t = c->t; + struct querystring *qs = t->qs; + struct repo_dir *repo_dir = t->repo_dir; + char *smallerthan, *newline; + char *age = NULL; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, + "
Commit Briefs
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (qs->action == SUMMARY) { + qs->action = BRIEFS; + error = got_get_repo_commits(c, D_MAXSLCOMMDISP); + } else + error = got_get_repo_commits(c, srv->max_commits_display); + if (error) + goto done; + + TAILQ_FOREACH(rc, &t->repo_commits, entry) { + error = gotweb_get_time_str(&age, rc->committer_time, TM_DIFF); + if (error) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, age ? age : "") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + smallerthan = strchr(rc->author, '<'); + if (smallerthan) + *smallerthan = '\0'; + if (fcgi_gen_response(c, rc->author) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + newline = strchr(rc->commit_msg, '\n'); + if (newline) + *newline = '\0'; + + if (fcgi_gen_response(c, "") == -1) + goto done; + if (fcgi_gen_response(c, rc->commit_msg) == -1) + goto done; + if (fcgi_gen_response(c, "") == -1) + goto done; + if (rc->refs_str) { + if (fcgi_gen_response(c, + " (") == -1) + goto done; + if (fcgi_gen_response(c, rc->refs_str) == -1) + goto done; + if (fcgi_gen_response(c, ")") == -1) + goto done; + } + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "\n") == -1) + goto done; + if (fcgi_gen_response(c, + "
\n") == -1) + goto done; + + free(age); + age = NULL; + } + + if (t->next_id || t->prev_id) { + error = gotweb_render_navs(c); + if (error) + goto done; + } + fcgi_gen_response(c, "
\n"); +done: + free(age); + return error; +} + +static const struct got_error * +gotweb_render_commits(struct request *c) +{ + const struct got_error *error = NULL; + struct repo_commit *rc = NULL; + struct server *srv = c->srv; + struct transport *t = c->t; + struct querystring *qs = t->qs; + struct repo_dir *repo_dir = t->repo_dir; + char *age = NULL, *author = NULL; + /* int commit_found = 0; */ + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, + "
Commits
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + error = got_get_repo_commits(c, srv->max_commits_display); + if (error) + goto done; + + TAILQ_FOREACH(rc, &t->repo_commits, entry) { + error = gotweb_get_time_str(&age, rc->committer_time, TM_LONG); + if (error) + goto done; + error = gotweb_escape_html(&author, rc->author); + if (error) + goto done; + + if (fcgi_gen_response(c, + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + + if (fcgi_gen_response(c, "
Commit:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, rc->commit_id) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Author:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, author ? author : "") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Date:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, age ? age : "") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, rc->commit_msg) == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "\n") == -1) + goto done; + if (fcgi_gen_response(c, + "
\n") == -1) + goto done; + free(age); + age = NULL; + free(author); + author = NULL; + } + + if (t->next_id || t->prev_id) { + error = gotweb_render_navs(c); + if (error) + goto done; + } + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + fcgi_gen_response(c, "\n"); +done: + free(age); + return error; +} + +static const struct got_error * +gotweb_render_branches(struct request *c) +{ + const struct got_error *error = NULL; + struct got_reflist_head refs; + struct got_reflist_entry *re; + struct transport *t = c->t; + struct querystring *qs = t->qs; + struct got_repository *repo = t->repo; + char *age = NULL; + + TAILQ_INIT(&refs); + + error = got_ref_list(&refs, repo, "refs/heads", + got_ref_cmp_by_name, NULL); + if (error) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, + "
Branches
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + TAILQ_FOREACH(re, &refs, entry) { + char *refname = NULL; + + if (got_ref_is_symbolic(re->ref)) + continue; + + refname = strdup(got_ref_get_name(re->ref)); + if (refname == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + if (strncmp(refname, "refs/heads/", 11) != 0) + continue; + + error = got_get_repo_age(&age, c, qs->path, refname, + TM_DIFF); + if (error) + goto done; + + if (strncmp(refname, "refs/heads/", 11) == 0) + refname += 11; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, age ? age : "") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, " ") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, "") == -1) + goto done; + if (fcgi_gen_response(c, refname) == -1) + goto done; + if (fcgi_gen_response(c, "") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "\n") == -1) + goto done; + + if (fcgi_gen_response(c, + "
\n") == -1) + goto done; + + free(age); + age = NULL; + + } + fcgi_gen_response(c, "
\n"); +done: + return error; +} + +static const struct got_error * +gotweb_render_tree(struct request *c) +{ + const struct got_error *error = NULL; + struct transport *t = c->t; + struct repo_commit *rc = NULL; + char *age = NULL; + + error = got_get_repo_commits(c, 1); + if (error) + return error; + + rc = TAILQ_FIRST(&t->repo_commits); + + error = gotweb_get_time_str(&age, rc->committer_time, TM_LONG); + if (error) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
Tree
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Tree:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, rc->tree_id) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Date:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, age ? age : "") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Message:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, rc->commit_msg) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + error = got_output_repo_tree(c); + if (error) + goto done; + + fcgi_gen_response(c, "
\n"); + fcgi_gen_response(c, "
\n"); +done: + return error; +} + +static const struct got_error * +gotweb_render_diff(struct request *c) +{ + const struct got_error *error = NULL; + struct transport *t = c->t; + struct repo_commit *rc = NULL; + char *age = NULL, *author = NULL; + + error = got_get_repo_commits(c, 1); + if (error) + return error; + + rc = TAILQ_FIRST(&t->repo_commits); + + error = gotweb_get_time_str(&age, rc->committer_time, TM_LONG); + if (error) + goto done; + error = gotweb_escape_html(&author, rc->author); + if (error) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, + "
Commit Diff
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Diff:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, rc->parent_id) == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, rc->commit_id) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Commit:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, rc->commit_id) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Tree:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, rc->tree_id) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Author:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, author ? author : "") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Date:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, age ? age : "") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Message:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, rc->commit_msg) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + error = got_output_repo_diff(c); + if (error) + goto done; + + fcgi_gen_response(c, "
\n"); +done: + fcgi_gen_response(c, "
\n"); + free(age); + free(author); + return error; +} + +static const struct got_error * +gotweb_render_summary(struct request *c) +{ + const struct got_error *error = NULL; + struct transport *t = c->t; + struct server *srv = c->srv; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (!srv->show_repo_description) + goto owner; + + if (fcgi_gen_response(c, "
" + "Description:
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, t->repo_dir->description) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; +owner: + if (!srv->show_repo_owner) + goto last_change; + + if (fcgi_gen_response(c, "
" + "Owner:
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, t->repo_dir->owner) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; +last_change: + if (!srv->show_repo_age) + goto clone_url; + + if (fcgi_gen_response(c, "
" + "Last Change:
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, t->repo_dir->age) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; +clone_url: + if (!srv->show_repo_cloneurl) + goto content; + + if (fcgi_gen_response(c, "
" + "Clone URL:
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, t->repo_dir->url) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; +content: + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + error = gotweb_render_briefs(c); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto done; + } + + error = gotweb_render_tags(c); + if (error) { + log_warnx("%s: %s", __func__, error->msg); + goto done; + } + + error = gotweb_render_branches(c); + if (error) + log_warnx("%s: %s", __func__, error->msg); +done: + return error; +} + +static const struct got_error * +gotweb_render_tag(struct request *c) +{ + const struct got_error *error = NULL; + struct repo_tag *rt = NULL; + struct transport *t = c->t; + char *age = NULL, *author = NULL; + + error = got_get_repo_tags(c, 1); + if (error) + goto done; + + if (t->tag_count == 0) { + error = got_error_set_errno(GOT_ERR_BAD_OBJ_ID, + "bad commit id"); + goto done; + } + + rt = TAILQ_LAST(&t->repo_tags, repo_tags_head); + + error = gotweb_get_time_str(&age, rt->tagger_time, TM_LONG); + if (error) + goto done; + error = gotweb_escape_html(&author, rt->tagger); + if (error) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
Tag
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Commit:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, rt->commit_id) == -1) + goto done; + + if (strncmp(rt->tag_name, "refs/", 5) == 0) + rt->tag_name += 5; + + if (fcgi_gen_response(c, " (") == -1) + goto done; + if (fcgi_gen_response(c, rt->tag_name) == -1) + goto done; + if (fcgi_gen_response(c, ")") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Tagger:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, author ? author : "") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Date:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, age ? age : "") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
Message:" + "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, rt->commit_msg) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, rt->tag_commit) == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + fcgi_gen_response(c, "
\n"); +done: + free(age); + free(author); + return error; +} + +static const struct got_error * +gotweb_render_tags(struct request *c) +{ + const struct got_error *error = NULL; + struct repo_tag *rt = NULL; + struct server *srv = c->srv; + struct transport *t = c->t; + struct querystring *qs = t->qs; + struct repo_dir *repo_dir = t->repo_dir; + char *newline; + char *age = NULL; + int commit_found = 0; + + if (qs->action == BRIEFS) { + qs->action = TAGS; + error = got_get_repo_tags(c, D_MAXSLCOMMDISP); + } else + error = got_get_repo_tags(c, srv->max_commits_display); + if (error) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, + "
Tags
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (t->tag_count == 0) { + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, + "This repository contains no tags\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + } + + TAILQ_FOREACH(rt, &t->repo_tags, entry) { + if (commit_found == 0 && qs->commit != NULL) { + if (strcmp(qs->commit, rt->commit_id) != 0) + continue; + else + commit_found = 1; + } + error = gotweb_get_time_str(&age, rt->tagger_time, TM_DIFF); + if (error) + goto done; + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (fcgi_gen_response(c, age ? age : "") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (strncmp(rt->tag_name, "refs/tags/", 10) == 0) + rt->tag_name += 10; + if (fcgi_gen_response(c, rt->tag_name) == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "
") == -1) + goto done; + if (rt->tag_commit != NULL) { + newline = strchr(rt->tag_commit, '\n'); + if (newline) + *newline = '\0'; + } + + if (fcgi_gen_response(c, "") == -1) + goto done; + if (rt->tag_commit != NULL && + fcgi_gen_response(c, rt->tag_commit) == -1) + goto done; + if (fcgi_gen_response(c, "") == -1) + goto done; + if (fcgi_gen_response(c, "
\n") == -1) + goto done; + + if (fcgi_gen_response(c, "\n") == -1) + goto done; + if (fcgi_gen_response(c, + "
\n") == -1) + goto done; + + free(age); + age = NULL; + } + if (t->next_id || t->prev_id) { + error = gotweb_render_navs(c); + if (error) + goto done; + } + fcgi_gen_response(c, "
\n"); +done: + free(age); + return error; +} + +const struct got_error * +gotweb_escape_html(char **escaped_html, const char *orig_html) +{ + const struct got_error *error = NULL; + struct escape_pair { + char c; + const char *s; + } esc[] = { + { '>', ">" }, + { '<', "<" }, + { '&', "&" }, + { '"', """ }, + { '\'', "'" }, + { '\n', "
" }, + }; + size_t orig_len, len; + int i, j, x; + + orig_len = strlen(orig_html); + len = orig_len; + for (i = 0; i < orig_len; i++) { + for (j = 0; j < nitems(esc); j++) { + if (orig_html[i] != esc[j].c) + continue; + len += strlen(esc[j].s) - 1 /* escaped char */; + } + } + + *escaped_html = calloc(len + 1 /* NUL */, sizeof(**escaped_html)); + if (*escaped_html == NULL) + return got_error_from_errno("calloc"); + + x = 0; + for (i = 0; i < orig_len; i++) { + int escaped = 0; + for (j = 0; j < nitems(esc); j++) { + if (orig_html[i] != esc[j].c) + continue; + + if (strlcat(*escaped_html, esc[j].s, len + 1) + >= len + 1) { + error = got_error(GOT_ERR_NO_SPACE); + goto done; + } + x += strlen(esc[j].s); + escaped = 1; + break; + } + if (!escaped) { + (*escaped_html)[x] = orig_html[i]; + x++; + } + } +done: + if (error) { + free(*escaped_html); + *escaped_html = NULL; + } else { + (*escaped_html)[x] = '\0'; + } + + return error; +} + +static const struct got_error * +gotweb_load_got_path(struct request *c, struct repo_dir *repo_dir) +{ + const struct got_error *error = NULL; + struct socket *sock = c->sock; + struct server *srv = c->srv; + struct transport *t = c->t; + DIR *dt; + char *dir_test; + int opened = 0; + + if (asprintf(&dir_test, "%s/%s/%s", srv->repos_path, repo_dir->name, + GOTWEB_GIT_DIR) == -1) + return got_error_from_errno("asprintf"); + + dt = opendir(dir_test); + if (dt == NULL) { + free(dir_test); + } else { + repo_dir->path = strdup(dir_test); + if (repo_dir->path == NULL) { + opened = 1; + error = got_error_from_errno("strdup"); + goto err; + } + opened = 1; + goto done; + } + + if (asprintf(&dir_test, "%s/%s/%s", srv->repos_path, repo_dir->name, + GOTWEB_GOT_DIR) == -1) { + dir_test = NULL; + error = got_error_from_errno("asprintf"); + goto err; + } + + dt = opendir(dir_test); + if (dt == NULL) + free(dir_test); + else { + opened = 1; + error = got_error(GOT_ERR_NOT_GIT_REPO); + goto err; + } + + if (asprintf(&dir_test, "%s/%s", srv->repos_path, + repo_dir->name) == -1) { + error = got_error_from_errno("asprintf"); + dir_test = NULL; + goto err; + } + + repo_dir->path = strdup(dir_test); + if (repo_dir->path == NULL) { + opened = 1; + error = got_error_from_errno("strdup"); + goto err; + } + + dt = opendir(dir_test); + if (dt == NULL) { + error = got_error_path(repo_dir->name, GOT_ERR_NOT_GIT_REPO); + goto err; + } else + opened = 1; +done: + error = got_repo_open(&t->repo, repo_dir->path, NULL, sock->pack_fds); + if (error) + goto err; + error = gotweb_get_repo_description(&repo_dir->description, srv, + repo_dir->path); + if (error) + goto err; + error = got_get_repo_owner(&repo_dir->owner, c, repo_dir->path); + if (error) + goto err; + error = got_get_repo_age(&repo_dir->age, c, repo_dir->path, + NULL, TM_DIFF); + if (error) + goto err; + error = gotweb_get_clone_url(&repo_dir->url, srv, repo_dir->path); +err: + free(dir_test); + if (opened) + if (dt != NULL && closedir(dt) == EOF && error == NULL) + error = got_error_from_errno("closedir"); + return error; +} + +static const struct got_error * +gotweb_init_repo_dir(struct repo_dir **repo_dir, const char *dir) +{ + const struct got_error *error; + + *repo_dir = calloc(1, sizeof(**repo_dir)); + if (*repo_dir == NULL) + return got_error_from_errno("calloc"); + + if (asprintf(&(*repo_dir)->name, "%s", dir) == -1) { + error = got_error_from_errno("asprintf"); + free(*repo_dir); + *repo_dir = NULL; + return error; + } + (*repo_dir)->owner = NULL; + (*repo_dir)->description = NULL; + (*repo_dir)->url = NULL; + (*repo_dir)->age = NULL; + (*repo_dir)->path = NULL; + + return NULL; +} + +static const struct got_error * +gotweb_get_repo_description(char **description, struct server *srv, char *dir) +{ + const struct got_error *error = NULL; + FILE *f = NULL; + char *d_file = NULL; + unsigned int len; + size_t n; + + *description = NULL; + if (srv->show_repo_description == 0) + return NULL; + + if (asprintf(&d_file, "%s/description", dir) == -1) + return got_error_from_errno("asprintf"); + + f = fopen(d_file, "r"); + if (f == NULL) { + if (errno == ENOENT || errno == EACCES) + return NULL; + error = got_error_from_errno2("fopen", d_file); + goto done; + } + + if (fseek(f, 0, SEEK_END) == -1) { + error = got_ferror(f, GOT_ERR_IO); + goto done; + } + len = ftell(f); + if (len == -1) { + error = got_ferror(f, GOT_ERR_IO); + goto done; + } + + if (len == 0) + goto done; + + if (fseek(f, 0, SEEK_SET) == -1) { + error = got_ferror(f, GOT_ERR_IO); + goto done; + } + *description = calloc(len + 1, sizeof(**description)); + if (*description == NULL) { + error = got_error_from_errno("calloc"); + goto done; + } + + n = fread(*description, 1, len, f); + if (n == 0 && ferror(f)) + error = got_ferror(f, GOT_ERR_IO); +done: + if (f != NULL && fclose(f) == EOF && error == NULL) + error = got_error_from_errno("fclose"); + free(d_file); + return error; +} + +static const struct got_error * +gotweb_get_clone_url(char **url, struct server *srv, char *dir) +{ + const struct got_error *error = NULL; + FILE *f; + char *d_file = NULL; + unsigned int len; + size_t n; + + *url = NULL; + + if (srv->show_repo_cloneurl == 0) + return NULL; + + if (asprintf(&d_file, "%s/cloneurl", dir) == -1) + return got_error_from_errno("asprintf"); + + f = fopen(d_file, "r"); + if (f == NULL) { + if (errno != ENOENT && errno != EACCES) + error = got_error_from_errno2("fopen", d_file); + goto done; + } + + if (fseek(f, 0, SEEK_END) == -1) { + error = got_ferror(f, GOT_ERR_IO); + goto done; + } + len = ftell(f); + if (len == -1) { + error = got_ferror(f, GOT_ERR_IO); + goto done; + } + if (len == 0) + goto done; + + if (fseek(f, 0, SEEK_SET) == -1) { + error = got_ferror(f, GOT_ERR_IO); + goto done; + } + + *url = calloc(len + 1, sizeof(**url)); + if (*url == NULL) { + error = got_error_from_errno("calloc"); + goto done; + } + + n = fread(*url, 1, len, f); + if (n == 0 && ferror(f)) + error = got_ferror(f, GOT_ERR_IO); +done: + if (f != NULL && fclose(f) == EOF && error == NULL) + error = got_error_from_errno("fclose"); + free(d_file); + return error; +} + +const struct got_error * +gotweb_get_time_str(char **repo_age, time_t committer_time, int ref_tm) +{ + struct tm tm; + time_t diff_time; + const char *years = "years ago", *months = "months ago"; + const char *weeks = "weeks ago", *days = "days ago"; + const char *hours = "hours ago", *minutes = "minutes ago"; + const char *seconds = "seconds ago", *now = "right now"; + char *s; + char datebuf[29]; + + *repo_age = NULL; + + switch (ref_tm) { + case TM_DIFF: + diff_time = time(NULL) - committer_time; + if (diff_time > 60 * 60 * 24 * 365 * 2) { + if (asprintf(repo_age, "%lld %s", + (diff_time / 60 / 60 / 24 / 365), years) == -1) + return got_error_from_errno("asprintf"); + } else if (diff_time > 60 * 60 * 24 * (365 / 12) * 2) { + if (asprintf(repo_age, "%lld %s", + (diff_time / 60 / 60 / 24 / (365 / 12)), + months) == -1) + return got_error_from_errno("asprintf"); + } else if (diff_time > 60 * 60 * 24 * 7 * 2) { + if (asprintf(repo_age, "%lld %s", + (diff_time / 60 / 60 / 24 / 7), weeks) == -1) + return got_error_from_errno("asprintf"); + } else if (diff_time > 60 * 60 * 24 * 2) { + if (asprintf(repo_age, "%lld %s", + (diff_time / 60 / 60 / 24), days) == -1) + return got_error_from_errno("asprintf"); + } else if (diff_time > 60 * 60 * 2) { + if (asprintf(repo_age, "%lld %s", + (diff_time / 60 / 60), hours) == -1) + return got_error_from_errno("asprintf"); + } else if (diff_time > 60 * 2) { + if (asprintf(repo_age, "%lld %s", (diff_time / 60), + minutes) == -1) + return got_error_from_errno("asprintf"); + } else if (diff_time > 2) { + if (asprintf(repo_age, "%lld %s", diff_time, + seconds) == -1) + return got_error_from_errno("asprintf"); + } else { + if (asprintf(repo_age, "%s", now) == -1) + return got_error_from_errno("asprintf"); + } + break; + case TM_LONG: + if (gmtime_r(&committer_time, &tm) == NULL) + return got_error_from_errno("gmtime_r"); + + s = asctime_r(&tm, datebuf); + if (s == NULL) + return got_error_from_errno("asctime_r"); + + if (asprintf(repo_age, "%s UTC", datebuf) == -1) + return got_error_from_errno("asprintf"); + break; + } + return NULL; +} \ No newline at end of file blob - /dev/null blob + d73977323f12761149928c250d279753f884a9b3 (mode 644) --- /dev/null +++ gotwebd/gotwebd.8 @@ -0,0 +1,152 @@ +.\" +.\" Copyright (c) 2020 Stefan Sperling +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate$ +.Dt GOTWEB 8 +.Os +.Sh NAME +.Nm gotweb +.Nd Game of Trees Git repository server for web browsers -- which obviously +needs to be updated to gotwebd +.Sh SYNOPSIS +.Nm +.Sh DESCRIPTION +.Nm +provides a web interface allowing Git repository contents to be viewed +with a web browser. +.Pp +.Nm +is a CGI program based on +.Xr got 1 +and +.Xr kcgi 3 +which is intended to run in a +.Xr chroot 2 +environment in +.Pa /var/www . +The program has been designed to work out of the box with +the +.Xr httpd 8 +web server in conjunction with +.Xr slowcgi 8 . +.Pp +Enabling +.Nm +requires the following steps: +.Bl -enum +.It +The +.Xr httpd.conf 5 +configuration file must be adjusted to run +.Nm +as a CGI program with +.Xr slowcgi 8 . +The +.Sx EXAMPLES +section below contains an appropriate configuration file sample. +.It +httpd(8) and slowcgi(8) must be enabled and started: +.Bd -literal -offset indent + # rcctl enable httpd slowcgi + # rcctl start httpd slowcgi +.Ed +.It +Optionally, the run-time behaviour of +.Nm +can be configured via the +.Xr gotweb.conf 5 +configuration file. +.It +Git repositories must be created at a suitable location inside the +web server's +.Xr chroot 2 +environment. +These repositories should +.Em not +be writable by the user ID of the +.Xr httpd 8 +server. +The default location for repositories published by +.Nm +is +.Pa /var/www/got/public . +.It +Git repositories served by +.Nm +should be kept up-to-date with a mechanism such as +.Cm got fetch , +.Xr git-fetch 1 , +or +.Xr rsync 1 , +scheduled by +.Xr cron 8 . +.El +.Sh FILES +.Bl -tag -width /var/www/got/public/ -compact +.It Pa /var/www/got/public/ +Default location for Git repositories served by +.Nm . +This location can be adjusted in the +.Xr gotweb.conf 5 +configuration file. +.It Pa /var/www/cgi-bin/gotweb/gotweb +The +.Nm +CGI program, statically linked for use in a +.Xr chroot 2 +environment. +.It Pa /var/www/cgi-bin/gotweb/gw_tmpl/ +Directory for template files used by +.Nm . +.It Pa /var/www/cgi-bin/gotweb/libexec/ +Directory containing statically linked +.Xr got 1 +helper programs which are run by +.Nm +to read Git repositories. +.It Pa /var/www/htdocs/gotweb/ +Directory containing HTML, CSS, and image files used by +.Nm . +.It Pa /var/www/got/tmp/ +Directory for temporary files created by +.Nm . +.El +.Sh EXAMPLES +Example configuration for httpd.conf: +.Bd -literal -offset indent + + types { include "/usr/share/misc/mime.types" } + server "gotweb.example.com" { + listen on * port 80 + root "/htdocs/gotweb" + location "/cgi-bin/*" { + root "/" + fastcgi + } + location "/*" { + directory index "index.html" + } + } +.Ed +.Sh SEE ALSO +.Xr got 1 , +.Xr kcgi 3 , +.Xr git-repository 5 , +.Xr gotweb.conf 5 , +.Xr httpd 8 , +.Xr slowcgi 8 +.Sh AUTHORS +.An Tracey Emery Aq Mt tracey@traceyemery.net +.An Stefan Sperling Aq Mt stsp@openbsd.org blob - /dev/null blob + c6930e3119c2231e948970c004f3f21d33c34c16 (mode 644) --- /dev/null +++ gotwebd/gotwebd.c @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2016, 2019, 2020-2021 Tracey Emery + * Copyright (c) 2015 Reyk Floeter + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "got_opentemp.h" + +#include "proc.h" +#include "gotwebd.h" + +__dead void usage(void); + +int main(int, char **); +int gotwebd_configure(struct gotwebd *); +void gotwebd_configure_done(struct gotwebd *); +void gotwebd_sighdlr(int sig, short event, void *arg); +void gotwebd_shutdown(void); +int gotwebd_dispatch_sockets(int, struct privsep_proc *, struct imsg *); + +struct gotwebd *gotwebd_env; + +static struct privsep_proc procs[] = { + { "sockets", PROC_SOCKS, gotwebd_dispatch_sockets, sockets, + sockets_shutdown }, +}; + +int +gotwebd_dispatch_sockets(int fd, struct privsep_proc *p, struct imsg *imsg) +{ + struct privsep *ps = p->p_ps; + struct gotwebd *env = ps->ps_env; + + switch (imsg->hdr.type) { + case IMSG_CFG_DONE: + gotwebd_configure_done(env); + break; + default: + return (-1); + } + + return (0); +} + +void +gotwebd_sighdlr(int sig, short event, void *arg) +{ + /* struct privsep *ps = arg; */ + + if (privsep_process != PROC_GOTWEBD) + return; + + switch (sig) { + case SIGHUP: + log_info("%s: ignoring SIGHUP", __func__); + break; + case SIGPIPE: + log_info("%s: ignoring SIGPIPE", __func__); + break; + case SIGUSR1: + log_info("%s: ignoring SIGUSR1", __func__); + break; + case SIGTERM: + case SIGINT: + gotwebd_shutdown(); + break; + default: + fatalx("unexpected signal"); + } +} + +__dead void +usage(void) +{ + fprintf(stderr, "usage: %s [-dnv] [-D macro=value] [-f file]\n", + getprogname()); + exit(1); +} + +int +main(int argc, char **argv) +{ + struct gotwebd *env; + struct privsep *ps; + unsigned int proc; + int ch; + const char *conffile = GOTWEBD_CONF; + enum privsep_procid proc_id = PROC_GOTWEBD; + int proc_instance = 0; + const char *errp, *title = NULL; + int argc0 = argc; + + env = calloc(1, sizeof(*env)); + if (env == NULL) + fatal("%s: calloc", __func__); + + /* XXX: add s and S for both sockets */ + while ((ch = getopt(argc, argv, "D:P:I:df:vn")) != -1) { + switch (ch) { + case 'D': + if (cmdline_symset(optarg) < 0) + log_warnx("could not parse macro definition %s", + optarg); + break; + case 'd': + env->gotwebd_debug = 2; + break; + case 'f': + conffile = optarg; + break; + case 'v': + env->gotwebd_verbose++; + break; + case 'n': + env->gotwebd_debug = 2; + env->gotwebd_noaction = 1; + break; + case 'P': + title = optarg; + proc_id = proc_getid(procs, nitems(procs), title); + if (proc_id == PROC_MAX) + fatalx("invalid process name"); + break; + case 'I': + proc_instance = strtonum(optarg, 0, + PROC_MAX_INSTANCES, &errp); + if (errp) + fatalx("invalid process instance"); + break; + default: + usage(); + } + } + + /* log to stderr until daemonized */ + log_init(env->gotwebd_debug ? env->gotwebd_debug : 1, LOG_DAEMON); + + argc -= optind; + if (argc > 0) + usage(); + + ps = calloc(1, sizeof(*ps)); + if (ps == NULL) + fatal("%s: calloc:", __func__); + + gotwebd_env = env; + env->gotwebd_ps = ps; + ps->ps_env = env; + env->gotwebd_conffile = conffile; + + if (parse_config(env->gotwebd_conffile, env) == -1) + exit(1); + + if (env->gotwebd_noaction && !env->gotwebd_debug) + env->gotwebd_debug = 1; + + /* check for root privileges */ + if (env->gotwebd_noaction == 0) { + if (geteuid()) + fatalx("need root privileges"); + } + + ps->ps_pw = getpwnam(GOTWEBD_USER); + if (ps->ps_pw == NULL) + fatalx("unknown user %s", GOTWEBD_USER); + + log_init(env->gotwebd_debug, LOG_DAEMON); + log_setverbose(env->gotwebd_verbose); + + if (env->gotwebd_noaction) + ps->ps_noaction = 1; + + ps->ps_instances[PROC_SOCKS] = env->prefork_gotwebd; + ps->ps_instance = proc_instance; + if (title != NULL) + ps->ps_title[proc_id] = title; + + for (proc = 0; proc < nitems(procs); proc++) + procs[proc].p_chroot = strlen(env->httpd_chroot) ? + env->httpd_chroot : D_HTTPD_CHROOT; + + /* only the gotwebd returns */ + proc_init(ps, procs, nitems(procs), argc0, argv, proc_id); + + log_procinit("gotwebd"); + if (!env->gotwebd_debug && daemon(0, 0) == -1) + fatal("can't daemonize"); + + if (ps->ps_noaction == 0) + log_info("%s startup", getprogname()); + + event_init(); + + signal_set(&ps->ps_evsigint, SIGINT, gotwebd_sighdlr, ps); + signal_set(&ps->ps_evsigterm, SIGTERM, gotwebd_sighdlr, ps); + signal_set(&ps->ps_evsighup, SIGHUP, gotwebd_sighdlr, ps); + signal_set(&ps->ps_evsigpipe, SIGPIPE, gotwebd_sighdlr, ps); + signal_set(&ps->ps_evsigusr1, SIGUSR1, gotwebd_sighdlr, ps); + + signal_add(&ps->ps_evsigint, NULL); + signal_add(&ps->ps_evsigterm, NULL); + signal_add(&ps->ps_evsighup, NULL); + signal_add(&ps->ps_evsigpipe, NULL); + signal_add(&ps->ps_evsigusr1, NULL); + + if (!env->gotwebd_noaction) + proc_connect(ps); + + if (gotwebd_configure(env) == -1) + fatalx("configuration failed"); + +#ifdef PROFILE + if (unveil("gmon.out", "rwc") != 0) + err(1, "gmon.out"); +#endif + + if (unveil(strlen(env->httpd_chroot) > 0 ? env->httpd_chroot : + D_HTTPD_CHROOT, "rwc") == -1) + err(1, "unveil"); + + if (unveil(GOT_TMPDIR_STR, "rw") == -1) + err(1, "unveil"); + + if (unveil(GOTWEBD_CONF, "r") == -1) + err(1, "unveil"); + + if (unveil(NULL, NULL) != 0) + err(1, "unveil"); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath inet unix", NULL) == -1) + err(1, "pledge"); +#endif + + event_dispatch(); + + log_debug("%s gotwebd exiting", getprogname()); + + return (0); +} + +int +gotwebd_configure(struct gotwebd *env) +{ + struct server *srv; + struct socket *sock; + int id; + + if (env->gotwebd_noaction) { + fprintf(stderr, "configuration OK\n"); + proc_kill(env->gotwebd_ps); + exit(0); + } + + /* gotweb need to reload its config. */ + env->gotwebd_reload = env->prefork_gotwebd; + + /* send our gotweb servers */ + TAILQ_FOREACH(srv, env->servers, entry) { + if (config_setserver(env, srv) == -1) + fatalx("%s: send server error", __func__); + } + + /* send our sockets */ + TAILQ_FOREACH(sock, env->sockets, entry) { + if (config_setsock(env, sock) == -1) + fatalx("%s: send socket error", __func__); + if (config_setfd(env, sock) == -1) + fatalx("%s: send priv_fd error", __func__); + } + + for (id = 0; id < PROC_MAX; id++) { + if (id == privsep_process) + continue; + proc_compose(env->gotwebd_ps, id, IMSG_CFG_DONE, NULL, 0); + } + + return (0); +} + +void +gotwebd_configure_done(struct gotwebd *env) +{ + int id; + + if (env->gotwebd_reload == 0) { + log_warnx("%s: configuration already finished", __func__); + return; + } + + env->gotwebd_reload--; + if (env->gotwebd_reload == 0) { + for (id = 0; id < PROC_MAX; id++) { + if (id == privsep_process) + continue; + proc_compose(env->gotwebd_ps, id, IMSG_CTL_START, + NULL, 0); + } + } +} + +void +gotwebd_shutdown(void) +{ + proc_kill(gotwebd_env->gotwebd_ps); + + /* unlink(gotwebd_env->gotweb->gotweb_conf.gotweb_unix_socket_name); */ + /* free(gotwebd_env->gotweb); */ + free(gotwebd_env); + + log_warnx("gotwebd terminating"); + exit(0); +} blob - /dev/null blob + 7dba54734ea8a323a1f5597f698f84a790722531 (mode 644) --- /dev/null +++ gotwebd/gotwebd.conf.5 @@ -0,0 +1,135 @@ +.\" +.\" Copyright (c) 2020 Tracey Emery +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate$ +.Dt GOTWEB.CONF 5 +.Os +.Sh NAME +.Nm gotweb.conf +.Nd gotweb configuration file +.Sh DESCRIPTION +.Nm +is the run-time configuration file for +.Xr gotweb 8 . +.Pp +The file format is line-based, with one configuration directive per line. +Any lines beginning with a +.Sq # +are treated as comments and ignored. +.Pp +Paths mentioned in +.Nm +must be relative to +.Pa /var/www , +the +.Xr chroot 2 +environment of +.Xr httpd 8 . +.Sh GLOBAL CONFIGURATION +The available configuration directives are as follows: +.Bl -tag -width Ds +.It Ic got_max_commits_display Ar number +Set the maximum amount of commits displayed per page. +.It Ic got_logo Ar path +Set the path to an image file containing a logo to be displayed. +.It Ic got_logo_url Ar url +Set a hyperlink for the logo. +.It Ic got_max_repos Ar number +Set the maximum amount of repositories +.Xr gotweb 8 +will work with. +.It Ic got_max_repos_display Ar number +Set the maximum amount of repositories displayed on the index screen. +.It Ic got_show_repo_age Ar on | off +Toggle display of last repository modification date. +.It Ic got_show_repo_cloneurl Ar on | off +Toggle display of clone URLs for a repository. +This requires the creation of a +.Pa cloneurl +file inside the repository which contains one URL per line. +.It Ic got_show_repo_description Ar on | off +Toggle display of the repository description. +The +.Pa description +file in the repository should be updated with an appropriate description. +.It Ic got_repos_path Ar path +Set the path to the directory which contains Git repositories that +.Xr gotweb 8 +should publish. +.It Ic got_show_repo_owner Ar on | off +Set whether to display the repository owner. +Displaying the owner requires owner information to be added to the +.Pa config +file in the repository. +.Xr gotweb 8 +will parse owner information from either a [gotweb] or a [gitweb] section. +For example: +.Bd -literal -offset indent +[gotweb] +owner = "Your Name" +.Ed +.It Ic got_site_link Ar string +Set the displayed site link name for the index page. +.It Ic got_site_name Ar string +Set the displayed site name title. +.It Ic got_site_owner Ar string +Set the displayed site owner. +.It Ic got_show_site_owner Ar on | off +Toggle display of the site owner. +.It Ic got_www_path Ar string +Set the public gotweb httpd path. +.El +.Sh EXAMPLES +These are the currently configurable items for +.Xr gotweb 8 +with their default values. +.Bd -literal -offset indent + +# +# gotweb options +# all paths relative to /var/www (httpd chroot jail) +# + +got_repos_path "/got/public" +got_www_path "/gotweb" + +#got_max_repos 100 +#got_max_repos_display 25 +got_max_commits_display 50 + +got_site_name "my public repos" +got_site_owner "Got Owner" +got_site_link "repos" + +got_logo "got.png" +got_logo_url "https://gameoftrees.org" + +# on by default +#got_show_site_owner off +#got_show_repo_owner off +#got_show_repo_age false +#got_show_repo_description no +#got_show_repo_cloneurl off +.Ed +.Sh FILES +.Bl -tag -width Ds -compact +.It Pa /var/www/etc/gotweb.conf +Location of the +.Nm +configuration file. +.El +.Sh SEE ALSO +.Xr got 1 , +.Xr gotweb 8 blob - /dev/null blob + 38c0c930eedf474fcef860b7a5336ec4ae64724d (mode 644) --- /dev/null +++ gotwebd/gotwebd.h @@ -0,0 +1,468 @@ +/* + * Copyright (c) 2016, 2019, 2020-2022 Tracey Emery + * Copyright (c) 2015 Mike Larkin + * Copyright (c) 2013 David Gwynne + * Copyright (c) 2013 Florian Obser + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include + +#include +#include + +#ifdef DEBUG +#define dprintf(x...) do { log_debug(x); } while(0) +#else +#define dprintf(x...) +#endif /* DEBUG */ + +#ifndef nitems +#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0])) +#endif + +/* GOTWEBD DEFAULTS */ +#define GOTWEBD_CONF "/etc/gotwebd.conf" + +#define GOTWEBD_USER "www" + +#define GOTWEBD_MAXCLIENTS 1024 +#define GOTWEBD_MAXTEXT 511 +#define GOTWEBD_MAXNAME 64 +#define GOTWEBD_MAXPORT 6 +#define GOTWEBD_NUMPROC 3 +#define GOTWEBD_MAXIFACE 16 + +/* GOTWEB DEFAULTS */ +#define MAX_QUERYSTRING 2048 +#define MAX_DOCUMENT_ROOT 255 +#define MAX_SERVER_NAME 255 + +#define GOTWEB_GOT_DIR ".got" +#define GOTWEB_GIT_DIR ".git" + +#define D_HTTPD_CHROOT "/var/www" +#define D_UNIX_SOCKET "/run/gotweb.sock" +#define D_FCGI_PORT "9000" +#define D_GOTPATH "/got/public" +#define D_SITENAME "Gotweb" +#define D_SITEOWNER "Got Owner" +#define D_SITELINK "Repos" +#define D_GOTLOGO "got.png" +#define D_GOTURL "https://gameoftrees.org" +#define D_GOTWEBCSS "gotweb.css" + +#define D_SHOWROWNER 1 +#define D_SHOWSOWNER 1 +#define D_SHOWAGE 1 +#define D_SHOWDESC 1 +#define D_SHOWURL 1 +#define D_MAXREPO 0 +#define D_MAXREPODISP 25 +#define D_MAXSLCOMMDISP 10 +#define D_MAXCOMMITDISP 25 + +#define BUF 8192 + +#define TIMEOUT_DEFAULT 120 + +#define FCGI_CONTENT_SIZE 65535 +#define FCGI_PADDING_SIZE 255 +#define FCGI_RECORD_SIZE \ + (sizeof(struct fcgi_record_header) + FCGI_CONTENT_SIZE + FCGI_PADDING_SIZE) + +#define FCGI_ALIGNMENT 8 +#define FCGI_ALIGN(n) \ + (((n) + (FCGI_ALIGNMENT - 1)) & ~(FCGI_ALIGNMENT - 1)) + +#define FD_RESERVE 5 +#define FD_NEEDED 6 + +#define FCGI_BEGIN_REQUEST 1 +#define FCGI_ABORT_REQUEST 2 +#define FCGI_END_REQUEST 3 +#define FCGI_PARAMS 4 +#define FCGI_STDIN 5 +#define FCGI_STDOUT 6 +#define FCGI_STDERR 7 +#define FCGI_DATA 8 +#define FCGI_GET_VALUES 9 +#define FCGI_GET_VALUES_RESULT 10 +#define FCGI_UNKNOWN_TYPE 11 +#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE) + +#define FCGI_REQUEST_COMPLETE 0 +#define FCGI_CANT_MPX_CONN 1 +#define FCGI_OVERLOADED 2 +#define FCGI_UNKNOWN_ROLE 3 + +/* XXX: move this later after ig */ +#define GOT_PACK_NUM_TEMPFILES 32 + +enum imsg_type { + IMSG_CFG_SRV = IMSG_PROC_MAX, + IMSG_CFG_SOCK, + IMSG_CFG_FD, + IMSG_CFG_DONE, + IMSG_CTL_START, +}; + +struct env_val { + SLIST_ENTRY(env_val) entry; + char *val; +}; +SLIST_HEAD(env_head, env_val); + +struct fcgi_record_header { + uint8_t version; + uint8_t type; + uint16_t id; + uint16_t content_len; + uint8_t padding_len; + uint8_t reserved; +}__packed; + +struct fcgi_response { + TAILQ_ENTRY(fcgi_response) entry; + uint8_t data[FCGI_RECORD_SIZE]; + size_t data_pos; + size_t data_len; +}; + +struct repo_dir { + char *name; + char *owner; + char *description; + char *url; + char *age; + char *path; +}; + +struct repo_tag { + TAILQ_ENTRY(repo_tag) entry; + char *commit_id; + char *tag_name; + char *tag_commit; + char *commit_msg; + char *tagger; + time_t tagger_time; +}; + +struct repo_commit { + TAILQ_ENTRY(repo_commit) entry; + char *path; + char *refs_str; + char *commit_id; /* id_str1 */ + char *parent_id; /* id_str2 */ + char *tree_id; + char *author; + char *committer; + char *commit_msg; + time_t committer_time; +}; + +struct got_repository; +struct transport { + TAILQ_HEAD(repo_commits_head, repo_commit) repo_commits; + TAILQ_HEAD(repo_tags_head, repo_tag) repo_tags; + struct got_repository *repo; + struct repo_dir *repo_dir; + struct querystring *qs; + char *next_id; + char *prev_id; + unsigned int repos_total; + unsigned int next_disp; + unsigned int prev_disp; + unsigned int tag_count; +}; + +enum socket_priv_fds { + DIFF_FD_1, + DIFF_FD_2, + DIFF_FD_3, + DIFF_FD_4, + DIFF_FD_5, + BLAME_FD_1, + BLAME_FD_2, + BLAME_FD_3, + BLAME_FD_4, + BLAME_FD_5, + BLAME_FD_6, + BLOB_FD_1, + BLOB_FD_2, + PRIV_FDS__MAX, +}; + +struct request { + struct socket *sock; + struct server *srv; + struct transport *t; + struct event ev; + struct event tmo; + + uint16_t id; + int fd; + int priv_fd[PRIV_FDS__MAX]; + + uint8_t buf[FCGI_RECORD_SIZE]; + size_t buf_pos; + size_t buf_len; + + char querystring[MAX_QUERYSTRING]; + char http_host[GOTWEBD_MAXTEXT]; + char document_root[MAX_DOCUMENT_ROOT]; + char server_name[MAX_SERVER_NAME]; + + struct env_head env; + int env_count; + + uint8_t request_started; +}; + +struct fcgi_begin_request_body { + uint16_t role; + uint8_t flags; + uint8_t reserved[5]; +}__packed; + +struct fcgi_end_request_body { + uint32_t app_status; + uint8_t protocol_status; + uint8_t reserved[3]; +}__packed; + +struct address { + TAILQ_ENTRY(address) entry; + struct sockaddr_storage ss; + int ipproto; + int prefixlen; + in_port_t port; + char ifname[IFNAMSIZ]; +}; +TAILQ_HEAD(addresslist, address); + +struct server { + TAILQ_ENTRY(server) entry; + struct addresslist *al; + + char name[GOTWEBD_MAXTEXT]; + + char repos_path[PATH_MAX]; + char site_name[GOTWEBD_MAXNAME]; + char site_owner[GOTWEBD_MAXNAME]; + char site_link[GOTWEBD_MAXTEXT]; + char logo[GOTWEBD_MAXTEXT]; + char logo_url[GOTWEBD_MAXTEXT]; + char custom_css[PATH_MAX]; + + size_t max_repos; + size_t max_repos_display; + size_t max_commits_display; + + int show_site_owner; + int show_repo_owner; + int show_repo_age; + int show_repo_description; + int show_repo_cloneurl; + + int unix_socket; + char unix_socket_name[PATH_MAX]; + + int fcgi_socket; + char fcgi_socket_bind[GOTWEBD_MAXTEXT]; + in_port_t fcgi_socket_port; +}; +TAILQ_HEAD(serverlist, server); + +enum client_action { + CLIENT_CONNECT, + CLIENT_DISCONNECT, +}; + +enum sock_type { + UNIX, + FCGI, +}; + +struct socket_conf { + struct addresslist *al; + + char name[GOTWEBD_MAXTEXT]; + char srv_name[GOTWEBD_MAXTEXT]; + + int id; + int child_id; + int parent_id; + + int ipv4; + int ipv6; + + int type; + char unix_socket_name[PATH_MAX]; + in_port_t fcgi_socket_port; +}; + +struct socket { + TAILQ_ENTRY(socket) entry; + struct socket_conf conf; + + int fd; + int pack_fds[GOT_PACK_NUM_TEMPFILES]; + int priv_fd[PRIV_FDS__MAX]; + + struct event evt; + struct event ev; + struct event pause; + + int client_status; +}; +TAILQ_HEAD(socketlist, socket); + +struct gotwebd { + struct serverlist *servers; + struct socketlist *sockets; + + struct privsep *gotwebd_ps; + const char *gotwebd_conffile; + + int gotwebd_debug; + int gotwebd_verbose; + int gotwebd_noaction; + + uint16_t prefork_gotwebd; + int gotwebd_reload; + + int server_cnt; + + char httpd_chroot[PATH_MAX]; + + int unix_socket; + char unix_socket_name[PATH_MAX]; + + int fcgi_socket; + char fcgi_socket_bind[GOTWEBD_MAXTEXT]; + in_port_t fcgi_socket_port; +}; + +struct querystring { + uint8_t action; + char *commit; + char *previd; + char *prevset; + char *file; + char *folder; + char *headref; + int index_page; + char *index_page_str; + char *path; + int page; + char *page_str; +}; + +struct querystring_keys { + const char *name; + int element; +}; + +struct action_keys { + const char *name; + int action; +}; + +enum querystring_elements { + ACTION, + COMMIT, + RFILE, + FOLDER, + HEADREF, + INDEX_PAGE, + PATH, + PAGE, + PREVID, + QSELEM__MAX, +}; + +enum query_actions { + BLAME, + BLOB, + BRIEFS, + COMMITS, + DIFF, + ERR, + INDEX, + SUMMARY, + TAG, + TAGS, + TREE, + ACTIONS__MAX, +}; + +extern struct gotwebd *gotwebd_env; + +/* sockets.c */ +void sockets(struct privsep *, struct privsep_proc *); +void sockets_shutdown(void); +void sockets_parse_sockets(struct gotwebd *); +void sockets_socket_accept(int, short, void *); +int sockets_privinit(struct gotwebd *, struct socket *); + +/* gotweb.c */ +const struct got_error *gotweb_render_content_type(struct request *, + const uint8_t *); +const struct got_error + *gotweb_render_content_type_file(struct request *, const uint8_t *, char *); +const struct got_error *gotweb_get_time_str(char **, time_t, int); +const struct got_error *gotweb_init_transport(struct transport **); +const struct got_error *gotweb_escape_html(char **, const char *); +void gotweb_free_repo_commit(struct repo_commit *); +void gotweb_free_repo_tag(struct repo_tag *); +void gotweb_process_request(struct request *); +void gotweb_free_transport(struct transport *); + +/* parse.y */ +int parse_config(const char *, struct gotwebd *); +int cmdline_symset(char *); + +/* fcgi.c */ +void fcgi_request(int, short, void *); +void fcgi_timeout(int, short, void *); +void fcgi_cleanup_request(struct request *); +void fcgi_create_end_record(struct request *); +void dump_fcgi_record(const char *, struct fcgi_record_header *); +int fcgi_gen_response(struct request *, const char *); +int fcgi_gen_binary_response(struct request *, const uint8_t *, int); + +/* got_operations.c */ +const struct got_error *got_get_repo_owner(char **, struct request *, char *); +const struct got_error *got_get_repo_age(char **, struct request *, char *, + const char *, int); +const struct got_error *got_get_repo_commits(struct request *, int); +const struct got_error *got_get_repo_tags(struct request *, int); +const struct got_error *got_get_repo_heads(struct request *); +const struct got_error *got_output_repo_diff(struct request *); +const struct got_error *got_output_repo_tree(struct request *); +const struct got_error *got_output_file_blob(struct request *); +const struct got_error *got_output_file_blame(struct request *); + +/* config.c */ +int config_setserver(struct gotwebd *, struct server *); +int config_getserver(struct gotwebd *, struct imsg *); +int config_setsock(struct gotwebd *, struct socket *); +int config_getsock(struct gotwebd *, struct imsg *); +int config_setfd(struct gotwebd *, struct socket *); +int config_getfd(struct gotwebd *, struct imsg *); +int config_getcfg(struct gotwebd *, struct imsg *); +int config_init(struct gotwebd *); blob - /dev/null blob + 5fd34708bd3654bc05060446ff5d55557747cfd3 (mode 644) --- /dev/null +++ gotwebd/libexec/Makefile @@ -0,0 +1,4 @@ +SUBDIR = got-read-blob got-read-commit got-read-object got-read-tree \ + got-read-tag got-read-pack got-read-gitconfig got-read-gotconfig + +.include blob - /dev/null blob + 85bee26728643214f5d4570f003572ac1fc36d05 (mode 644) --- /dev/null +++ gotwebd/libexec/Makefile.inc @@ -0,0 +1,11 @@ +.include "../Makefile.inc" + +realinstall: + if [ ! -d ${DESTDIR}${CHROOT_DIR}${LIBEXECDIR}/. ]; then \ + ${INSTALL} -d -o root -g daemon -m 755 \ + ${DESTDIR}${CHROOT_DIR}${LIBEXECDIR}; \ + fi + ${INSTALL} ${INSTALL_COPY} -o root -g daemon -m 755 ${PROG} \ + ${DESTDIR}${CHROOT_DIR}${LIBEXECDIR}/${PROG} + +NOMAN = Yes blob - /dev/null blob + 8a3f38ce4c45ce1386bdc180e342b043f8abe809 (mode 644) --- /dev/null +++ gotwebd/libexec/got-read-blob/Makefile @@ -0,0 +1,16 @@ + +.include "../../../got-version.mk" +.include "../Makefile.inc" + +PROG= got-read-blob +SRCS= got-read-blob.c error.c inflate.c object_parse.c \ + path.c privsep.c sha1.c + +CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib +LDADD = -lutil -lz +DPADD = ${LIBZ} ${LIBUTIL} +LDSTATIC = ${STATIC} + +.PATH: ${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-blob + +.include blob - /dev/null blob + 0996068f0f3d0bb71779d03c1c1a7ec355644166 (mode 644) --- /dev/null +++ gotwebd/libexec/got-read-commit/Makefile @@ -0,0 +1,16 @@ + +.include "../../../got-version.mk" +.include "../Makefile.inc" + +PROG= got-read-commit +SRCS= got-read-commit.c error.c inflate.c object_parse.c \ + path.c privsep.c sha1.c + +CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib +LDADD = -lutil -lz +DPADD = ${LIBZ} ${LIBUTIL} +LDSTATIC = ${STATIC} + +.PATH: ${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-commit + +.include blob - /dev/null blob + 77cc7852cae9199c991f2908f2d9e8a27da650ee (mode 644) --- /dev/null +++ gotwebd/libexec/got-read-gitconfig/Makefile @@ -0,0 +1,16 @@ + +.include "../../../got-version.mk" +.include "../Makefile.inc" + +PROG= got-read-gitconfig +SRCS= got-read-gitconfig.c error.c inflate.c object_parse.c \ + path.c privsep.c sha1.c gitconfig.c + +CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib +LDADD = -lutil -lz +DPADD = ${LIBZ} ${LIBUTIL} +LDSTATIC = ${STATIC} + +.PATH: ${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-gitconfig + +.include blob - /dev/null blob + 29605918a07b9e1969e3a2722605c3424e77d13f (mode 644) --- /dev/null +++ gotwebd/libexec/got-read-gotconfig/Makefile @@ -0,0 +1,17 @@ + +.include "../../../got-version.mk" +.include "../Makefile.inc" + +PROG= got-read-gotconfig +SRCS= got-read-gotconfig.c error.c inflate.c object_parse.c \ + path.c privsep.c sha1.c parse.y + +CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \ + -I${.CURDIR}/../../../libexec/got-read-gotconfig +LDADD = -lutil -lz +DPADD = ${LIBZ} ${LIBUTIL} +LDSTATIC = ${STATIC} + +.PATH: ${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-gotconfig + +.include blob - /dev/null blob + 4889fe0bab46bbcdb20610ea9255916cd11d0e0d (mode 644) --- /dev/null +++ gotwebd/libexec/got-read-object/Makefile @@ -0,0 +1,16 @@ + +.include "../../../got-version.mk" +.include "../Makefile.inc" + +PROG= got-read-object +SRCS= got-read-object.c error.c inflate.c object_parse.c \ + path.c privsep.c sha1.c + +CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib +LDADD = -lutil -lz +DPADD = ${LIBZ} ${LIBUTIL} +LDSTATIC = ${STATIC} + +.PATH: ${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-object + +.include blob - /dev/null blob + a28b9cfd2d1c5d17ffcbe771790c2183e406f924 (mode 644) --- /dev/null +++ gotwebd/libexec/got-read-pack/Makefile @@ -0,0 +1,17 @@ + +.include "../../../got-version.mk" +.include "../Makefile.inc" + +PROG= got-read-pack +SRCS= got-read-pack.c delta.c error.c inflate.c object_cache.c \ + object_idset.c object_parse.c opentemp.c pack.c path.c \ + privsep.c sha1.c delta_cache.c + +CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib +LDADD = -lutil -lz +DPADD = ${LIBZ} ${LIBUTIL} +LDSTATIC = ${STATIC} + +.PATH: ${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-pack + +.include blob - /dev/null blob + 3a0b798c57ea849bde67efe66e3b721f7486e287 (mode 644) --- /dev/null +++ gotwebd/libexec/got-read-tag/Makefile @@ -0,0 +1,16 @@ + +.include "../../../got-version.mk" +.include "../Makefile.inc" + +PROG= got-read-tag +SRCS= got-read-tag.c error.c inflate.c object_parse.c \ + path.c privsep.c sha1.c + +CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib +LDADD = -lutil -lz +DPADD = ${LIBZ} ${LIBUTIL} +LDSTATIC = ${STATIC} + +.PATH: ${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-tag + +.include blob - /dev/null blob + 19a4c9cfa3379ca5fec4fafdc691d38c80c996e8 (mode 644) --- /dev/null +++ gotwebd/libexec/got-read-tree/Makefile @@ -0,0 +1,16 @@ + +.include "../../../got-version.mk" +.include "../Makefile.inc" + +PROG= got-read-tree +SRCS= got-read-tree.c error.c inflate.c object_parse.c \ + path.c privsep.c sha1.c + +CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib +LDADD = -lutil -lz +DPADD = ${LIBZ} ${LIBUTIL} +LDSTATIC = ${STATIC} + +.PATH: ${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-tree + +.include blob - /dev/null blob + 79d3d334581eddd8ffcc0f02f067e5c64d0be72e (mode 644) --- /dev/null +++ gotwebd/log.c @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2003, 2004 Henning Brauer + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include + +static int debug; +static int verbose; +const char *log_procname; + +void log_init(int, int); +void log_procinit(const char *); +void log_setverbose(int); +int log_getverbose(void); +void log_warn(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_warnx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_info(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_debug(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void logit(int, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); +void vlog(int, const char *, va_list) + __attribute__((__format__ (printf, 2, 0))); +__dead void fatal(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +__dead void fatalx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); + +void +log_init(int n_debug, int facility) +{ + debug = n_debug; + verbose = n_debug; + log_procinit(getprogname()); + + if (!debug) + openlog(getprogname(), LOG_PID | LOG_NDELAY, facility); + + tzset(); +} + +void +log_procinit(const char *procname) +{ + if (procname != NULL) + log_procname = procname; +} + +void +log_setverbose(int v) +{ + verbose = v; +} + +int +log_getverbose(void) +{ + return (verbose); +} + +void +logit(int pri, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vlog(pri, fmt, ap); + va_end(ap); +} + +void +vlog(int pri, const char *fmt, va_list ap) +{ + char *nfmt; + int saved_errno = errno; + + if (debug) { + /* best effort in out of mem situations */ + if (asprintf(&nfmt, "%s\n", fmt) == -1) { + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + } else { + vfprintf(stderr, nfmt, ap); + free(nfmt); + } + fflush(stderr); + } else + vsyslog(pri, fmt, ap); + + errno = saved_errno; +} + +void +log_warn(const char *emsg, ...) +{ + char *nfmt; + va_list ap; + int saved_errno = errno; + + /* best effort to even work in out of memory situations */ + if (emsg == NULL) + logit(LOG_CRIT, "%s", strerror(saved_errno)); + else { + va_start(ap, emsg); + + if (asprintf(&nfmt, "%s: %s", emsg, + strerror(saved_errno)) == -1) { + /* we tried it... */ + vlog(LOG_CRIT, emsg, ap); + logit(LOG_CRIT, "%s", strerror(saved_errno)); + } else { + vlog(LOG_CRIT, nfmt, ap); + free(nfmt); + } + va_end(ap); + } + + errno = saved_errno; +} + +void +log_warnx(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vlog(LOG_CRIT, emsg, ap); + va_end(ap); +} + +void +log_info(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vlog(LOG_INFO, emsg, ap); + va_end(ap); +} + +void +log_debug(const char *emsg, ...) +{ + va_list ap; + + if (verbose > 1) { + va_start(ap, emsg); + vlog(LOG_DEBUG, emsg, ap); + va_end(ap); + } +} + +static void +vfatalc(int code, const char *emsg, va_list ap) +{ + static char s[BUFSIZ]; + const char *sep; + + if (emsg != NULL) { + (void)vsnprintf(s, sizeof(s), emsg, ap); + sep = ": "; + } else { + s[0] = '\0'; + sep = ""; + } + if (code) + logit(LOG_CRIT, "%s: %s%s%s", + log_procname, s, sep, strerror(code)); + else + logit(LOG_CRIT, "%s%s%s", log_procname, sep, s); +} + +void +fatal(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vfatalc(errno, emsg, ap); + va_end(ap); + exit(1); +} + +void +fatalx(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vfatalc(0, emsg, ap); + va_end(ap); + exit(1); +} blob - /dev/null blob + c7b300955ba979db3cd4d8308802a8179f9d7ac4 (mode 644) --- /dev/null +++ gotwebd/parse.y @@ -0,0 +1,1339 @@ +/* + * Copyright (c) 2016-2019, 2020-2021 Tracey Emery + * Copyright (c) 2004, 2005 Esben Norby + * Copyright (c) 2004 Ryan McBride + * Copyright (c) 2002, 2003, 2004 Henning Brauer + * Copyright (c) 2001 Markus Friedl. All rights reserved. + * Copyright (c) 2001 Daniel Hartmeier. All rights reserved. + * Copyright (c) 2001 Theo de Raadt. All rights reserved. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +%{ +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "proc.h" +#include "gotwebd.h" + +TAILQ_HEAD(files, file) files = TAILQ_HEAD_INITIALIZER(files); +static struct file { + TAILQ_ENTRY(file) entry; + FILE *stream; + char *name; + int lineno; + int errors; +} *file; +struct file *newfile(const char *, int); +static void closefile(struct file *); +int check_file_secrecy(int, const char *); +int yyparse(void); +int yylex(void); +int yyerror(const char *, ...) + __attribute__((__format__ (printf, 1, 2))) + __attribute__((__nonnull__ (1))); +int kw_cmp(const void *, const void *); +int lookup(char *); +int lgetc(int); +int lungetc(int); +int findeol(void); + +TAILQ_HEAD(symhead, sym) symhead = TAILQ_HEAD_INITIALIZER(symhead); +struct sym { + TAILQ_ENTRY(sym) entry; + int used; + int persist; + char *nam; + char *val; +}; + +int symset(const char *, const char *, int); +char *symget(const char *); + +static int errors; + +static struct gotwebd *gotwebd; +static struct server *new_srv; +static struct server *conf_new_server(const char *); +int getservice(const char *); +int n; + +int get_addrs(const char *, struct addresslist *, in_port_t); +struct address *host_v4(const char *); +struct address *host_v6(const char *); +int host_dns(const char *, struct addresslist *, + int, in_port_t, const char *, int); +int host_if(const char *, struct addresslist *, + int, in_port_t, const char *, int); +int host(const char *, struct addresslist *, + int, in_port_t, const char *, int); +int is_if_in_group(const char *, const char *); + +typedef struct { + union { + long long number; + char *string; + in_port_t port; + } v; + int lineno; +} YYSTYPE; + +%} + +%token BIND INTERFACE WWW_PATH MAX_REPOS SITE_NAME SITE_OWNER SITE_LINK LOGO +%token LOGO_URL SHOW_REPO_OWNER SHOW_REPO_AGE SHOW_REPO_DESCRIPTION +%token MAX_REPOS_DISPLAY REPOS_PATH MAX_COMMITS_DISPLAY ON ERROR +%token SHOW_SITE_OWNER SHOW_REPO_CLONEURL PORT PREFORK FCGI_SOCKET +%token UNIX_SOCKET UNIX_SOCKET_NAME SERVER CHROOT CUSTOM_CSS + +%token STRING +%type fcgiport +%token NUMBER +%type boolean + +%% + +grammar : + | grammar '\n' + | grammar main '\n' + | grammar server '\n' + ; + +boolean : STRING { + if (strcasecmp($1, "1") == 0 || + strcasecmp($1, "yes") == 0 || + strcasecmp($1, "on") == 0) + $$ = 1; + else if (strcasecmp($1, "0") == 0 || + strcasecmp($1, "off") == 0 || + strcasecmp($1, "no") == 0) + $$ = 0; + else { + yyerror("invalid boolean value '%s'", $1); + free($1); + YYERROR; + } + free($1); + } + | ON { $$ = 1; } + | NUMBER { $$ = $1; } + ; + +fcgiport : NUMBER { + if ($1 <= 0 || $1 > (int)USHRT_MAX) { + yyerror("invalid port: %lld", $1); + YYERROR; + } + $$ = htons($1); + } + | STRING { + int val; + + if ((val = getservice($1)) == -1) { + yyerror("invalid port: %s", $1); + free($1); + YYERROR; + } + free($1); + + $$ = val; + } + ; + +main : PREFORK NUMBER { + gotwebd->prefork_gotwebd = $2; + } + | CHROOT STRING { + n = strlcpy(gotwebd->httpd_chroot, $2, + sizeof(gotwebd->httpd_chroot)); + if (n >= sizeof(gotwebd->httpd_chroot)) { + yyerror("%s: httpd_chroot truncated", __func__); + free($2); + YYERROR; + } + free($2); + } + | FCGI_SOCKET boolean { + gotwebd->fcgi_socket = $2; + } + | FCGI_SOCKET boolean { + gotwebd->fcgi_socket = $2; + } '{' optnl socketopts4 '}' + | UNIX_SOCKET boolean { + gotwebd->unix_socket = $2; + } + | UNIX_SOCKET_NAME STRING { + n = snprintf(gotwebd->unix_socket_name, + sizeof(gotwebd->unix_socket_name), "%s%s", + strlen(gotwebd->httpd_chroot) ? + gotwebd->httpd_chroot : D_HTTPD_CHROOT, $2); + if (n < 0) { + yyerror("%s: unix_socket_name truncated", + __func__); + free($2); + YYERROR; + } + free($2); + } + ; + +server : SERVER STRING { + struct server *srv; + + TAILQ_FOREACH(srv, gotwebd->servers, entry) { + if (strcmp(srv->name, $2) == 0) { + yyerror("server name exists '%s'", $2); + free($2); + YYERROR; + } + } + + new_srv = conf_new_server($2); + if (new_srv->fcgi_socket) + if (get_addrs(new_srv->fcgi_socket_bind, + new_srv->al, + new_srv->fcgi_socket_port) == -1) { + yyerror("could not get tcp iface " + "addrs"); + YYERROR; + } + log_debug("adding server %s", $2); + free($2); + } + | SERVER STRING { + struct server *srv; + + TAILQ_FOREACH(srv, gotwebd->servers, entry) { + if (strcmp(srv->name, $2) == 0) { + yyerror("server name exists '%s'", $2); + free($2); + YYERROR; + } + } + + new_srv = conf_new_server($2); + log_debug("adding server %s", $2); + free($2); + } '{' optnl serveropts2 '}' { + if (get_addrs(new_srv->fcgi_socket_bind, + new_srv->al, new_srv->fcgi_socket_port) == -1) { + yyerror("could not get tcp iface addrs"); + YYERROR; + } + } + ; + +serveropts1 : REPOS_PATH STRING { + n = strlcpy(new_srv->repos_path, $2, + sizeof(new_srv->repos_path)); + if (n >= sizeof(new_srv->repos_path)) { + yyerror("%s: repos_path truncated", __func__); + free($2); + YYERROR; + } + free($2); + } + | SITE_NAME STRING { + n = strlcpy(new_srv->site_name, $2, + sizeof(new_srv->site_name)); + if (n >= sizeof(new_srv->site_name)) { + yyerror("%s: site_name truncated", __func__); + free($2); + YYERROR; + } + free($2); + } + | SITE_OWNER STRING { + n = strlcpy(new_srv->site_owner, $2, + sizeof(new_srv->site_owner)); + if (n >= sizeof(new_srv->site_owner)) { + yyerror("%s: site_owner truncated", __func__); + free($2); + YYERROR; + } + free($2); + } + | SITE_LINK STRING { + n = strlcpy(new_srv->site_link, $2, + sizeof(new_srv->site_link)); + if (n >= sizeof(new_srv->site_link)) { + yyerror("%s: site_link truncated", __func__); + free($2); + YYERROR; + } + free($2); + } + | LOGO STRING { + n = strlcpy(new_srv->logo, $2, sizeof(new_srv->logo)); + if (n >= sizeof(new_srv->logo)) { + yyerror("%s: logo truncated", __func__); + free($2); + YYERROR; + } + free($2); + } + | LOGO_URL STRING { + n = strlcpy(new_srv->logo_url, $2, + sizeof(new_srv->logo_url)); + if (n >= sizeof(new_srv->logo_url)) { + yyerror("%s: logo_url truncated", __func__); + free($2); + YYERROR; + } + free($2); + } + | CUSTOM_CSS STRING { + n = strlcpy(new_srv->custom_css, $2, + sizeof(new_srv->custom_css)); + if (n >= sizeof(new_srv->custom_css)) { + yyerror("%s: custom_css truncated", __func__); + free($2); + YYERROR; + } + free($2); + } + | MAX_REPOS NUMBER { + if ($2 > 0) + new_srv->max_repos = $2; + } + | SHOW_SITE_OWNER boolean { + new_srv->show_site_owner = $2; + } + | SHOW_REPO_OWNER boolean { + new_srv->show_repo_owner = $2; + } + | SHOW_REPO_AGE boolean { + new_srv->show_repo_age = $2; + } + | SHOW_REPO_DESCRIPTION boolean { + new_srv->show_repo_description = $2; + } + | SHOW_REPO_CLONEURL boolean { + new_srv->show_repo_cloneurl = $2; + } + | MAX_REPOS_DISPLAY NUMBER { + new_srv->max_repos_display = $2; + } + | MAX_COMMITS_DISPLAY NUMBER { + if ($2 > 0) + new_srv->max_commits_display = $2; + } + | FCGI_SOCKET boolean { + new_srv->fcgi_socket = $2; + } + | FCGI_SOCKET boolean { + new_srv->fcgi_socket = $2; + } '{' optnl socketopts2 '}' + | UNIX_SOCKET boolean { + new_srv->unix_socket = $2; + } + | UNIX_SOCKET_NAME STRING { + n = snprintf(new_srv->unix_socket_name, + sizeof(new_srv->unix_socket_name), "%s%s", + strlen(gotwebd->httpd_chroot) ? + gotwebd->httpd_chroot : D_HTTPD_CHROOT, $2); + if (n < 0) { + yyerror("%s: unix_socket_name truncated", + __func__); + free($2); + YYERROR; + } + free($2); + } + ; + +serveropts2 : serveropts2 serveropts1 nl + | serveropts1 optnl + ; + +socketopts1 : BIND INTERFACE STRING { + n = strlcpy(new_srv->fcgi_socket_bind, $3, + sizeof(new_srv->fcgi_socket_bind)); + if (n >= sizeof(new_srv->fcgi_socket_bind)) { + yyerror("%s: fcgi_socket_bind truncated", + __func__); + free($3); + YYERROR; + } + free($3); + } + | PORT fcgiport { + struct server *srv; + + TAILQ_FOREACH(srv, gotwebd->servers, entry) { + if (srv->fcgi_socket_port == $2) { + yyerror("port already assigned"); + YYERROR; + } + } + new_srv->fcgi_socket_port = $2; + } + ; + +socketopts2 : socketopts2 socketopts1 nl + | socketopts1 optnl + ; + +socketopts3 : BIND INTERFACE STRING { + n = strlcpy(gotwebd->fcgi_socket_bind, $3, + sizeof(gotwebd->fcgi_socket_bind)); + if (n >= sizeof(gotwebd->fcgi_socket_bind)) { + yyerror("%s: fcgi_socket_bind truncated", + __func__); + free($3); + YYERROR; + } + free($3); + } + | PORT fcgiport { + gotwebd->fcgi_socket_port = $2; + } + ; + +socketopts4 : socketopts4 socketopts3 nl + | socketopts3 optnl + ; + +nl : '\n' optnl + ; + +optnl : '\n' optnl /* zero or more newlines */ + | /* empty */ + ; + +%% + +struct keywords { + const char *k_name; + int k_val; +}; + +int +yyerror(const char *fmt, ...) +{ + va_list ap; + char *msg; + + file->errors++; + va_start(ap, fmt); + if (vasprintf(&msg, fmt, ap) == -1) + fatalx("yyerror vasprintf"); + va_end(ap); + logit(LOG_CRIT, "%s:%d: %s", file->name, yylval.lineno, msg); + free(msg); + return (0); +} + +int +kw_cmp(const void *k, const void *e) +{ + return (strcmp(k, ((const struct keywords *)e)->k_name)); +} + +int +lookup(char *s) +{ + /* This has to be sorted always. */ + static const struct keywords keywords[] = { + { "bind", BIND }, + { "chroot", CHROOT }, + { "custom_css", CUSTOM_CSS }, + { "fcgi_socket", FCGI_SOCKET }, + { "interface", INTERFACE }, + { "logo", LOGO }, + { "logo_url" , LOGO_URL }, + { "max_commits_display", MAX_COMMITS_DISPLAY }, + { "max_repos", MAX_REPOS }, + { "max_repos_display", MAX_REPOS_DISPLAY }, + { "port", PORT }, + { "prefork", PREFORK }, + { "repos_path", REPOS_PATH }, + { "server", SERVER }, + { "show_repo_age", SHOW_REPO_AGE }, + { "show_repo_cloneurl", SHOW_REPO_CLONEURL }, + { "show_repo_description", SHOW_REPO_DESCRIPTION }, + { "show_repo_owner", SHOW_REPO_OWNER }, + { "show_site_owner", SHOW_SITE_OWNER }, + { "site_link", SITE_LINK }, + { "site_name", SITE_NAME }, + { "site_owner", SITE_OWNER }, + { "unix_socket", UNIX_SOCKET }, + { "unix_socket_name", UNIX_SOCKET_NAME }, + }; + const struct keywords *p; + + p = bsearch(s, keywords, sizeof(keywords)/sizeof(keywords[0]), + sizeof(keywords[0]), kw_cmp); + + if (p) + return (p->k_val); + else + return (STRING); +} + +#define MAXPUSHBACK 128 + +unsigned char *parsebuf; +int parseindex; +unsigned char pushback_buffer[MAXPUSHBACK]; +int pushback_index = 0; + +int +lgetc(int quotec) +{ + int c, next; + + if (parsebuf) { + /* Read character from the parsebuffer instead of input. */ + if (parseindex >= 0) { + c = parsebuf[parseindex++]; + if (c != '\0') + return (c); + parsebuf = NULL; + } else + parseindex++; + } + + if (pushback_index) + return (pushback_buffer[--pushback_index]); + + if (quotec) { + c = getc(file->stream); + if (c == EOF) + yyerror("reached end of file while parsing " + "quoted string"); + return (c); + } + + c = getc(file->stream); + while (c == '\\') { + next = getc(file->stream); + if (next != '\n') { + c = next; + break; + } + yylval.lineno = file->lineno; + file->lineno++; + c = getc(file->stream); + } + + return (c); +} + +int +lungetc(int c) +{ + if (c == EOF) + return (EOF); + if (parsebuf) { + parseindex--; + if (parseindex >= 0) + return (c); + } + if (pushback_index < MAXPUSHBACK-1) + return (pushback_buffer[pushback_index++] = c); + else + return (EOF); +} + +int +findeol(void) +{ + int c; + + parsebuf = NULL; + + /* Skip to either EOF or the first real EOL. */ + while (1) { + if (pushback_index) + c = pushback_buffer[--pushback_index]; + else + c = lgetc(0); + if (c == '\n') { + file->lineno++; + break; + } + if (c == EOF) + break; + } + return (ERROR); +} + +int +yylex(void) +{ + unsigned char buf[8096]; + unsigned char *p, *val; + int quotec, next, c; + int token; + +top: + p = buf; + c = lgetc(0); + while (c == ' ' || c == '\t') + c = lgetc(0); /* nothing */ + + yylval.lineno = file->lineno; + if (c == '#') { + c = lgetc(0); + while (c != '\n' && c != EOF) + c = lgetc(0); /* nothing */ + } + if (c == '$' && parsebuf == NULL) { + while (1) { + c = lgetc(0); + if (c == EOF) + return (0); + + if (p + 1 >= buf + sizeof(buf) - 1) { + yyerror("string too long"); + return (findeol()); + } + if (isalnum(c) || c == '_') { + *p++ = c; + continue; + } + *p = '\0'; + lungetc(c); + break; + } + val = symget(buf); + if (val == NULL) { + yyerror("macro '%s' not defined", buf); + return (findeol()); + } + parsebuf = val; + parseindex = 0; + goto top; + } + + switch (c) { + case '\'': + case '"': + quotec = c; + while (1) { + c = lgetc(quotec); + if (c == EOF) + return (0); + if (c == '\n') { + file->lineno++; + continue; + } else if (c == '\\') { + next = lgetc(quotec); + if (next == EOF) + return (0); + if (next == quotec || c == ' ' || c == '\t') + c = next; + else if (next == '\n') { + file->lineno++; + continue; + } else + lungetc(next); + } else if (c == quotec) { + *p = '\0'; + break; + } else if (c == '\0') { + yyerror("syntax error"); + return (findeol()); + } + if (p + 1 >= buf + sizeof(buf) - 1) { + yyerror("string too long"); + return (findeol()); + } + *p++ = c; + } + yylval.v.string = strdup(buf); + if (yylval.v.string == NULL) + err(1, "yylex: strdup"); + return (STRING); + } + +#define allowed_to_end_number(x) \ + (isspace(x) || x == ')' || x ==',' || x == '/' || x == '}' || x == '=') + + if (c == '-' || isdigit(c)) { + do { + *p++ = c; + if ((unsigned)(p-buf) >= sizeof(buf)) { + yyerror("string too long"); + return (findeol()); + } + c = lgetc(0); + } while (c != EOF && isdigit(c)); + lungetc(c); + if (p == buf + 1 && buf[0] == '-') + goto nodigits; + if (c == EOF || allowed_to_end_number(c)) { + const char *errstr = NULL; + + *p = '\0'; + yylval.v.number = strtonum(buf, LLONG_MIN, + LLONG_MAX, &errstr); + if (errstr) { + yyerror("\"%s\" invalid number: %s", + buf, errstr); + return (findeol()); + } + return (NUMBER); + } else { +nodigits: + while (p > buf + 1) + lungetc(*--p); + c = *--p; + if (c == '-') + return (c); + } + } + +#define allowed_in_string(x) \ + (isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \ + x != '{' && x != '}' && \ + x != '!' && x != '=' && x != '#' && \ + x != ',')) + + if (isalnum(c) || c == ':' || c == '_') { + do { + *p++ = c; + if ((unsigned)(p-buf) >= sizeof(buf)) { + yyerror("string too long"); + return (findeol()); + } + c = lgetc(0); + } while (c != EOF && (allowed_in_string(c))); + lungetc(c); + *p = '\0'; + token = lookup(buf); + if (token == STRING) { + yylval.v.string = strdup(buf); + if (yylval.v.string == NULL) + err(1, "yylex: strdup"); + } + return (token); + } + if (c == '\n') { + yylval.lineno = file->lineno; + file->lineno++; + } + if (c == EOF) + return (0); + return (c); +} + +int +check_file_secrecy(int fd, const char *fname) +{ + struct stat st; + + if (fstat(fd, &st)) { + log_warn("cannot stat %s", fname); + return (-1); + } + if (st.st_uid != 0 && st.st_uid != getuid()) { + log_warnx("%s: owner not root or current user", fname); + return (-1); + } + if (st.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO)) { + log_warnx("%s: group writable or world read/writable", fname); + return (-1); + } + return (0); +} + +struct file * +newfile(const char *name, int secret) +{ + struct file *nfile; + + nfile = calloc(1, sizeof(struct file)); + if (nfile == NULL) { + log_warn("calloc"); + return (NULL); + } + nfile->name = strdup(name); + if (nfile->name == NULL) { + log_warn("strdup"); + free(nfile); + return (NULL); + } + nfile->stream = fopen(nfile->name, "r"); + if (nfile->stream == NULL) { + /* no warning, we don't require a conf file */ + free(nfile->name); + free(nfile); + return (NULL); + } else if (secret && + check_file_secrecy(fileno(nfile->stream), nfile->name)) { + fclose(nfile->stream); + free(nfile->name); + free(nfile); + return (NULL); + } + nfile->lineno = 1; + return (nfile); +} + +static void +closefile(struct file *xfile) +{ + fclose(xfile->stream); + free(xfile->name); + free(xfile); +} + +int +parse_config(const char *filename, struct gotwebd *env) +{ + struct sym *sym, *next; + + file = newfile(filename, 0); + if (file == NULL) + /* just return, as we don't require a conf file */ + return (0); + + if (config_init(env) == -1) + fatalx("failed to initialize configuration"); + + gotwebd = env; + + yyparse(); + errors = file->errors; + closefile(file); + + /* Free macros and check which have not been used. */ + TAILQ_FOREACH_SAFE(sym, &symhead, entry, next) { + if ((gotwebd->gotwebd_verbose > 1) && !sym->used) + fprintf(stderr, "warning: macro '%s' not used\n", + sym->nam); + if (!sym->persist) { + free(sym->nam); + free(sym->val); + TAILQ_REMOVE(&symhead, sym, entry); + free(sym); + } + } + + if (errors) + return (-1); + + /* just add default server if no config specified */ + if (gotwebd->server_cnt == 0) { + new_srv = conf_new_server(D_SITENAME); + log_debug("%s: adding default server %s", __func__, D_SITENAME); + } + + /* setup our listening sockets */ + sockets_parse_sockets(env); + + return (0); +} + +struct server * +conf_new_server(const char *name) +{ + struct server *srv = NULL; + int val; + + srv = calloc(1, sizeof(*srv)); + if (srv == NULL) + fatalx("%s: calloc", __func__); + + n = strlcpy(srv->name, name, sizeof(srv->name)); + if (n >= sizeof(srv->name)) + fatalx("%s: strlcpy", __func__); + n = snprintf(srv->unix_socket_name, + sizeof(srv->unix_socket_name), "%s%s", D_HTTPD_CHROOT, + D_UNIX_SOCKET); + if (n < 0) + fatalx("%s: snprintf", __func__); + n = strlcpy(srv->repos_path, D_GOTPATH, + sizeof(srv->repos_path)); + if (n >= sizeof(srv->repos_path)) + fatalx("%s: strlcpy", __func__); + n = strlcpy(srv->site_name, D_SITENAME, + sizeof(srv->site_name)); + if (n >= sizeof(srv->site_name)) + fatalx("%s: strlcpy", __func__); + n = strlcpy(srv->site_owner, D_SITEOWNER, + sizeof(srv->site_owner)); + if (n >= sizeof(srv->site_owner)) + fatalx("%s: strlcpy", __func__); + n = strlcpy(srv->site_link, D_SITELINK, + sizeof(srv->site_link)); + if (n >= sizeof(srv->site_link)) + fatalx("%s: strlcpy", __func__); + n = strlcpy(srv->logo, D_GOTLOGO, + sizeof(srv->logo)); + if (n >= sizeof(srv->logo)) + fatalx("%s: strlcpy", __func__); + n = strlcpy(srv->logo_url, D_GOTURL, sizeof(srv->logo_url)); + if (n >= sizeof(srv->logo_url)) + fatalx("%s: strlcpy", __func__); + n = strlcpy(srv->custom_css, D_GOTWEBCSS, sizeof(srv->custom_css)); + if (n >= sizeof(srv->custom_css)) + fatalx("%s: strlcpy", __func__); + + val = getservice(D_FCGI_PORT); + srv->fcgi_socket_port = gotwebd->fcgi_socket_port ? + gotwebd->fcgi_socket_port: htons(val); + + srv->show_site_owner = D_SHOWSOWNER; + srv->show_repo_owner = D_SHOWROWNER; + srv->show_repo_age = D_SHOWAGE; + srv->show_repo_description = D_SHOWDESC; + srv->show_repo_cloneurl = D_SHOWURL; + + srv->max_repos_display = D_MAXREPODISP; + srv->max_commits_display = D_MAXCOMMITDISP; + srv->max_repos = D_MAXREPO; + + srv->unix_socket = 1; + srv->fcgi_socket = gotwebd->fcgi_socket ? gotwebd->fcgi_socket : 0; + + if ((srv->al = calloc(1, sizeof(*srv->al))) == NULL) + fatalx("%s: calloc", __func__); + + TAILQ_INIT(srv->al); + TAILQ_INSERT_TAIL(gotwebd->servers, srv, entry); + gotwebd->server_cnt++; + + return srv; +}; + +int +symset(const char *nam, const char *val, int persist) +{ + struct sym *sym; + + TAILQ_FOREACH(sym, &symhead, entry) { + if (strcmp(nam, sym->nam) == 0) + break; + } + + if (sym != NULL) { + if (sym->persist == 1) + return (0); + else { + free(sym->nam); + free(sym->val); + TAILQ_REMOVE(&symhead, sym, entry); + free(sym); + } + } + sym = calloc(1, sizeof(*sym)); + if (sym == NULL) + return (-1); + + sym->nam = strdup(nam); + if (sym->nam == NULL) { + free(sym); + return (-1); + } + sym->val = strdup(val); + if (sym->val == NULL) { + free(sym->nam); + free(sym); + return (-1); + } + sym->used = 0; + sym->persist = persist; + TAILQ_INSERT_TAIL(&symhead, sym, entry); + return (0); +} + +int +cmdline_symset(char *s) +{ + char *sym, *val; + int ret; + size_t len; + + val = strrchr(s, '='); + if (val == NULL) + return (-1); + + len = strlen(s) - strlen(val) + 1; + sym = malloc(len); + if (sym == NULL) + fatal("%s: malloc", __func__); + + memcpy(&sym, s, len); + + ret = symset(sym, val + 1, 1); + free(sym); + + return (ret); +} + +char * +symget(const char *nam) +{ + struct sym *sym; + + TAILQ_FOREACH(sym, &symhead, entry) { + if (strcmp(nam, sym->nam) == 0) { + sym->used = 1; + return (sym->val); + } + } + return (NULL); +} + +int +getservice(const char *n) +{ + struct servent *s; + const char *errstr; + long long llval; + + llval = strtonum(n, 0, UINT16_MAX, &errstr); + if (errstr) { + s = getservbyname(n, "tcp"); + if (s == NULL) + s = getservbyname(n, "udp"); + if (s == NULL) + return (-1); + return (s->s_port); + } + + return (htons((unsigned short)llval)); +} + +struct address * +host_v4(const char *s) +{ + struct in_addr ina; + struct sockaddr_in *sain; + struct address *h; + + memset(&ina, 0, sizeof(ina)); + if (inet_pton(AF_INET, s, &ina) != 1) + return (NULL); + + if ((h = calloc(1, sizeof(*h))) == NULL) + fatal(__func__); + sain = (struct sockaddr_in *)&h->ss; + sain->sin_len = sizeof(struct sockaddr_in); + sain->sin_family = AF_INET; + sain->sin_addr.s_addr = ina.s_addr; + if (sain->sin_addr.s_addr == INADDR_ANY) + h->prefixlen = 0; /* 0.0.0.0 address */ + else + h->prefixlen = -1; /* host address */ + return (h); +} + +struct address * +host_v6(const char *s) +{ + struct addrinfo hints, *res; + struct sockaddr_in6 *sa_in6; + struct address *h = NULL; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET6; + hints.ai_socktype = SOCK_DGRAM; /* dummy */ + hints.ai_flags = AI_NUMERICHOST; + if (getaddrinfo(s, "0", &hints, &res) == 0) { + if ((h = calloc(1, sizeof(*h))) == NULL) + fatal(__func__); + sa_in6 = (struct sockaddr_in6 *)&h->ss; + sa_in6->sin6_len = sizeof(struct sockaddr_in6); + sa_in6->sin6_family = AF_INET6; + memcpy(&sa_in6->sin6_addr, + &((struct sockaddr_in6 *)res->ai_addr)->sin6_addr, + sizeof(sa_in6->sin6_addr)); + sa_in6->sin6_scope_id = + ((struct sockaddr_in6 *)res->ai_addr)->sin6_scope_id; + if (memcmp(&sa_in6->sin6_addr, &in6addr_any, + sizeof(sa_in6->sin6_addr)) == 0) + h->prefixlen = 0; /* any address */ + else + h->prefixlen = -1; /* host address */ + freeaddrinfo(res); + } + + return (h); +} + +int +host_dns(const char *s, struct addresslist *al, int max, + in_port_t port, const char *ifname, int ipproto) +{ + struct addrinfo hints, *res0, *res; + int error, cnt = 0; + struct sockaddr_in *sain; + struct sockaddr_in6 *sin6; + struct address *h; + + if ((cnt = host_if(s, al, max, port, ifname, ipproto)) != 0) + return (cnt); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; /* DUMMY */ + hints.ai_flags = AI_ADDRCONFIG; + error = getaddrinfo(s, NULL, &hints, &res0); + if (error == EAI_AGAIN || error == EAI_NODATA || error == EAI_NONAME) + return (0); + if (error) { + log_warnx("%s: could not parse \"%s\": %s", __func__, s, + gai_strerror(error)); + return (-1); + } + + for (res = res0; res && cnt < max; res = res->ai_next) { + if (res->ai_family != AF_INET && + res->ai_family != AF_INET6) + continue; + if ((h = calloc(1, sizeof(*h))) == NULL) + fatal(__func__); + + if (port) + h->port = port; + if (ifname != NULL) { + if (strlcpy(h->ifname, ifname, sizeof(h->ifname)) >= + sizeof(h->ifname)) { + log_warnx("%s: interface name truncated", + __func__); + freeaddrinfo(res0); + free(h); + return (-1); + } + } + if (ipproto != -1) + h->ipproto = ipproto; + h->ss.ss_family = res->ai_family; + h->prefixlen = -1; /* host address */ + + if (res->ai_family == AF_INET) { + sain = (struct sockaddr_in *)&h->ss; + sain->sin_len = sizeof(struct sockaddr_in); + sain->sin_addr.s_addr = ((struct sockaddr_in *) + res->ai_addr)->sin_addr.s_addr; + } else { + sin6 = (struct sockaddr_in6 *)&h->ss; + sin6->sin6_len = sizeof(struct sockaddr_in6); + memcpy(&sin6->sin6_addr, &((struct sockaddr_in6 *) + res->ai_addr)->sin6_addr, sizeof(struct in6_addr)); + } + + TAILQ_INSERT_HEAD(al, h, entry); + cnt++; + } + if (cnt == max && res) { + log_warnx("%s: %s resolves to more than %d hosts", __func__, + s, max); + } + freeaddrinfo(res0); + return (cnt); +} + +int +host_if(const char *s, struct addresslist *al, int max, + in_port_t port, const char *ifname, int ipproto) +{ + struct ifaddrs *ifap, *p; + struct sockaddr_in *sain; + struct sockaddr_in6 *sin6; + struct address *h; + int cnt = 0, af; + + if (getifaddrs(&ifap) == -1) + fatal("getifaddrs"); + + /* First search for IPv4 addresses */ + af = AF_INET; + + nextaf: + for (p = ifap; p != NULL && cnt < max; p = p->ifa_next) { + if (p->ifa_addr == NULL || + p->ifa_addr->sa_family != af || + (strcmp(s, p->ifa_name) != 0 && + !is_if_in_group(p->ifa_name, s))) + continue; + if ((h = calloc(1, sizeof(*h))) == NULL) + fatal("calloc"); + + if (port) + h->port = port; + if (ifname != NULL) { + if (strlcpy(h->ifname, ifname, sizeof(h->ifname)) >= + sizeof(h->ifname)) { + log_warnx("%s: interface name truncated", + __func__); + free(h); + freeifaddrs(ifap); + return (-1); + } + } + if (ipproto != -1) + h->ipproto = ipproto; + h->ss.ss_family = af; + h->prefixlen = -1; /* host address */ + + if (af == AF_INET) { + sain = (struct sockaddr_in *)&h->ss; + sain->sin_len = sizeof(struct sockaddr_in); + sain->sin_addr.s_addr = ((struct sockaddr_in *) + p->ifa_addr)->sin_addr.s_addr; + } else { + sin6 = (struct sockaddr_in6 *)&h->ss; + sin6->sin6_len = sizeof(struct sockaddr_in6); + memcpy(&sin6->sin6_addr, &((struct sockaddr_in6 *) + p->ifa_addr)->sin6_addr, sizeof(struct in6_addr)); + sin6->sin6_scope_id = ((struct sockaddr_in6 *) + p->ifa_addr)->sin6_scope_id; + } + + TAILQ_INSERT_HEAD(al, h, entry); + cnt++; + } + if (af == AF_INET) { + /* Next search for IPv6 addresses */ + af = AF_INET6; + goto nextaf; + } + + if (cnt > max) { + log_warnx("%s: %s resolves to more than %d hosts", __func__, + s, max); + } + freeifaddrs(ifap); + return (cnt); +} + +int +host(const char *s, struct addresslist *al, int max, + in_port_t port, const char *ifname, int ipproto) +{ + struct address *h; + + h = host_v4(s); + + /* IPv6 address? */ + if (h == NULL) + h = host_v6(s); + + if (h != NULL) { + if (port) + h->port = port; + if (ifname != NULL) { + if (strlcpy(h->ifname, ifname, sizeof(h->ifname)) >= + sizeof(h->ifname)) { + log_warnx("%s: interface name truncated", + __func__); + free(h); + return (-1); + } + } + if (ipproto != -1) + h->ipproto = ipproto; + + TAILQ_INSERT_HEAD(al, h, entry); + return (1); + } + + return (host_dns(s, al, max, port, ifname, ipproto)); +} + +int +is_if_in_group(const char *ifname, const char *groupname) +{ + unsigned int len; + struct ifgroupreq ifgr; + struct ifg_req *ifg; + int s; + int ret = 0; + + if ((s = socket(AF_INET, SOCK_DGRAM, 0)) == -1) + err(1, "socket"); + + memset(&ifgr, 0, sizeof(ifgr)); + if (strlcpy(ifgr.ifgr_name, ifname, IFNAMSIZ) >= IFNAMSIZ) + err(1, "IFNAMSIZ"); + if (ioctl(s, SIOCGIFGROUP, (caddr_t)&ifgr) == -1) { + if (errno == EINVAL || errno == ENOTTY) + goto end; + err(1, "SIOCGIFGROUP"); + } + + len = ifgr.ifgr_len; + ifgr.ifgr_groups = calloc(len / sizeof(struct ifg_req), + sizeof(struct ifg_req)); + if (ifgr.ifgr_groups == NULL) + err(1, "getifgroups"); + if (ioctl(s, SIOCGIFGROUP, (caddr_t)&ifgr) == -1) + err(1, "SIOCGIFGROUP"); + + ifg = ifgr.ifgr_groups; + for (; ifg && len >= sizeof(struct ifg_req); ifg++) { + len -= sizeof(struct ifg_req); + if (strcmp(ifg->ifgrq_group, groupname) == 0) { + ret = 1; + break; + } + } + free(ifgr.ifgr_groups); + +end: + close(s); + return (ret); +} + +int +get_addrs(const char *addr, struct addresslist *al, in_port_t port) +{ + if (strcmp("", addr) == 0) { + if (host("0.0.0.0", al, 1, port, "0.0.0.0", -1) <= 0) { + yyerror("invalid listen ip: %s", + "0.0.0.0"); + return (-1); + } + if (host("::", al, 1, port, "::", -1) <= 0) { + yyerror("invalid listen ip: %s", "::"); + return (-1); + } + } else { + if (host(addr, al, GOTWEBD_MAXIFACE, port, addr, + -1) <= 0) { + yyerror("invalid listen ip: %s", addr); + return (-1); + } + } + return (0); +} blob - /dev/null blob + 037993ed0667b68af3bc09186201595197a814c9 (mode 644) --- /dev/null +++ gotwebd/proc.c @@ -0,0 +1,837 @@ +/* + * Copyright (c) 2010 - 2016 Reyk Floeter + * Copyright (c) 2008 Pierre-Yves Ritschard + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "proc.h" + +void proc_exec(struct privsep *, struct privsep_proc *, unsigned int, + int, char **); +void proc_setup(struct privsep *, struct privsep_proc *, unsigned int); +void proc_open(struct privsep *, int, int); +void proc_accept(struct privsep *, int, enum privsep_procid, + unsigned int); +void proc_close(struct privsep *); +int proc_ispeer(struct privsep_proc *, unsigned int, enum privsep_procid); +void proc_shutdown(struct privsep_proc *); +void proc_sig_handler(int, short, void *); +int proc_dispatch_null(int, struct privsep_proc *, struct imsg *); + +enum privsep_procid privsep_process; + +int +proc_ispeer(struct privsep_proc *procs, unsigned int nproc, + enum privsep_procid type) +{ + unsigned int i; + + for (i = 0; i < nproc; i++) + if (procs[i].p_id == type) + return (1); + + return (0); +} + +enum privsep_procid +proc_getid(struct privsep_proc *procs, unsigned int nproc, + const char *proc_name) +{ + struct privsep_proc *p; + unsigned int proc; + + for (proc = 0; proc < nproc; proc++) { + p = &procs[proc]; + if (strcmp(p->p_title, proc_name)) + continue; + + return (p->p_id); + } + + return (PROC_MAX); +} + +void +proc_exec(struct privsep *ps, struct privsep_proc *procs, unsigned int nproc, + int argc, char **argv) +{ + unsigned int proc, nargc, i, proc_i; + char **nargv; + struct privsep_proc *p; + char num[32]; + int fd; + + /* Prepare the new process argv. */ + nargv = calloc(argc + 5, sizeof(char *)); + if (nargv == NULL) + fatal("%s: calloc", __func__); + + /* Copy call argument first. */ + nargc = 0; + nargv[nargc++] = argv[0]; + + /* Set process name argument and save the position. */ + nargv[nargc] = strdup("-P"); + if (nargv[nargc] == NULL) + fatal("%s: strdup", __func__); + nargc++; + proc_i = nargc; + nargc++; + + /* Point process instance arg to stack and copy the original args. */ + nargv[nargc] = strdup("-I"); + if (nargv[nargc] == NULL) + fatal("%s: strdup", __func__); + nargc++; + nargv[nargc++] = num; + for (i = 1; i < (unsigned int) argc; i++) + nargv[nargc++] = argv[i]; + + nargv[nargc] = NULL; + + for (proc = 0; proc < nproc; proc++) { + p = &procs[proc]; + + /* Update args with process title. */ + nargv[proc_i] = (char *)(uintptr_t)p->p_title; + + /* Fire children processes. */ + for (i = 0; i < ps->ps_instances[p->p_id]; i++) { + /* Update the process instance number. */ + snprintf(num, sizeof(num), "%u", i); + + fd = ps->ps_pipes[p->p_id][i].pp_pipes[PROC_GOTWEBD][0]; + ps->ps_pipes[p->p_id][i].pp_pipes[PROC_GOTWEBD][0] = -1; + + switch (fork()) { + case -1: + fatal("%s: fork", __func__); + break; + case 0: + /* First create a new session */ + if (setsid() == -1) + fatal("setsid"); + + /* Prepare parent socket. */ + if (fd != PROC_GOTWEBD_SOCK_FILENO) { + if (dup2(fd, PROC_GOTWEBD_SOCK_FILENO) + == -1) + fatal("dup2"); + } else if (fcntl(fd, F_SETFD, 0) == -1) + fatal("fcntl"); + + execvp(argv[0], nargv); + fatal("%s: execvp", __func__); + break; + default: + /* Close child end. */ + close(fd); + break; + } + } + } + + free(nargv); +} + +void +proc_connect(struct privsep *ps) +{ + struct imsgev *iev; + unsigned int src, dst, inst; + + /* Don't distribute any sockets if we are not really going to run. */ + if (ps->ps_noaction) + return; + + for (dst = 0; dst < PROC_MAX; dst++) { + /* We don't communicate with ourselves. */ + if (dst == PROC_GOTWEBD) + continue; + + for (inst = 0; inst < ps->ps_instances[dst]; inst++) { + iev = &ps->ps_ievs[dst][inst]; + imsg_init(&iev->ibuf, ps->ps_pp->pp_pipes[dst][inst]); + event_set(&iev->ev, iev->ibuf.fd, iev->events, + iev->handler, iev->data); + event_add(&iev->ev, NULL); + } + } + + /* Distribute the socketpair()s for everyone. */ + for (src = 0; src < PROC_MAX; src++) + for (dst = src; dst < PROC_MAX; dst++) { + /* Parent already distributed its fds. */ + if (src == PROC_GOTWEBD || dst == PROC_GOTWEBD) + continue; + + proc_open(ps, src, dst); + } +} + +void +proc_init(struct privsep *ps, struct privsep_proc *procs, unsigned int nproc, + int argc, char **argv, enum privsep_procid proc_id) +{ + struct privsep_proc *p = NULL; + struct privsep_pipes *pa, *pb; + unsigned int proc; + unsigned int dst; + int fds[2]; + + /* Don't initiate anything if we are not really going to run. */ + if (ps->ps_noaction) + return; + + if (proc_id == PROC_GOTWEBD) { + privsep_process = PROC_GOTWEBD; + proc_setup(ps, procs, nproc); + + /* + * Create the children sockets so we can use them + * to distribute the rest of the socketpair()s using + * proc_connect() later. + */ + for (dst = 0; dst < PROC_MAX; dst++) { + /* Don't create socket for ourselves. */ + if (dst == PROC_GOTWEBD) + continue; + + for (proc = 0; proc < ps->ps_instances[dst]; proc++) { + pa = &ps->ps_pipes[PROC_GOTWEBD][0]; + pb = &ps->ps_pipes[dst][proc]; + if (socketpair(AF_UNIX, + SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, + PF_UNSPEC, fds) == -1) + fatal("%s: socketpair", __func__); + + pa->pp_pipes[dst][proc] = fds[0]; + pb->pp_pipes[PROC_GOTWEBD][0] = fds[1]; + } + } + + /* Engage! */ + proc_exec(ps, procs, nproc, argc, argv); + return; + } + + /* Initialize a child */ + for (proc = 0; proc < nproc; proc++) { + if (procs[proc].p_id != proc_id) + continue; + p = &procs[proc]; + break; + } + if (p == NULL || p->p_init == NULL) + fatalx("%s: process %d missing process initialization", + __func__, proc_id); + + p->p_init(ps, p); + + fatalx("failed to initiate child process"); +} + +void +proc_accept(struct privsep *ps, int fd, enum privsep_procid dst, + unsigned int n) +{ + struct privsep_pipes *pp = ps->ps_pp; + struct imsgev *iev; + + if (ps->ps_ievs[dst] == NULL) { +#if DEBUG > 1 + log_debug("%s: %s src %d %d to dst %d %d not connected", + __func__, ps->ps_title[privsep_process], + privsep_process, ps->ps_instance + 1, + dst, n + 1); +#endif + close(fd); + return; + } + + if (pp->pp_pipes[dst][n] != -1) { + log_warnx("%s: duplicated descriptor", __func__); + close(fd); + return; + } else + pp->pp_pipes[dst][n] = fd; + + iev = &ps->ps_ievs[dst][n]; + imsg_init(&iev->ibuf, fd); + event_set(&iev->ev, iev->ibuf.fd, iev->events, iev->handler, iev->data); + event_add(&iev->ev, NULL); +} + +void +proc_setup(struct privsep *ps, struct privsep_proc *procs, unsigned int nproc) +{ + unsigned int i, j, src, dst, id; + struct privsep_pipes *pp; + + /* Initialize parent title, ps_instances and procs. */ + ps->ps_title[PROC_GOTWEBD] = "parent"; + + for (src = 0; src < PROC_MAX; src++) + /* Default to 1 process instance */ + if (ps->ps_instances[src] < 1) + ps->ps_instances[src] = 1; + + for (src = 0; src < nproc; src++) { + procs[src].p_ps = ps; + if (procs[src].p_cb == NULL) + procs[src].p_cb = proc_dispatch_null; + + id = procs[src].p_id; + ps->ps_title[id] = procs[src].p_title; + ps->ps_ievs[id] = calloc(ps->ps_instances[id], + sizeof(struct imsgev)); + if (ps->ps_ievs[id] == NULL) + fatal("%s: calloc", __func__); + + /* With this set up, we are ready to call imsg_init(). */ + for (i = 0; i < ps->ps_instances[id]; i++) { + ps->ps_ievs[id][i].handler = proc_dispatch; + ps->ps_ievs[id][i].events = EV_READ; + ps->ps_ievs[id][i].proc = &procs[src]; + ps->ps_ievs[id][i].data = &ps->ps_ievs[id][i]; + } + } + + /* + * Allocate pipes for all process instances (incl. parent) + * + * - ps->ps_pipes: N:M mapping + * N source processes connected to M destination processes: + * [src][instances][dst][instances], for example + * [PROC_RELAY][3][PROC_CA][3] + * + * - ps->ps_pp: per-process 1:M part of ps->ps_pipes + * Each process instance has a destination array of socketpair fds: + * [dst][instances], for example + * [PROC_GOTWEBD][0] + */ + for (src = 0; src < PROC_MAX; src++) { + /* Allocate destination array for each process */ + ps->ps_pipes[src] = calloc(ps->ps_instances[src], + sizeof(struct privsep_pipes)); + if (ps->ps_pipes[src] == NULL) + fatal("%s: calloc", __func__); + + for (i = 0; i < ps->ps_instances[src]; i++) { + pp = &ps->ps_pipes[src][i]; + + for (dst = 0; dst < PROC_MAX; dst++) { + /* Allocate maximum fd integers */ + pp->pp_pipes[dst] = + calloc(ps->ps_instances[dst], + sizeof(int)); + if (pp->pp_pipes[dst] == NULL) + fatal("%s: calloc", __func__); + + /* Mark fd as unused */ + for (j = 0; j < ps->ps_instances[dst]; j++) + pp->pp_pipes[dst][j] = -1; + } + } + } + + ps->ps_pp = &ps->ps_pipes[privsep_process][ps->ps_instance]; +} + +void +proc_kill(struct privsep *ps) +{ + char *cause; + pid_t pid; + int len, status; + + if (privsep_process != PROC_GOTWEBD) + return; + + proc_close(ps); + + do { + pid = waitpid(WAIT_ANY, &status, 0); + if (pid <= 0) + continue; + + if (WIFSIGNALED(status)) { + len = asprintf(&cause, "terminated; signal %d", + WTERMSIG(status)); + } else if (WIFEXITED(status)) { + if (WEXITSTATUS(status) != 0) + len = asprintf(&cause, "exited abnormally"); + else + len = 0; + } else + len = -1; + + if (len == 0) { + /* child exited OK, don't print a warning message */ + } else if (len != -1) { + log_warnx("lost child: pid %u %s", pid, cause); + free(cause); + } else + log_warnx("lost child: pid %u", pid); + } while (pid != -1 || (pid == -1 && errno == EINTR)); +} + +void +proc_open(struct privsep *ps, int src, int dst) +{ + struct privsep_pipes *pa, *pb; + struct privsep_fd pf; + int fds[2]; + unsigned int i, j; + + /* Exchange pipes between process. */ + for (i = 0; i < ps->ps_instances[src]; i++) { + for (j = 0; j < ps->ps_instances[dst]; j++) { + /* Don't create sockets for ourself. */ + if (src == dst && i == j) + continue; + + pa = &ps->ps_pipes[src][i]; + pb = &ps->ps_pipes[dst][j]; + if (socketpair(AF_UNIX, + SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, + PF_UNSPEC, fds) == -1) + fatal("%s: socketpair", __func__); + + pa->pp_pipes[dst][j] = fds[0]; + pb->pp_pipes[src][i] = fds[1]; + + pf.pf_procid = src; + pf.pf_instance = i; + if (proc_compose_imsg(ps, dst, j, IMSG_CTL_PROCFD, + -1, pb->pp_pipes[src][i], &pf, sizeof(pf)) == -1) + fatal("%s: proc_compose_imsg", __func__); + + pf.pf_procid = dst; + pf.pf_instance = j; + if (proc_compose_imsg(ps, src, i, IMSG_CTL_PROCFD, + -1, pa->pp_pipes[dst][j], &pf, sizeof(pf)) == -1) + fatal("%s: proc_compose_imsg", __func__); + + /* + * We have to flush to send the descriptors and close + * them to avoid the fd ramp on startup. + */ + if (proc_flush_imsg(ps, src, i) == -1 || + proc_flush_imsg(ps, dst, j) == -1) + fatal("%s: imsg_flush", __func__); + } + } +} + +void +proc_close(struct privsep *ps) +{ + unsigned int dst, n; + struct privsep_pipes *pp; + + if (ps == NULL) + return; + + pp = ps->ps_pp; + + for (dst = 0; dst < PROC_MAX; dst++) { + if (ps->ps_ievs[dst] == NULL) + continue; + + for (n = 0; n < ps->ps_instances[dst]; n++) { + if (pp->pp_pipes[dst][n] == -1) + continue; + + /* Cancel the fd, close and invalidate the fd */ + event_del(&(ps->ps_ievs[dst][n].ev)); + imsg_clear(&(ps->ps_ievs[dst][n].ibuf)); + close(pp->pp_pipes[dst][n]); + pp->pp_pipes[dst][n] = -1; + } + free(ps->ps_ievs[dst]); + } +} + +void +proc_shutdown(struct privsep_proc *p) +{ + struct privsep *ps = p->p_ps; + + if (p->p_shutdown != NULL) + (*p->p_shutdown)(); + + proc_close(ps); + + log_info("%s, %s exiting, pid %d", getprogname(), p->p_title, getpid()); + + exit(0); +} + +void +proc_sig_handler(int sig, short event, void *arg) +{ + struct privsep_proc *p = arg; + + switch (sig) { + case SIGINT: + case SIGTERM: + proc_shutdown(p); + break; + case SIGCHLD: + case SIGHUP: + case SIGPIPE: + case SIGUSR1: + /* ignore */ + break; + default: + fatalx("proc_sig_handler: unexpected signal"); + /* NOTREACHED */ + } +} + +void +proc_run(struct privsep *ps, struct privsep_proc *p, + struct privsep_proc *procs, unsigned int nproc, + void (*run)(struct privsep *, struct privsep_proc *, void *), void *arg) +{ + struct passwd *pw; + const char *root; + + log_procinit(p->p_title); + + /* Set the process group of the current process */ + setpgid(0, 0); + + /* Use non-standard user */ + if (p->p_pw != NULL) + pw = p->p_pw; + else + pw = ps->ps_pw; + + /* Change root directory */ + if (p->p_chroot != NULL) + root = p->p_chroot; + else + root = pw->pw_dir; + + if (chroot(root) == -1) + fatal("proc_run: chroot"); + if (chdir("/") == -1) + fatal("proc_run: chdir(\"/\")"); + + privsep_process = p->p_id; + + setproctitle("%s", p->p_title); + + if (setgroups(1, &pw->pw_gid) || + setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || + setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) + fatal("proc_run: cannot drop privileges"); + + event_init(); + + signal_set(&ps->ps_evsigint, SIGINT, proc_sig_handler, p); + signal_set(&ps->ps_evsigterm, SIGTERM, proc_sig_handler, p); + signal_set(&ps->ps_evsigchld, SIGCHLD, proc_sig_handler, p); + signal_set(&ps->ps_evsighup, SIGHUP, proc_sig_handler, p); + signal_set(&ps->ps_evsigpipe, SIGPIPE, proc_sig_handler, p); + signal_set(&ps->ps_evsigusr1, SIGUSR1, proc_sig_handler, p); + + signal_add(&ps->ps_evsigint, NULL); + signal_add(&ps->ps_evsigterm, NULL); + signal_add(&ps->ps_evsigchld, NULL); + signal_add(&ps->ps_evsighup, NULL); + signal_add(&ps->ps_evsigpipe, NULL); + signal_add(&ps->ps_evsigusr1, NULL); + + proc_setup(ps, procs, nproc); + proc_accept(ps, PROC_GOTWEBD_SOCK_FILENO, PROC_GOTWEBD, 0); + + DPRINTF("%s: %s %d/%d, pid %d", __func__, p->p_title, + ps->ps_instance + 1, ps->ps_instances[p->p_id], getpid()); + + if (run != NULL) + run(ps, p, arg); + + event_dispatch(); + + proc_shutdown(p); +} + +void +proc_dispatch(int fd, short event, void *arg) +{ + struct imsgev *iev = arg; + struct privsep_proc *p = iev->proc; + struct privsep *ps = p->p_ps; + struct imsgbuf *ibuf; + struct imsg imsg; + ssize_t n; + int verbose; + const char *title; + struct privsep_fd pf; + + title = ps->ps_title[privsep_process]; + ibuf = &iev->ibuf; + + if (event & EV_READ) { + n = imsg_read(ibuf); + if (n == -1 && errno != EAGAIN) + fatal("%s: imsg_read", __func__); + if (n == 0) { + /* this pipe is dead, so remove the event handler */ + event_del(&iev->ev); + event_loopexit(NULL); + return; + } + } + + if (event & EV_WRITE) { + n = msgbuf_write(&ibuf->w); + if (n == -1 && errno != EAGAIN) + fatal("%s: msgbuf_write", __func__); + if (n == 0) { + /* this pipe is dead, so remove the event handler */ + event_del(&iev->ev); + event_loopexit(NULL); + return; + } + } + + for (;;) { + n = imsg_get(ibuf, &imsg); + if (n == -1) + fatal("%s: imsg_get", __func__); + if (n == 0) + break; + +#if DEBUG > 1 + log_debug("%s: %s %d got imsg %d peerid %d from %s %d", + __func__, title, ps->ps_instance + 1, + imsg.hdr.type, imsg.hdr.peerid, p->p_title, imsg.hdr.pid); +#endif + + /* + * Check the message with the program callback + */ + if ((p->p_cb)(fd, p, &imsg) == 0) { + /* Message was handled by the callback, continue */ + imsg_free(&imsg); + continue; + } + + /* + * Generic message handling + */ + switch (imsg.hdr.type) { + case IMSG_CTL_VERBOSE: + log_info("%s", __func__); + IMSG_SIZE_CHECK(&imsg, &verbose); + memcpy(&verbose, imsg.data, sizeof(verbose)); + log_setverbose(verbose); + break; + case IMSG_CTL_PROCFD: + IMSG_SIZE_CHECK(&imsg, &pf); + memcpy(&pf, imsg.data, sizeof(pf)); + proc_accept(ps, imsg.fd, pf.pf_procid, + pf.pf_instance); + break; + default: + log_warnx("%s: %s %d got invalid imsg %d peerid %d " + "from %s %d", + __func__, title, ps->ps_instance + 1, + imsg.hdr.type, imsg.hdr.peerid, + p->p_title, imsg.hdr.pid); + } + imsg_free(&imsg); + } + imsg_event_add(iev); +} + +int +proc_dispatch_null(int fd, struct privsep_proc *p, struct imsg *imsg) +{ + return (-1); +} + +/* + * imsg helper functions + */ + +void +imsg_event_add(struct imsgev *iev) +{ + if (iev->handler == NULL) { + imsg_flush(&iev->ibuf); + return; + } + + iev->events = EV_READ; + if (iev->ibuf.w.queued) + iev->events |= EV_WRITE; + + event_del(&iev->ev); + event_set(&iev->ev, iev->ibuf.fd, iev->events, iev->handler, iev->data); + event_add(&iev->ev, NULL); +} + +int +imsg_compose_event(struct imsgev *iev, uint16_t type, uint32_t peerid, + pid_t pid, int fd, void *data, uint16_t datalen) +{ + int ret; + + ret = imsg_compose(&iev->ibuf, type, peerid, pid, fd, data, datalen); + if (ret == -1) + return (ret); + imsg_event_add(iev); + return (ret); +} + +int +imsg_composev_event(struct imsgev *iev, uint16_t type, uint32_t peerid, + pid_t pid, int fd, const struct iovec *iov, int iovcnt) +{ + int ret; + + ret = imsg_composev(&iev->ibuf, type, peerid, pid, fd, iov, iovcnt); + if (ret == -1) + return (ret); + imsg_event_add(iev); + return (ret); +} + +void +proc_range(struct privsep *ps, enum privsep_procid id, int *n, int *m) +{ + if (*n == -1) { + /* Use a range of all target instances */ + *n = 0; + *m = ps->ps_instances[id]; + } else { + /* Use only a single slot of the specified peer process */ + *m = *n + 1; + } +} + +int +proc_compose_imsg(struct privsep *ps, enum privsep_procid id, int n, + uint16_t type, uint32_t peerid, int fd, void *data, uint16_t datalen) +{ + int m; + + proc_range(ps, id, &n, &m); + for (; n < m; n++) { + if (imsg_compose_event(&ps->ps_ievs[id][n], + type, peerid, ps->ps_instance + 1, fd, data, datalen) == -1) + return (-1); + } + + return (0); +} + +int +proc_compose(struct privsep *ps, enum privsep_procid id, + uint16_t type, void *data, uint16_t datalen) +{ + return (proc_compose_imsg(ps, id, -1, type, -1, -1, data, datalen)); +} + +int +proc_composev_imsg(struct privsep *ps, enum privsep_procid id, int n, + uint16_t type, uint32_t peerid, int fd, const struct iovec *iov, int iovcnt) +{ + int m; + + proc_range(ps, id, &n, &m); + for (; n < m; n++) + if (imsg_composev_event(&ps->ps_ievs[id][n], + type, peerid, ps->ps_instance + 1, fd, iov, iovcnt) == -1) + return (-1); + + return (0); +} + +int +proc_composev(struct privsep *ps, enum privsep_procid id, + uint16_t type, const struct iovec *iov, int iovcnt) +{ + return (proc_composev_imsg(ps, id, -1, type, -1, -1, iov, iovcnt)); +} + +int +proc_forward_imsg(struct privsep *ps, struct imsg *imsg, + enum privsep_procid id, int n) +{ + return (proc_compose_imsg(ps, id, n, imsg->hdr.type, + imsg->hdr.peerid, imsg->fd, imsg->data, IMSG_DATA_SIZE(imsg))); +} + +struct imsgbuf * +proc_ibuf(struct privsep *ps, enum privsep_procid id, int n) +{ + int m; + + proc_range(ps, id, &n, &m); + return (&ps->ps_ievs[id][n].ibuf); +} + +struct imsgev * +proc_iev(struct privsep *ps, enum privsep_procid id, int n) +{ + int m; + + proc_range(ps, id, &n, &m); + return (&ps->ps_ievs[id][n]); +} + +/* This function should only be called with care as it breaks async I/O */ +int +proc_flush_imsg(struct privsep *ps, enum privsep_procid id, int n) +{ + struct imsgbuf *ibuf; + int m, ret = 0; + + proc_range(ps, id, &n, &m); + for (; n < m; n++) { + ibuf = proc_ibuf(ps, id, n); + if (ibuf == NULL) + return (-1); + do { + ret = imsg_flush(ibuf); + } while (ret == -1 && errno == EAGAIN); + if (ret == -1) + break; + imsg_event_add(&ps->ps_ievs[id][n]); + } + + return (ret); +} blob - /dev/null blob + 524112fcf4b0c72a25c6f5a4cb85c16bd33ef3a9 (mode 644) --- /dev/null +++ gotwebd/proc.h @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2010-2015 Reyk Floeter + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +enum { + IMSG_NONE, + IMSG_CTL_OK, + IMSG_CTL_FAIL, + IMSG_CTL_VERBOSE, + IMSG_CTL_NOTIFY, + IMSG_CTL_RESET, + IMSG_CTL_PROCFD, + IMSG_PROC_MAX +}; + +/* imsg */ +struct imsgev { + struct imsgbuf ibuf; + void (*handler)(int, short, void *); + struct event ev; + struct privsep_proc *proc; + void *data; + short events; +}; + +#define IMSG_SIZE_CHECK(imsg, p) do { \ + if (IMSG_DATA_SIZE(imsg) < sizeof(*p)) \ + fatalx("bad length imsg received (%s)", #p); \ +} while (0) +#define IMSG_DATA_SIZE(imsg) ((imsg)->hdr.len - IMSG_HEADER_SIZE) + +struct ctl_conn { + TAILQ_ENTRY(ctl_conn) entry; + uint8_t flags; + unsigned int waiting; +#define CTL_CONN_NOTIFY 0x01 + struct imsgev iev; + uid_t uid; +}; +TAILQ_HEAD(ctl_connlist, ctl_conn); +extern struct ctl_connlist ctl_conns; + +/* privsep */ +enum privsep_procid { + PROC_GOTWEBD = 0, + PROC_SOCKS, + PROC_MAX, +}; +extern enum privsep_procid privsep_process; + +#define CONFIG_RELOAD 0x00 +#define CONFIG_SOCKS 0x01 +#define CONFIG_ALL 0xff + +struct privsep_pipes { + int *pp_pipes[PROC_MAX]; +}; + +struct privsep { + struct privsep_pipes *ps_pipes[PROC_MAX]; + struct privsep_pipes *ps_pp; + + struct imsgev *ps_ievs[PROC_MAX]; + const char *ps_title[PROC_MAX]; + uint8_t ps_what[PROC_MAX]; + + struct passwd *ps_pw; + int ps_noaction; + + unsigned int ps_instances[PROC_MAX]; + unsigned int ps_instance; + + /* Event and signal handlers */ + struct event ps_evsigint; + struct event ps_evsigterm; + struct event ps_evsigchld; + struct event ps_evsighup; + struct event ps_evsigpipe; + struct event ps_evsigusr1; + + void *ps_env; +}; + +struct privsep_proc { + const char *p_title; + enum privsep_procid p_id; + int (*p_cb)(int, struct privsep_proc *, + struct imsg *); + void (*p_init)(struct privsep *, + struct privsep_proc *); + void (*p_shutdown)(void); + const char *p_chroot; + struct passwd *p_pw; + struct privsep *p_ps; +}; + +struct privsep_fd { + enum privsep_procid pf_procid; + unsigned int pf_instance; +}; + +#if DEBUG +#define DPRINTF log_debug +#else +#define DPRINTF(x...) do {} while(0) +#endif + +#define PROC_GOTWEBD_SOCK_FILENO 3 +#define PROC_MAX_INSTANCES 32 + +/* proc.c */ +void proc_init(struct privsep *, struct privsep_proc *, unsigned int, + int, char **, enum privsep_procid); +void proc_kill(struct privsep *); +void proc_connect(struct privsep *ps); +void proc_dispatch(int, short event, void *); +void proc_range(struct privsep *, enum privsep_procid, int *, int *); +void proc_run(struct privsep *, struct privsep_proc *, + struct privsep_proc *, unsigned int, + void (*)(struct privsep *, struct privsep_proc *, void *), void *); +void imsg_event_add(struct imsgev *); +int imsg_compose_event(struct imsgev *, uint16_t, uint32_t, + pid_t, int, void *, uint16_t); +int imsg_composev_event(struct imsgev *, uint16_t, uint32_t, + pid_t, int, const struct iovec *, int); +int proc_compose_imsg(struct privsep *, enum privsep_procid, int, + uint16_t, uint32_t, int, void *, uint16_t); +int proc_compose(struct privsep *, enum privsep_procid, + uint16_t, void *data, uint16_t); +int proc_composev_imsg(struct privsep *, enum privsep_procid, int, + uint16_t, uint32_t, int, const struct iovec *, int); +int proc_composev(struct privsep *, enum privsep_procid, + uint16_t, const struct iovec *, int); +int proc_forward_imsg(struct privsep *, struct imsg *, + enum privsep_procid, int); +struct imsgbuf * + proc_ibuf(struct privsep *, enum privsep_procid, int); +struct imsgev * + proc_iev(struct privsep *, enum privsep_procid, int); +enum privsep_procid + proc_getid(struct privsep_proc *, unsigned int, const char *); +int proc_flush_imsg(struct privsep *, enum privsep_procid, int); + +/* log.c */ +void log_init(int, int); +void log_procinit(const char *); +void log_setverbose(int); +int log_getverbose(void); +void log_warn(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_warnx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_info(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_debug(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void logit(int, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); +void vlog(int, const char *, va_list) + __attribute__((__format__ (printf, 2, 0))); +__dead void fatal(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +__dead void fatalx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); blob - /dev/null blob + f07a3be3b97818f5ddfc016d4f5b93952636de34 (mode 644) --- /dev/null +++ gotwebd/sockets.c @@ -0,0 +1,724 @@ +/* + * Copyright (c) 2016, 2019, 2020-2021 Tracey Emery + * Copyright (c) 2015 Mike Larkin + * Copyright (c) 2013 David Gwynne + * Copyright (c) 2013 Florian Obser + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "got_error.h" +#include "got_opentemp.h" + +#include "proc.h" +#include "gotwebd.h" + +#define SOCKS_BACKLOG 5 +#define MAXIMUM(a, b) (((a) > (b)) ? (a) : (b)) + + +volatile int client_cnt; + +struct timeval timeout = { TIMEOUT_DEFAULT, 0 }; + +void sockets_sighdlr(int, short, void *); +void sockets_run(struct privsep *, struct privsep_proc *, void *); +void sockets_launch(void); +void sockets_purge(struct gotwebd *); +void sockets_accept_paused(int, short, void *); +void sockets_dup_new_socket(struct socket *, struct socket *); +void sockets_rlimit(int); + + +int sockets_dispatch_gotwebd(int, struct privsep_proc *, struct imsg *); +int sockets_unix_socket_listen(struct privsep *, struct socket *); +int sockets_create_socket(struct addresslist *, in_port_t); +int sockets_socket_af(struct sockaddr_storage *, in_port_t); +int sockets_accept_reserve(int, struct sockaddr *, socklen_t *, int, + volatile int *); + +struct socket + *sockets_conf_new_socket(struct gotwebd *, struct server *, int, int, + int); + +int cgi_inflight = 0; + +static struct privsep_proc procs[] = { + { "gotwebd", PROC_GOTWEBD, sockets_dispatch_gotwebd }, +}; + +void +sockets(struct privsep *ps, struct privsep_proc *p) +{ + proc_run(ps, p, procs, nitems(procs), sockets_run, NULL); +} + +void +sockets_run(struct privsep *ps, struct privsep_proc *p, void *arg) +{ + if (config_init(ps->ps_env) == -1) + fatal("failed to initialize configuration"); + + p->p_shutdown = sockets_shutdown; + + sockets_rlimit(-1); + + signal_del(&ps->ps_evsigchld); + signal_set(&ps->ps_evsigchld, SIGCHLD, sockets_sighdlr, ps); + signal_add(&ps->ps_evsigchld, NULL); + +#ifndef PROFILE + if (pledge("stdio rpath wpath cpath inet recvfd proc exec sendfd", + NULL) == -1) + fatal("pledge"); +#endif +} + +void +sockets_parse_sockets(struct gotwebd *env) +{ + struct server *srv; + struct socket *sock, *new_sock = NULL; + struct address *a; + int sock_id = 0, ipv4 = 0, ipv6 = 0; + + TAILQ_FOREACH(srv, env->servers, entry) { + if (srv->unix_socket) { + sock_id++; + new_sock = sockets_conf_new_socket(env, srv, + sock_id, UNIX, 0); + TAILQ_INSERT_TAIL(env->sockets, new_sock, entry); + } + + if (srv->fcgi_socket) { + sock_id++; + new_sock = sockets_conf_new_socket(env, srv, + sock_id, FCGI, 0); + TAILQ_INSERT_TAIL(env->sockets, new_sock, entry); + + /* add ipv6 children */ + TAILQ_FOREACH(sock, env->sockets, entry) { + ipv4 = ipv6 = 0; + + TAILQ_FOREACH(a, sock->conf.al, entry) { + if (a->ss.ss_family == AF_INET) + ipv4 = 1; + if (a->ss.ss_family == AF_INET6) + ipv6 = 1; + } + + /* create ipv6 sock */ + if (ipv4 == 1 && ipv6 == 1) { + sock_id++; + sock->conf.child_id = sock_id; + new_sock = sockets_conf_new_socket(env, + srv, sock_id, FCGI, 1); + sockets_dup_new_socket(sock, new_sock); + TAILQ_INSERT_TAIL(env->sockets, + new_sock, entry); + continue; + } + } + } + } +} + +void +sockets_dup_new_socket(struct socket *p_sock, struct socket *sock) +{ + struct address *a, *acp; + int n; + + sock->conf.parent_id = p_sock->conf.id; + sock->conf.ipv4 = 0; + sock->conf.ipv6 = 1; + + memcpy(&sock->conf.srv_name, p_sock->conf.srv_name, + sizeof(sock->conf.srv_name)); + + n = snprintf(sock->conf.name, GOTWEBD_MAXTEXT, "%s_child", + p_sock->conf.srv_name); + if (n < 0) { + free(p_sock->conf.al); + free(p_sock); + free(sock->conf.al); + free(sock); + fatalx("%s: snprintf", __func__); + } + + TAILQ_FOREACH(a, p_sock->conf.al, entry) { + if (a->ss.ss_family == AF_INET) + continue; + + if ((acp = calloc(1, sizeof(*acp))) == NULL) + fatal("%s: calloc", __func__); + memcpy(&acp->ss, &a->ss, sizeof(acp->ss)); + acp->ipproto = a->ipproto; + acp->prefixlen = a->prefixlen; + acp->port = a->port; + if (strlen(a->ifname) != 0) { + if (strlcpy(acp->ifname, a->ifname, + sizeof(acp->ifname)) >= sizeof(acp->ifname)) { + fatalx("%s: interface name truncated", + __func__); + } + } + + TAILQ_INSERT_TAIL(sock->conf.al, acp, entry); + } +} + +struct socket * +sockets_conf_new_socket(struct gotwebd *env, struct server *srv, int id, + int type, int is_dup) +{ + struct socket *sock; + struct address *a, *acp; + int n; + + if ((sock = calloc(1, sizeof(*sock))) == NULL) + fatalx("%s: calloc", __func__); + + if ((sock->conf.al = calloc(1, sizeof(*sock->conf.al))) == NULL) { + free(sock); + fatalx("%s: calloc", __func__); + } + TAILQ_INIT(sock->conf.al); + + sock->conf.parent_id = 0; + sock->conf.id = id; + + sock->fd = -1; + + sock->conf.type = type; + + if (type == UNIX) { + if (strlcpy(sock->conf.unix_socket_name, + srv->unix_socket_name, + sizeof(sock->conf.unix_socket_name)) >= + sizeof(sock->conf.unix_socket_name)) { + free(sock->conf.al); + free(sock); + fatalx("%s: strlcpy", __func__); + } + } else + sock->conf.ipv4 = 1; + + sock->conf.fcgi_socket_port = srv->fcgi_socket_port; + + if (is_dup) + goto done; + + n = snprintf(sock->conf.name, GOTWEBD_MAXTEXT, "%s_parent", + srv->name); + if (n < 0) { + free(sock->conf.al); + free(sock); + fatalx("%s: snprintf", __func__); + } + + if (strlcpy(sock->conf.srv_name, srv->name, + sizeof(sock->conf.srv_name)) >= sizeof(sock->conf.srv_name)) { + free(sock->conf.al); + free(sock); + fatalx("%s: strlcpy", __func__); + } + + TAILQ_FOREACH(a, srv->al, entry) { + if ((acp = calloc(1, sizeof(*acp))) == NULL) { + free(sock->conf.al); + free(sock); + fatal("%s: calloc", __func__); + } + memcpy(&acp->ss, &a->ss, sizeof(acp->ss)); + acp->ipproto = a->ipproto; + acp->prefixlen = a->prefixlen; + acp->port = a->port; + if (strlen(a->ifname) != 0) { + if (strlcpy(acp->ifname, a->ifname, + sizeof(acp->ifname)) >= sizeof(acp->ifname)) { + fatalx("%s: interface name truncated", + __func__); + } + } + + TAILQ_INSERT_TAIL(sock->conf.al, acp, entry); + } +done: + return (sock); +} + +int +sockets_socket_af(struct sockaddr_storage *ss, in_port_t port) +{ + switch (ss->ss_family) { + case AF_INET: + ((struct sockaddr_in *)ss)->sin_port = port; + ((struct sockaddr_in *)ss)->sin_len = + sizeof(struct sockaddr_in); + break; + case AF_INET6: + ((struct sockaddr_in6 *)ss)->sin6_port = port; + ((struct sockaddr_in6 *)ss)->sin6_len = + sizeof(struct sockaddr_in6); + break; + default: + return -1; + } + + return 0; +} + +void +sockets_launch(void) +{ + struct socket *sock; + + TAILQ_FOREACH(sock, gotwebd_env->sockets, entry) { + log_debug("%s: configuring socket %d (%d)", __func__, + sock->conf.id, sock->fd); + + event_set(&sock->ev, sock->fd, EV_READ | EV_PERSIST, + sockets_socket_accept, sock); + + if (event_add(&sock->ev, NULL)) + fatalx("event add sock"); + + evtimer_set(&sock->pause, sockets_accept_paused, sock); + + log_debug("%s: running socket listener %d", __func__, + sock->conf.id); + } +} + +void +sockets_purge(struct gotwebd *env) +{ + struct socket *sock, *tsock; + + /* shutdown and remove sockets */ + TAILQ_FOREACH_SAFE(sock, env->sockets, entry, tsock) { + if (event_initialized(&sock->ev)) + event_del(&sock->ev); + if (evtimer_initialized(&sock->evt)) + evtimer_del(&sock->evt); + if (evtimer_initialized(&sock->pause)) + evtimer_del(&sock->pause); + if (sock->fd != -1) + close(sock->fd); + TAILQ_REMOVE(env->sockets, sock, entry); + } +} + +int +sockets_dispatch_gotwebd(int fd, struct privsep_proc *p, struct imsg *imsg) +{ + struct privsep *ps = p->p_ps; + int res = 0, cmd = 0, verbose; + + switch (imsg->hdr.type) { + case IMSG_CFG_SRV: + config_getserver(gotwebd_env, imsg); + break; + case IMSG_CFG_SOCK: + config_getsock(gotwebd_env, imsg); + break; + case IMSG_CFG_FD: + config_getfd(gotwebd_env, imsg); + break; + case IMSG_CFG_DONE: + config_getcfg(gotwebd_env, imsg); + break; + case IMSG_CTL_START: + sockets_launch(); + break; + case IMSG_CTL_VERBOSE: + IMSG_SIZE_CHECK(imsg, &verbose); + memcpy(&verbose, imsg->data, sizeof(verbose)); + log_setverbose(verbose); + break; + default: + return -1; + } + + switch (cmd) { + case 0: + break; + default: + if (proc_compose_imsg(ps, PROC_GOTWEBD, -1, cmd, + imsg->hdr.peerid, -1, &res, sizeof(res)) == -1) + return -1; + break; + } + + return 0; +} + +void +sockets_sighdlr(int sig, short event, void *arg) +{ + switch (sig) { + case SIGHUP: + log_info("%s: ignoring SIGHUP", __func__); + break; + case SIGPIPE: + log_info("%s: ignoring SIGPIPE", __func__); + break; + case SIGUSR1: + log_info("%s: ignoring SIGUSR1", __func__); + break; + case SIGCHLD: + break; + default: + log_info("SIGNAL: %d", sig); + fatalx("unexpected signal"); + } +} + +void +sockets_shutdown(void) +{ + struct server *srv, *tsrv; + struct socket *sock, *tsock; + + sockets_purge(gotwebd_env); + + /* clean sockets */ + TAILQ_FOREACH_SAFE(sock, gotwebd_env->sockets, entry, tsock) { + TAILQ_REMOVE(gotwebd_env->sockets, sock, entry); + close(sock->fd); + free(sock); + } + + /* clean servers */ + TAILQ_FOREACH_SAFE(srv, gotwebd_env->servers, entry, tsrv) + free(srv); + + free(gotwebd_env->sockets); + free(gotwebd_env->servers); + free(gotwebd_env); +} + +int +sockets_privinit(struct gotwebd *env, struct socket *sock) +{ + struct privsep *ps = env->gotwebd_ps; + + if (sock->conf.type == UNIX) { + log_debug("%s: initializing unix socket %s", __func__, + sock->conf.unix_socket_name); + sock->fd = sockets_unix_socket_listen(ps, sock); + if (sock->fd == -1) { + log_warnx("%s: create unix socket failed", __func__); + return -1; + } + } + + if (sock->conf.type == FCGI) { + log_debug("%s: initializing fcgi socket for %s", __func__, + sock->conf.name); + sock->fd = sockets_create_socket(sock->conf.al, + sock->conf.fcgi_socket_port); + if (sock->fd == -1) { + log_warnx("%s: create unix socket failed", __func__); + return -1; + } + } + + return 0; +} + +int +sockets_unix_socket_listen(struct privsep *ps, struct socket *sock) +{ + struct gotwebd *env = ps->ps_env; + struct sockaddr_un sun; + struct socket *tsock; + int u_fd = -1; + mode_t old_umask, mode; + + TAILQ_FOREACH(tsock, env->sockets, entry) { + if (strcmp(tsock->conf.unix_socket_name, + sock->conf.unix_socket_name) == 0 && + tsock->fd != -1) + return (tsock->fd); + } + + u_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK| SOCK_CLOEXEC, 0); + if (u_fd == -1) { + log_warn("%s: socket", __func__); + return -1; + } + + sun.sun_family = AF_UNIX; + if (strlcpy(sun.sun_path, sock->conf.unix_socket_name, + sizeof(sun.sun_path)) >= sizeof(sun.sun_path)) { + log_warn("%s: %s name too long", __func__, + sock->conf.unix_socket_name); + close(u_fd); + return -1; + } + + if (unlink(sock->conf.unix_socket_name) == -1) { + if (errno != ENOENT) { + log_warn("%s: unlink %s", __func__, + sock->conf.unix_socket_name); + close(u_fd); + return -1; + } + } + + old_umask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH); + mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP; + + if (bind(u_fd, (struct sockaddr *)&sun, sizeof(sun)) == -1) { + log_warn("%s: bind: %s", __func__, sock->conf.unix_socket_name); + close(u_fd); + (void)umask(old_umask); + return -1; + } + + (void)umask(old_umask); + + if (chmod(sock->conf.unix_socket_name, mode) == -1) { + log_warn("%s: chmod", __func__); + close(u_fd); + (void)unlink(sock->conf.unix_socket_name); + return -1; + } + + if (chown(sock->conf.unix_socket_name, ps->ps_pw->pw_uid, + ps->ps_pw->pw_gid) == -1) { + log_warn("%s: chown", __func__); + close(u_fd); + (void)unlink(sock->conf.unix_socket_name); + return -1; + } + + if (listen(u_fd, SOCKS_BACKLOG) == -1) { + log_warn("%s: listen", __func__); + return -1; + } + + return u_fd; +} + +int +sockets_create_socket(struct addresslist *al, in_port_t port) +{ + struct addrinfo hints; + struct address *a; + int fd = -1, o_val = 1, flags; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags |= AI_PASSIVE; + + TAILQ_FOREACH(a, al, entry) { + if (sockets_socket_af(&a->ss, port) == -1) { + log_warnx("%s: sockets_socket_af", __func__); + goto fail; + } + + fd = socket(a->ss.ss_family, hints.ai_socktype, + a->ipproto); + log_debug("%s: opening socket (%d) for %s", __func__, + fd, a->ifname); + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &o_val, + sizeof(int)) == -1) { + log_warn("%s: setsockopt error", __func__); + return -1; + } + + /* non-blocking */ + flags = fcntl(fd, F_GETFL); + flags |= O_NONBLOCK; + fcntl(fd, F_SETFL, flags); + + if (bind(fd, (struct sockaddr *)&a->ss, a->ss.ss_len) == -1) { + close(fd); + log_info("%s: can't bind to port %d", __func__, + ntohs(port)); + goto fail; + } + + if (listen(fd, SOMAXCONN) == -1) { + log_warn("%s, unable to listen on socket", __func__); + goto fail; + } + } + + free(a); + return (fd); +fail: + free(a); + return -1; +} + +int +sockets_accept_reserve(int sockfd, struct sockaddr *addr, socklen_t *addrlen, + int reserve, volatile int *counter) +{ + int ret; + + if (getdtablecount() + reserve + + ((*counter + 1) * FD_NEEDED) >= getdtablesize()) { + log_debug("inflight fds exceeded"); + errno = EMFILE; + return -1; + } + + if ((ret = accept4(sockfd, addr, addrlen, + SOCK_NONBLOCK | SOCK_CLOEXEC)) > -1) { + (*counter)++; + log_debug("inflight incremented, now %d", *counter); + } + + return ret; +} + +void +sockets_accept_paused(int fd, short events, void *arg) +{ + struct socket *sock = (struct socket *)arg; + + event_add(&sock->ev, NULL); +} + +void +sockets_socket_accept(int fd, short event, void *arg) +{ + struct socket *sock = (struct socket *)arg; + struct sockaddr_storage ss; + struct timeval backoff; + struct request *c = NULL; + socklen_t len; + int s; + + backoff.tv_sec = 1; + backoff.tv_usec = 0; + + event_add(&sock->ev, NULL); + if (event & EV_TIMEOUT) + return; + + len = sizeof(ss); + + s = sockets_accept_reserve(fd, (struct sockaddr *)&ss, &len, + FD_RESERVE, &cgi_inflight); + + if (s == -1) { + switch (errno) { + case EINTR: + case EWOULDBLOCK: + case ECONNABORTED: + return; + case EMFILE: + case ENFILE: + event_del(&sock->ev); + evtimer_add(&sock->pause, &backoff); + return; + default: + log_warn("%s: accept", __func__); + } + } + + if (client_cnt > GOTWEBD_MAXCLIENTS) + goto err; + + c = calloc(1, sizeof(struct request)); + if (c == NULL) { + log_warn("%s", __func__); + close(s); + cgi_inflight--; + return; + } + + c->fd = s; + c->sock = sock; + memcpy(c->priv_fd, sock->priv_fd, sizeof(c->priv_fd)); + c->buf_pos = 0; + c->buf_len = 0; + c->request_started = 0; + c->sock->client_status = CLIENT_CONNECT; + + event_set(&c->ev, s, EV_READ, fcgi_request, c); + event_add(&c->ev, NULL); + + evtimer_set(&c->tmo, fcgi_timeout, c); + evtimer_add(&c->tmo, &timeout); + + client_cnt++; + + return; +err: + cgi_inflight--; + close(s); + if (c != NULL) + free(c); +} + +void +sockets_rlimit(int maxfd) +{ + struct rlimit rl; + + if (getrlimit(RLIMIT_NOFILE, &rl) == -1) + fatal("%s: failed to get resource limit", __func__); + log_debug("%s: max open files %llu", __func__, rl.rlim_max); + + /* + * Allow the maximum number of open file descriptors for this + * login class (which should be the class "daemon" by default). + */ + if (maxfd == -1) + rl.rlim_cur = rl.rlim_max; + else + rl.rlim_cur = MAXIMUM(rl.rlim_max, (rlim_t)maxfd); + if (setrlimit(RLIMIT_NOFILE, &rl) == -1) + fatal("%s: failed to set resource limit", __func__); +} \ No newline at end of file