commit 3e4749f7f9b6c37c1870ed3c0561083de17f2197 from: Omar Polo date: Fri Oct 02 17:39:00 2020 UTC initial commit commit - /dev/null commit + 3e4749f7f9b6c37c1870ed3c0561083de17f2197 blob - /dev/null blob + 9b1c514b6ac3057e90a07914b91c47799f6468fb (mode 644) --- /dev/null +++ .gitignore @@ -0,0 +1,6 @@ +cert.pem +key.pem +TAGS +gmid +*.o +docs blob - /dev/null blob + 76256b7dcf84f8002c97d62473907654109b4556 (mode 644) --- /dev/null +++ INSTALL.gmi @@ -0,0 +1,15 @@ +# Installing gmid + +## dependencies + +gmid depends on libtls and a C compiler. It's reported to compile with gcc 4.2, so it should work pretty everywhere now. + +The compilation is as easy as + +``` +make +``` + +Note that there isn't an install target yet. + +If you're a packager, don't forget to install also the manpage gmid.1 blob - /dev/null blob + 9f005bf026bc8d76f5ee1f8b7bfa68270a41bab0 (mode 644) --- /dev/null +++ Makefile @@ -0,0 +1,19 @@ +CC = cc +CFLAGS = -Wall -Wextra -g +LDFLAGS = -ltls + +.PHONY: all clean + +all: gmid TAGS README.md + +gmid: gmid.o + ${CC} gmid.o -o gmid ${LDFLAGS} + +TAGS: gmid.c + -etags gmid.c + +README.md: gmid.1 + mandoc -Tmarkdown gmid.1 | sed -e '1d' -e '$d' > README.md + +clean: + rm -f gmid.o gmid blob - /dev/null blob + 0b85f411030f07f17b20a3b88b7f3c1a2297d9b2 (mode 644) --- /dev/null +++ README.md @@ -0,0 +1,90 @@ + +# NAME + +**gmid** - dead simple gemini server + +# SYNOPSIS + +**gmid** +\[**-h**] +\[**-c** *cert.pem*] +\[**-d** *docs*] +\[**-k** *key.pem*] + +# DESCRIPTION + +**gmid** +is a very simple and minimal gemini server. +It only supports serving static content, and strive to be as simple as +possible. + +**gmid** +will strip any sequence of +*../* +or trailing +*..* +in the requests made by clients, so it's impossible to serve content +outside the +*docs* +directory by mistake. +Furthermore, on OpenBSD, +pledge(3) +and +unveil(3) +are used to ensure that +**gmid** +dosen't do anything else than read files from the given directory and +accept network connections. + +It should be noted that +**gmid** +is very simple in its implementation, and so it may not be appropriate +for serving site with lots of users. +After all, the code is single threaded and use a single process. + +The options are as follows: + +**-c** *cert.pem* + +> The certificate to use, by default is +> *cert.pem* + +**-d** *docs* + +> The root directory to serve. +> **gmid** +> won't serve any file that is outside that directory. + +**-h** + +> Print the usage and exit + +**-k** *key.pem* + +> The key for the certificate, by default is +> *key.pem* + +# EXAMPLES + +To quickly getting started + + $ # generate a cert and a key + $ openssl req -x509 -newkey rsa:4096 -keyout key.pem \ + -out cert.pem -days 365 -nodes + $ mkdir docs + $ cat < docs/index.gmi + # Hello world + test paragraph... + EOF + $ gmid -c cert.pem -k key.pem -d docs + + now you can visit gemini://localhost/ with your preferred gemini client. + +# CAVEATS + +* it doesn't support virtual host: the host part of the request URL is + completely ignored. + +* it doesn't fork in the background or anything like that. + +OpenBSD 6.8 - October 2, 2020 blob - /dev/null blob + 9f9dcc991d8bfa91f3da86d9634bb302097e3b4c (mode 644) --- /dev/null +++ gmid.1 @@ -0,0 +1,95 @@ +.\" Copyright (c) 2020 Omar Polo +.\" +.\" 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: October 2 2020$ +.Dt GMIND 1 +.Os +.Sh NAME +.Nm gmid +.Nd dead simple gemini server +.Sh SYNOPSIS +.Nm +.Bk -words +.Op Fl h +.Op Fl c Ar cert.pem +.Op Fl d Ar docs +.Op Fl k Ar key.pem +.Ek +.Sh DESCRIPTION +.Nm +is a very simple and minimal gemini server. +It only supports serving static content, and strive to be as simple as +possible. +.Pp +.Nm +will strip any sequence of +.Pa ../ +or trailing +.Pa .. +in the requests made by clients, so it's impossible to serve content +outside the +.Pa docs +directory by mistake. +Furthermore, on OpenBSD, +.Xr pledge 3 +and +.Xr unveil 3 +are used to ensure that +.Nm +dosen't do anything else than read files from the given directory and +accept network connections. +.Pp +It should be noted that +.Nm +is very simple in its implementation, and so it may not be appropriate +for serving site with lots of users. +After all, the code is single threaded and use a single process. +.Pp +The options are as follows: +.Bl -tag -width 12m +.It Fl c Ar cert.pem +The certificate to use, by default is +.Pa cert.pem +.It Fl d Ar docs +The root directory to serve. +.Nm +won't serve any file that is outside that directory. +.It Fl h +Print the usage and exit +.It Fl k Ar key.pem +The key for the certificate, by default is +.Pa key.pem +.El +.Sh EXAMPLES +To quickly getting started +.Bd -literal -indent +$ # generate a cert and a key +$ openssl req -x509 -newkey rsa:4096 -keyout key.pem \\ + -out cert.pem -days 365 -nodes +$ mkdir docs +$ cat < docs/index.gmi +# Hello world +test paragraph... +EOF +$ gmid -c cert.pem -k key.pem -d docs +.El +.Pp +now you can visit gemini://localhost/ with your preferred gemini client. +.Sh CAVEATS +.Bl -bullet +.It +it doesn't support virtual host: the host part of the request URL is +completely ignored. +.It +it doesn't fork in the background or anything like that. +.El blob - /dev/null blob + b7db8b9d8d61e6d11bbdc113c406ad21f1dba133 (mode 644) --- /dev/null +++ gmid.c @@ -0,0 +1,431 @@ +/* + * Copyright (c) 2020 Omar Polo + * + * 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 + +#ifndef __OpenBSD__ +# define pledge(a, b) 0 +# define unveil(a, b) 0 +#endif /* __OpenBSD__ */ + +#define GEMINI_URL_LEN (1024+3) /* URL max len + \r\n + \0 */ + +/* large enough to hold a copy of a gemini URL and still have room */ +#define PATHBUF (2048) + +#define FILEBUF 1024 + +#define SUCCESS 20 +#define NOT_FOUND 51 +#define BAD_REQUEST 59 + +int dirfd; + +char * +url_after_proto(char *url) +{ + char *s; + const char *proto = "gemini"; + const char *marker = "://"; + + if ((s = strstr(url, marker)) == NULL) + return url; + + /* not a gemini:// URL */ + + if (s - strlen(proto) < url) + return NULL; + /* TODO: */ + /* if (strcmp(s - strlen(proto), proto)) */ + /* return NULL; */ + + /* a valid gemini:// URL */ + return s + strlen(marker); +} + +char * +url_start_of_request(char *url) +{ + char *s, *t; + + if ((s = url_after_proto(url)) == NULL) + return NULL; + + if ((t = strstr(s, "/")) == NULL) + return s + strlen(s); + return t; +} + +int +url_trim(char *url) +{ + const char *e = "\r\n"; + char *s; + + if ((s = strstr(url, e)) == NULL) + return 0; + s[0] = '\0'; + s[1] = '\0'; + + if (s[2] != '\0') { + fprintf(stderr, "last byte of request isn't NULL\n"); + return 0; + } + + return 1; +} + +void +adjust_path(char *path) +{ + char *s; + size_t len; + + /* /.. -> / */ + len = strlen(path); + if (len >= 3) { + if (!strcmp(&path[len-3], "/..")) { + path[len-2] = '\0'; + } + } + + /* if the path is only `..` trim out and exit */ + if (!strcmp(path, "..")) { + path[0] = '\0'; + return; + } + + /* remove every ../ in the path */ + while (1) { + if ((s = strstr(path, "../")) == NULL) + return; + memmove(s - 3, s, strlen(s)+1); /* copy also the \0 */ + } +} + +int +path_isdir(char *path) +{ + size_t len; + + if (*path == '\0') + return 1; + + len = strlen(path); + + /* len >= 1 */ + if (path[len-1] == '/') + return 1; + + return 0; +} + +void +start_reply(struct tls *ctx, int code, const char *reason) +{ + char buf[1030] = {0}; /* status + ' ' + max reply len + \r\n\0 */ + int len; + + len = snprintf(buf, sizeof(buf), "%d %s\r\n", code, reason); + assert(len < (int)sizeof(buf)); + tls_write(ctx, buf, len); +} + +int +isdir(int fd) +{ + struct stat sb; + + if (fstat(fd, &sb) == -1) { + warn("fstat"); + return 1; /* we'll fail later on anyway */ + } + + return S_ISDIR(sb.st_mode); +} + +void senddir(char*, struct tls*); + +void +sendfile(char *path, struct tls *ctx) +{ + int fd; + char fpath[PATHBUF]; + char buf[FILEBUF]; + size_t i; + ssize_t t, w; + + bzero(fpath, sizeof(fpath)); + + if (*path != '.') + strlcpy(fpath, ".", PATHBUF); + + strlcat(fpath, path, PATHBUF); + + if ((fd = openat(dirfd, fpath, O_RDONLY | O_NOFOLLOW)) == -1) { + warn("open: %s", fpath); + start_reply(ctx, NOT_FOUND, "not found"); + return; + } + + if (isdir(fd)) { + warnx("%s is a directory, trying %s/index.gmi", fpath, fpath); + close(fd); + senddir(fpath, ctx); + return; + } + + /* assume it's a text/gemini file */ + start_reply(ctx, SUCCESS, "text/gemini"); + + while (1) { + if ((w = read(fd, buf, sizeof(buf))) == -1) { + warn("read: %s", fpath); + goto exit; + } + + if (w == 0) + break; + + t = w; + i = 0; + + while (w > 0) { + if ((t = tls_write(ctx, buf + i, w)) == -1) { + warnx("tls_write (path=%s) : %s", + fpath, + tls_error(ctx)); + goto exit; + } + w -= t; + i += t; + } + } + +exit: + close(fd); +} + +void +senddir(char *path, struct tls *ctx) +{ + char fpath[PATHBUF]; + size_t len; + + bzero(fpath, PATHBUF); + + if (*path != '.') + fpath[0] = '.'; + + /* this cannot fail since sizeof(fpath) > maxlen of path */ + strlcat(fpath, path, PATHBUF); + len = strlen(fpath); + + /* add a trailing / in case. */ + if (fpath[len-1] != '/') { + fpath[len] = '/'; + } + + strlcat(fpath, "index.gmi", sizeof(fpath)); + + sendfile(fpath, ctx); +} + +void +handle(char *url, struct tls *ctx) +{ + char *path; + + if (!url_trim(url)) { + start_reply(ctx, BAD_REQUEST, "bad request"); + return; + } + + if ((path = url_start_of_request(url)) == NULL) + return; + + adjust_path(path); + + fprintf(stderr, "requested path: %s\n", path); + + if (path_isdir(path)) + senddir(path, ctx); + else + sendfile(path, ctx); +} + +int +make_socket(int port) +{ + int sock, v; + struct sockaddr_in addr; + + if ((sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) + err(1, "socket"); + + v = 1; + if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &v, sizeof(v)) == -1) + err(1, "setsockopt(SO_REUSEADDR)"); + + v = 1; + if (setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &v, sizeof(v)) == -1) + err(1, "setsockopt(SO_REUSEPORT)"); + + bzero(&addr, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = INADDR_ANY; + + if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) == -1) + err(1, "bind"); + + if (listen(sock, 16) == -1) + err(1, "listen"); + + return sock; +} + +void +loop(struct tls *ctx, int sock) +{ + int fd; + struct sockaddr_in client; + socklen_t len; + struct tls *clientctx; + char buf[GEMINI_URL_LEN]; + ssize_t r; + + for (;;) { + len = sizeof(client); + if ((fd = accept(sock, (struct sockaddr*)&client, &len)) == -1) + err(1, "accept"); + + if (tls_accept_socket(ctx, &clientctx, fd) == -1) { + warnx("tls_accept_socket: %s", tls_error(ctx)); + continue; + } + + bzero(buf, GEMINI_URL_LEN); + if ((r = tls_read(clientctx, buf, sizeof(buf)-1)) == -1) { + warnx("tls_read: %s", tls_error(clientctx)); + goto clienterr; + } + + handle(buf, clientctx); + + clienterr: + if (tls_close(clientctx) == -1) + warn("tls_close: client"); + tls_free(clientctx); + close(fd); + } +} + +void +usage(const char *me) +{ + fprintf(stderr, + "USAGE: %s [-h] [-c cert.pem] [-d docs] [-k key.pem]\n", + me); +} + +int +main(int argc, char **argv) +{ + const char *cert = "cert.pem", *key = "key.pem", *dir = "docs"; + struct tls *ctx = NULL; + struct tls_config *conf; + void *m; + size_t mlen; + int sock, ch; + + while ((ch = getopt(argc, argv, "c:d:hk:")) != -1) { + switch (ch) { + case 'c': + cert = optarg; + break; + + case 'd': + dir = optarg; + break; + + case 'h': + usage(*argv); + return 0; + + case 'k': + key = optarg; + break; + + default: + usage(*argv); + return 1; + } + } + + if ((conf = tls_config_new()) == NULL) + err(1, "tls_config_new"); + + if ((m = tls_load_file(cert, &mlen, NULL)) == NULL) + err(1, "tls_load_file: %s", cert); + if (tls_config_set_cert_mem(conf, m, mlen) == -1) + err(1, "tls_config_set_cert_mem: server certificate"); + + if ((m = tls_load_file(key, &mlen, NULL)) == NULL) + err(1, "tls_load_file: %s", key); + if (tls_config_set_key_mem(conf, m, mlen) == -1) + err(1, "tls_config_set_cert_mem: server key"); + + if ((ctx = tls_server()) == NULL) + err(1, "tls_server"); + + if (tls_configure(ctx, conf) == -1) + errx(1, "tls_configure: %s", tls_error(ctx)); + + sock = make_socket(1965); + + if ((dirfd = open(dir, O_RDONLY | O_DIRECTORY)) == -1) + err(1, "open: %s", dir); + + if (unveil(dir, "r") == -1) + err(1, "unveil"); + + if (pledge("stdio rpath inet", "") == -1) + err(1, "pledge"); + + loop(ctx, sock); + + if (1) { + printf("why im I here?\n"); + abort(); + } + + close(sock); + tls_free(ctx); + tls_config_free(conf); +}