Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

29 changed files with 331 additions and 1049 deletions

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -1,8 +1,12 @@
# A simplistic and secure Gemini server
**Vger** is a gemini server supporting chroot, virtualhosts, CGI, default language choice, redirections and MIME types detection.
**Vger** is a gemini server supporting chroot, virtualhosts, default
language choice 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:
@ -15,36 +19,30 @@ 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. More explanations about Vger security can be found [on this link](https://dataswamp.org/~solene/2021-01-14-vger-security.html).
**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.
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
sudo 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
@ -52,50 +50,39 @@ Some files under `/var/gemini/` are required to test the code path without a `-d
**Vger** has a few parameters you can use in inetd configuration.
- `-d PATH`: use `PATH` as the data directory to serve files from. Default is `/var/gemini`
- `-l LANG`: change the language in the status return code. Default is no language specified.
- `-l LANG`: change the language in the status return code. Default is `en`
- `-v`: enable virtualhost support, the hostname in the query will be considered as a directory name.
- `-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 an user `gemini_user`.
Create directory `/var/gemini/` (I'd allow this to be configured
later), files will be served from there.
Add this line to inetd.conf:
```
127.0.0.1:11965 stream tcp nowait gemini_user /usr/local/bin/vger vger
11965 stream tcp nowait gemini_user /usr/local/bin/vger vger
```
Add this to relayd.conf
```
log connection
tcp protocol "gemini" {
tls keypair hostname.example
}
relay "gemini" {
listen on hostname.example port 1965 tls
protocol "gemini"
forward to 127.0.0.1 port 11965
}
```
Make sure certificates files match hostname:
`/etc/ssl/private/hostname.example.key` and
`/etc/ssl/hostname.example.crt`.
Make links to the certificates and key files according to relayd.conf documentation
```
# ln -s /etc/ssl/acme/cert.pem /etc/ssl/hostname.example\:1965.crt
# ln -s /etc/ssl/acme/private/privkey.pem /etc/ssl/private/hostname.example\:1965.key
```
On OpenBSD, enable inetd and relayd and start them:
```
# rcctl enable relayd inetd
# rcctl start relayd inetd
```
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.

View File

@ -1 +0,0 @@
EXTRAFLAGS=

14
configure vendored
View File

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

308
main.c
View File

@ -1,97 +1,265 @@
#include "vger.c"
#include <sys/stat.h>
#include <err.h>
#include <errno.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"
#define BUFF_LEN_2 1025
#define BUFF_LEN_3 1024
#define GEMINI_PART 9
#define DEFAULT_LANG "en"
#define DEFAULT_CHROOT "/var/gemini/"
void display_file(const char *, const char *);
void status (const int, const char *, const char *);
void drop_privileges(const char *, const char *);
void
drop_privileges(const char *user, const char *path)
{
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
*/
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");
}
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");
}
path = "/";
}
#ifdef __OpenBSD__
/*
* prevent access to files other than the one in path
*/
if (unveil(path, "r") == -1) {
syslog(LOG_DAEMON, "unveil on %s failed", path);
err(1, "unveil");
}
/*
* 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, const char *lang)
{
printf("%i %s; lang=%s\r\n",
code, file_mime, lang);
}
void
display_file(const char *path, const char *lang)
{
FILE *fd;
struct stat sb;
ssize_t nread;
char *buffer[BUFSIZ];
char extension[10];
const char *file_mime;
/* this is to check if path is a directory */
if (stat(path, &sb) == -1)
goto err;
/* open the file requested */
if ((fd = fopen(path, "r")) == NULL)
goto err;
/* check if directory */
if (S_ISDIR(sb.st_mode) == 1)
goto err;
file_mime = get_file_mime(path);
status(20, file_mime, lang);
/* read the file and write it to stdout */
while ((nread = fread(buffer, sizeof(char), sizeof(buffer), fd)) != 0)
fwrite(buffer, sizeof(char), nread, stdout);
fclose(fd);
syslog(LOG_DAEMON, "path served %s", path);
return;
err:
/* return an error code and no content */
status(40, "text/gemini", lang);
syslog(LOG_DAEMON, "path invalid %s", path);
}
int
main(int argc, char **argv)
{
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;
char buffer [BUFF_LEN_2];
char request [BUFF_LEN_2];
char hostname [BUFF_LEN_2];
char file [BUFF_LEN_2];
char path [BUFF_LEN_2] = DEFAULT_CHROOT;
char lang [3] = DEFAULT_LANG;
char user [_SC_LOGIN_NAME_MAX] = "";
int virtualhost = 0;
int option;
int chroot = 0;
int start_with_gemini;
char *pos;
/*
* 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) {
while ((option = getopt(argc, argv, ":d:l:u:v")) != -1) {
switch (option) {
case 'd':
esnprintf(chroot_dir, sizeof(chroot_dir), "%s", optarg);
break;
case 'l':
esnprintf(lang, sizeof(lang), "lang=%s", optarg);
break;
case 'm':
esnprintf(default_mime, sizeof(default_mime), "%s", optarg);
break;
case 'u':
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
strlcpy(path, optarg, sizeof(path));
break;
case 'v':
virtualhost = 1;
break;
case 'i':
doautoidx = 1;
case 'l':
strlcpy(lang, optarg, sizeof(lang));
break;
case 'u':
strlcpy(user, optarg, sizeof(user));
chroot = 1;
break;
}
}
read_request(request);
split_request(request, hostname, path, query);
/*
* do chroot if an user is supplied run pledge/unveil if OpenBSD
*/
drop_privileges(user, path);
/* do chroot if an user is supplied */
if (*user)
drop_privileges(user);
/* change basedir to / to build the filepath if we use chroot */
if (chroot == 1)
strlcpy(path, "/", sizeof(path));
/* set actual chroot_dir */
if (virtualhost) {
esnprintf(tmp, sizeof(tmp), "%s/%s", chroot_dir, hostname);
esnprintf(chroot_dir, sizeof(chroot_dir), "%s", tmp);
/*
* read 1024 chars from stdin
* to get the request
*/
fgets(request, BUFF_LEN_3, stdin);
/* 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://
*/
start_with_gemini = strncmp(request, "gemini://", 9);
/* the request must start with gemini:// */
if (start_with_gemini != 0) {
/* error code url malformed */
syslog(LOG_DAEMON, "request «%s» doesn't match gemini:// at index %i",
request, start_with_gemini);
exit(1);
}
syslog(LOG_DAEMON, "request %s", request);
/* cgi_dir is in chroot_dir */
if (*rel_cgi_dir)
esnprintf(cgi_dir, sizeof(cgi_dir),
"%s/%s", chroot_dir, rel_cgi_dir);
/* remove the gemini:// part */
strlcpy(buffer, request + GEMINI_PART, sizeof(buffer) - GEMINI_PART);
strlcpy(request, buffer, sizeof(request));
set_rootdir(chroot_dir, cgi_dir, user);
/*
* look for the first / after the hostname
* in order to split hostname and uri
*/
pos = strchr(request, '/');
if (strlen(path) == 0) { /* this is root dir */
esnprintf(path, sizeof(path), "./");
if (pos != NULL) {
/* if there is a / found */
int position = -1;
for (int i = 0; i < sizeof(request); i++) {
if (*pos == request[i]) {
position = i;
break;
}
}
/* separate hostname and uri */
if (position != -1) {
strlcpy(hostname, request, position + 1);
strlcpy(file, request + position + 1, sizeof(request));
/*
* use a default file if no file are requested this
* can happen in two cases gemini://hostname/
* gemini://hostname/directory/
*/
if (strlen(file) == 0)
strlcpy(file, "/index.gmi", 11);
if (file[strlen(file) - 1] == '/')
strlcat(file, "index.gmi", sizeof(file));
} else {
syslog(LOG_DAEMON, "unknown situation after parsing query");
exit(2);
}
} else {
uridecode(path);
remove_double_dot(path);
/*
* there are no slash / in the request
* -2 to remove \r\n
*/
strlcpy(hostname, request, sizeof(hostname));
strlcpy(file, "/index.gmi", 11);
}
uridecode(query);
/*
* if virtualhost feature is actived looking under the default path +
* hostname directory gemini://foobar/hello will look for
* path/foobar/hello
*/
if (virtualhost) {
strlcat(path, hostname, sizeof(path));
strlcat(path, "/", sizeof(path));
}
/* add the base dir to the file requested */
strlcat(path, file, sizeof(path));
/* 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 *** */
/* open file and send it to stdout */
display_file(path, lang);
/* check if path available */
check_path(path, sizeof(path));
/* regular file to stdout */
display_file(path);
stop(EXIT_SUCCESS, NULL);
return (0);
}

29
mimes.c
View File

@ -3,15 +3,11 @@
#include <string.h>
#include "mimes.h"
#include "opts.h"
/* extension to mimetype table */
static const struct {
const char *extension;
const char *type;
} database[] = {
{"gmi", "text/gemini"},
{"gemini", "text/gemini"},
{"7z", "application/x-7z-compressed"},
{"atom", "application/atom+xml"},
{"avi", "video/x-msvideo"},
@ -28,7 +24,9 @@ static const struct {
{"exe", "application/octet-stream"},
{"flv", "video/x-flv"},
{"fs", "application/octet-stream"},
{"gemini", "text/gemini"},
{"gif", "image/gif"},
{"gmi", "text/gemini"},
{"hqx", "application/mac-binhex40"},
{"htc", "text/x-component"},
{"html", "text/html"},
@ -120,20 +118,21 @@ static const struct {
#endif
const char *
get_file_mime(const char *path, const char *default_mime)
get_file_mime(const char *path)
{
size_t i;
int i;
char *extension;
/* 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);
}
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);
}
/* no MIME found, set a default one */
return default_mime;
out:
/* if no MIME have been found, set a default one */
return ("text/gemini");
}

View File

@ -1 +1 @@
const char *get_file_mime(const char *, const char *);
const char *get_file_mime(const char *);

16
opts.h
View File

@ -1,16 +0,0 @@
#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
/*
* 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;

View File

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

50
tests.c
View File

@ -1,50 +0,0 @@
#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);
}

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

@ -4,122 +4,77 @@ set -x
# md5 is BSD md5 binary
# Linux uses md5sum
which md5 && MD5CMD="md5" || MD5CMD="md5sum"
MD5()
{
$MD5CMD | awk '{print $1}'
}
MD5=md5
type md5 2>/dev/null
if [ $? -ne 0 ]; then
MD5=md5sum
fi
# serving a file
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
OUT=$(printf "gemini://host.name/main.gmi\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5)
if ! [ $OUT = "d11e0c0ff074f5627f2d2af72fd07104" ] ; 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)
if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1 ; fi
OUT=$(printf "gemini://host.name\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5)
if ! [ $OUT = "3edd48286850d386592403956aec770f" ] ; 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)
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)
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)
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
OUT=$(printf "gemini://host.name/\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | $MD5)
if ! [ $OUT = "3edd48286850d386592403956aec770f" ] ; 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 = "09c82ffe243ce3b3cfb04c2bc4a91acb" ] ; 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 = "e663f17730d5ddc24010c14a238e1e78" ] ; 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 = "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 = "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)
if ! [ $OUT = "cb4597b6fcc82cbc366ac9002fb60dac" ] ; then echo "error" ; exit 1 ; fi
# file from local directory with lang=fr and unknwon MIME type (default to text/gemini)
OUT=$(printf "gemini://perso.pw/foobar.unknown\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | $MD5)
if ! [ $OUT = "649a2e224632b679fd7599eafb13c001" ] ; 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)
if ! [ $OUT = "5e5fca557e79f4521b21d4b81dc964c6" ] ; then echo "error" ; exit 1 ; fi
OUT=$(printf "gemini://perso.pw/index.gmi\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | $MD5)
if ! [ $OUT = "0d36a423a4e8be813fda4022f08b3844" ] ; 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)
if ! [ $OUT = "5e5fca557e79f4521b21d4b81dc964c6" ] ; then echo "error" ; exit 1 ; fi
OUT=$(printf "gemini://perso.pw\r\n" | ../vger -v -d var/gemini/ | tee /dev/stderr | $MD5)
if ! [ $OUT = "0d36a423a4e8be813fda4022f08b3844" ] ; 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)
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)
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)
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 = "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 = "f78c481e1614f1713e077b89aba5ab94" ] ; then echo "error" ; exit 1 ; fi
fi
echo "SUCCESS"

View File

@ -1,9 +0,0 @@
#!/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

@ -1,10 +0,0 @@
#!/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

@ -1 +0,0 @@
gemini://perso.pw/new_location

View File

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

View File

@ -1,9 +0,0 @@
#!/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

@ -1 +0,0 @@
hello

View File

@ -1 +0,0 @@
With accents

View File

@ -1 +0,0 @@
love in a boat!

124
utils.c
View File

@ -1,124 +0,0 @@
#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';
}
}

View File

@ -1,8 +0,0 @@
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 *, ...);

42
vger.8
View File

@ -8,11 +8,8 @@
.Nm vger
.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
.Sh DESCRIPTION
.Nm
is a secure gemini server that is meant to be run on
@ -23,45 +20,17 @@ behind a relay daemon offering TLS capabilities like
If an incoming gemini query doesn't explicitly request a file,
.Nm
will serves a default "index.gmi" file if present.
.Pp
It is possible to create redirections by creating a symbolic link
containing the new file location.
.Sh OPTIONS
.Bl -tag -width Ds
.It Op Fl l Ar lang
Set the lang in the return code to
.Ar lang .
A list can be specified, i.e "-l en,fr" will send "lang=en,fr".
Default is no lang metadata.
.It Op Fl i
Enable auto index if no index.gmi is found in a directory.
The index is a file that will contain a link to every file within the current directory.
Set the default lang in the return code to
.Ar lang
instead of "en".
.It Op Fl v
Enable virtualhost support, the hostname in the query will be considered as a directory name.
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
instead of the "application/octet-stream" as content type for which
.Nm
is unable to find the type.
.It Op Fl d Ar path
Use
.Ar path
@ -71,7 +40,7 @@ On
.Nm
will use
.Xr unveil 2
on this path to only allow read-only file access within this directory.
on this path in read-only to prevent file access outside this directory.
.It Op Fl u Ar username
Enable
.Xr chroot 2
@ -90,7 +59,7 @@ can be used on any port because it won't be public.
.Pp
/etc/inetd.conf example using a dedicated gemini_user:
.Bd -literal -offset indent
127.0.0.1:11965 stream tcp nowait gemini_user /usr/local/bin/vger vger
11965 stream tcp nowait gemini_user /usr/local/bin/vger vger
.Ed
.Pp
The public port TCP/1965 must be served by a daemon like
@ -99,7 +68,6 @@ which negociate TLS connections and forward them
to the inetd daemon on the
.Nm
port.
Do not forget to open the TCP/1965 port in your firewall.
.Pp
.Xr relayd.conf 5
configuration example:

468
vger.c
View File

@ -1,468 +0,0 @@
#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
View File

@ -1,57 +0,0 @@
#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