Compare commits

...

88 Commits
1.03 ... master

Author SHA1 Message Date
Solene Rapenne f647ff347a vger: add LOG_INFO into syslog 2022-11-11 11:08:41 +01:00
solene cad05817d3 Merge pull request 'relative_cgi' (#10) from relative_cgi into master
Reviewed-on: #10
2022-09-20 12:04:10 +00:00
prx 1e13a08865 improve check of cgi_dir request 2022-09-20 12:57:45 +02:00
prx aa6f001022 follow manpage for tests 2022-09-20 12:39:21 +02:00
prx 86626e77f2 fix indent 2022-09-20 12:39:09 +02:00
prx c09063b10e -c is now relative to chroot and can be used with multiple vhosts 2022-09-19 22:10:09 +02:00
solene de579c346a Merge pull request 'replace strlcpy and strlcat by snprintf to reduce calls' (#8) from simpler_string_handling into master
Reviewed-on: #8
2022-08-31 16:19:43 +00:00
prx 62bc1d852f use fork() to get the real byte size sent when using cgi 2022-08-22 14:25:26 +02:00
prx 750ca51e43 rename function name 2022-08-22 13:49:38 +02:00
prx bc306eaf8a remove '../' after percent decoding 2022-08-22 11:20:24 +02:00
prx 46f2c7238a replace strlcpy and strlcat by snprintf to reduce calls 2022-08-20 15:10:05 +02:00
solene 3ca16cf38f Merge pull request 'regex' (#7) from regex into master
Reviewed-on: #7
2022-08-19 13:13:19 +00:00
prx 9d23a48ae2 remove useless functions 2022-08-18 22:23:59 +02:00
prx 875f167eb9 skip useless test 2022-08-18 15:27:44 +02:00
prx 75bfaee73e remove useless function && reorder declarations 2022-08-18 14:34:04 +02:00
prx 45e5ae32fa useless line 2022-08-18 14:32:19 +02:00
prx b1f92f7c9c fix indent 2022-08-18 14:27:52 +02:00
prx fc5c2a1b41 fix cgi and simplify now we use regex 2022-08-18 14:12:38 +02:00
prx 34667eb018 Revert "test no longer required since path can't be empty with set_path"
This reverts commit 5cb310dd1b.
2022-08-18 14:02:45 +02:00
prx 5cb310dd1b test no longer required since path can't be empty with set_path 2022-08-18 14:01:12 +02:00
prx 63616a97fc remove unused function 2022-08-18 14:00:51 +02:00
prx 27549119ef simplify and fix redirections after regex 2022-08-18 13:59:54 +02:00
prx e3932483ef remove unused parameter 2022-08-18 11:06:37 +02:00
prx f8d215869d remove unused function 2022-08-18 11:05:56 +02:00
prx e2567fcf01 modify function to set path according to virtualhost 2022-08-18 11:05:21 +02:00
prx b18f3a3c7b rename function 2022-08-18 10:58:38 +02:00
prx 504dd3f759 rename check_request to read_request 2022-08-18 10:54:13 +02:00
prx f388d2a57a improve regex to handle :1234 in url 2022-08-18 10:53:08 +02:00
prx 5063f3e95b ensure errors msg are followed by \n, specify in a define the number of matches we need, remove bad structure init 2022-08-17 22:08:16 +02:00
prx be0f86df8d keep globals notation 2022-08-17 21:36:54 +02:00
prx 883bfed7a7 make tests easier to read 2022-08-17 21:34:37 +02:00
prx 26ca6c422d move regex to vger.h 2022-08-17 21:32:06 +02:00
prx 2b835bc39c import regex functions to parse request 2022-08-17 21:28:09 +02:00
solene bfd713c131 Merge pull request 'Reorganize vger code into functions' (#6) from nospaghettis into master
Reviewed-on: #6
2022-08-12 12:51:35 +00:00
prx fd0e70ab5b really fix size computation 2022-08-08 23:16:55 +02:00
prx 4d3b585951 fix wrong data size calculation 2022-08-08 23:12:12 +02:00
prx f973351945 reformat as much as possible.
Now main() is much simpler.
Removed all goto.
Less variables in main.
Simplified status_ to status().
Use a stop() function to log, send messages to stderr if necessary and close vger.
Minor fixes with defaults, mimes
There is still work to do to compare path using stat().
2022-08-08 22:57:03 +02:00
Solene Rapenne 76fafe0a9d fix a makefile source requirement 2022-07-04 21:52:45 +02:00
Solene Rapenne 4496e744e8 introduce the unit tests file 2022-07-04 21:52:20 +02:00
Solene Rapenne 9cca2408c3 Separate vger functions into a new file
This will ease the creation of unit tests
and code reusability
2022-07-04 21:00:31 +02:00
Solene Rapenne 8bd1144178 README: use repology to list where vger is packaged 2022-05-01 23:53:37 +02:00
Solene Rapenne 6743c54359 README: openbsd and arch linux has a package, mentions nix shell 2022-05-01 23:50:03 +02:00
solene bbd0f05663 Merge pull request 'fix cgi support for PATH_INFO' (#3) from phoebos/vger:merge into master
Reviewed-on: #3
2022-03-31 08:07:06 +00:00
aabacchus aa1affb6c2
fix cgi support for PATH_INFO
looks for files immediately after the cgidir, and anything after that file
if there are '/'s becomes PATH_INFO.

also adds a function strip_trailing_slash which may be useful in other parts too.
2022-03-26 20:31:50 +00:00
solene 8efcdb7512 Merge pull request 'remove any query_string before chdir' (#4) from phoebos/vger:query_string_slashes into master
Reviewed-on: #4
2022-03-26 08:27:24 +00:00
aabacchus 394b86bca8
remove any query_string before chdir
a query string could contain a '/' character, which would make vger try
to chdir to an incorrect directory. remove the query_string before this,
and before percent-decoding (in case there is an encoded '?'). This
should happen even if we are not doing cgi, because some clients might
send a query_string anyway, which should be ignored.
2022-03-19 14:49:44 +00:00
Solene Rapenne 01f2503376 fix NetBSD macro check 2022-01-26 13:04:04 +01:00
Solene Rapenne ed6dc1ed12 add .gitignore for build artifacts 2021-12-13 21:44:03 +01:00
Solene Rapenne 0d3d453498 test.sh should be executable out of the box 2021-12-13 21:34:49 +01:00
Solene Rapenne 914a143c3f add Nix shell file 2021-12-13 21:34:35 +01:00
Solene Rapenne a319de23f7 Add configure script 2021-12-13 21:32:08 +01:00
prx 15d09d2c01 fix user chroot issue + style 2021-10-21 11:41:22 +02:00
prx 843d1f0ab7 send header before other messages 2021-04-30 12:45:34 +02:00
prx f8aff7fe05 fix cgi error (file not found) and useless 'unveil' pledge promise 2021-04-30 09:38:12 +02:00
prx 4972df5999 Fix virtualhost support changing the way request is parsed 2021-03-22 21:44:23 +01:00
Solene Rapenne 365e99400a Support all other BSD ! <3 2021-03-14 21:29:51 +01:00
Solene Rapenne 807d1b8409 Add a configure script to make compiling on Linux easier 2021-03-14 14:18:06 +01:00
prx 02d2d1dc7d percent-decoding 2021-03-09 20:45:12 +01:00
prx de52acecfc Merge branch 'master' of tildegit.org:solene/vger 2021-03-09 20:37:46 +01:00
Omar Polo ee8569c6e6 simplify cgi function
Don't fork+execlp the script.  There's no need to do so since on exec
the new process will inherit our file descriptor table (and hence our
stdout), so copying from its stdout to ours is just a waste of time.
This allows to drop the ``proc'' pledge(2) promise and to (slightly)
improve performance.
2021-03-09 19:14:42 +01:00
Omar Polo 16a5ed7b30 drop unnecessary unveil(NULL, NULL)
the next line is a call to pledge, that alone is will block further
calls to unveil(2) since ``unveil'' isn't in the set of pledges.
2021-03-09 19:14:42 +01:00
Omar Polo 8454548b51 add test for redirect with trailing slash with vhosts on 2021-03-09 19:14:42 +01:00
Omar Polo cbcf4ec9b6 fix redirect when vhost support is enabled 2021-03-09 19:14:42 +01:00
Omar Polo 7431d3eeec Use the correct error codes and meaningful explanations
Introduce status_error: it's like status or status_redirect but for
errors, thus it doesn't add ``;lang=$lang'' at the end.
2021-03-09 19:14:42 +01:00
prx e9be1b73a7 percent-decode uri 2021-03-02 10:06:09 +01:00
prx e87b36c991 check request length for empty and too long 2021-03-02 10:04:04 +01:00
Solene Rapenne e3b5fb2ab3 Revert "deal with too small/long requests"
This reverts commit efa1f639fc.
2021-03-01 19:35:41 +01:00
Solene Rapenne 9525d66afb Revert "follow spec, fread() get nmemb-1"
This reverts commit fbacb35170.
2021-03-01 19:35:37 +01:00
Solene Rapenne d086262d1a Revert "empty request should works all the time, not only when in virtualhost"
This broke vger from inetd but it passed the tests.

This reverts commit 7b0686bdfa.
2021-03-01 19:35:17 +01:00
Solene Rapenne 7b0686bdfa empty request should works all the time, not only when in virtualhost 2021-02-27 19:59:28 +01:00
prx fbacb35170 follow spec, fread() get nmemb-1 2021-02-25 20:30:30 +01:00
prx efa1f639fc deal with too small/long requests 2021-02-25 18:37:48 +01:00
Solene Rapenne 458592594e Repair chroot, the only feature that isn't covered by tests 2021-02-23 23:24:12 +01:00
Solene Rapenne 31d384833f Rewording 2021-02-08 21:55:55 +01:00
Solene Rapenne ff78ea5049 Documentation rewording 2021-02-05 21:28:41 +01:00
Solene Rapenne 349e56c28c Bump LICENSE to include 2021 2021-02-05 21:28:21 +01:00
Solene Rapenne 2191a8a18b Fix make clean, find conditions are tricky 2021-02-05 21:24:32 +01:00
prx 84120dca09 remove double ticks 2021-02-03 21:01:07 +01:00
prx 43170e6804 restore all tests after mistake 2021-02-03 21:00:38 +01:00
prx f0dbd2c9ed ready for Linux && disable solene's specific tests 2021-02-03 20:46:36 +01:00
prx 55042768e5 restore parent link in autoindex and add comments 2021-01-31 22:05:48 +01:00
prx 189803ab52 useless space 2021-01-31 21:21:46 +01:00
prx de7cd12f9f ignore after ? and make cgi+virtualhost work (sort of) 2021-01-31 21:21:15 +01:00
prx 3510035711 fix unacceptable ../ in autoidx and code formatting 2021-01-31 13:59:24 +01:00
prx 2cc63136f8 alphasort autoindex + add proper error code fir cgi 2021-01-14 14:30:11 +01:00
prx 495fa3213f small tip for cgi 2021-01-14 13:54:29 +01:00
prx f6bc000adc fix status code in cgi script 2021-01-14 13:49:46 +01:00
prx 470e47a018 Add simple cgi support +:
* read file byte after byte
* format code (syslog + err)
* move functions in utils.c
2021-01-14 13:31:51 +01:00
21 changed files with 962 additions and 431 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.o
vger

View File

@ -1,4 +1,4 @@
Copyright (c) 2020 Solène Rapenne <solene@openbsd.org>
Copyright (c) 2020-2021 Solène Rapenne <solene@openbsd.org>
BSD 2-clause License

View File

@ -1,18 +1,33 @@
include config.mk
PREFIX?=/usr/local/
CFLAGS += -pedantic -Wall -Wextra -Wmissing-prototypes \
-Wstrict-prototypes -Wwrite-strings
-Wstrict-prototypes -Wwrite-strings ${EXTRAFLAGS}
.SUFFIXES: .c .o
.c.o:
${CC} ${CFLAGS} -c $<
all: vger
clean:
rm -f vger *.core *.o
find . \( -name vger -o \
-name unit_test -o \
-name "*.o" -o \
-name "*.core" \) \
-delete
vger: main.o mimes.o opts.h
${CC} -o vger main.o mimes.o
vger: main.c vger.c mimes.o utils.o opts.h
${CC} ${CFLAGS} -o $@ main.c mimes.o utils.o
install: vger
install -o root -g wheel vger ${PREFIX}/bin/
install -o root -g wheel vger.8 ${PREFIX}/man/man8/
test: vger
unit_test: tests.c vger.o
${CC} ${CFLAGS} -o $@ vger.o tests.c mimes.o utils.o
test: vger unit_test
./unit_test
cd tests && sh test.sh

View File

@ -1,12 +1,8 @@
# A simplistic and secure Gemini server
**Vger** is a gemini server supporting chroot, virtualhosts, default
language choice, redirections and MIME types detection.
**Vger** is a gemini server supporting chroot, virtualhosts, CGI, default language choice, redirections and MIME types detection.
**Vger** design is relying on inetd and a daemon to take care of
TLS. The idea is to delegate TLS and network to daemons which
proved doing it correctly, so vger takes its request from stdin and
output the result to stdout.
**Vger** design is relying on inetd and a daemon to take care of TLS. The idea is to delegate TLS and network to daemons which proved doing it correctly, so vger takes its request from stdin and output the result to stdout.
The average setup should look like:
@ -19,30 +15,36 @@ The average setup should look like:
vger on inetd
```
**Vger** is perfectly secure if run on **OpenBSD**, using `unveil()`
the filesystem access is restricted to one directory (default to
`/var/gemini/`) and with `pledge()` only systems calls related to
reading files and reading input/output are allowed.
**Vger** is perfectly secure if run on **OpenBSD**, using `unveil()` the filesystem access is restricted to one directory (default to `/var/gemini/`) and with `pledge()` only systems calls related to reading files and reading input/output are allowed. More explanations about Vger security can be found [on this link](https://dataswamp.org/~solene/2021-01-14-vger-security.html).
For all supported OS, it's possible to run **Vger** in a chroot
and drop privileges to a dedicated user.
For all supported OS, it's possible to run **Vger** in a chroot and drop privileges to a dedicated user.
# Install
`vger` is available as a package for the following systems:
[![Packaging status](https://repology.org/badge/vertical-allrepos/vger.svg)](https://repology.org/project/vger/versions)
# Building from sources
```
git clone https://tildegit.org/solene/vger.git
cd vger
./configure (only really useful for Linux)
make
doas make install
```
On GNU/Linux, make sure you installed `libbsd`, it has been reported that using clang was required too.
For NixOS/Nix users, there is a `shell.nix` listing the dependencies.
# Running tests
**Vger** comes with a test suite you can use with `make test`.
Some files under `/var/gemini/` are required to test the code path
without a `-d` parameter.
Some files under `/var/gemini/` are required to test the code path without a `-d` parameter.
# Command line parameters
@ -55,13 +57,12 @@ without a `-d` parameter.
- `-u username`: enable chroot to the data directory and drop privileges to `username`.
- `-m MIME` : use MIME as default instead of "application/octet-stream".
- `-i` : Enable auto index if no "index.gmi" file is found in a directory.
- `-c CGI_PATH` : files in CGI_PATH are executed and their output is returned to the client.
# How to configure Vger using relayd and inetd
Create directory `/var/gemini/` (I'd allow this to be configured
later), files will be served from there.
Create directory `/var/gemini/` (I'd allow this to be configured later), files will be served from there.
Create an user `gemini_user`.
@ -97,6 +98,4 @@ On OpenBSD, enable inetd and relayd and start them:
Don't forget to open the TCP port 1965 in your firewall.
Vger will serve files named `index.gmi` if no explicit filename is given.
If this file doesn't exist and auto index is enabled, an index file
with a link to every file in the directory will be served.
Vger will serve files named `index.gmi` if no explicit filename is given. If this file doesn't exist and auto index is enabled, an index file with a link to every file in the directory will be served.

1
config.mk Normal file
View File

@ -0,0 +1 @@
EXTRAFLAGS=

14
configure vendored Executable file
View File

@ -0,0 +1,14 @@
#!/bin/sh
OS="$(uname -s)"
case "$OS" in
Linux)
EXTRAFLAGS=-lbsd
;;
*)
EXTRAFLAGS=""
;;
esac
echo "EXTRAFLAGS=${EXTRAFLAGS}" > config.mk

408
main.c
View File

@ -1,287 +1,47 @@
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <limits.h>
#include <pwd.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include "mimes.h"
#include "opts.h"
#define GEMINI_PART 9
#define GEMINI_REQUEST_MAX 1024 /* see https://gemini.circumlunar.space/docs/specification.html */
void autoindex(const char *);
void display_file(const char *);
void status(const int, const char *);
void status_redirect(const int, const char *);
void drop_privileges(const char *, const char *);
void eunveil(const char *, const char *);
size_t estrlcat(char *, const char *, size_t);
size_t estrlcpy(char *, const char *, size_t);
void
eunveil(const char *path, const char *permissions)
{
if (unveil(path, permissions) == -1) {
syslog(LOG_DAEMON, "unveil on %s failed", path);
err(1, "unveil");
}
}
size_t
estrlcpy(char *dst, const char *src, size_t dstsize)
{
size_t n = 0;
n = strlcpy(dst, src, dstsize);
if (n >= dstsize) {
err(1, "strlcyp failed for %s = %s", dst, src);
}
return n;
}
size_t
estrlcat(char *dst, const char *src, size_t dstsize)
{
size_t size;
if ((size = strlcat(dst, src, dstsize)) >= dstsize)
err(1, "strlcat on %s + %s", dst, src);
return size;
}
void
drop_privileges(const char *user, const char *path)
{
struct passwd *pw;
int chrooted = 0;
/*
* use chroot() if an user is specified requires root user to be
* running the program to run chroot() and then drop privileges
*/
if (strlen(user) > 0) {
/* is root? */
if (getuid() != 0) {
syslog(LOG_DAEMON, "chroot requires program to be run as root");
errx(1, "chroot requires root user");
}
/* search user uid from name */
if ((pw = getpwnam(user)) == NULL) {
syslog(LOG_DAEMON, "the user %s can't be found on the system", user);
err(1, "finding user");
}
/* chroot worked? */
if (chroot(path) != 0) {
syslog(LOG_DAEMON, "the chroot_dir %s can't be used for chroot", path);
err(1, "chroot");
}
chrooted = 1;
if (chdir("/") == -1) {
syslog(LOG_DAEMON, "failed to chdir(\"/\")");
err(1, "chdir");
}
/* drop privileges */
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)) {
syslog(LOG_DAEMON, "dropping privileges to user %s (uid=%i) failed",
user, pw->pw_uid);
err(1, "Can't drop privileges");
}
}
#ifdef __OpenBSD__
/*
* prevent access to files other than the one in path
*/
if (chrooted) {
eunveil("/", "r");
} else {
eunveil(path, "r");
}
/*
* prevent system calls other parsing queryfor fread file and
* write to stdio
*/
if (pledge("stdio rpath", NULL) == -1) {
syslog(LOG_DAEMON, "pledge call failed");
err(1, "pledge");
}
#endif
}
void
status(const int code, const char *file_mime)
{
printf("%i %s; %s\r\n",
code, file_mime, lang);
}
void
status_redirect(const int code, const char *url)
{
printf("%i %s\r\n",
code, url);
}
void
display_file(const char *uri)
{
FILE *fd = NULL;
struct stat sb = {0};
ssize_t nread = 0;
const char *file_mime;
char *buffer[BUFSIZ];
char target[FILENAME_MAX] = {'\0'};
char fp[PATH_MAX] = {'\0'};
/* build file path inside chroot */
estrlcpy(fp, chroot_dir, sizeof(fp));
estrlcat(fp, uri, sizeof(fp));
/* this is to check if path exists and obtain metadata later */
if (stat(fp, &sb) == -1) {
/* check if fp is a symbolic link
* if so, redirect using its target */
if (lstat(fp, &sb) != -1 && S_ISLNK(sb.st_mode) == 1)
goto redirect;
else
goto err;
}
/* check if directory */
if (S_ISDIR(sb.st_mode) != 0) {
if (fp[strlen(fp) -1 ] != '/') {
/* no ending "/", redirect to "path/" */
char new_uri[PATH_MAX] = {'\0'};
estrlcpy(new_uri, uri, sizeof(fp));
estrlcat(new_uri, "/", sizeof(fp));
status_redirect(31, new_uri);
return;
} else {
/* there is a leading "/", display index.gmi */
char index_path[PATH_MAX] = {'\0'};
estrlcpy(index_path, fp, sizeof(index_path));
estrlcat(index_path, "index.gmi", sizeof(index_path));
/* check if index.gmi exists or show autoindex */
if (stat(index_path, &sb) == 0) {
estrlcpy(fp, index_path, sizeof(fp));
} else if (doautoidx != 0) {
autoindex(fp);
return;
} else {
goto err;
}
}
}
/* open the file requested */
if ((fd = fopen(fp, "r")) == NULL) { goto err; }
file_mime = get_file_mime(fp, default_mime);
status(20, file_mime);
/* read the file and write it to stdout */
while ((nread = fread(buffer, sizeof(char), sizeof(buffer), fd)) != 0)
fwrite(buffer, sizeof(char), nread, stdout);
goto closefd;
syslog(LOG_DAEMON, "path served %s", fp);
return;
err:
/* return an error code and no content */
status(51, "text/gemini");
syslog(LOG_DAEMON, "path invalid %s", fp);
goto closefd;
redirect:
/* read symbolic link target to redirect */
if (readlink(fp, target, FILENAME_MAX) == -1) {
goto err;
}
status_redirect(30, target);
syslog(LOG_DAEMON, "redirection from %s to %s", fp, target);
closefd:
if (S_ISREG(sb.st_mode) != 0) {
fclose(fd);
}
}
void
autoindex(const char *path)
{
struct dirent *dp;
DIR *fd;
if (!(fd = opendir(path))) {
err(1,"opendir '%s':", path);
}
syslog(LOG_DAEMON, "autoindex: %s", path);
status(20, "text/gemini");
/* TODO : add ending / in name if directory */
while ((dp = readdir(fd))) {
/* skip self */
if (!strcmp(dp->d_name, ".")) {
continue;
}
if (dp->d_type == DT_DIR) {
printf("=> ./%s/ %s/\n", dp->d_name, dp->d_name);
} else {
printf("=> ./%s %s\n", dp->d_name, dp->d_name);
}
}
closedir(fd);
}
#include "vger.c"
int
main(int argc, char **argv)
{
char request [GEMINI_REQUEST_MAX] = {'\0'};
char hostname [GEMINI_REQUEST_MAX] = {'\0'};
char uri [PATH_MAX] = {'\0'};
char user [_SC_LOGIN_NAME_MAX] = "";
int virtualhost = 0;
int option = 0;
char *pos = NULL;
char request[GEMINI_REQUEST_MAX] = {'\0'};
char user[_SC_LOGIN_NAME_MAX] = {'\0'};
char hostname[GEMINI_REQUEST_MAX] = {'\0'};
char path[GEMINI_REQUEST_MAX] = {'\0'};
char query[GEMINI_REQUEST_MAX] = {'\0'};
char cgi_dir[PATH_MAX] = {'\0'};
char rel_cgi_dir[PATH_MAX] = {'\0'};
char chroot_dir[PATH_MAX] = DEFAULT_CHROOT;
char tmp[PATH_MAX] = {'\0'};
int option = 0;
int virtualhost = 0;
while ((option = getopt(argc, argv, ":d:l:m:u:vi")) != -1) {
/*
* request : contain the whole request from client : gemini://...\r\n
* user : username, used in drop_privileges()
* hostname : extracted from hostname. used with virtualhosts and cgi SERVER_NAME
* query : after a ? in cgi : gemini://...?query
*/
while ((option = getopt(argc, argv, ":d:l:m:u:c:vi")) != -1) {
switch (option) {
case 'd':
estrlcpy(chroot_dir, optarg, sizeof(chroot_dir));
esnprintf(chroot_dir, sizeof(chroot_dir), "%s", optarg);
break;
case 'l':
estrlcpy(lang, "lang=", sizeof(lang));
estrlcat(lang, optarg, sizeof(lang));
esnprintf(lang, sizeof(lang), "lang=%s", optarg);
break;
case 'm':
estrlcpy(default_mime, optarg, sizeof(default_mime));
esnprintf(default_mime, sizeof(default_mime), "%s", optarg);
break;
case 'u':
estrlcpy(user, optarg, sizeof(user));
esnprintf(user, sizeof(user), "%s", optarg);
break;
case 'c':
esnprintf(rel_cgi_dir, sizeof(rel_cgi_dir), "%s", optarg);
/* remove leading / */
while (*rel_cgi_dir == '/')
memmove(rel_cgi_dir, rel_cgi_dir+1,
strlen(rel_cgi_dir)); // strlen +1-1
break;
case 'v':
virtualhost = 1;
@ -292,80 +52,46 @@ main(int argc, char **argv)
}
}
/*
* do chroot if an user is supplied run pledge/unveil if OpenBSD
*/
drop_privileges(user, chroot_dir);
read_request(request);
split_request(request, hostname, path, query);
/*
* read 1024 chars from stdin
* to get the request
*/
if (fgets(request, GEMINI_REQUEST_MAX, stdin) == NULL) {
status(59, "request is too long (1024 max)");
syslog(LOG_DAEMON, "request is too long (1024 max): %s", request);
exit(1);
}
/* do chroot if an user is supplied */
if (*user)
drop_privileges(user);
/* remove \r\n at the end of string */
pos = strchr(request, '\r');
if (pos != NULL)
*pos = '\0';
/*
* check if the beginning of the request starts with
* gemini://
*/
if (strncmp(request, "gemini://", GEMINI_PART) != 0) {
/* error code url malformed */
syslog(LOG_DAEMON, "request «%s» doesn't match gemini://",
request);
exit(1);
}
syslog(LOG_DAEMON, "request %s", request);
/* remove the gemini:// part */
memmove(request, request + GEMINI_PART, sizeof(request) - GEMINI_PART);
/*
* look for the first / after the hostname
* in order to split hostname and uri
*/
pos = strchr(request, '/');
if (pos != NULL) {
/* if there is a / found */
/* separate hostname and uri */
estrlcpy(uri, pos, strlen(pos)+1);
/* just keep hostname in request */
pos[0] = '\0';
}
/* check if client added :port at end of request */
pos = strchr(request, ':');
if (pos != NULL) {
/* end string at :*/
pos[0] = '\0';
}
/* copy hostname from request */
estrlcpy(hostname, request, sizeof(hostname));
/*
* if virtualhost feature is actived looking under the chroot_path +
* hostname directory gemini://foobar/hello will look for
* chroot_path/foobar/hello
*/
/* set actual chroot_dir */
if (virtualhost) {
if (strlen(uri) == 0) {
estrlcpy(uri, "/index.gmi", sizeof(uri));
}
char new_uri[PATH_MAX] = {'\0'};
estrlcpy(new_uri, hostname, sizeof(new_uri));
estrlcat(new_uri, uri, sizeof(new_uri));
estrlcpy(uri, new_uri, sizeof(uri));
esnprintf(tmp, sizeof(tmp), "%s/%s", chroot_dir, hostname);
esnprintf(chroot_dir, sizeof(chroot_dir), "%s", tmp);
}
/* open file and send it to stdout */
display_file(uri);
/* cgi_dir is in chroot_dir */
if (*rel_cgi_dir)
esnprintf(cgi_dir, sizeof(cgi_dir),
"%s/%s", chroot_dir, rel_cgi_dir);
return (0);
set_rootdir(chroot_dir, cgi_dir, user);
if (strlen(path) == 0) { /* this is root dir */
esnprintf(path, sizeof(path), "./");
} else {
uridecode(path);
remove_double_dot(path);
}
uridecode(query);
/* is it cgi ? */
if (*cgi_dir)
if (do_cgi(rel_cgi_dir, path, hostname, query) == 0)
stop(EXIT_SUCCESS, NULL);
/* *** from here, cgi didn't run *** */
/* check if path available */
check_path(path, sizeof(path));
/* regular file to stdout */
display_file(path);
stop(EXIT_SUCCESS, NULL);
}

20
mimes.c
View File

@ -5,6 +5,7 @@
#include "mimes.h"
#include "opts.h"
/* extension to mimetype table */
static const struct {
const char *extension;
const char *type;
@ -124,16 +125,15 @@ get_file_mime(const char *path, const char *default_mime)
size_t i;
char *extension;
if ((extension = strrchr(path, '.')) == NULL)
goto out;
/* look for the MIME in the database */
for (i = 0; i < nitems(database); i++) {
if (strcmp(database[i].extension, extension + 1) == 0)
return (database[i].type);
/* search for extension after last '.' in path */
if ((extension = strrchr(path, '.')) != NULL) {
/* look for the MIME in the database */
for (i = 0; i < nitems(database); i++) {
if (strcmp(database[i].extension, extension + 1) == 0)
return (database[i].type);
}
}
out:
/* if no MIME have been found, set a default one */
return (default_mime);
/* no MIME found, set a default one */
return default_mime;
}

14
opts.h
View File

@ -1,12 +1,16 @@
#include <limits.h> /* PATH_MAX */
/* Defaults values */
#define DEFAULT_MIME "application/octet-stream"
#define DEFAULT_LANG ""
#define DEFAULT_CHROOT "/var/gemini"
#define DEFAULT_INDEX "index.gmi"
#define DEFAULT_AUTOIDX 0
/* longest is 56 so 64 should be enough */
static char default_mime[64] = DEFAULT_MIME;
static char chroot_dir[PATH_MAX] = DEFAULT_CHROOT;
static char lang[16] = DEFAULT_LANG;
static unsigned int doautoidx = DEFAULT_AUTOIDX;
/*
* Options used later
*/
/* longest hardcoded mimetype is 56 long so 64 should be enough */
static char default_mime[64] = DEFAULT_MIME;
static char lang[16] = DEFAULT_LANG;
static unsigned int doautoidx = DEFAULT_AUTOIDX;

4
shell.nix Normal file
View File

@ -0,0 +1,4 @@
with (import <nixpkgs> {});
mkShell {
buildInputs = [ gcc libbsd gnumake ];
}

50
tests.c Normal file
View File

@ -0,0 +1,50 @@
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "vger.h"
// to test
void test_status(void);
void test_status_error(void);
void test_uridecode(char*, const int);
void
test_uridecode(char *str, const int result)
{
char reference[GEMINI_REQUEST_MAX] = {'\0'};
strlcpy(reference, str, sizeof(reference));
uridecode(str);
if (strncmp(reference, str, strlen(str)) != result)
{
printf("uridecode error\n");
printf("Strings should be %s\n", (result == 0) ? "identical" : "different");
printf("passed %s\n", reference);
printf("got %s\n", str);
exit(1);
}
}
void
test_status(void)
{
status(20, "text/gemini");
}
void
test_status_error(void)
{
status(51, "file not found");
status(50, "Forbidden path");
status(50, "Internal server error");
}
int
main(void)
{
test_status_error();
test_status();
//test_uridecode("host.name", 0);
//test_uridecode("host.name/percent%25-encode%3.gmi", 1);
return(0);
}

109
tests/test.sh Normal file → Executable file
View File

@ -4,109 +4,122 @@ set -x
# md5 is BSD md5 binary
# Linux uses md5sum
MD5=md5
type md5 2>/dev/null
if [ $? -ne 0 ]; then
MD5=md5sum
fi
which md5 && MD5CMD="md5" || MD5CMD="md5sum"
MD5()
{
$MD5CMD | awk '{print $1}'
}
# serving a file
OUT=$(printf "gemini://host.name/main.gmi\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5)
OUT=$(printf "gemini://host.name/main.gmi\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "c7e352d6aae4ee7e7604548f7874fb9d" ] ; then echo "error" ; exit 1 ; fi
# default index.gmi file
OUT=$(printf "gemini://host.name\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5)
OUT=$(printf "gemini://host.name\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1 ; fi
# default index.gmi file when using a trailing slash
OUT=$(printf "gemini://host.name/\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5)
OUT=$(printf "gemini://host.name/\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1 ; fi
# default index.gmi file when client specify port
OUT=$(printf "gemini://host.name:1965\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5)
OUT=$(printf "gemini://host.name:1965\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1 ; fi
# redirect to uri with trailing / if directory
OUT=$(printf "gemini://host.name/subdir\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5)
OUT=$(printf "gemini://host.name/subdir\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "84e5e7bb3eee0dfcc8db14865dc83e77" ] ; then echo "error" ; exit 1 ; fi
# redirect to uri with trailing / if directory and vhost enabled
OUT=$(printf "gemini://perso.pw/cgi-bin\r\n" | ../vger -vd var/gemini | tee /dev/stderr | MD5)
if ! [ $OUT = "e0eb3a8e31bdb30c89d92d1d2b0a1fa1" ] ; then echo "error" ; exit 1 ; fi
# file from local directory with lang=fr and markdown MIME type
OUT=$(printf "gemini://perso.pw/file.md\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | $MD5)
if ! [ $OUT = "e663f17730d5ddc24010c14a238e1e78" ] ; then echo "error" ; exit 1 ; fi
OUT=$(printf "gemini://perso.pw/file.md\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | MD5)
if ! [ $OUT = "09c82ffe243ce3b3cfb04c2bc4a91acb" ] ; then echo "error" ; exit 1 ; fi
# file from local directory with lang=fr and unknown MIME type (default to application/octet-stream)
OUT=$(printf "gemini://perso.pw/foobar.unknown\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | $MD5)
if ! [ $OUT = "a23b0053d759863a45da4afbffd847d2" ] ; then echo "error" ; exit 1 ; fi
OUT=$(printf "gemini://perso.pw/foobar.unknown\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | MD5)
if ! [ $OUT = "2c73bfb33dd2d12be322ebb85e03c015" ] ; then echo "error" ; exit 1 ; fi
# file from local directory and unknown MIME type, default forced to text/plain
OUT=$(printf "gemini://perso.pw/foobar.unknown\r\n" | ../vger -d var/gemini/ -m text/plain | tee /dev/stderr | $MD5)
if ! [ $OUT = "383a5a5ddb7bb30e3553ecb666378ebc" ] ; then echo "error" ; exit 1 ; fi
OUT=$(printf "gemini://perso.pw/foobar.unknown\r\n" | ../vger -d var/gemini/ -m text/plain | tee /dev/stderr | MD5)
if ! [ $OUT = "8169f43fbb2032f4054b153c38fe61d6" ] ; then echo "error" ; exit 1 ; fi
# redirect file
OUT=$(printf "gemini://perso.pw/old_location\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5)
OUT=$(printf "gemini://perso.pw/old_location\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "cb4597b6fcc82cbc366ac9002fb60dac" ] ; then echo "error" ; exit 1 ; fi
# file from local directory using virtualhosts
OUT=$(printf "gemini://perso.pw/index.gmi\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | $MD5)
OUT=$(printf "gemini://perso.pw/index.gmi\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "5e5fca557e79f4521b21d4b81dc964c6" ] ; then echo "error" ; exit 1 ; fi
# file from local directory using virtualhosts without specifying a file
OUT=$(printf "gemini://perso.pw\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | $MD5)
OUT=$(printf "gemini://perso.pw\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "5e5fca557e79f4521b21d4b81dc964c6" ] ; then echo "error" ; exit 1 ; fi
# file from local directory using virtualhosts without specifying a file using lang = fr
OUT=$(printf "gemini://perso.pw\r\n" | ../vger -v -d var/gemini/ -l fr | tee /dev/stderr | $MD5)
OUT=$(printf "gemini://perso.pw/\r\n" | ../vger -v -d var/gemini/ -l fr | tee /dev/stderr | MD5)
if ! [ $OUT = "7db981ce93fee268f29324912800f00d" ] ; then echo "error" ; exit 1 ; fi
# file from local directory using virtualhosts and IRI
OUT=$(printf "gemini://virtualhoßt/é è.gmi\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | $MD5)
OUT=$(printf "gemini://virtualhoßt/é è.gmi\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "282cee071d3bd20dbb6e6af38f217a29" ] ; then echo "error" ; exit 1 ; fi
# file from local directory using virtualhosts and IRI both with emojis
OUT=$(printf "gemini://⛴//❤️.gmi\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | $MD5)
OUT=$(printf "gemini://⛴//❤️.gmi\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "e354a1a29ea8273faaf0cdc29c1d8583" ] ; then echo "error" ; exit 1 ; fi
# auto index in directory without index.gmi must redirect
OUT=$(printf "gemini://host.name/autoidx\r\n" | ../vger -d var/gemini/ -i | tee /dev/stderr | $MD5)
OUT=$(printf "gemini://host.name/autoidx\r\n" | ../vger -d var/gemini/ -i | tee /dev/stderr | MD5)
if ! [ $OUT = "874f5e1af67eff6b93bedf8ac8033066" ] ; then echo "error" ; exit 1 ; fi
# auto index in directory
OUT=$(printf "gemini://host.name/autoidx/\r\n" | ../vger -d var/gemini/ -i | tee /dev/stderr | $MD5)
if ! [ $OUT = "770a987b8f5cf7169e6bc3c6563e1570" ] ; then echo "error" ; exit 1 ; fi
OUT=$(printf "gemini://host.name/autoidx/\r\n" | ../vger -d var/gemini/ -i | tee /dev/stderr | MD5)
if ! [ $OUT = "765bbbe2add810be8eb191bbde59e258" ] ; then echo "error" ; exit 1 ; fi
# cgi simple script
OUT=$(printf "gemini://host.name/cgi-bin/test.cgi\r\n" | ../vger -d var/gemini/ -c cgi-bin | tee /dev/stderr | MD5)
if ! [ $OUT = "666e48200f90018b5e96c2cf974882dc" ] ; then echo "error" ; exit 1 ; fi
# cgi with use of variables
OUT=$(printf "gemini://host.name/cgi-bin/who.cgi?user=jean-mi\r\n" | ../vger -d var/gemini/ -c cgi-bin | tee /dev/stderr | MD5)
if ! [ $OUT = "fa065a67d1f7c973501d4a9e3ca2ea57" ] ; then echo "error" ; exit 1 ; fi
# cgi with error
OUT=$(printf "gemini://host.name/cgi-bin/nope\r\n" | ../vger -d var/gemini/ -c cgi-bin | tee /dev/stderr | MD5)
if ! [ $OUT = "31b98e160402a073298c12f763d5db64" ] ; then echo "error" ; exit 1 ; fi
# cgi with PATH_INFO
OUT=$(printf "gemini://host.name/cgi-bin/test.cgi/path/info\r\n" | ../vger -d var/gemini -c cgi-bin | tee /dev/stderr | MD5)
if ! [ $OUT = "ec64da76dc578ffb479fbfb23e3a7a5b" ] ; then echo "error" ; exit 1 ; fi
# virtualhost + cgi
OUT=$(printf "gemini://perso.pw/cgi-bin/test.cgi\r\n" | ../vger -v -d var/gemini/ -c cgi-bin | tee /dev/stderr | MD5)
if ! [ $OUT = "666e48200f90018b5e96c2cf974882dc" ] ; then echo "error" ; exit 1 ; fi
# percent-decoding
OUT=$(printf "%s\r\n" "gemini://host.name/percent%25-encode%3f.gmi" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "83d59cca9ed7040145ac6df1992f5daf" ] ; then echo "error" ; exit 1 ; fi
# percent-decoding failing
OUT=$(printf "%s\r\n" "gemini://host.name/percent%25-encode%3.gmi" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
if ! [ $OUT = "c782da4173898f57033a0804b8e96fc3" ] ; then echo "error" ; exit 1 ; fi
# must fail only on OpenBSD !
# try to escape from unveil
if [ -f /bsd ]
then
OUT=$(printf "gemini://fail_on_openbsd/../../test.sh\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | $MD5)
if [ $OUT = "$( ( printf '20 text/gemini; lang=fr\r\n' ; cat $0) | $MD5)" ] ; then echo "error" ; exit 1 ; fi
OUT=$(printf "gemini://fail_on_openbsd/../../test.sh\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | MD5)
if [ $OUT = "$( ( printf '20 text/gemini; lang=fr\r\n' ; cat $0) | MD5)" ] ; then echo "error" ; exit 1 ; fi
fi
#type doas 2>/dev/null
#if [ $? -eq 0 ]; then
# # file from local directory chroot
# OUT=$(printf "gemini://perso.pw\r\n" | doas ../vger -v -d var/gemini/ -u solene -l fr | tee /dev/stderr | $MD5)
# OUT=$(printf "gemini://perso.pw\r\n" | doas ../vger -v -d var/gemini/ -u solene -l fr | tee /dev/stderr | MD5)
# if ! [ $OUT = "7db981ce93fee268f29324912800f00d" ] ; then echo "error" ; exit 1 ; fi
#fi
#### no -d parameter from here
if [ -d /var/gemini/ ]
then
# file from /var/gemini/index.md
OUT=$(printf "gemini://host.name/index.md\r\n" | ../vger | tee /dev/stderr | $MD5)
if ! [ $OUT = "1f7ed3966d50b08ea138b7d8c0a08ec6" ] ; then echo "error" ; exit 1 ; fi
# file from /var/gemini/blog/
OUT=$(printf "gemini://host.name/blog/\r\n" | ../vger | tee /dev/stderr | $MD5)
if ! [ $OUT = "83bd01c9af0e44d5439b9ac95dc28132" ] ; then echo "error" ; exit 1 ; fi
# file from /var/gemini/blog
OUT=$(printf "gemini://host.name/blog\r\n" | ../vger | tee /dev/stderr | $MD5)
if ! [ $OUT = "83bd01c9af0e44d5439b9ac95dc28132" ] ; then echo "error" ; exit 1 ; fi
fi
echo "SUCCESS"

View File

@ -0,0 +1,9 @@
#!/bin/sh
printf "%s %s: cgi_test\r\n" "20 text/plain"
echo "env vars:"
echo $GATEWAY_INTERFACE
echo $SERVER_SOFTWARE
echo $PATH_INFO
echo $QUERY_STRING

View File

@ -0,0 +1,10 @@
#!/bin/sh
printf "%s %s: cgi_test\r\n" "20 text/plain"
u=""
if [ -n "${QUERY_STRING}" ]; then
u="$(printf "%s" "${QUERY_STRING}" | cut -d'=' -f2)" #yeah, it's awful..
fi
echo "hello $u"

View File

@ -0,0 +1,2 @@
solene, here is a % for you
and a λ of course :)

View File

@ -0,0 +1,9 @@
#!/bin/sh
printf "%s %s: cgi_test\r\n" "20 text/plain"
echo "env vars:"
echo $GATEWAY_INTERFACE
echo $SERVER_SOFTWARE
echo $PATH_INFO
echo $QUERY_STRING

124
utils.c Normal file
View File

@ -0,0 +1,124 @@
#include <sys/types.h>
#include <err.h>
#include <errno.h>
#include <regex.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <syslog.h>
#include <unistd.h>
#include "utils.h"
#include "vger.h"
#if defined(__OpenBSD__) || defined(__FreeBSD__) || defined( __NetBSD__) || defined(__DragonFly__)
#include <string.h>
#else
#include <bsd/string.h>
#endif
/* e*foo() functions are the equivalent of foo() but handle errors.
* In case an error happens:
* The error is printed to stdout
* return 1
*/
#ifdef __OpenBSD__
void
eunveil(const char *path, const char *permissions)
{
if (unveil(path, permissions) == -1) {
status(41, "Error when unveil(), see logs");
stop(EXIT_FAILURE, "unveil on %s failed", path);
}
}
void
epledge(const char *promises, const char *execpromises)
{
if (pledge(promises, execpromises) == -1) {
status(41, "Error when pledge(), see logs");
stop(EXIT_FAILURE, "pledge failed for: %s", promises);
}
}
#endif
size_t
esnprintf(char *str, size_t size, const char *format, ...)
{
/* usage : esnprintf(str, sizeof(str), "%s ... %s", arg1, arg2); */
va_list ap;
size_t ret = 0;
va_start(ap, format);
ret = vsnprintf(str, size, format, ap);
va_end(ap);
if (ret < 0 || ret >= size) {
status(41, "vnsprintf failed: Output trunkated");
stop(EXIT_FAILURE, "vsnprintf: Output trunkated");
}
return ret;
}
int
esetenv(const char *name, const char *value, int overwrite)
{
int ret = 0;
ret = setenv(name, value, overwrite);
if (ret != 0) {
status(41, "setenv() failed, see logs");
stop(EXIT_FAILURE, "setenv() %s:%s", name, value);
}
return ret;
}
void
echdir(const char *path)
{
if (chdir(path) == -1) {
switch (errno) {
case ENOTDIR: /* FALLTHROUGH */
case ENOENT:
status(51, "file not found");
break;
case EACCES:
status(50, "Forbidden path");
break;
default:
status(50, "Internal server error");
break;
}
stop(EXIT_FAILURE, "chdir(%s) failed", path);
}
}
/* read the file fd byte after byte in buffer and write it to stdout
* return number of bytes read
*/
size_t
print_file(FILE *fd)
{
ssize_t nread = 0;
ssize_t datasent = 0;
char *buffer[BUFSIZ];
while ((nread = fread(buffer, 1, sizeof(buffer), fd)) != 0)
datasent += fwrite(buffer, 1, nread, stdout);
return datasent;
}
void
getsubexp(const char *str, regmatch_t m, char *dst)
{
size_t len = 0;
if ((len = m.rm_eo - m.rm_so) > 0) { /* skip empty substring */
len = m.rm_eo - m.rm_so;
memcpy(dst, str + m.rm_so, len);
dst[len] = '\0';
}
}

8
utils.h Normal file
View File

@ -0,0 +1,8 @@
void getsubexp(const char *, regmatch_t, char *);
void echdir(const char *);
void epledge(const char *, const char *);
void eunveil(const char *, const char *);
int esetenv(const char *, const char *, int);
size_t esnprintf(char *, size_t, const char *, ...);
size_t print_file(FILE *fd);
void set_errmsg(const char *, ...);

18
vger.8
View File

@ -9,6 +9,7 @@
.Op Fl l Ar lang
.Op Fl v
.Op Fl i
.Op Fl c Ar cgi_path
.Op Fl d Ar path
.Op Fl u Ar username
.Op Fl m Ar mimetype
@ -40,6 +41,21 @@ Enable virtualhost support, the hostname in the query will be considered as a di
As example, for request gemini://hostname.example/file.gmi
.Nm
will read the file /var/gemini/hostname.example/file.gmi
.It Op Fl c
Enable CGI support.
.Ar cgi_path
files will be executed as a cgi script instead of returning their content.
.Ar cgi_path should be relative to chroot so cgi can be called for different virtualhosts.
As example, for a request gemini://hostname.example/cgi-bin/hello.cgi, one must set:
.Bd -literal -offset indent
vger -c cgi-bin
.Ed
.Pp
In this case,
.Xr pledge 2
promises and unveil permission are set to enable cgi execution.
.Pp
Be very careful on how you write your CGI, it can read outside the chroot.
.It Op Fl m Ar mimetype
Use
.Ar mimetype
@ -55,7 +71,7 @@ On
.Nm
will use
.Xr unveil 2
on this path in read-only to prevent file access outside this directory.
on this path to only allow read-only file access within this directory.
.It Op Fl u Ar username
Enable
.Xr chroot 2

468
vger.c Normal file
View File

@ -0,0 +1,468 @@
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <ctype.h>
#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pwd.h>
#include <regex.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include "mimes.h"
#include "opts.h"
#include "utils.h"
#include "vger.h"
void
stop(const int r, const char *fmt, ...)
{
va_list ap, ap2;
fflush(stdout); /* ensure all data is sent */
/* log the request and retcode */
syslog(LOG_DAEMON|LOG_INFO, "\"%s\" %i %zd", _request, _retcode, _datasiz);
if (r != EXIT_SUCCESS) {
/* log and print error */
va_copy(ap2, ap);
va_start(ap, fmt);
vsyslog(LOG_ERR, fmt, ap);
va_end(ap);
va_start(ap2, fmt);
vfprintf(stderr, fmt, ap2);
va_end(ap2);
}
exit(r);
}
void
status(const int code, const char *fmt, ...)
{
va_list ap;
_datasiz += fprintf(stdout, "%i ", code);
va_start(ap, fmt);
_datasiz += vfprintf(stdout, fmt, ap);
va_end(ap);
_datasiz += fprintf(stdout, "\r\n"); /* make sure status end correctly */
_retcode = code; /* store return code for logs */
}
int
uridecode(char *uri)
{
int n = 0;
char c = '\0';
long l = 0;
char *pos = NULL;
if ((pos = strchr(uri, '%')) == NULL)
return n;
while ((pos = strchr(pos, '%')) != NULL) {
if (strlen(pos) < 3)
return n;
char hex[3] = {'\0'};
for (size_t i = 0; i < 2; i++)
hex[i] = tolower(pos[i + 1]);
errno = 0;
l = strtol(hex, 0, 16);
if (errno == ERANGE && (l == LONG_MAX || l == LONG_MIN))
continue; /* conversion failed */
c = (char)l;
pos[0] = c;
/* rewind of two char to remove %hex */
memmove(pos + 1, pos + 3, strlen(pos + 3) + 1); /* +1 for \0 */
n++;
pos++; /* avoid infinite loop */
}
return n;
}
void
drop_privileges(const char *user)
{
struct passwd *pw;
/*
* use chroot() if an user is specified requires root user to be
* running the program to run chroot() and then drop privileges
*/
/* is root? */
if (getuid() != 0) {
status(41, "privileges issue, see logs");
stop(EXIT_FAILURE, "%s",
"chroot requires program to be run as root");
}
/* search user uid from name */
if ((pw = getpwnam(user)) == NULL) {
status(41, "privileges issue, see logs");
stop(EXIT_FAILURE,
"the user %s can't be found on the system", user);
}
/* drop privileges */
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)) {
status(41, "privileges issue, see logs");
stop(EXIT_FAILURE,
"dropping privileges to user %s (uid=%i) failed", \
user, pw->pw_uid);
}
}
void
set_rootdir(const char *chroot_dir, const char *cgi_dir, const char *user)
{
char capsule_dir[PATH_MAX] = {'\0'};
if (*user) {
/* chroot worked? */
if (chroot(chroot_dir) != 0) {
status(41, "privileges issue, see logs");
stop(EXIT_FAILURE,
"the chroot_dir %s can't be used for chroot", chroot_dir);
}
/* now chroot_dir is / */
esnprintf(capsule_dir, sizeof(capsule_dir), "%s", "/");
} else {
esnprintf(capsule_dir, sizeof(capsule_dir), "%s", chroot_dir);
}
#ifdef __OpenBSD__
/*
* prevent access to files other than the one in chroot_dir
*/
eunveil(capsule_dir, "r");
/* permission to execute what's inside cgi_dir */
if (*cgi_dir)
eunveil(cgi_dir, "rx");
eunveil(NULL, NULL); /* no more call to unveil() */
/* promise permissions */
if (*cgi_dir)
epledge("stdio rpath exec proc", NULL);
else
epledge("stdio rpath", NULL);
#endif
echdir(capsule_dir); /* move to the gemini data directory */
}
ssize_t
display_file(const char *path)
{
FILE *fd = NULL;
const char *file_mime;
/*
* special case : path ends with "/". The user requested a dir
*/
if ((path[strlen(path)-1] == '/') && (doautoidx)) {
/* no index.gmi, so display autoindex if enabled */
_datasiz += autoindex(path);
return _datasiz;
}
/* open the file requested */
if ((fd = fopen(path, "r")) != NULL) {
file_mime = get_file_mime(path, default_mime);
if (strcmp(file_mime, "text/gemini") == 0)
status(20, "%s; %s", file_mime, lang);
else
status(20, "%s", file_mime);
_datasiz += print_file(fd);
fclose(fd); /* close file descriptor */
} else {
/* return an error code and no content.
* seems unlikely to happen unless the file vanished
* since we checked with stat() if it exists
*/
status(51, "%s", "file not found and may have vanished");
}
return _datasiz;
}
int
do_cgi(const char *rel_cgi_dir, const char *path, const char *hostname, const char *query)
{
struct stat sbcgi = {0};
struct stat sbpath = {0};
char cgifp[PATH_MAX] = {'\0'}; /* cgi file to execute */
char path_dir[PATH_MAX] = {'\0'};
char *path_info = NULL;
/* get beginning of path */
/* path_dir is initialized so there is an \0 at the end */
memcpy(path_dir, path, strlen(rel_cgi_dir));
if (stat(rel_cgi_dir, &sbcgi) + stat(path_dir, &sbpath) != 0)
goto nocgi;
/* compare inodes */
if (sbcgi.st_ino != sbpath.st_ino)
goto nocgi; /* not in cgi_dir, go to display_file */
/* set env variables for CGI
* see
* https://lists.orbitalfox.eu/archives/gemini/2020/000315.html
*/
esetenv("GATEWAY_INTERFACE", "CGI/1.1", 1);
esetenv("SERVER_PROTOCOL", "GEMINI", 1);
esetenv("SERVER_SOFTWARE", "vger/1", 1);
if (*query)
esetenv("QUERY_STRING", query, 1);
/*
* if in cgi_dir, only the first file after cgi_dir/FILE
* is to be executed
* the rest is PATH_INFO
*/
/* find next item after cgi_dir in path:
* path + strlen(rel_cgi_dir) + 1 (skip '/')
*/
/* cgi file to execute */
esnprintf(cgifp, sizeof(cgifp), "%s", path + strlen(rel_cgi_dir) + 1);
if (!(*cgifp)) /* problem with cgi file, abort */
goto nocgi;
/* check if there is something after cgi file for PATH_INFO */
path_info = strchr(cgifp, '/');
if (path_info != NULL) {
esetenv("PATH_INFO", path_info, 1);
*path_info = '\0'; /* stop cgifp before PATH_INFO */
}
esetenv("SCRIPT_NAME", cgifp, 1);
esetenv("SERVER_NAME", hostname, 1);
echdir(rel_cgi_dir);
cgi(cgifp);
return 0;
nocgi:
return 1;
}
ssize_t
autoindex(const char *path)
{
/* display list of files in path + a link to parent (..) */
int n = 0;
struct dirent **namelist; /* this must be freed at last */
size_t bs = 0;
/* use alphasort to always have the same order on every system */
if ((n = scandir(path, &namelist, NULL, alphasort)) < 0) {
status(50, "Can't scan %s", path);
} else {
status(20, "text/gemini");
bs += fprintf(stdout, "=> .. ../\n"); /* display link to parent */
for (int j = 0; j < n; j++) {
/* skip self and parent */
if ((strcmp(namelist[j]->d_name, ".") == 0) ||
(strcmp(namelist[j]->d_name, "..") == 0)) {
continue;
}
/* add "/" at the end of a directory path */
if (namelist[j]->d_type == DT_DIR) {
bs += fprintf(stdout, "=> ./%s/ %s/\n",
namelist[j]->d_name, namelist[j]->d_name);
} else {
bs += fprintf(stdout, "=> ./%s %s\n",
namelist[j]->d_name, namelist[j]->d_name);
}
free(namelist[j]);
}
free(namelist);
}
return bs;
}
void
cgi(const char *cgicmd)
{
int fildes[2] = {0};
int retcode = 0;
pid_t pid = 0;
FILE *output = NULL;
if (pipe(fildes) != 0)
goto cgierr;
if ((pid = fork()) < 0)
goto cgierr;
if (pid > 0) { /* parent */
close(fildes[1]); /* make sure entry is closed to get EOF */
if ((output = fdopen(fildes[0], "r")) == NULL)
goto cgierr;
_datasiz += print_file(output);
close(fildes[0]);
fclose(output);
waitpid(pid, &retcode, 0);
stop(EXIT_SUCCESS, "cgi ran with exit code %d", status);
} else { /* child */
/* set pipe output equal to stdout & stderr */
dup2(fildes[1], STDOUT_FILENO);
close(fildes[1]); /* no longer required */
execl(cgicmd, cgicmd, NULL);
}
cgierr:
/* if execl is ok, this will never be reached */
close(fildes[0]);
close(fildes[1]);
status(42, "error when trying run cgi");
stop(EXIT_FAILURE, "error when trying to execl %s", cgicmd);
}
char *
read_request(char *request)
{
/* read 1024 +1 chars from stdin to get the request (1024 + \0) */
if (fgets(request, GEMINI_REQUEST_MAX, stdin) == NULL) {
/* EOF reached before reading anything */
if (feof(stdin)) {
status(59, "%s", "request is too short and probably empty");
stop(EXIT_FAILURE, "%s", "request is too short and probably empty");
/* error before reading anything */
} else if (ferror(stdin)) {
status(59, "Error while reading request: %s", request);
stop(EXIT_FAILURE, "Error while reading request: %s", request);
}
}
/* check if string ends with '\n', or to long */
if (request[strnlen(request, GEMINI_REQUEST_MAX) - 1] != '\n') {
status(59, "request is too long (1024 max): %s", request);
stop(EXIT_FAILURE, "request is too long (1024 max): %s", request);
}
/* remove \r\n at the end of string */
request[strcspn(request, "\r\n")] = '\0';
/* save request for logs */
esnprintf(_request, sizeof(_request), "%s", request);
return request;
}
void
remove_double_dot(char *request)
{
char *pos = NULL;
/* remove all "/.." for safety reasons */
while ((pos = strstr(request, "/..")) != NULL)
memmove(request, pos + 3, strlen(pos) + 1 - 3); /* "/.." = 3 */
}
void
check_path(char *path, size_t pathsiz)
{
struct stat sb = {0};
char tmp[PATH_MAX] = {'\0'};
if (stat(path, &sb) == -1) {
if (lstat(path, &sb) != -1 && S_ISLNK(sb.st_mode) == 1) {
if (readlink(path, tmp, sizeof(tmp)) > 0) {
status(30, "%s", tmp);
stop(EXIT_SUCCESS, NULL);
}
}
status(51, "%s", "file not found");
stop(EXIT_SUCCESS, NULL);
}
if (S_ISDIR(sb.st_mode)) {
/* check if dir path end with "/" */
if (path[strlen(path) - 1] != '/') {
esnprintf(tmp, sizeof(tmp), "/%s/", path);
status(31, "%s", tmp);
stop(EXIT_SUCCESS, NULL);
}
/* check if DEFAULT_INDEX exists in directory */
esnprintf(tmp, sizeof(tmp), "%s/%s", path, DEFAULT_INDEX);
if (stat(tmp, &sb) == 0)
esnprintf(path, pathsiz, "%s", tmp);
}
}
void
split_request(const char *request, char *hostname, char *path, char *query)
{
size_t nmatch = SE_MAX; /* 3 "()" + 1 for whole match */
char buf[BUFSIZ] = {'\0'}; /* to handle error messages */
int ret = 0;
regex_t greg; /* compiled gemini regex */
regmatch_t match[SE_MAX]; /* matches founds */
ret = regcomp(&greg, _gemini_regex, REG_EXTENDED);
if (ret != 0) {
regerror(ret, &greg, buf, sizeof(buf));
regfree(&greg);
status(50, "Internal server error");
stop(EXIT_FAILURE, "%s", buf);
}
ret = regexec(&greg, request, nmatch, match, 0);
if (ret != 0) {
regerror(ret, &greg, buf, sizeof(buf));
regfree(&greg);
status(59, "Malformed request");
stop(EXIT_FAILURE, "Malformed request, error:%s", buf);
}
/* one may want to check the return of getsubexp
* and change memcpy to strlcpy
* to make sure we didn't try to copy too long
* and that string isn't trunkated.
* It is unlikely to happen since dest string are as long as request
*/
getsubexp(request, match[1], hostname);
getsubexp(request, match[2], path);
getsubexp(request, match[3], query);
regfree(&greg);
}

57
vger.h Normal file
View File

@ -0,0 +1,57 @@
#ifndef vger_h_INCLUDED
#define vger_h_INCLUDED
/* length of "gemini://" */
#define GEMINI_PART 9
/*
* number of bytes to read with fgets() : 2014 + 1.
* fgets() reads at most size-1 (1024 here).
* See https://gemini.circumlunar.space/docs/specification.html.
*/
#define GEMINI_REQUEST_MAX 1025
/* max subexpression in regex : 3 + 1 */
#define SE_MAX 4
/* gemini_regex:
* =============
* ^gemini://+ : in case of gemini:///
* 1: hostname
* ([^/|^\?|^:]*) :
* catch everything, stop when /, ? or : is found
* don't catch :port
* [:[0-9]*]? : skip :1234 (port number) if any
* / * : skip "/" if any
* 2: path
* ([^\?]*) :
* catch everything and stop at ? if any
* 3 : query
* [\?]?(.*)$:
* catch everything after ? if any
*/
static const char *_gemini_regex =
"^gemini://+([^/|^\?|^:]*)[:[0-9]*]?/*([^\?]*)[\?]?(.*)$";
/* global vars */
static int _retcode = 0;
static ssize_t _datasiz = 0;
static char _request[GEMINI_REQUEST_MAX] = {'\0'};
/* functions */
ssize_t autoindex(const char *);
void cgi(const char *);
char * read_request(char *);
void check_path(char *, size_t);
ssize_t display_file(const char *);
int do_cgi(const char *, const char *, const char *, const char *);
void drop_privileges(const char *);
void set_rootdir(const char *, const char *, const char *);
void remove_double_dot(char *);
void split_request(const char *, char *, char *, char *);
void status(const int, const char *, ...);
void stop(const int, const char *, ...);
int uridecode (char *);
#endif // vger_h_INCLUDED