From: Stefan Sperling Subject: gotwebd website support To: gameoftrees@openbsd.org Date: Sat, 29 Nov 2025 18:46:06 +0100 Add a new feature to gotwebd which allows serving static websites straight out of Git repositories. In the simplest case, declaring a website in gotwebd.conf works as follows: server "www.example.com" { # Serve the www.git repository as a web site when # the browser visits "www.example.com". website "/" { repository "www" } } See the docs added to gotwebd.conf.5 for details. A tricky part of this is requesting routing. We can now have URL paths which map to repositories and may require authentication, and URL paths which map to websites which never require authentication (similar to how static gotwebd assets are not protected by authentication). It took me some time to get this right. We must have good test coverage for this to avoid introducing authentication bypass bugs later. For now, I am adding some basic tests to the gotsysd regression test suite. The tests should be extended later to cover more cases. Adding more tests will be much easier gone gotsysd has learned to reconfigure gotwebd on the fly. Until gotsysd can reconfigure gotwebd, test cases which require a unique gotwebd.conf file need to be start in separate scripts. ok? M gotwebd/auth.c | 41+ 8- M gotwebd/config.c | 65+ 0- M gotwebd/fcgi.c | 2+ 0- M gotwebd/gotweb.c | 486+ 43- M gotwebd/gotwebd.c | 13+ 0- M gotwebd/gotwebd.conf.5 | 183+ 1- M gotwebd/gotwebd.h | 21+ 1- M gotwebd/login.c | 2+ 0- M gotwebd/pages.tmpl | 2+ 1- M gotwebd/parse.y | 244+ 0- M gotwebd/sockets.c | 2+ 0- M regress/gotsysd/.gitignore | 1+ 0- M regress/gotsysd/Makefile | 115+ 6- A regress/gotsysd/test_gotwebd_repos_www.sh | 203+ 0- A regress/gotsysd/test_gotwebd_www.sh | 172+ 0- 15 files changed, 1552 insertions(+), 60 deletions(-) commit - 72d0793cf749207e9b2a9056b0cc856f16fc27a7 commit + 9362ba094edef407488a2143f35d32cdd7249392 blob - cd3697aa4826e66a9c32ff9bb1685f03c0d92a63 blob + 9282838c15b7ace89c1eb4581c04faef543d6c00 --- gotwebd/auth.c +++ gotwebd/auth.c @@ -67,6 +67,7 @@ auth_shutdown(void) config_free_access_rules(&srv->access_rules); config_free_repos(&srv->repos); + config_free_websites(&srv->websites); free(srv); } @@ -237,7 +238,9 @@ render_error(struct request *c, const struct got_error c->t->error = error; - if (error->code == GOT_ERR_LOGIN_FAILED) + if (error->code == GOT_ERR_NOT_FOUND) + status = 404; + else if (error->code == GOT_ERR_LOGIN_FAILED) status = 401; else status = 400; @@ -441,10 +444,13 @@ process_request(struct request *c) struct gotwebd *env = gotwebd_env; uid_t uid; struct server *srv; + struct website *site; struct gotwebd_repo *repo = NULL; enum gotwebd_auth_config auth_config; char *hostname = NULL; const char *identifier = NULL; + char *request_path = NULL; + int is_repository_request = 0; srv = gotweb_get_server(c->fcgi_params.server_name); if (srv == NULL) { @@ -453,24 +459,42 @@ process_request(struct request *c) goto done; } + error = gotweb_route_request(&is_repository_request, &site, + &request_path, c); + if (error) + goto done; + /* * Static gotwebd assets (images, CSS, ...) are not protected * by authentication. */ - if (got_path_cmp(srv->gotweb_url_root, c->fcgi_params.document_uri, - strlen(srv->gotweb_url_root), - strlen(c->fcgi_params.document_uri)) != 0) { + if (is_repository_request && !got_path_is_root_dir(request_path)) { forward_request(c); + free(request_path); return; } + /* Web site content is not protected by authentication either. */ + if (site) { + /* Ignore the querystring while serving web sites. */ + fcgi_init_querystring(&c->fcgi_params.qs); + + forward_request(c); + free(request_path); + return; + } + + free(request_path); + request_path = NULL; + auth_config = srv->auth_config; - if (c->fcgi_params.qs.path[0] != '\0') { + + if (c->fcgi_params.qs.path[0] != '\0') repo = gotweb_get_repository(srv, c->fcgi_params.qs.path); - if (repo) - auth_config = repo->auth_config; - } + if (repo) + auth_config = repo->auth_config; + switch (auth_config) { case GOTWEBD_AUTH_SECURE: case GOTWEBD_AUTH_INSECURE: @@ -603,6 +627,7 @@ permitted: } done: free(hostname); + free(request_path); if (error) render_error(c, error); else @@ -943,6 +968,14 @@ auth_dispatch_main(int fd, short event, void *arg) srv = TAILQ_LAST(&env->servers, serverlist); config_get_repository(&srv->repos, &imsg); break; + case GOTWEBD_IMSG_CFG_WEBSITE: + if (TAILQ_EMPTY(&env->servers)) { + fatalx("%s: unexpected CFG_WEBSITE msg", + __func__); + } + srv = TAILQ_LAST(&env->servers, serverlist); + config_get_website(&srv->websites, &imsg); + break; case GOTWEBD_IMSG_CTL_PIPE: if (env->iev_sockets == NULL) recv_sockets_pipe(env, &imsg); blob - e8c48e960e86b8d7fe58b0f342eef37fefe5c533 blob + 254bf8dccfe73be47d35b6bd7293c29d8f912e4c --- gotwebd/config.c +++ gotwebd/config.c @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -41,6 +42,8 @@ #include "got_opentemp.h" #include "got_reference.h" #include "got_object.h" +#include "got_path.h" +#include "got_error.h" #include "gotwebd.h" #include "log.h" @@ -94,6 +97,7 @@ config_getserver(struct gotwebd *env, struct imsg *ims memcpy(srv, p, sizeof(*srv)); STAILQ_INIT(&srv->access_rules); TAILQ_INIT(&srv->repos); + RB_INIT(&srv->websites); /* log server info */ log_debug("%s: server=%s", __func__, srv->name); @@ -331,3 +335,64 @@ config_get_repository(struct gotwebd_repolist *repos, TAILQ_INSERT_TAIL(repos, repo, entry); } + +void +config_free_websites(struct got_pathlist_head *websites) +{ + got_pathlist_free(websites, GOT_PATHLIST_FREE_DATA); +} + +void +config_set_website(struct imsgev *iev, struct website *website) +{ + if (imsg_compose_event(iev, + GOTWEBD_IMSG_CFG_WEBSITE, 0, -1, -1, + website, sizeof(*website)) == -1) + fatal("imsg_compose_event GOTWEBD_IMSG_CFG_WEBSITE"); +} + +void +config_get_website(struct got_pathlist_head *websites, struct imsg *imsg) +{ + const struct got_error *error; + struct website *site; + struct got_pathlist_entry *new; + size_t len; + + site = calloc(1, sizeof(*site)); + if (site == NULL) + fatal("malloc"); + + if (imsg_get_data(imsg, site, sizeof(*site))) + fatalx("%s: invalid CFG_WEBSITE message", __func__); + + len = strnlen(site->repo_name, sizeof(site->repo_name)); + if (len == 0 || len >= sizeof(site->repo_name)) + fatalx("%s: invalid CFG_WEBSITE message", __func__); + + if (strchr(site->repo_name, '/') != NULL) { + fatalx("repository names must not contain slashes: %s", + site->repo_name); + } + + if (strchr(site->repo_name, '\n') != NULL) { + fatalx("repository names must not contain linefeeds: %s", + site->repo_name); + } + + if (strchr(site->url_path, '\n') != NULL) { + fatalx("URL paths must not contain linefeeds: %s", + site->url_path); + } + + if (!got_path_is_absolute(site->url_path)) { + fatalx("URL paths must be absolute paths: %s", + site->url_path); + } + + error = got_pathlist_insert(&new, websites, site->url_path, site); + if (error) + fatalx("%s: %s", __func__, error->msg); + if (new == NULL) + fatalx("%s: duplicate web site '%s'", __func__, site->url_path); +} blob - 72d224172bff2531ebcbbdeb3c28089e8392c9d7 blob + ff4e53e93502e25453a36bd7a656041afa2649af --- gotwebd/fcgi.c +++ gotwebd/fcgi.c @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -40,6 +41,7 @@ #include "got_error.h" #include "got_reference.h" #include "got_object.h" +#include "got_path.h" #include "got_lib_poll.h" blob - ab011444f5fd3d10a47a59ee42390f24281009f2 blob + 6cb8c9a15bd4034ae7586a166448313c03891714 --- gotwebd/gotweb.c +++ gotwebd/gotweb.c @@ -56,7 +56,7 @@ static int gotweb_render_index(struct template *); static const struct got_error *gotweb_load_got_path(struct repo_dir **, - const char *, struct request *); + const char *, struct request *, int); static const struct got_error *gotweb_load_file(char **, const char *, const char *, int); static const struct got_error *gotweb_get_repo_description(char **, @@ -260,40 +260,17 @@ gotweb_log_request(struct request *c) } const struct got_error * -gotweb_serve_htdocs(struct request *c) +gotweb_serve_htdocs(struct request *c, const char *request_path) { const struct got_error *error = NULL; struct server *srv = c->srv;; - struct gotwebd_fcgi_params *p = &c->fcgi_params; - char *document_uri = p->document_uri; - char *request_path = NULL; - char *child_path = NULL; char *ondisk_path = NULL; int fd = -1; const char *mime_type = "application/octet-stream"; char *ext; - while (document_uri[0] == '/') - document_uri++; - if (asprintf(&request_path, "/%s", document_uri) == -1) - return got_error_from_errno("asprintf"); - - if (!got_path_is_child(request_path, - srv->gotweb_url_root, strlen(srv->gotweb_url_root))) { - error = got_error(GOT_ERR_NOT_FOUND); - goto done; - } - - error = got_path_skip_common_ancestor(&child_path, - srv->gotweb_url_root, request_path); - if (error) { - if (error->code == GOT_ERR_BAD_PATH) - error = got_error(GOT_ERR_NOT_FOUND); - goto done; - } - if (asprintf(&ondisk_path, "%s/%s/%s", gotwebd_env->httpd_chroot, - srv->htdocs_path, child_path) == -1) { + srv->htdocs_path, request_path) == -1) { error = got_error_from_errno("asprintf"); goto done; } @@ -362,18 +339,411 @@ gotweb_serve_htdocs(struct request *c) done: if (fd != -1 && close(fd) == -1 && error == NULL) error = got_error_from_errno("close"); - free(request_path); - free(child_path); free(ondisk_path); return error; } +static const struct got_error * +serve_blob(int *response_code, struct request *c, struct got_repository *repo, + struct got_object_id *obj_id, const char *basename) +{ + const struct got_error *error = NULL; + struct got_blob_object *blob = NULL; + int binary; + const char *mime_type = "application/octet-stream"; + + c->t->fd = dup(c->priv_fd[BLOB_FD_1]); + if (c->t->fd == -1) { + error = got_error_from_errno("dup"); + goto done; + } + + error = got_object_open_as_blob(&blob, repo, obj_id, BUF, c->t->fd); + if (error) + goto done; + + error = got_object_blob_is_binary(&binary, blob); + if (error) + goto done; + + if (binary) { + if (gotweb_reply_file(c, "application/octet-stream", + basename, NULL) == -1) { + error = got_error(GOT_ERR_IO); + *response_code = 500; + goto done; + } + } else { + char *ext; + + /* TODO: Port generic mime-type handling from httpd. */ + ext = strrchr(basename, '.'); + if (ext) { + if (strcmp(ext, ".css") == 0) + mime_type = "text/css"; + if (strcmp(ext, ".html") == 0) + mime_type = "text/html"; + if (strcmp(ext, ".ico") == 0) + mime_type = "image/x-icon"; + if (strcmp(ext, ".png") == 0) + mime_type = "image/png"; + if (strcmp(ext, ".svg") == 0) + mime_type = "image/svg+xml"; + if (strcmp(ext, ".txt") == 0) + mime_type = "text/plain"; + if (strcmp(ext, ".webmanifest") == 0) + mime_type = "application/manifest+json"; + if (strcmp(ext, ".xml") == 0) + mime_type = "text/xml"; + } + + if (gotweb_reply(c, 200, mime_type, NULL) == -1) { + error = got_error(GOT_ERR_IO); + *response_code = 500; + goto done; + } + } + + if (template_flush(c->tp) == -1) { + error = got_error(GOT_ERR_IO); + *response_code = 500; + goto done; + } + + for (;;) { + const uint8_t *buf; + size_t len; + + error = got_object_blob_read_block(&len, blob); + if (error) + goto done; + if (len == 0) + break; + + buf = got_object_blob_get_read_buf(blob); + if (fcgi_write(c, buf, len) == -1) { + error = got_error(GOT_ERR_IO); + *response_code = 500; + goto done; + } + } + +done: + if (blob) + got_object_blob_close(blob); + return error; +} + +static int +gotweb_serve_website(struct request *c, struct website *site, + struct repo_dir *repo_dir, const char *request_path) +{ + const struct got_error *error = NULL; + struct transport *t = c->t; + struct got_repository *repo = t->repo; + char *refname = NULL; + struct got_reference *ref = NULL; + struct got_object_id *id = NULL, *obj_id = NULL; + struct got_commit_object *commit = NULL; + struct got_tree_object *tree = NULL; + char *basename = NULL; + int response_code = 404; + char *in_repo_child = NULL, *in_repo_path = NULL; + int obj_type; + + if (got_path_is_root_dir(request_path)) { + in_repo_child = strdup(request_path); + if (in_repo_child == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else { + if (got_path_is_root_dir(site->url_path)) { + in_repo_child = strdup(request_path); + if (in_repo_child == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else if (got_path_cmp(site->url_path, request_path, + strlen(site->url_path), strlen(request_path)) == 0) { + in_repo_child = strdup("/"); + if (in_repo_child == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else { + error = got_path_skip_common_ancestor(&in_repo_child, + site->url_path, request_path); + if (error) + goto done; + } + } + + if (site->path[0] != '\0') { + char *s = in_repo_child; + + while (*s == '/') + s++; + + if (asprintf(&in_repo_path, "%s/%s", site->path, s) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + + got_path_strip_trailing_slashes(in_repo_path); + } else { + in_repo_path = in_repo_child; + in_repo_child = NULL; + } + + if (site->branch_name[0] != 0) { + const char *branch = site->branch_name; + + if (strncmp("refs/", branch, 5) != 0) { + branch += 5; + if (asprintf(&refname, "refs/heads/%s", branch) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + } else { + refname = strdup(branch); + if (refname == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + + } else { + refname = strdup(GOT_REF_HEAD); + if (refname == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + + error = got_ref_open(&ref, repo, refname, 0); + if (error) + goto done; + + error = got_ref_resolve(&id, repo, ref); + if (error) + goto done; + + error = got_object_open_as_commit(&commit, repo, id); + if (error) + goto done; + + error = got_object_id_by_path(&obj_id, repo, commit, in_repo_path); + if (error) + goto done; + + error = got_object_get_type(&obj_type, repo, obj_id); + if (error) + goto done; + + switch (obj_type) { + case GOT_OBJ_TYPE_BLOB: + error = got_path_basename(&basename, in_repo_path); + if (error) + goto done; + error = serve_blob(&response_code, c, repo, obj_id, + basename); + if (error) + goto done; + break; + case GOT_OBJ_TYPE_TREE: { + struct got_tree_entry *te; + int nentries, i; + const char *name; + mode_t mode; + + error = got_object_open_as_tree(&tree, repo, obj_id); + if (error) + goto done; + nentries = got_object_tree_get_nentries(tree); + + for (i = 0; i < nentries; i++) { + struct gotweb_url url = { + .index_page = -1, + .action = -1, + }; + + te = got_object_tree_get_entry(tree, i); + + name = got_tree_entry_get_name(te); + mode = got_tree_entry_get_mode(te); + if (!S_ISREG(mode) || + strcasecmp(name, "index.html") != 0) + continue; + + /* XXX gotweb_reply uses request struct field */ + if (strlcat(c->fcgi_params.document_uri, "/index.html", + sizeof(c->fcgi_params.document_uri)) >= + sizeof(c->fcgi_params.document_uri)) { + error = got_error(GOT_ERR_NO_SPACE); + goto done; + } + + if (gotweb_reply(c, 302, NULL, &url) == -1) { + error = got_error(GOT_ERR_IO); + goto done; + } + break; + } + break; + } + default: + error = got_error(GOT_ERR_NOT_FOUND); + goto done; + } +done: + free(in_repo_child); + free(in_repo_path); + free(refname); + free(basename); + free(id); + free(obj_id); + if (ref) + got_ref_close(ref); + if (commit) + got_object_commit_close(commit); + if (tree) + got_object_tree_close(tree); + + if (error) { + char *safe_path = NULL; + + if (stravis(&safe_path, request_path, VIS_SAFE) == -1) { + log_warn("stravis"); + safe_path = NULL; + } + log_warnx("%s: %s: %d: %s", __func__, + safe_path ? safe_path : "?", response_code, error->msg); + free(safe_path); + + if (response_code == 404) + c->t->error = got_error(GOT_ERR_NOT_FOUND); + else + c->t->error = error; + + if (gotweb_reply(c, response_code, "text/html", NULL) == -1) + return -1; + return gotweb_render_page(c->tp, gotweb_render_error); + } + + return 0; +} + +const struct got_error * +gotweb_route_request(int *is_repository_request, struct website **site, + char **request_path, struct request *c) +{ + const struct got_error *error = NULL; + struct gotwebd_fcgi_params *p = &c->fcgi_params; + struct server *srv = c->srv;; + char *child_path = NULL; + + *is_repository_request = 0; + *site = NULL; + *request_path = NULL; + + if (got_path_cmp(srv->full_repos_url_path, p->document_uri, + strlen(srv->full_repos_url_path), + strlen(p->document_uri)) == 0) { + /* + * Requesting / in repository url path space. + * We will be rendering Git repository data. + */ + *request_path = strdup("/"); + if (*request_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + *is_repository_request = 1; + } else if (got_path_is_child(p->document_uri, + srv->full_repos_url_path, strlen(srv->full_repos_url_path))) { + /* + * Requesting something within repository url path space. + * We will be returning a static asset for a repository page. + */ + error = got_path_skip_common_ancestor(&child_path, + srv->full_repos_url_path, p->document_uri); + if (error) + goto done; + *is_repository_request = 1; + + if (asprintf(request_path, "/%s", child_path) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + } else if (got_path_is_child(p->document_uri, + srv->gotweb_url_root, strlen(srv->gotweb_url_root))) { + /* + * Requesting something outside repository url path space. + * This will result in a 404 error unless the request is + * matched by a website. + */ + if (got_path_is_root_dir(p->document_uri)) { + *request_path = strdup("/"); + if (*request_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } else { + error = got_path_skip_common_ancestor(&child_path, + srv->gotweb_url_root, p->document_uri); + if (error) + goto done; + if (asprintf(request_path, "/%s", child_path) == -1) { + error = got_error_from_errno("asprintf"); + goto done; + } + } + } else { + /* + * Requesting something outside gotweb url path space. + * This will result in a 404 error. + */ + *request_path = strdup(p->document_uri); + if (*request_path == NULL) { + error = got_error_from_errno("strdup"); + goto done; + } + } + + if (!got_path_is_root_dir(*request_path)) + got_path_strip_trailing_slashes(*request_path); + + *site = gotweb_get_website(srv, *request_path); + + /* + * If the target of the request is ambiguous, the longer path wins. + */ + if (*is_repository_request && *site) { + if (got_path_cmp(srv->repos_url_path, (*site)->url_path, + strlen(srv->repos_url_path), + strlen((*site)->url_path)) <= 0) + *is_repository_request = 0; + else + *site = NULL; + } +done: + free(child_path); + if (error) { + free(*request_path); + *request_path = NULL; + *site = NULL; + } + + return error; +} + int gotweb_process_request(struct request *c) { const struct got_error *error = NULL; struct server *srv = c->srv;; - struct gotwebd_fcgi_params *p = &c->fcgi_params; + struct website *site; struct querystring *qs = NULL; struct repo_dir *repo_dir = NULL; struct repo_commit *commit; @@ -381,6 +751,8 @@ gotweb_process_request(struct request *c) const uint8_t *buf; size_t len; int r, binary = 0; + char *request_path = NULL; + int is_repository_request = 0; /* querystring */ qs = &c->fcgi_params.qs; @@ -388,15 +760,34 @@ gotweb_process_request(struct request *c) gotweb_log_request(c); - if (got_path_cmp(srv->gotweb_url_root, p->document_uri, - strlen(srv->gotweb_url_root), strlen(p->document_uri)) != 0) { - error = gotweb_serve_htdocs(c); + error = gotweb_route_request(&is_repository_request, &site, + &request_path, c); + if (error) + goto err; + + if (is_repository_request && !got_path_is_root_dir(request_path)) { + error = gotweb_serve_htdocs(c, request_path); if (error) goto err; + free(request_path); return 0; } + if (site) { + error = gotweb_load_got_path(&repo_dir, site->repo_name, c, -1); + c->t->repo_dir = repo_dir; + if (error) + goto err; + + r = gotweb_serve_website(c, site, repo_dir, request_path); + free(request_path); + return r; + } + + free(request_path); + request_path = NULL; + /* * certain actions require a commit id in the querystring. this stops * bad actors from exploiting this by manually manipulating the @@ -418,7 +809,7 @@ gotweb_process_request(struct request *c) goto err; } - error = gotweb_load_got_path(&repo_dir, qs->path, c); + error = gotweb_load_got_path(&repo_dir, qs->path, c, 0); c->t->repo_dir = repo_dir; if (error) goto err; @@ -627,6 +1018,7 @@ gotweb_process_request(struct request *c) } err: + free(request_path); c->t->error = error; if (error->code == GOT_ERR_NOT_FOUND) { if (gotweb_reply(c, 404, "text/html", NULL) == -1) @@ -654,6 +1046,36 @@ gotweb_get_server(const char *server_name) return TAILQ_FIRST(&gotwebd_env->servers); }; +struct website * +gotweb_get_website(struct server *srv, const char *url_path) +{ + struct got_pathlist_entry *pe; + size_t url_path_len = strlen(url_path); + struct website *site = NULL; + + if (RB_EMPTY(&srv->websites)) + return NULL; + + /* + * Parent paths are sorted before children so we can match the + * provided URL path against the most specific web site URL path + * by walking the list backwards. + */ + pe = RB_MAX(got_pathlist_head, &srv->websites); + while (pe) { + if (got_path_cmp(url_path, pe->path, + url_path_len, pe->path_len) == 0 || + got_path_is_child(url_path, pe->path, pe->path_len)) { + site = pe->data; + break; + } + + pe = RB_PREV(got_pathlist_head, &srv->websites, pe); + } + + return site; +} + const struct got_error * gotweb_init_transport(struct transport **t) { @@ -845,7 +1267,7 @@ gotweb_render_index(struct template *tp) } error = gotweb_load_got_path(&repo_dir, sd_dent[d_i]->d_name, - c); + c, 0); if (error) { if (error->code != GOT_ERR_NOT_GIT_REPO) log_warnx("%s: %s: %s", __func__, @@ -1143,7 +1565,7 @@ auth_check(struct request *c, struct gotwebd_access_ru static const struct got_error * gotweb_load_got_path(struct repo_dir **rp, const char *dir, - struct request *c) + struct request *c, int requesting_website) { const struct got_error *error = NULL; struct gotwebd *env = gotwebd_env; @@ -1209,6 +1631,10 @@ gotweb_load_got_path(struct repo_dir **rp, const char break; case GOTWEBD_AUTH_SECURE: case GOTWEBD_AUTH_INSECURE: + if (requesting_website) { + access = GOTWEBD_ACCESS_PERMITTED; + break; + } access = auth_check(c, &repo->access_rules); if (access == GOTWEBD_ACCESS_NO_MATCH) access = auth_check(c, &srv->access_rules); @@ -1227,6 +1653,10 @@ gotweb_load_got_path(struct repo_dir **rp, const char break; case GOTWEBD_AUTH_SECURE: case GOTWEBD_AUTH_INSECURE: + if (requesting_website) { + access = GOTWEBD_ACCESS_PERMITTED; + break; + } access = auth_check(c, &srv->access_rules); if (access == GOTWEBD_ACCESS_NO_MATCH) access = auth_check(c, &env->access_rules); @@ -1245,15 +1675,19 @@ gotweb_load_got_path(struct repo_dir **rp, const char goto err; } - if (repo_is_hidden) { - error = got_error_path(repo_dir->name, GOT_ERR_NOT_GIT_REPO); - goto err; - } + if (!requesting_website) { + if (repo_is_hidden) { + error = got_error_path(repo_dir->name, + GOT_ERR_NOT_GIT_REPO); + goto err; + } - if (srv->respect_exportok && - faccessat(dirfd(dt), "git-daemon-export-ok", F_OK, 0) == -1) { - error = got_error_path(repo_dir->name, GOT_ERR_NOT_GIT_REPO); - goto err; + if (srv->respect_exportok && faccessat(dirfd(dt), + "git-daemon-export-ok", F_OK, 0) == -1) { + error = got_error_path(repo_dir->name, + GOT_ERR_NOT_GIT_REPO); + goto err; + } } error = got_repo_open(&t->repo, repo_dir->path, NULL, @@ -1421,6 +1855,7 @@ gotweb_shutdown(void) config_free_access_rules(&srv->access_rules); config_free_repos(&srv->repos); + config_free_websites(&srv->websites); free(srv); } @@ -1682,6 +2117,14 @@ gotweb_dispatch_main(int fd, short event, void *arg) srv = TAILQ_LAST(&env->servers, serverlist); config_get_repository(&srv->repos, &imsg); break; + case GOTWEBD_IMSG_CFG_WEBSITE: + if (TAILQ_EMPTY(&env->servers)) { + fatalx("%s: unexpected CFG_WEBSITE msg", + __func__); + } + srv = TAILQ_LAST(&env->servers, serverlist); + config_get_website(&srv->websites, &imsg); + break; case GOTWEBD_IMSG_CFG_FD: config_getfd(env, &imsg); break; blob - cf2987ed090a63f0c3bfe94f264c4b8c4afa1fe3 blob + 589946d8f305607c70ec6615af063261cd63768b --- gotwebd/gotwebd.c +++ gotwebd/gotwebd.c @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -44,6 +45,7 @@ #include "got_opentemp.h" #include "got_reference.h" #include "got_object.h" +#include "got_path.h" #include "gotwebd.h" #include "log.h" @@ -917,6 +919,7 @@ gotwebd_configure(struct gotwebd *env, uid_t uid, gid_ struct server *srv; struct socket *sock; struct gotwebd_repo *repo; + struct got_pathlist_entry *pe; char secret[32]; int i; @@ -955,6 +958,16 @@ gotwebd_configure(struct gotwebd *env, uid_t uid, gid_ &srv->access_rules); } + /* send web sites */ + RB_FOREACH(pe, got_pathlist_head, &srv->websites) { + struct website *site = pe->data; + + for (i = 0; i < env->prefork; i++) { + config_set_website(&env->iev_auth[i], site); + config_set_website(&env->iev_gotweb[i], site); + } + } + /* send repositories and per-repository access rules */ TAILQ_FOREACH(repo, &srv->repos, entry) { for (i = 0; i < env->prefork; i++) { blob - 1859bfdc440fab4f2e62f6aef97053da48819414 blob + 1a19c85d1b044541bb933e61910161081f1a0883 --- gotwebd/gotwebd.conf.5 +++ gotwebd/gotwebd.conf.5 @@ -1,4 +1,4 @@ -.\" +\" .\" Copyright (c) 2020 Tracey Emery .\" .\" Permission to use, copy, modify, and distribute this software for any @@ -111,6 +111,27 @@ are served directly from .Xr httpd 8 . .Pp This setting can also be configured on a per-server basis. +.It Ic repos_url_path Ar path +Sets the URL path under which Git repositories will be displayed by +.Xr gotwebd 8 . +The +.Ar path +specified here is relative to +.Ic gotweb_url_root . +.Pp +Defaults to +.Dq / . +This default should only be changed if a +.Ic website +is configured as the landing page instead of the usual index page shown by +.Xr gotwebd 8 . +Otherwise, changing +.Ic repos_url_path +will result in +.Dq not found +errors when browsers attempt to visit the gotweb root URL path. +.Pp +This setting can also be configured on a per-server basis. .It Ic disable authentication Disable authentication, allowing any browser to view any repository not hidden via the @@ -314,6 +335,29 @@ are served directly from If not set then the global .Ic gotweb_url_root setting will be used. +.It Ic repos_url_path Ar path +Sets the URL path under which Git repositories will be displayed by +.Xr gotwebd 8 . +The +.Ar path +specified here is relative to +.Ic gotweb_url_root . +.Pp +Defaults to +.Dq / . +This default should only be changed if a +.Ic website +is configured as the landing page instead of the usual index page shown by +.Xr gotwebd 8 . +Otherwise, changing +.Ic repos_url_path +will result in +.Dq not found +errors when browsers attempt to visit the gotweb root URL path. +.Pp +If not set then the global +.Ic repos_url_path +setting will be used. .It Ic disable authentication Disable authentication for this server, allowing any browser to view any repository not hidden via the @@ -463,6 +507,56 @@ parameter determines whether .Xr gotwebd 8 will display the repository. .El +.It Ic website Ar url-path Brq ... +Show a web site when the browser visits the given +.Ar url-path . +The web site's content is composed of files in a Git repository. +.Pp +While the underlying repository is subject to authentication as usual, +web site content is always public, and cannot be hidden via the +.Ic hide repository +or +.Ic respect_exportok +directives. +.Pp +The available +.Ic website +configuration paramters are as follows: +.Pp +.Bl -tag -width Ds +.It Ic repository Ar name +Serve web site content from the specified Git repository. +The repository will be looked up within the server's +.Ar repos_path , +where the directory +.Ar name +can exist with or without a +.Dq .git +suffix. +.Pp +The +.Ic repository +parameter is mandatory. +.It Ic branch Ar name +Look up files to serve as web site content on the specified branch +in the repository. +By default, the branch resolved via the repository's HEAD reference is used. +.Pp +If the +.Ar name +does not begin with +.Dq refs/heads +then the +.Ar name +is searched in the +.Dq refs/heads +reference namespace. +.It Ic path Ar path +Look up files to serve as web site content at the specified path +in the repository. +Defaults to the root directory, +.Dq / . +.El .It Ic respect_exportok Ar on | off Set whether to display the repository only if it contains the magic .Pa git-daemon-export-ok @@ -565,6 +659,94 @@ server "localhost" { } .Ed .Pp +This example illustrates how +.Xr gotwebd 8 +can serve static web sites out of Git repositories: +.Bd -literal -offset indent +# This server displays just one web site, no Git repositories. +server "www.example.com" { + # Do not display any Git repository data. + hide repositories on + + # Serve the www.git repository as a web site when + # the browser visits "www.example.com". + website "/" { + repository "www" # /var/www/got/public/www.git + + # Any child URL paths the browser wants to + # visit will be looked up in www.git, on + # the branch which the symbolic HEAD + # reference points to. + } +} + +# This server displays both web sites and Git repositories. +server "project.example.com" { + repos_path "/var/git" + + # Display Git repositories when the URL path "/gotweb/" is + # is visited by the browser. + gotweb url_path "/gotweb" + + website "/" { + # Display the "www" repository as a web site when + # the browser visits "project.example.com". + repository "www" # /var/git/www or /var/git/www.git + + # Any child URL path the browser wants to visit will + # be looked up in www.git, except for "gotweb/" which + # maps to repos_path, and "docs/" which is used by + # other repositories defined below. + } + + repository "www.git" { # /var/git/www or /var/git/www.git + # Otherwise, hide data in this repository. It will + # not be shown under the "/gotweb" URL path. + hide repository on + } + + # Display the src.git repository's "docs/html" directory + # as found on the main branch as a web site at the + # URL "project.example.com/docs/". + website "/docs" { + repository "src" + path "/docs/html" + branch "main" # same as "refs/heads/main" + + # Any child URL paths the browser wants to visit will + # be looked up in src.git, except for "gotweb/" + # which maps to repos_path, and "docs/api/" which is + # used by another web site defined below. + } + + repository "src" { # /var/git/src or /var/git/src.git + # Otherwise, hide data in this repository. It will + # not be shown under the "/gotweb" URL path. + hide repository on + } + + # Display the "main" branch of api-docs.git repository + # as a web site at the URL + # "project.example.com/docs/api/latest/". + website "/docs/api/latest" { + repository "api-docs.git" + branch "main" + } + + # Display the "1.x-stable" branch of the api-docs.git + # as a web site at the URL + # "project.example.com/docs/api/1.x/". + website "/docs/api/1.x" { + repository "api-docs.git" + branch "1.x-stable" + } + + # The api-docs repository will also be shown under "/gotweb" + # because the hide repositories and hide repository + # setings default to "off". +} +.Ed +.Pp The following example illustrates the use of directives related to authentication: .Bd -literal -offset indent blob - 7e2e0ab861ef74a5a904a144605d36d05a6540ec blob + 76014c6e0170b7e9ca7cbcaf2d2878d193fe1b42 --- gotwebd/gotwebd.h +++ gotwebd/gotwebd.h @@ -61,6 +61,7 @@ #define MAX_SERVER_NAME 255 #define MAX_AUTH_COOKIE 255 #define MAX_IDENTIFIER_SIZE 32 +#define MAX_BRANCH_NAME 255 #define GOTWEB_GIT_DIR ".git" @@ -143,6 +144,7 @@ enum imsg_type { GOTWEBD_IMSG_CFG_FD, GOTWEBD_IMSG_CFG_ACCESS_RULE, GOTWEBD_IMSG_CFG_REPO, + GOTWEBD_IMSG_CFG_WEBSITE, GOTWEBD_IMSG_CFG_DONE, GOTWEBD_IMSG_CTL_PIPE, GOTWEBD_IMSG_CTL_START, @@ -391,6 +393,14 @@ struct gotwebd_repo { }; TAILQ_HEAD(gotwebd_repolist, gotwebd_repo); +struct website { + STAILQ_ENTRY(website) entry; + char repo_name[NAME_MAX]; + char url_path[MAX_DOCUMENT_URI]; + char branch_name[MAX_BRANCH_NAME]; + char path[PATH_MAX]; +}; + struct server { TAILQ_ENTRY(server) entry; @@ -399,6 +409,8 @@ struct server { char gotweb_url_root[MAX_DOCUMENT_URI]; char repos_path[PATH_MAX]; + char repos_url_path[MAX_DOCUMENT_URI]; + char full_repos_url_path[MAX_DOCUMENT_URI * 2 + 2]; char site_name[GOTWEBD_MAXNAME]; char site_owner[GOTWEBD_MAXNAME]; char site_link[GOTWEBD_MAXTEXT]; @@ -424,6 +436,7 @@ struct server { struct gotwebd_access_rule_list access_rules; struct gotwebd_repolist repos; + struct got_pathlist_head websites; }; TAILQ_HEAD(serverlist, server); @@ -494,6 +507,7 @@ struct gotwebd { char httpd_chroot[PATH_MAX]; char htdocs_path[PATH_MAX]; char gotweb_url_root[MAX_DOCUMENT_URI]; + char repos_url_path[MAX_DOCUMENT_URI]; uid_t www_uid; char login_hint_user[MAX_IDENTIFIER_SIZE]; @@ -575,6 +589,7 @@ void gotwebd_auth(struct gotwebd *, int); /* gotweb.c */ struct server *gotweb_get_server(const char *); +struct website *gotweb_get_website(struct server *, const char *); struct gotwebd_repo * gotweb_get_repository(struct server *, const char *); int gotweb_reply(struct request *c, int status, const char *ctype, struct gotweb_url *); @@ -588,7 +603,9 @@ int gotweb_render_absolute_url(struct request *, struc void gotweb_free_repo_commit(struct repo_commit *); void gotweb_free_repo_tag(struct repo_tag *); void gotweb_log_request(struct request *); -const struct got_error *gotweb_serve_htdocs(struct request *); +const struct got_error *gotweb_serve_htdocs(struct request *, const char *); +const struct got_error *gotweb_route_request(int *, struct website **, + char **, struct request *); int gotweb_process_request(struct request *); void gotweb_free_transport(struct transport *); void gotweb(struct gotwebd *, int); @@ -658,4 +675,7 @@ void config_free_access_rules(struct gotwebd_access_ru void config_set_repository(struct imsgev *, struct gotwebd_repo *); void config_get_repository(struct gotwebd_repolist *, struct imsg *); void config_free_repos(struct gotwebd_repolist *); +void config_free_websites(struct got_pathlist_head *); +void config_set_website(struct imsgev *, struct website *); +void config_get_website(struct got_pathlist_head *, struct imsg *); int config_init(struct gotwebd *); blob - badfb813912d0706c15044d339362ce437b9bd56 blob + 05a894bc5a6b1fdebdf747acaf7d215b24352cbd --- gotwebd/login.c +++ gotwebd/login.c @@ -16,6 +16,7 @@ */ #include +#include #include #include @@ -38,6 +39,7 @@ #include "got_error.h" #include "got_reference.h" #include "got_object.h" +#include "got_path.h" #include "gotwebd.h" #include "log.h" blob - 568ef2685b1fd3d82078302d5d5c4eb687a4c6b2 blob + 119c384edf7ec7e2066f6e84b02c2cba0fed4f52 --- gotwebd/pages.tmpl +++ gotwebd/pages.tmpl @@ -169,10 +169,11 @@ nextsep(char *s, char **t) struct server *srv = c->srv; const struct querystring *qs = c->t->qs; struct gotweb_url u_path; - char prefix[MAX_DOCUMENT_URI]; + char prefix[MAX_DOCUMENT_URI * 2]; const char *css = srv->custom_css; strlcpy(prefix, srv->gotweb_url_root, sizeof(prefix)); + strlcat(prefix, srv->repos_url_path, sizeof(prefix)); got_path_strip_trailing_slashes(prefix); memset(&u_path, 0, sizeof(u_path)); blob - 6c5defc898c84d1d497ce9239c2741a045bea0e0 blob + 0804f9b818380c437ebb197db76175e8c24dbf02 --- gotwebd/parse.y +++ gotwebd/parse.y @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -52,6 +53,8 @@ #include "got_reference.h" #include "got_object.h" +#include "got_path.h" +#include "got_error.h" #include "gotwebd.h" #include "log.h" @@ -108,6 +111,9 @@ struct address *get_unix_addr(const char *); int addr_dup_check(struct addresslist *, struct address *); void add_addr(struct address *); +static struct website *new_website; +static struct website *conf_new_website(struct server *, const char *); + static struct gotwebd_repo *new_repo; static struct gotwebd_repo *conf_new_repo(struct server *, const char *); static void conf_new_access_rule( @@ -132,6 +138,7 @@ typedef struct { %token SERVER CHROOT CUSTOM_CSS SOCKET HINT HTDOCS GOTWEB_URL_ROOT %token SUMMARY_COMMITS_DISPLAY SUMMARY_TAGS_DISPLAY USER AUTHENTICATION %token ENABLE DISABLE INSECURE REPOSITORY REPOSITORIES PERMIT DENY HIDE +%token WEBSITE PATH BRANCH REPOS_URL_PATH %token STRING %token NUMBER @@ -367,6 +374,9 @@ main : PREFORK NUMBER { YYERROR; } + if (!got_path_is_root_dir($2)) + got_path_strip_trailing_slashes($2); + n = strlcpy(gotwebd->gotweb_url_root, $2, sizeof(gotwebd->gotweb_url_root)); if (n >= sizeof(gotwebd->gotweb_url_root)) { @@ -384,8 +394,39 @@ main : PREFORK NUMBER { YYERROR; } + free($2); } + | REPOS_URL_PATH STRING { + if (*$2 == '\0') { + yyerror("repos_url_path can't be an empty" + " string"); + free($2); + YYERROR; + } + + if (!got_path_is_root_dir($2)) + got_path_strip_trailing_slashes($2); + + n = strlcpy(gotwebd->repos_url_path, $2, + sizeof(gotwebd->repos_url_path)); + if (n >= sizeof(gotwebd->repos_url_path)) { + yyerror("repos_url_path too long, exceeds " + "%zd bytes: %s", + sizeof(gotwebd->repos_url_path), $2); + free($2); + YYERROR; + } + + if (gotwebd->repos_url_path[0] != '/') { + yyerror("repos_url_path must be an absolute " + "path: bad path %s", $2); + free($2); + YYERROR; + } + + free($2); + } ; server : SERVER STRING { @@ -615,6 +656,9 @@ serveropts1 : REPOS_PATH STRING { YYERROR; } + if (!got_path_is_root_dir($2)) + got_path_strip_trailing_slashes($2); + n = strlcpy(new_srv->gotweb_url_root, $2, sizeof(new_srv->gotweb_url_root)); if (n >= sizeof(new_srv->gotweb_url_root)) { @@ -634,13 +678,100 @@ serveropts1 : REPOS_PATH STRING { free($2); } + | REPOS_URL_PATH STRING { + if (*$2 == '\0') { + yyerror("repos_url_path can't be an empty" + " string"); + free($2); + YYERROR; + } + + if (!got_path_is_root_dir($2)) + got_path_strip_trailing_slashes($2); + + n = strlcpy(new_srv->repos_url_path, $2, + sizeof(new_srv->repos_url_path)); + if (n >= sizeof(new_srv->repos_url_path)) { + yyerror("repos_url_path too long, exceeds " + "%zd bytes: %s", + sizeof(new_srv->repos_url_path), $2); + free($2); + YYERROR; + } + + if (new_srv->repos_url_path[0] != '/') { + yyerror("repos_url_path must be an absolute " + "path: bad path %s", $2); + free($2); + YYERROR; + } + + free($2); + } | repository + | website ; serveropts2 : serveropts2 serveropts1 nl | serveropts1 optnl ; +websiteopts2 : websiteopts2 websiteopts1 nl + | websiteopts1 optnl + +websiteopts1 : REPOSITORY STRING { + n = strlcpy(new_website->repo_name, $2, + sizeof(new_website->repo_name)); + if (n >= sizeof(new_website->repo_name)) { + yyerror("website repository name too long, " + "exceeds %zd bytes", + sizeof(new_website->repo_name) - 1); + free($2); + YYERROR; + } + free($2); + } + | PATH STRING { + n = strlcpy(new_website->path, $2, + sizeof(new_website->path)); + if (n >= sizeof(new_website->path)) { + yyerror("website in-repository path too long, " + "exceeds %zd bytes", + sizeof(new_website->path) - 1); + free($2); + YYERROR; + } + + if (new_website->path[0] != '/') { + yyerror("a website path must be an absolute " + "path: bad path %s", $2); + free($2); + YYERROR; + } + + free($2); + } + | BRANCH STRING { + n = strlcpy(new_website->branch_name, $2, + sizeof(new_website->branch_name)); + if (n >= sizeof(new_website->branch_name)) { + yyerror("website branch name too long, " + "exceeds %zd bytes", + sizeof(new_website->branch_name) - 1); + free($2); + YYERROR; + } + free($2); + } + ; + +website : WEBSITE STRING { + new_website = conf_new_website(new_srv, $2); + free($2); + } '{' optnl websiteopts2 '}' { + } + ; + repository : REPOSITORY STRING { struct gotwebd_repo *repo; @@ -746,6 +877,7 @@ lookup(char *s) /* This has to be sorted always. */ static const struct keywords keywords[] = { { "authentication", AUTHENTICATION }, + { "branch", BRANCH }, { "chroot", CHROOT }, { "custom_css", CUSTOM_CSS }, { "deny", DENY }, @@ -763,10 +895,12 @@ lookup(char *s) { "max_commits_display", MAX_COMMITS_DISPLAY }, { "max_repos_display", MAX_REPOS_DISPLAY }, { "on", ON }, + { "path", PATH }, { "permit", PERMIT }, { "port", PORT }, { "prefork", PREFORK }, { "repos_path", REPOS_PATH }, + { "repos_url_path", REPOS_URL_PATH }, { "repositories", REPOSITORIES }, { "repository", REPOSITORY }, { "respect_exportok", RESPECT_EXPORTOK }, @@ -783,6 +917,7 @@ lookup(char *s) { "summary_commits_display", SUMMARY_COMMITS_DISPLAY }, { "summary_tags_display", SUMMARY_TAGS_DISPLAY }, { "user", USER }, + { "website", WEBSITE }, { "www", WWW }, }; const struct keywords *p; @@ -1281,8 +1416,66 @@ parse_config(const char *filename, struct gotwebd *env sizeof(srv->gotweb_url_root) - 1); } } + + if (srv->repos_url_path[0] == '\0') { + if (strlcpy(srv->repos_url_path, + env->repos_url_path, + sizeof(srv->repos_url_path)) >= + sizeof(srv->repos_url_path)) { + yyerror("repos_url_path too long, " + "exceeds %zd bytes", + sizeof(srv->repos_url_path) - 1); + } + } } + TAILQ_FOREACH(srv, &env->servers, entry) { + const char *gotweb_url_root = srv->gotweb_url_root; + const char *repos_url_path = srv->repos_url_path; + struct got_pathlist_entry *pe; + int ret; + + while (gotweb_url_root[0] == '/') + gotweb_url_root++; + + while (repos_url_path[0] == '/') + repos_url_path++; + + if (gotweb_url_root[0] == '\0' && repos_url_path[0] == '\0') { + srv->full_repos_url_path[0] = '/'; + srv->full_repos_url_path[1] = '\0'; + } else { + ret = snprintf(srv->full_repos_url_path, + sizeof(srv->full_repos_url_path), + "/%s%s%s", gotweb_url_root, + gotweb_url_root[0] ? "/" : "", + repos_url_path); + if (ret == -1) { + yyerror("snprintf"); + } + if ((size_t)ret >= sizeof(srv->full_repos_url_path)) { + yyerror("gotweb_url_root and " + "repos_url_path too long, exceed %zd bytes", + sizeof(srv->full_repos_url_path) - 1); + } + } + + if (!got_path_is_root_dir(srv->full_repos_url_path)) { + got_path_strip_trailing_slashes( + srv->full_repos_url_path); + } + + RB_FOREACH(pe, got_pathlist_head, &srv->websites) { + const char *url_path = pe->path; + struct website *site = pe->data; + + if (site->repo_name[0] == '\0') { + yyerror("no repository defined for website " + "'%s' on server %s", url_path, srv->name); + } + } + } + return (0); } @@ -1348,6 +1541,7 @@ conf_new_server(const char *name) STAILQ_INIT(&srv->access_rules); TAILQ_INIT(&srv->repos); + RB_INIT(&srv->websites); TAILQ_INSERT_TAIL(&gotwebd->servers, srv, entry); @@ -1545,7 +1739,57 @@ add_addr(struct address *h) free(h); } +static struct website * +conf_new_website(struct server *server, const char *url_path) +{ + const struct got_error *error; + struct website *site; + struct got_pathlist_entry *new; + if (url_path[0] == '\0') { + fatalx("syntax error: empty URL path found in %s", + file->name); + } + + if (strchr(url_path, '\n') != NULL) + fatalx("URL path must not contain linefeeds: %s", url_path); + + site = calloc(1, sizeof(*site)); + if (site == NULL) + fatal("calloc"); + + if (!got_path_is_absolute(url_path)) { + int ret; + + ret = snprintf(site->url_path, sizeof(site->url_path), + "/%s", url_path); + if (ret == -1) + fatal("snprintf"); + if ((size_t)ret >= sizeof(site->url_path)) { + fatalx("URL path too long (exceeds %zd bytes): %s", + sizeof(site->url_path) - 1, url_path); + } + } else { + if (strlcpy(site->url_path, url_path, + sizeof(site->url_path)) >= + sizeof(site->url_path)) { + fatalx("URL path too long (exceeds %zd bytes): %s", + sizeof(site->url_path) - 1, url_path); + } + } + + error = got_pathlist_insert(&new, &server->websites, + site->url_path, site); + if (error) + fatalx("%s: %s", __func__, error->msg); + if (new == NULL) { + fatalx("duplicate web site '%s' in server '%s'", + url_path, server->name); + } + + return site; +} + struct gotwebd_repo * gotwebd_new_repo(const char *name) { blob - 12439a75c47c732ae30b0e45a6d5ba8d5efb2cac blob + f70495430f50e31b79ea07f70a6d4ee6f1447c05 --- gotwebd/sockets.c +++ gotwebd/sockets.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -54,6 +55,7 @@ #include "got_reference.h" #include "got_object.h" +#include "got_path.h" #include "gotwebd.h" #include "log.h" blob - beacbf8ebd70eb677fe66c20e7b50f3d3af5617b blob + ddcc9391f5551474c92849b0d86201fb1cbc586a --- regress/gotsysd/.gitignore +++ regress/gotsysd/.gitignore @@ -5,6 +5,7 @@ bsd.rd.fs got.conf gotd.conf gotwebd.conf +gotwebd-www.conf gotsys.conf gotsysd.conf gotsysd_bsd.rd blob - 70204cccd779939249ca70e2db5c63c629d0676c blob + 52abf86340197bda89a7e44126522b65004402f8 --- regress/gotsysd/Makefile +++ regress/gotsysd/Makefile @@ -1,6 +1,7 @@ .include "../../got-version.mk" -REGRESS_TARGETS=test_gotsysd test_gotwebd +REGRESS_TARGETS=test_gotsysd test_gotwebd test_gotwebd_www \ + test_gotwebd_repos_www REGRESS_SETUP_ONCE=setup_test_vm REGRESS_CLEANUP=stop_test_vm @@ -10,7 +11,7 @@ CLEANFILES= SHA256.sig bsd.rd bsd.rd.fs bsd.rd.decomp ${GOTSYSD_BSD_RD} ${GOTSYSD_SSH_KEY} ${GOTSYSD_SSH_PUBKEY} \ ${GOTSYSD_TEST_VM_BASE_IMAGE} ${GOTSYSD_VM_PASSWD_FILE} ${GOTD_CONF} \ ${GOTSYSD_CONF} ${GOTSYS_CONF} ${GOT_CONF} ${INSTALL_SITE} \ - ${HTTPD_CONF} ${GOTWEBD_CONF} + ${HTTPD_CONF} ${GOTWEBD_CONF} ${GOTWEBD_WWW_CONF} .PHONY: ensure_root vm start_test_vm build_got @@ -40,6 +41,8 @@ GOTSYSD_TEST_HTTP_PORT=8000 GOTSYSD_TEST_HMAC_SECRET!=openssl rand -base64 32 HTTPD_CONF=httpd.conf GOTWEBD_CONF=gotwebd.conf +GOTWEBD_WWW_CONF=gotwebd-www.conf +GOTWEBD_REPOS_WWW_CONF=gotwebd-repos-www.conf GOTSYSD_TEST_USER?=${DOAS_USER} .if empty(GOTSYSD_TEST_USER) @@ -216,9 +219,7 @@ $(HTTPD_CONF): @${UNPRIV} 'echo server "VMIP" { > $@' @${UNPRIV} 'echo \ \ listen on VMIP port http >> $@' @${UNPRIV} 'echo \ \ root \"/htdocs/gotwebd\" >> $@' - @${UNPRIV} 'echo \ \ location \"/\" { >> $@' - @${UNPRIV} 'echo \ \ \ \ fastcgi socket \"/run/gotweb.sock\" >> $@' - @${UNPRIV} 'echo \ \ } >> $@' + @${UNPRIV} 'echo \ \ fastcgi socket \"/run/gotweb.sock\" >> $@' @${UNPRIV} 'echo } >> $@' $(GOTWEBD_CONF): @@ -257,6 +258,47 @@ $(GOTWEBD_CONF): @${UNPRIV} 'echo \ \ } >> $@' @${UNPRIV} 'echo } >> $@' + +$(GOTWEBD_WWW_CONF): + @${UNPRIV} 'echo prefork 1 > $@' + @${UNPRIV} 'echo enable authentication insecure >> $@' + @${UNPRIV} 'echo permit ${GOTSYSD_TEST_USER} >> $@' + @${UNPRIV} 'echo deny ${GOTSYSD_DEV_USER} >> $@' + @${UNPRIV} 'echo server \"VMIP\" { >> $@' + @${UNPRIV} 'echo \ \ repos_path \"/git\" >> $@' + @${UNPRIV} 'echo \ \ hide repositories on >> $@' + @${UNPRIV} 'echo \ \ login hint user anonymous >> $@' + @${UNPRIV} 'echo \ \ show_repo_age off >> $@' + @${UNPRIV} 'echo \ \ show_repo_description off >> $@' + @${UNPRIV} 'echo \ \ show_repo_owner off >> $@' + @${UNPRIV} 'echo \ \ show_site_owner off >> $@' + @${UNPRIV} 'echo \ \ repos_url_path \"/repos\" >> $@' + @${UNPRIV} 'echo \ \ website \"/\" { >> $@' + @${UNPRIV} 'echo \ \ \ \ repository \"www\" >> $@' + @${UNPRIV} 'echo \ \ } >> $@' + @${UNPRIV} 'echo } >> $@' + +$(GOTWEBD_REPOS_WWW_CONF): + @${UNPRIV} 'echo prefork 1 > $@' + @${UNPRIV} 'echo enable authentication insecure >> $@' + @${UNPRIV} 'echo permit ${GOTSYSD_TEST_USER} >> $@' + @${UNPRIV} 'echo deny ${GOTSYSD_DEV_USER} >> $@' + @${UNPRIV} 'echo server \"VMIP\" { >> $@' + @${UNPRIV} 'echo \ \ repos_path \"/git\" >> $@' + @${UNPRIV} 'echo \ \ repository \"gotsys\" { >> $@' + @${UNPRIV} 'echo \ \ \ \ hide repository on >> $@' + @${UNPRIV} 'echo \ \ } >> $@' + @${UNPRIV} 'echo \ \ login hint user anonymous >> $@' + @${UNPRIV} 'echo \ \ show_repo_age off >> $@' + @${UNPRIV} 'echo \ \ show_repo_description off >> $@' + @${UNPRIV} 'echo \ \ show_repo_owner off >> $@' + @${UNPRIV} 'echo \ \ show_site_owner off >> $@' + @${UNPRIV} 'echo \ \ repos_url_path \"/\" >> $@' + @${UNPRIV} 'echo \ \ website \"/website\" { >> $@' + @${UNPRIV} 'echo \ \ \ \ repository \"www\" >> $@' + @${UNPRIV} 'echo \ \ } >> $@' + @${UNPRIV} 'echo } >> $@' + build_got: @set -e; \ VMID=`vmctl status ${GOTSYSD_VM_NAME} | tail -n1 | \ @@ -346,7 +388,8 @@ test_gotsysd: GWIP="100.64.$$VMID.2"; \ ${UNPRIV} "env ${GOTSYSD_TEST_ENV} VMIP=$${VMIP} GWIP=$${GWIP} \ sh ./test_gotsysd.sh" -test_gotwebd: + +test_gotwebd: @set -e; \ VMID=`vmctl status ${GOTSYSD_VM_NAME} | tail -n1 | \ awk '{print $$1}'`; \ @@ -373,4 +416,70 @@ test_gotwebd: ${UNPRIV} "env ${GOTSYSD_TEST_ENV} VMIP=$${VMIP} GWIP=$${GWIP} \ sh ./test_gotwebd.sh" +test_gotwebd_www: ${GOTWEBD_WWW_CONF} + @set -e; \ + VMID=`vmctl status ${GOTSYSD_VM_NAME} | tail -n1 | \ + awk '{print $$1}'`; \ + VMIP="100.64.$$VMID.3"; \ + GWIP="100.64.$$VMID.2"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + 'rm -rf /git/*.git /tmp/gotsys'"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + got init /git/${GOTSYS_REPO}"; \ + ${UNPRIV} "${GOTSYSD_SCP_CMD} \ + ${GOT_CONF} root@$${VMIP}:/git/${GOTSYS_REPO}/got.conf"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} mkdir /tmp/gotsys "; \ + ${UNPRIV} "${GOTSYSD_SCP_CMD} \ + ${GOTSYS_CONF} root@$${VMIP}:/tmp/gotsys/"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + got import -m init -r /git/${GOTSYS_REPO} \ + /tmp/gotsys >/dev/null"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + chown -R _gotd:_gotd /git"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} gotctl reload"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + gotsys apply -w > /dev/null"; \ + ${UNPRIV} "${GOTSYSD_SCP_CMD} \ + ${GOTWEBD_WWW_CONF} root@$${VMIP}:/etc/gotwebd.conf"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + sed -i s/VMIP/$${VMIP}/ /etc/httpd.conf /etc/gotwebd.conf"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} pkill -x gotwebd || true"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + /usr/local/sbin/gotwebd -v"; \ + ${UNPRIV} "env ${GOTSYSD_TEST_ENV} VMIP=$${VMIP} GWIP=$${GWIP} \ + sh ./test_gotwebd_www.sh" + +test_gotwebd_repos_www: ${GOTWEBD_REPOS_WWW_CONF} + @set -e; \ + VMID=`vmctl status ${GOTSYSD_VM_NAME} | tail -n1 | \ + awk '{print $$1}'`; \ + VMIP="100.64.$$VMID.3"; \ + GWIP="100.64.$$VMID.2"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + 'rm -rf /git/*.git /tmp/gotsys'"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + got init /git/${GOTSYS_REPO}"; \ + ${UNPRIV} "${GOTSYSD_SCP_CMD} \ + ${GOT_CONF} root@$${VMIP}:/git/${GOTSYS_REPO}/got.conf"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} mkdir /tmp/gotsys "; \ + ${UNPRIV} "${GOTSYSD_SCP_CMD} \ + ${GOTSYS_CONF} root@$${VMIP}:/tmp/gotsys/"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + got import -m init -r /git/${GOTSYS_REPO} \ + /tmp/gotsys >/dev/null"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + chown -R _gotd:_gotd /git"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} gotctl reload"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + gotsys apply -w > /dev/null"; \ + ${UNPRIV} "${GOTSYSD_SCP_CMD} \ + ${GOTWEBD_REPOS_WWW_CONF} root@$${VMIP}:/etc/gotwebd.conf"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + sed -i s/VMIP/$${VMIP}/ /etc/httpd.conf /etc/gotwebd.conf"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} pkill -x gotwebd || true"; \ + ${UNPRIV} "${GOTSYSD_SSH_CMD} root@$${VMIP} \ + /usr/local/sbin/gotwebd -v"; \ + ${UNPRIV} "env ${GOTSYSD_TEST_ENV} VMIP=$${VMIP} GWIP=$${GWIP} \ + sh ./test_gotwebd_repos_www.sh" + .include blob - /dev/null blob + cb2da971e563e78a73126214458997e020722312 (mode 755) --- /dev/null +++ regress/gotsysd/test_gotwebd_repos_www.sh @@ -0,0 +1,203 @@ +#!/bin/sh +# +# Copyright (c) 2025 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. + +. ../cmdline/common.sh +. ./common.sh + + +test_repos_at_root_path_and_website() { + local testroot=`test_init repos_at_root_path_and_website 1` + + got checkout -q $testroot/${GOTSYS_REPO} $testroot/wt >/dev/null + ret=$? + if [ $ret -ne 0 ]; then + echo "got checkout failed unexpectedly" >&2 + test_done "$testroot" 1 + return 1 + fi + + crypted_vm_pw=`echo ${GOTSYSD_VM_PASSWORD} | encrypt | tr -d '\n'` + crypted_pw=`echo ${GOTSYSD_DEV_PASSWORD} | encrypt | tr -d '\n'` + sshkey=`cat ${GOTSYSD_SSH_PUBKEY}` + cat > ${testroot}/wt/gotsys.conf </dev/null) + local commit_id=`git_show_head $testroot/${GOTSYS_REPO}` + + got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/${GOTSYS_REPO} + ret=$? + if [ $ret -ne 0 ]; then + echo "got send failed unexpectedly" >&2 + test_done "$testroot" 1 + return 1 + fi + + # Wait for gotsysd to apply the new configuration. + echo "$commit_id" > $testroot/stdout.expected + for i in 1 2 3 4 5; do + sleep 1 + ssh -i ${GOTSYSD_SSH_KEY} root@${VMIP} \ + cat /var/db/gotsysd/commit > $testroot/stdout + if cmp -s $testroot/stdout.expected $testroot/stdout; then + break; + fi + done + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + echo "gotsysd failed to apply configuration" >&2 + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got init $testroot/www.git > /dev/null + mkdir -p $testroot/www + + cat > $testroot/www/index.html < + + + + testing gotwebd + + +

testing gotwebd

+

Testing the web site feature of gotwebd.

+ + +EOF + got import -m init -r $testroot/www.git $testroot/www > /dev/null + ret=$? + if [ $ret -ne 0 ]; then + echo "got import failed unexpectedly" >&2 + return 1 + fi + + cat > $testroot/www.git/got.conf <&2 + return 1 + fi + + # Attempt website access. + w3m "http://${VMIP}/website" -dump > $testroot/stdout + cat > $testroot/stdout.expected < $testroot/stdout + cat > $testroot/stdout.expected < $testroot/stdout + cat > $testroot/stdout.expected < $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + echo "ssh login failed failed unexpectedly" >&2 + test_done "$testroot" 1 + return 1 + fi + + # Request the index page again using the login token and + # storing the cookie sent by gotwebd. + url=$(cut -d: -f 2,3 < $testroot/stdout | sed -e 's/ https:/http:/') + w3m -cookie-jar "$testroot/cookies" "$url" -dump > $testroot/stdout + cat > $testroot/stdout.expected < +# +# 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. + +. ../cmdline/common.sh +. ./common.sh + + +test_website_at_root_path() { + local testroot=`test_init website_at_root_path 1` + + got checkout -q $testroot/${GOTSYS_REPO} $testroot/wt >/dev/null + ret=$? + if [ $ret -ne 0 ]; then + echo "got checkout failed unexpectedly" >&2 + test_done "$testroot" 1 + return 1 + fi + + crypted_vm_pw=`echo ${GOTSYSD_VM_PASSWORD} | encrypt | tr -d '\n'` + crypted_pw=`echo ${GOTSYSD_DEV_PASSWORD} | encrypt | tr -d '\n'` + sshkey=`cat ${GOTSYSD_SSH_PUBKEY}` + cat > ${testroot}/wt/gotsys.conf </dev/null) + local commit_id=`git_show_head $testroot/${GOTSYS_REPO}` + + got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/${GOTSYS_REPO} + ret=$? + if [ $ret -ne 0 ]; then + echo "got send failed unexpectedly" >&2 + test_done "$testroot" 1 + return 1 + fi + + # Wait for gotsysd to apply the new configuration. + echo "$commit_id" > $testroot/stdout.expected + for i in 1 2 3 4 5; do + sleep 1 + ssh -i ${GOTSYSD_SSH_KEY} root@${VMIP} \ + cat /var/db/gotsysd/commit > $testroot/stdout + if cmp -s $testroot/stdout.expected $testroot/stdout; then + break; + fi + done + cmp -s $testroot/stdout.expected $testroot/stdout + ret=$? + if [ $ret -ne 0 ]; then + echo "gotsysd failed to apply configuration" >&2 + diff -u $testroot/stdout.expected $testroot/stdout + test_done "$testroot" "$ret" + return 1 + fi + + got init $testroot/www.git > /dev/null + mkdir -p $testroot/www + + cat > $testroot/www/index.html < + + + + testing gotwebd + + +

testing gotwebd

+

Testing the web site feature of gotwebd.

+ + +EOF + got import -m init -r $testroot/www.git $testroot/www > /dev/null + ret=$? + if [ $ret -ne 0 ]; then + echo "got import failed unexpectedly" >&2 + return 1 + fi + + cat > $testroot/www.git/got.conf <&2 + return 1 + fi + + # Attempt website access. + w3m "http://${VMIP}/" -dump > $testroot/stdout + cat > $testroot/stdout.expected < $testroot/stdout + cat > $testroot/stdout.expected < $testroot/stdout + cat > $testroot/stdout.expected <