Compare commits
44 commits
Author | SHA1 | Date | |
---|---|---|---|
Mike Barnes | 5a9b2518be | ||
Mike Barnes | 47e075383f | ||
01f2503376 | |||
ed6dc1ed12 | |||
0d3d453498 | |||
914a143c3f | |||
a319de23f7 | |||
15d09d2c01 | |||
843d1f0ab7 | |||
f8aff7fe05 | |||
4972df5999 | |||
365e99400a | |||
807d1b8409 | |||
02d2d1dc7d | |||
de52acecfc | |||
ee8569c6e6 | |||
16a5ed7b30 | |||
8454548b51 | |||
cbcf4ec9b6 | |||
7431d3eeec | |||
e9be1b73a7 | |||
e87b36c991 | |||
e3b5fb2ab3 | |||
9525d66afb | |||
d086262d1a | |||
7b0686bdfa | |||
fbacb35170 | |||
efa1f639fc | |||
458592594e | |||
31d384833f | |||
ff78ea5049 | |||
349e56c28c | |||
2191a8a18b | |||
84120dca09 | |||
43170e6804 | |||
f0dbd2c9ed | |||
55042768e5 | |||
189803ab52 | |||
de7cd12f9f | |||
3510035711 | |||
2cc63136f8 | |||
495fa3213f | |||
f6bc000adc | |||
470e47a018 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*.o
|
||||||
|
vger
|
2
LICENSE
2
LICENSE
|
@ -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
|
BSD 2-clause License
|
||||||
|
|
||||||
|
|
18
Makefile
18
Makefile
|
@ -1,14 +1,24 @@
|
||||||
|
include config.mk
|
||||||
|
|
||||||
PREFIX?=/usr/local/
|
PREFIX?=/usr/local/
|
||||||
CFLAGS += -pedantic -Wall -Wextra -Wmissing-prototypes \
|
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
|
all: vger
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f vger *.core *.o
|
find . \( -name vger -o \
|
||||||
|
-name "*.o" -o \
|
||||||
|
-name "*.core" \) \
|
||||||
|
-delete
|
||||||
|
|
||||||
vger: main.o mimes.o opts.h
|
vger: main.o mimes.o utils.o opts.h
|
||||||
${CC} -o vger main.o mimes.o
|
${CC} ${CFLAGS} -o $@ main.o mimes.o utils.o
|
||||||
|
|
||||||
install: vger
|
install: vger
|
||||||
install -o root -g wheel vger ${PREFIX}/bin/
|
install -o root -g wheel vger ${PREFIX}/bin/
|
||||||
|
|
31
README.md
31
README.md
|
@ -1,12 +1,8 @@
|
||||||
# A simplistic and secure Gemini server
|
# A simplistic and secure Gemini server
|
||||||
|
|
||||||
**Vger** is a gemini server supporting chroot, virtualhosts, default
|
**Vger** is a gemini server supporting chroot, virtualhosts, CGI, default language choice, redirections and MIME types detection.
|
||||||
language choice, redirections and MIME types detection.
|
|
||||||
|
|
||||||
**Vger** design is relying on inetd and a daemon to take care of
|
**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.
|
||||||
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:
|
The average setup should look like:
|
||||||
|
|
||||||
|
@ -19,13 +15,9 @@ The average setup should look like:
|
||||||
vger on inetd
|
vger on inetd
|
||||||
```
|
```
|
||||||
|
|
||||||
**Vger** is perfectly secure if run on **OpenBSD**, using `unveil()`
|
**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).
|
||||||
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
|
For all supported OS, it's possible to run **Vger** in a chroot and drop privileges to a dedicated user.
|
||||||
and drop privileges to a dedicated user.
|
|
||||||
|
|
||||||
|
|
||||||
# Install
|
# Install
|
||||||
|
@ -33,16 +25,18 @@ and drop privileges to a dedicated user.
|
||||||
```
|
```
|
||||||
git clone https://tildegit.org/solene/vger.git
|
git clone https://tildegit.org/solene/vger.git
|
||||||
cd vger
|
cd vger
|
||||||
|
./configure (only really useful for Linux)
|
||||||
make
|
make
|
||||||
doas make install
|
doas make install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On GNU/Linux, make sure you installed `libbsd`, it has been reported that using clang was required too.
|
||||||
|
|
||||||
# Running tests
|
# Running tests
|
||||||
|
|
||||||
**Vger** comes with a test suite you can use with `make test`.
|
**Vger** comes with a test suite you can use with `make test`.
|
||||||
|
|
||||||
Some files under `/var/gemini/` are required to test the code path
|
Some files under `/var/gemini/` are required to test the code path without a `-d` parameter.
|
||||||
without a `-d` parameter.
|
|
||||||
|
|
||||||
|
|
||||||
# Command line parameters
|
# Command line parameters
|
||||||
|
@ -55,13 +49,12 @@ without a `-d` parameter.
|
||||||
- `-u username`: enable chroot to the data directory and drop privileges to `username`.
|
- `-u username`: enable chroot to the data directory and drop privileges to `username`.
|
||||||
- `-m MIME` : use MIME as default instead of "application/octet-stream".
|
- `-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.
|
- `-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
|
# How to configure Vger using relayd and inetd
|
||||||
|
|
||||||
Create directory `/var/gemini/` (I'd allow this to be configured
|
Create directory `/var/gemini/` (I'd allow this to be configured later), files will be served from there.
|
||||||
later), files will be served from there.
|
|
||||||
|
|
||||||
Create an user `gemini_user`.
|
Create an user `gemini_user`.
|
||||||
|
|
||||||
|
@ -97,6 +90,4 @@ On OpenBSD, enable inetd and relayd and start them:
|
||||||
|
|
||||||
Don't forget to open the TCP port 1965 in your firewall.
|
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.
|
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.
|
||||||
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.
|
|
||||||
|
|
14
configure
vendored
Executable file
14
configure
vendored
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
OS="$(uname -s)"
|
||||||
|
|
||||||
|
case "$OS" in
|
||||||
|
Linux)
|
||||||
|
EXTRAFLAGS=-lbsd
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
EXTRAFLAGS=""
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "EXTRAFLAGS=${EXTRAFLAGS}" > config.mk
|
531
main.c
531
main.c
|
@ -1,9 +1,12 @@
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
#include <ctype.h>
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <err.h>
|
#include <err.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <pwd.h>
|
#include <pwd.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
|
@ -15,58 +18,87 @@
|
||||||
|
|
||||||
#include "mimes.h"
|
#include "mimes.h"
|
||||||
#include "opts.h"
|
#include "opts.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
/* length of "gemini://" */
|
||||||
#define GEMINI_PART 9
|
#define GEMINI_PART 9
|
||||||
#define GEMINI_REQUEST_MAX 1024 /* see https://gemini.circumlunar.space/docs/specification.html */
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
void autoindex(const char *);
|
||||||
void autoindex(const char *);
|
void cgi (const char *cgicmd);
|
||||||
void display_file(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 drop_privileges(const char *, const char *);
|
||||||
void eunveil(const char *, const char *);
|
void echdir (const char *);
|
||||||
size_t estrlcat(char *, const char *, size_t);
|
void status (const int, const char *);
|
||||||
size_t estrlcpy(char *, const char *, size_t);
|
void status_redirect(const int, const char *);
|
||||||
|
void status_error(const int, const char *);
|
||||||
|
int uridecode (char *);
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
eunveil(const char *path, const char *permissions)
|
echdir(const char *path)
|
||||||
{
|
{
|
||||||
if (unveil(path, permissions) == -1) {
|
if (chdir(path) == -1) {
|
||||||
syslog(LOG_DAEMON, "unveil on %s failed", path);
|
switch (errno) {
|
||||||
err(1, "unveil");
|
case ENOTDIR: /* FALLTHROUGH */
|
||||||
|
case ENOENT:
|
||||||
|
status_error(51, "file not found");
|
||||||
|
break;
|
||||||
|
case EACCES:
|
||||||
|
status_error(50, "Forbidden path");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
status_error(50, "Internal server error");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
errlog("failed to chdir(%s)", path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t
|
int
|
||||||
estrlcpy(char *dst, const char *src, size_t dstsize)
|
uridecode(char *uri)
|
||||||
{
|
{
|
||||||
size_t n = 0;
|
int n = 0;
|
||||||
|
char c = '\0';
|
||||||
|
long l = 0;
|
||||||
|
char *pos = NULL;
|
||||||
|
|
||||||
n = strlcpy(dst, src, dstsize);
|
if ((pos = strchr(uri, '%')) == NULL)
|
||||||
if (n >= dstsize) {
|
return n;
|
||||||
err(1, "strlcyp failed for %s = %s", dst, src);
|
|
||||||
|
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;
|
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
|
void
|
||||||
drop_privileges(const char *user, const char *path)
|
drop_privileges(const char *user, const char *path)
|
||||||
{
|
{
|
||||||
struct passwd *pw;
|
struct passwd *pw;
|
||||||
int chrooted = 0;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* use chroot() if an user is specified requires root user to be
|
* use chroot() if an user is specified requires root user to be
|
||||||
|
@ -75,59 +107,68 @@ drop_privileges(const char *user, const char *path)
|
||||||
if (strlen(user) > 0) {
|
if (strlen(user) > 0) {
|
||||||
|
|
||||||
/* is root? */
|
/* is root? */
|
||||||
if (getuid() != 0) {
|
if (getuid() != 0)
|
||||||
syslog(LOG_DAEMON, "chroot requires program to be run as root");
|
errlog("chroot requires program to be run as root");
|
||||||
errx(1, "chroot requires root user");
|
|
||||||
}
|
|
||||||
/* search user uid from name */
|
/* search user uid from name */
|
||||||
if ((pw = getpwnam(user)) == NULL) {
|
if ((pw = getpwnam(user)) == NULL)
|
||||||
syslog(LOG_DAEMON, "the user %s can't be found on the system", user);
|
errlog("the user %s can't be found on the system", user);
|
||||||
err(1, "finding user");
|
|
||||||
}
|
|
||||||
/* chroot worked? */
|
/* chroot worked? */
|
||||||
if (chroot(path) != 0) {
|
if (chroot(path) != 0)
|
||||||
syslog(LOG_DAEMON, "the chroot_dir %s can't be used for chroot", path);
|
errlog("the chroot_dir %s can't be used for chroot", path);
|
||||||
err(1, "chroot");
|
|
||||||
}
|
|
||||||
chrooted = 1;
|
chrooted = 1;
|
||||||
if (chdir("/") == -1) {
|
echdir("/");
|
||||||
syslog(LOG_DAEMON, "failed to chdir(\"/\")");
|
|
||||||
err(1, "chdir");
|
|
||||||
}
|
|
||||||
/* drop privileges */
|
/* drop privileges */
|
||||||
|
#if defined (__OpenBSD__) || defined(__FreeBSD__)
|
||||||
if (setgroups(1, &pw->pw_gid) ||
|
if (setgroups(1, &pw->pw_gid) ||
|
||||||
setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
|
setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
|
||||||
setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) {
|
setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) {
|
||||||
syslog(LOG_DAEMON, "dropping privileges to user %s (uid=%i) failed",
|
errlog("dropping privileges to user %s (uid=%i) failed",
|
||||||
user, pw->pw_uid);
|
user, pw->pw_uid);
|
||||||
err(1, "Can't drop privileges");
|
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
if (setgroups(1, &pw->pw_gid) ||
|
||||||
|
setgid(pw->pw_gid) ||
|
||||||
|
setuid(pw->pw_uid)) {
|
||||||
|
errlog("dropping privileges to user %s (uid=%i) failed",
|
||||||
|
user, pw->pw_uid);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#ifdef __OpenBSD__
|
#ifdef __OpenBSD__
|
||||||
/*
|
/*
|
||||||
* prevent access to files other than the one in path
|
* prevent access to files other than the one in path
|
||||||
*/
|
*/
|
||||||
if (chrooted) {
|
if (chrooted)
|
||||||
eunveil("/", "r");
|
eunveil("/", "r");
|
||||||
} else {
|
else
|
||||||
eunveil(path, "r");
|
eunveil(path, "r");
|
||||||
}
|
|
||||||
/*
|
/* permission to execute what's inside cgidir */
|
||||||
* prevent system calls other parsing queryfor fread file and
|
if (strlen(cgidir) > 0)
|
||||||
* write to stdio
|
eunveil(cgidir, "rx");
|
||||||
*/
|
|
||||||
if (pledge("stdio rpath", NULL) == -1) {
|
eunveil(NULL, NULL); /* no more call to unveil() */
|
||||||
syslog(LOG_DAEMON, "pledge call failed");
|
|
||||||
err(1, "pledge");
|
/* promise permissions */
|
||||||
}
|
if (strlen(cgidir) > 0)
|
||||||
|
epledge("stdio rpath exec", NULL);
|
||||||
|
else
|
||||||
|
epledge("stdio rpath", NULL);
|
||||||
#endif
|
#endif
|
||||||
|
if (!chrooted)
|
||||||
|
echdir(path); /* move to the gemini data directory */
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
status(const int code, const char *file_mime)
|
status(const int code, const char *file_mime)
|
||||||
{
|
{
|
||||||
printf("%i %s; %s\r\n",
|
if (strcmp(file_mime, "text/gemini") == 0)
|
||||||
code, file_mime, lang);
|
printf("%i %s; %s\r\n", code, file_mime, lang);
|
||||||
|
else
|
||||||
|
printf("%i %s\r\n", code, file_mime);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -138,137 +179,166 @@ status_redirect(const int code, const char *url)
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
display_file(const char *uri)
|
status_error(const int code, const char *reason)
|
||||||
{
|
{
|
||||||
FILE *fd = NULL;
|
printf("%i %s\r\n",
|
||||||
struct stat sb = {0};
|
code, reason);
|
||||||
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 */
|
void
|
||||||
estrlcpy(fp, chroot_dir, sizeof(fp));
|
display_file(const char *fname)
|
||||||
estrlcat(fp, uri, sizeof(fp));
|
{
|
||||||
|
FILE *fd = NULL;
|
||||||
|
struct stat sb = {0};
|
||||||
|
ssize_t nread = 0;
|
||||||
|
const char *file_mime;
|
||||||
|
char *buffer[BUFSIZ];
|
||||||
|
char target[FILENAME_MAX] = {'\0'};
|
||||||
|
char tmp[PATH_MAX] = {'\0'}; /* used to build
|
||||||
|
* temporary path */
|
||||||
|
|
||||||
/* this is to check if path exists and obtain metadata later */
|
/*
|
||||||
if (stat(fp, &sb) == -1) {
|
* special case : fname empty. The user requested just the directory
|
||||||
|
* name
|
||||||
/* check if fp is a symbolic link
|
*/
|
||||||
* if so, redirect using its target */
|
if (strlen(fname) == 0) {
|
||||||
if (lstat(fp, &sb) != -1 && S_ISLNK(sb.st_mode) == 1)
|
if (stat("index.gmi", &sb) == 0) {
|
||||||
goto redirect;
|
/* there is index.gmi in the current directory */
|
||||||
else
|
display_file("index.gmi");
|
||||||
goto err;
|
return;
|
||||||
}
|
} else if (doautoidx) {
|
||||||
|
/* no index.gmi, so display autoindex if enabled */
|
||||||
/* check if directory */
|
autoindex(".");
|
||||||
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;
|
return;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
/* there is a leading "/", display index.gmi */
|
goto err;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* this is to check if path exists and obtain metadata later */
|
||||||
|
if (stat(fname, &sb) == -1) {
|
||||||
|
/*
|
||||||
|
* check if fname is a symbolic link if so, redirect using
|
||||||
|
* its target
|
||||||
|
*/
|
||||||
|
if (lstat(fname, &sb) != -1 && S_ISLNK(sb.st_mode) == 1)
|
||||||
|
goto redirect;
|
||||||
|
else
|
||||||
|
goto err;
|
||||||
|
}
|
||||||
|
/* check if directory */
|
||||||
|
if (S_ISDIR(sb.st_mode) != 0) {
|
||||||
|
/* no ending "/", redirect to "fname/" */
|
||||||
|
estrlcpy(tmp, fname, sizeof(tmp));
|
||||||
|
estrlcat(tmp, "/", sizeof(tmp));
|
||||||
|
status_redirect(31, tmp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
/* open the file requested */
|
/* open the file requested */
|
||||||
if ((fd = fopen(fp, "r")) == NULL) { goto err; }
|
if ((fd = fopen(fname, "r")) == NULL)
|
||||||
|
goto err;
|
||||||
|
|
||||||
file_mime = get_file_mime(fp, default_mime);
|
file_mime = get_file_mime(fname, default_mime);
|
||||||
|
|
||||||
status(20, file_mime);
|
status(20, file_mime);
|
||||||
|
|
||||||
/* read the file and write it to stdout */
|
/* read the file byte after byte in buffer and write it to stdout */
|
||||||
while ((nread = fread(buffer, sizeof(char), sizeof(buffer), fd)) != 0)
|
while ((nread = fread(buffer, 1, sizeof(buffer), fd)) != 0)
|
||||||
fwrite(buffer, sizeof(char), nread, stdout);
|
fwrite(buffer, 1, nread, stdout);
|
||||||
goto closefd;
|
goto closefd; /* close file descriptor */
|
||||||
syslog(LOG_DAEMON, "path served %s", fp);
|
syslog(LOG_INFO, "path served %s", fname);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
err:
|
err:
|
||||||
/* return an error code and no content */
|
/* return an error code and no content */
|
||||||
status(51, "text/gemini");
|
status_error(51, "file not found");
|
||||||
syslog(LOG_DAEMON, "path invalid %s", fp);
|
syslog(LOG_INFO, "path invalid %s", fname);
|
||||||
goto closefd;
|
goto closefd;
|
||||||
|
|
||||||
redirect:
|
redirect:
|
||||||
/* read symbolic link target to redirect */
|
/* read symbolic link target to redirect */
|
||||||
if (readlink(fp, target, FILENAME_MAX) == -1) {
|
if (readlink(fname, target, FILENAME_MAX) == -1)
|
||||||
goto err;
|
goto err;
|
||||||
}
|
|
||||||
|
|
||||||
status_redirect(30, target);
|
status_redirect(30, target);
|
||||||
syslog(LOG_DAEMON, "redirection from %s to %s", fp, target);
|
syslog(LOG_INFO, "redirection from %s to %s", fname, target);
|
||||||
|
|
||||||
closefd:
|
closefd:
|
||||||
if (S_ISREG(sb.st_mode) != 0) {
|
if (S_ISREG(sb.st_mode) != 0)
|
||||||
fclose(fd);
|
fclose(fd);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
autoindex(const char *path)
|
autoindex(const char *path)
|
||||||
{
|
{
|
||||||
struct dirent *dp;
|
/* display liks to files in path + a link to parent (..) */
|
||||||
DIR *fd;
|
|
||||||
|
|
||||||
|
int n = 0;
|
||||||
|
struct dirent **namelist; /* this must be freed at last */
|
||||||
|
|
||||||
if (!(fd = opendir(path))) {
|
syslog(LOG_INFO, "autoindex: %s", path);
|
||||||
err(1,"opendir '%s':", path);
|
|
||||||
}
|
|
||||||
|
|
||||||
syslog(LOG_DAEMON, "autoindex: %s", path);
|
/* use alphasort to always have the same order on every system */
|
||||||
|
if ((n = scandir(path, &namelist, NULL, alphasort)) < 0) {
|
||||||
status(20, "text/gemini");
|
status_error(50, "Internal server error");
|
||||||
|
errlog("Can't scan %s", path);
|
||||||
/* TODO : add ending / in name if directory */
|
} else {
|
||||||
while ((dp = readdir(fd))) {
|
status(20, "text/gemini");
|
||||||
/* skip self */
|
printf("=> .. ../\n"); /* display link to parent */
|
||||||
if (!strcmp(dp->d_name, ".")) {
|
for (int j = 0; j < n; j++) {
|
||||||
continue;
|
/* skip self and parent */
|
||||||
}
|
if ((strcmp(namelist[j]->d_name, ".") == 0) ||
|
||||||
if (dp->d_type == DT_DIR) {
|
(strcmp(namelist[j]->d_name, "..") == 0)) {
|
||||||
printf("=> ./%s/ %s/\n", dp->d_name, dp->d_name);
|
continue;
|
||||||
} else {
|
}
|
||||||
printf("=> ./%s %s\n", dp->d_name, dp->d_name);
|
/* add "/" at the end of a directory path */
|
||||||
|
if (namelist[j]->d_type == DT_DIR)
|
||||||
|
printf("=> ./%s/ %s/\n", namelist[j]->d_name, namelist[j]->d_name);
|
||||||
|
else
|
||||||
|
printf("=> ./%s %s\n", namelist[j]->d_name, namelist[j]->d_name);
|
||||||
|
free(namelist[j]);
|
||||||
}
|
}
|
||||||
|
free(namelist);
|
||||||
}
|
}
|
||||||
closedir(fd);
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
cgi(const char *cgicmd)
|
||||||
|
{
|
||||||
|
/* run cgicmd replacing current process */
|
||||||
|
execl(cgicmd, cgicmd, NULL);
|
||||||
|
/* if execl is ok, this will never be reached */
|
||||||
|
status(42, "Couldn't execute CGI script");
|
||||||
|
errlog("error when trying to execl %s", cgicmd);
|
||||||
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
main(int argc, char **argv)
|
main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
char request [GEMINI_REQUEST_MAX] = {'\0'};
|
char request [GEMINI_REQUEST_MAX] = {'\0'};
|
||||||
char hostname [GEMINI_REQUEST_MAX] = {'\0'};
|
|
||||||
char uri [PATH_MAX] = {'\0'};
|
|
||||||
char user [_SC_LOGIN_NAME_MAX] = "";
|
char user [_SC_LOGIN_NAME_MAX] = "";
|
||||||
int virtualhost = 0;
|
char hostname [GEMINI_REQUEST_MAX] = {'\0'};
|
||||||
|
char query [PATH_MAX] = {'\0'};
|
||||||
|
char chroot_dir[PATH_MAX] = DEFAULT_CHROOT;
|
||||||
|
char file [FILENAME_MAX] = DEFAULT_INDEX;
|
||||||
|
char dir [PATH_MAX] = {'\0'};
|
||||||
|
char *pos = NULL;
|
||||||
int option = 0;
|
int option = 0;
|
||||||
char *pos = NULL;
|
int virtualhost = 0;
|
||||||
|
int docgi = 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 : file requested in cgi : gemini://...?query
|
||||||
|
* file : file basename to display. Emtpy is a directory has been requested
|
||||||
|
* dir : directory requested. vger will chdir() in to find file
|
||||||
|
* pos : used to parse request and split into interesting parts
|
||||||
|
*/
|
||||||
|
|
||||||
|
while ((option = getopt(argc, argv, ":d:l:m:u:c:vi")) != -1) {
|
||||||
switch (option) {
|
switch (option) {
|
||||||
case 'd':
|
case 'd':
|
||||||
estrlcpy(chroot_dir, optarg, sizeof(chroot_dir));
|
estrlcpy(chroot_dir, optarg, sizeof(chroot_dir));
|
||||||
|
@ -283,6 +353,10 @@ main(int argc, char **argv)
|
||||||
case 'u':
|
case 'u':
|
||||||
estrlcpy(user, optarg, sizeof(user));
|
estrlcpy(user, optarg, sizeof(user));
|
||||||
break;
|
break;
|
||||||
|
case 'c':
|
||||||
|
estrlcpy(cgidir, optarg, sizeof(cgidir));
|
||||||
|
docgi = 1;
|
||||||
|
break;
|
||||||
case 'v':
|
case 'v':
|
||||||
virtualhost = 1;
|
virtualhost = 1;
|
||||||
break;
|
break;
|
||||||
|
@ -291,22 +365,38 @@ main(int argc, char **argv)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* set logging options and defaults
|
||||||
|
*/
|
||||||
|
openlog("vger", LOG_PID, LOG_DAEMON);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* do chroot if an user is supplied run pledge/unveil if OpenBSD
|
* do chroot if an user is supplied
|
||||||
*/
|
*/
|
||||||
drop_privileges(user, chroot_dir);
|
drop_privileges(user, chroot_dir);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* read 1024 chars from stdin
|
* read 1024 chars from stdin
|
||||||
* to get the request
|
* to get the request
|
||||||
|
* (actually 1024 + \0)
|
||||||
*/
|
*/
|
||||||
if (fgets(request, GEMINI_REQUEST_MAX, stdin) == NULL) {
|
if (fgets(request, GEMINI_REQUEST_MAX, stdin) == NULL) {
|
||||||
status(59, "request is too long (1024 max)");
|
/* EOF reached before reading anything */
|
||||||
syslog(LOG_DAEMON, "request is too long (1024 max): %s", request);
|
if (feof(stdin)) {
|
||||||
exit(1);
|
status(59, "request is too short and probably empty");
|
||||||
}
|
errlog("request is too short and probably empty");
|
||||||
|
|
||||||
|
/* error before reading anything */
|
||||||
|
} else if (ferror(stdin)) {
|
||||||
|
status(59, "Error while reading request");
|
||||||
|
errlog("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)");
|
||||||
|
errlog("request is too long (1024 max): %s", request);
|
||||||
|
}
|
||||||
/* remove \r\n at the end of string */
|
/* remove \r\n at the end of string */
|
||||||
pos = strchr(request, '\r');
|
pos = strchr(request, '\r');
|
||||||
if (pos != NULL)
|
if (pos != NULL)
|
||||||
|
@ -318,54 +408,119 @@ main(int argc, char **argv)
|
||||||
*/
|
*/
|
||||||
if (strncmp(request, "gemini://", GEMINI_PART) != 0) {
|
if (strncmp(request, "gemini://", GEMINI_PART) != 0) {
|
||||||
/* error code url malformed */
|
/* error code url malformed */
|
||||||
syslog(LOG_DAEMON, "request «%s» doesn't match gemini://",
|
errlog("request «%s» doesn't match gemini://",
|
||||||
request);
|
request);
|
||||||
exit(1);
|
|
||||||
}
|
}
|
||||||
syslog(LOG_DAEMON, "request %s", request);
|
syslog(LOG_INFO, "request %s", request);
|
||||||
|
|
||||||
/* remove the gemini:// part */
|
/* remove the gemini:// part */
|
||||||
memmove(request, request + GEMINI_PART, sizeof(request) - GEMINI_PART);
|
memmove(request, request + GEMINI_PART, strlen(request) + 1 - GEMINI_PART);
|
||||||
|
|
||||||
/*
|
/* remove all "/.." for safety reasons */
|
||||||
* look for the first / after the hostname
|
while ((pos = strstr(request, "/..")) != NULL)
|
||||||
* in order to split hostname and uri
|
memmove(request, pos + 3, strlen(pos) + 1 - 3); /* "/.." = 3 */
|
||||||
*/
|
|
||||||
|
/* look for hostname in request : first thing before first / if any */
|
||||||
pos = strchr(request, '/');
|
pos = strchr(request, '/');
|
||||||
|
|
||||||
if (pos != NULL) {
|
if (pos != NULL) {
|
||||||
/* if there is a / found */
|
/* copy what's after hostname in dir */
|
||||||
/* separate hostname and uri */
|
estrlcpy(dir, pos, strlen(pos) + 1);
|
||||||
estrlcpy(uri, pos, strlen(pos)+1);
|
/* just keep hostname in request : stop the string with \0 */
|
||||||
/* just keep hostname in request */
|
|
||||||
pos[0] = '\0';
|
pos[0] = '\0';
|
||||||
}
|
}
|
||||||
/* check if client added :port at end of request */
|
/* check if client added :port at end of hostname and remove it */
|
||||||
pos = strchr(request, ':');
|
pos = strchr(request, ':');
|
||||||
if (pos != NULL) {
|
if (pos != NULL) {
|
||||||
/* end string at :*/
|
/* end string at : */
|
||||||
pos[0] = '\0';
|
pos[0] = '\0';
|
||||||
}
|
}
|
||||||
/* copy hostname from request */
|
/* copy hostname from request */
|
||||||
estrlcpy(hostname, request, sizeof(hostname));
|
estrlcpy(hostname, request, sizeof(hostname));
|
||||||
|
|
||||||
/*
|
/* remove leading '/' in dir */
|
||||||
* if virtualhost feature is actived looking under the chroot_path +
|
while (dir[0] == '/')
|
||||||
* hostname directory gemini://foobar/hello will look for
|
memmove(dir, dir + 1, strlen(dir + 1) + 1);
|
||||||
* chroot_path/foobar/hello
|
|
||||||
*/
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
/* open file and send it to stdout */
|
if (virtualhost) {
|
||||||
display_file(uri);
|
/* add hostname at the beginning of the dir path */
|
||||||
|
char tmp [PATH_MAX] = {'\0'};
|
||||||
|
estrlcpy(tmp, hostname, sizeof(tmp));
|
||||||
|
estrlcat(tmp, "/", sizeof(tmp));
|
||||||
|
estrlcat(tmp, dir, sizeof(tmp));
|
||||||
|
estrlcpy(dir, tmp, sizeof(dir));
|
||||||
|
}
|
||||||
|
/* percent decode */
|
||||||
|
uridecode(dir);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* split dir and filename. file is last part after last '/'. if none
|
||||||
|
* found, then requested file is actually a directory
|
||||||
|
*/
|
||||||
|
if (strlen(dir) > 0) {
|
||||||
|
pos = strrchr(dir, '/');
|
||||||
|
if (pos != NULL) {
|
||||||
|
estrlcpy(file, pos + 1, sizeof(file)); /* +1 : no leading '/' */
|
||||||
|
pos[0] = '\0';
|
||||||
|
|
||||||
|
/* change directory to requested directory */
|
||||||
|
if (strlen(dir) > 0)
|
||||||
|
echdir(dir);
|
||||||
|
} else {
|
||||||
|
estrlcpy(file, dir, sizeof(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (docgi) {
|
||||||
|
/* check if directory is cgidir */
|
||||||
|
char cgifp [PATH_MAX] = {'\0'};
|
||||||
|
estrlcpy(cgifp, chroot_dir, sizeof(cgifp));
|
||||||
|
if (cgifp[strlen(cgifp) - 1] != '/')
|
||||||
|
estrlcat(cgifp, "/", sizeof(cgifp));
|
||||||
|
|
||||||
|
estrlcat(cgifp, dir, sizeof(cgifp));
|
||||||
|
|
||||||
|
/* not cgipath, display file content */
|
||||||
|
if (strcmp(cgifp, cgidir) != 0)
|
||||||
|
goto file_to_stdout;
|
||||||
|
|
||||||
|
/* set env variables for CGI */
|
||||||
|
/*
|
||||||
|
* see
|
||||||
|
* https://lists.orbitalfox.eu/archives/gemini/2020/000315.htm
|
||||||
|
* l
|
||||||
|
*/
|
||||||
|
esetenv("GATEWAY_INTERFACE", "CGI/1.1", 1);
|
||||||
|
esetenv("SERVER_PROTOCOL", "GEMINI", 1);
|
||||||
|
esetenv("SERVER_SOFTWARE", "vger/1", 1);
|
||||||
|
|
||||||
|
/* look for "?" if any to set query for cgi, remove it */
|
||||||
|
pos = strchr(file, '?');
|
||||||
|
if (pos != NULL) {
|
||||||
|
estrlcpy(query, pos + 1, sizeof(query));
|
||||||
|
esetenv("QUERY_STRING", query, 1);
|
||||||
|
pos[0] = '\0';
|
||||||
|
}
|
||||||
|
/* look for an extension to find PATH_INFO */
|
||||||
|
pos = strrchr(file, '.');
|
||||||
|
if (pos != NULL) {
|
||||||
|
/* found a dot */
|
||||||
|
pos = strchr(pos, '/');
|
||||||
|
if (pos != NULL) {
|
||||||
|
setenv("PATH_INFO", pos, 1);
|
||||||
|
pos[0] = '\0'; /* keep only script name */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
esetenv("SCRIPT_NAME", file, 1);
|
||||||
|
esetenv("SERVER_NAME", hostname, 1);
|
||||||
|
|
||||||
|
cgi(file);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
file_to_stdout:
|
||||||
|
/* regular file to stdout */
|
||||||
|
display_file(file);
|
||||||
|
|
||||||
|
/* end logging */
|
||||||
|
closelog();
|
||||||
|
|
||||||
return (0);
|
return (0);
|
||||||
}
|
}
|
||||||
|
|
2
mimes.c
2
mimes.c
|
@ -5,6 +5,7 @@
|
||||||
#include "mimes.h"
|
#include "mimes.h"
|
||||||
#include "opts.h"
|
#include "opts.h"
|
||||||
|
|
||||||
|
/* extension to mimetype table */
|
||||||
static const struct {
|
static const struct {
|
||||||
const char *extension;
|
const char *extension;
|
||||||
const char *type;
|
const char *type;
|
||||||
|
@ -124,6 +125,7 @@ get_file_mime(const char *path, const char *default_mime)
|
||||||
size_t i;
|
size_t i;
|
||||||
char *extension;
|
char *extension;
|
||||||
|
|
||||||
|
/* search for extension after last '.' in path */
|
||||||
if ((extension = strrchr(path, '.')) == NULL)
|
if ((extension = strrchr(path, '.')) == NULL)
|
||||||
goto out;
|
goto out;
|
||||||
|
|
||||||
|
|
16
opts.h
16
opts.h
|
@ -1,12 +1,18 @@
|
||||||
#include <limits.h> /* PATH_MAX */
|
#include <limits.h> /* PATH_MAX */
|
||||||
|
|
||||||
|
/* Defaults values */
|
||||||
#define DEFAULT_MIME "application/octet-stream"
|
#define DEFAULT_MIME "application/octet-stream"
|
||||||
#define DEFAULT_LANG ""
|
#define DEFAULT_LANG ""
|
||||||
#define DEFAULT_CHROOT "/var/gemini"
|
#define DEFAULT_CHROOT "/var/gemini"
|
||||||
|
#define DEFAULT_INDEX "index.gmi"
|
||||||
#define DEFAULT_AUTOIDX 0
|
#define DEFAULT_AUTOIDX 0
|
||||||
|
|
||||||
/* longest is 56 so 64 should be enough */
|
/*
|
||||||
static char default_mime[64] = DEFAULT_MIME;
|
* Options used later
|
||||||
static char chroot_dir[PATH_MAX] = DEFAULT_CHROOT;
|
*/
|
||||||
static char lang[16] = DEFAULT_LANG;
|
/* longest hardcoded mimetype is 56 long so 64 should be enough */
|
||||||
static unsigned int doautoidx = DEFAULT_AUTOIDX;
|
static char default_mime[64] = DEFAULT_MIME;
|
||||||
|
static char lang[16] = DEFAULT_LANG;
|
||||||
|
static unsigned int doautoidx = DEFAULT_AUTOIDX;
|
||||||
|
static char cgidir[PATH_MAX] = {'\0'};
|
||||||
|
static int chrooted = 0;
|
||||||
|
|
4
shell.nix
Normal file
4
shell.nix
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
with (import <nixpkgs> {});
|
||||||
|
mkShell {
|
||||||
|
buildInputs = [ gcc libbsd gnumake ];
|
||||||
|
}
|
109
tests/test.sh
Normal file → Executable file
109
tests/test.sh
Normal file → Executable file
|
@ -4,109 +4,118 @@ set -x
|
||||||
|
|
||||||
# md5 is BSD md5 binary
|
# md5 is BSD md5 binary
|
||||||
# Linux uses md5sum
|
# Linux uses md5sum
|
||||||
MD5=md5
|
which md5 && MD5CMD="md5" || MD5CMD="md5sum"
|
||||||
type md5 2>/dev/null
|
|
||||||
if [ $? -ne 0 ]; then
|
MD5()
|
||||||
MD5=md5sum
|
{
|
||||||
fi
|
$MD5CMD | awk '{print $1}'
|
||||||
|
}
|
||||||
|
|
||||||
# serving a file
|
# 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
|
if ! [ $OUT = "c7e352d6aae4ee7e7604548f7874fb9d" ] ; then echo "error" ; exit 1 ; fi
|
||||||
|
|
||||||
# default index.gmi file
|
# 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
|
if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1 ; fi
|
||||||
|
|
||||||
# default index.gmi file when using a trailing slash
|
# 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
|
if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1 ; fi
|
||||||
|
|
||||||
# default index.gmi file when client specify port
|
# 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
|
if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1 ; fi
|
||||||
|
|
||||||
# redirect to uri with trailing / if directory
|
# 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
|
if ! [ $OUT = "b0e7e20db5ca7b80918025e7c15a8b02" ] ; 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 = "827eef65a3cd71e2ce805bc1e05eac44" ] ; then echo "error" ; exit 1 ; fi
|
||||||
|
|
||||||
# file from local directory with lang=fr and markdown MIME type
|
# 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)
|
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
|
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)
|
# 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)
|
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
|
if ! [ $OUT = "2c73bfb33dd2d12be322ebb85e03c015" ] ; then echo "error" ; exit 1 ; fi
|
||||||
|
|
||||||
# file from local directory and unknown MIME type, default forced to text/plain
|
# 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)
|
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
|
if ! [ $OUT = "8169f43fbb2032f4054b153c38fe61d6" ] ; then echo "error" ; exit 1 ; fi
|
||||||
|
|
||||||
# redirect file
|
# 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
|
if ! [ $OUT = "cb4597b6fcc82cbc366ac9002fb60dac" ] ; then echo "error" ; exit 1 ; fi
|
||||||
|
|
||||||
# file from local directory using virtualhosts
|
# 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
|
if ! [ $OUT = "5e5fca557e79f4521b21d4b81dc964c6" ] ; then echo "error" ; exit 1 ; fi
|
||||||
|
|
||||||
# file from local directory using virtualhosts without specifying a file
|
# 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
|
if ! [ $OUT = "5e5fca557e79f4521b21d4b81dc964c6" ] ; then echo "error" ; exit 1 ; fi
|
||||||
|
|
||||||
# file from local directory using virtualhosts without specifying a file using lang = fr
|
# 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
|
if ! [ $OUT = "7db981ce93fee268f29324912800f00d" ] ; then echo "error" ; exit 1 ; fi
|
||||||
|
|
||||||
# file from local directory using virtualhosts and IRI
|
# 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
|
if ! [ $OUT = "282cee071d3bd20dbb6e6af38f217a29" ] ; then echo "error" ; exit 1 ; fi
|
||||||
|
|
||||||
# file from local directory using virtualhosts and IRI both with emojis
|
# 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
|
if ! [ $OUT = "e354a1a29ea8273faaf0cdc29c1d8583" ] ; then echo "error" ; exit 1 ; fi
|
||||||
|
|
||||||
# auto index in directory without index.gmi must redirect
|
# 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
|
if ! [ $OUT = "5742b21d465e377074408045a71656dc" ] ; then echo "error" ; exit 1 ; fi
|
||||||
|
|
||||||
# auto index in directory
|
# auto index in directory
|
||||||
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 = "770a987b8f5cf7169e6bc3c6563e1570" ] ; then echo "error" ; exit 1 ; fi
|
if ! [ $OUT = "2d4a82fea3f10ab3e123e9f9d5dd1fbc" ] ; 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 var/gemini/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 var/gemini/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 var/gemini/cgi-bin | tee /dev/stderr | MD5)
|
||||||
|
if ! [ $OUT = "74ba4b36dcebec9ce9dae33033f3378a" ] ; 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 var/gemini/perso.pw/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 !
|
# must fail only on OpenBSD !
|
||||||
# try to escape from unveil
|
# try to escape from unveil
|
||||||
if [ -f /bsd ]
|
if [ -f /bsd ]
|
||||||
then
|
then
|
||||||
OUT=$(printf "gemini://fail_on_openbsd/../../test.sh\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | $MD5)
|
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
|
if [ $OUT = "$( ( printf '20 text/gemini; lang=fr\r\n' ; cat $0) | MD5)" ] ; then echo "error" ; exit 1 ; fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
#type doas 2>/dev/null
|
#type doas 2>/dev/null
|
||||||
#if [ $? -eq 0 ]; then
|
#if [ $? -eq 0 ]; then
|
||||||
# # file from local directory chroot
|
# # 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
|
# if ! [ $OUT = "7db981ce93fee268f29324912800f00d" ] ; then echo "error" ; exit 1 ; fi
|
||||||
#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"
|
echo "SUCCESS"
|
||||||
|
|
9
tests/var/gemini/cgi-bin/test.cgi
Executable file
9
tests/var/gemini/cgi-bin/test.cgi
Executable 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
|
10
tests/var/gemini/cgi-bin/who.cgi
Executable file
10
tests/var/gemini/cgi-bin/who.cgi
Executable 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"
|
2
tests/var/gemini/percent%-encode?.gmi
Normal file
2
tests/var/gemini/percent%-encode?.gmi
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
solene, here is a % for you
|
||||||
|
and a λ of course :)
|
9
tests/var/gemini/perso.pw/cgi-bin/test.cgi
Executable file
9
tests/var/gemini/perso.pw/cgi-bin/test.cgi
Executable 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
|
94
utils.c
Normal file
94
utils.c
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
#include <err.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <syslog.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "utils.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) {
|
||||||
|
syslog(LOG_ERR, "unveil on %s failed", path);
|
||||||
|
err(1, "unveil on %s failed", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
epledge(const char *promises, const char *execpromises)
|
||||||
|
{
|
||||||
|
if (pledge(promises, execpromises) == -1) {
|
||||||
|
syslog(LOG_ERR, "pledge failed for: %s", promises);
|
||||||
|
err(1, "pledge failed for: %s", promises);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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, "strlcpy 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
esetenv(const char *name, const char *value, int overwrite)
|
||||||
|
{
|
||||||
|
int ret = 0;
|
||||||
|
ret = setenv(name, value, overwrite);
|
||||||
|
|
||||||
|
if (ret != 0) {
|
||||||
|
err(1, "setenv %s:%s", name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* send error in syslog, to stdout and die */
|
||||||
|
void
|
||||||
|
errlog(const char *format, ...)
|
||||||
|
{
|
||||||
|
char e[1024] = {'\0'};
|
||||||
|
va_list ap;
|
||||||
|
|
||||||
|
fflush(stdout); /* make sure older messages are printed */
|
||||||
|
|
||||||
|
va_start(ap, format);
|
||||||
|
vsnprintf(e, sizeof(e), format, ap);
|
||||||
|
va_end(ap);
|
||||||
|
|
||||||
|
syslog(LOG_ERR, "%s", e);
|
||||||
|
err(1, "%s", e);
|
||||||
|
}
|
6
utils.h
Normal file
6
utils.h
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
void epledge(const char *, const char *);
|
||||||
|
void errlog(const char *format, ...);
|
||||||
|
void eunveil(const char *, const char *);
|
||||||
|
int esetenv(const char *, const char *, int);
|
||||||
|
size_t estrlcat(char *, const char *, size_t);
|
||||||
|
size_t estrlcpy(char *, const char *, size_t);
|
19
vger.8
19
vger.8
|
@ -9,6 +9,7 @@
|
||||||
.Op Fl l Ar lang
|
.Op Fl l Ar lang
|
||||||
.Op Fl v
|
.Op Fl v
|
||||||
.Op Fl i
|
.Op Fl i
|
||||||
|
.Op Fl c Ar cgi_path
|
||||||
.Op Fl d Ar path
|
.Op Fl d Ar path
|
||||||
.Op Fl u Ar username
|
.Op Fl u Ar username
|
||||||
.Op Fl m Ar mimetype
|
.Op Fl m Ar mimetype
|
||||||
|
@ -40,6 +41,22 @@ Enable virtualhost support, the hostname in the query will be considered as a di
|
||||||
As example, for request gemini://hostname.example/file.gmi
|
As example, for request gemini://hostname.example/file.gmi
|
||||||
.Nm
|
.Nm
|
||||||
will read the file /var/gemini/hostname.example/file.gmi
|
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 must not end with '/'.
|
||||||
|
If using virtualhost, you must insert the virtualhost directory in the cgi path.
|
||||||
|
As example, for a request gemini://hostname.example/cgi-bin/hello.cgi, one must set:
|
||||||
|
.Bd -literal -offset indent
|
||||||
|
vger -c /var/gemini/hostname.example/cgi-bin/hello.cgi
|
||||||
|
.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
|
.It Op Fl m Ar mimetype
|
||||||
Use
|
Use
|
||||||
.Ar mimetype
|
.Ar mimetype
|
||||||
|
@ -55,7 +72,7 @@ On
|
||||||
.Nm
|
.Nm
|
||||||
will use
|
will use
|
||||||
.Xr unveil 2
|
.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
|
.It Op Fl u Ar username
|
||||||
Enable
|
Enable
|
||||||
.Xr chroot 2
|
.Xr chroot 2
|
||||||
|
|
Loading…
Reference in a new issue