Compare commits
74 Commits
Author | SHA1 | Date |
---|---|---|
Solene Rapenne | f647ff347a | |
solene | cad05817d3 | |
prx | 1e13a08865 | |
prx | aa6f001022 | |
prx | 86626e77f2 | |
prx | c09063b10e | |
solene | de579c346a | |
prx | 62bc1d852f | |
prx | 750ca51e43 | |
prx | bc306eaf8a | |
prx | 46f2c7238a | |
solene | 3ca16cf38f | |
prx | 9d23a48ae2 | |
prx | 875f167eb9 | |
prx | 75bfaee73e | |
prx | 45e5ae32fa | |
prx | b1f92f7c9c | |
prx | fc5c2a1b41 | |
prx | 34667eb018 | |
prx | 5cb310dd1b | |
prx | 63616a97fc | |
prx | 27549119ef | |
prx | e3932483ef | |
prx | f8d215869d | |
prx | e2567fcf01 | |
prx | b18f3a3c7b | |
prx | 504dd3f759 | |
prx | f388d2a57a | |
prx | 5063f3e95b | |
prx | be0f86df8d | |
prx | 883bfed7a7 | |
prx | 26ca6c422d | |
prx | 2b835bc39c | |
solene | bfd713c131 | |
prx | fd0e70ab5b | |
prx | 4d3b585951 | |
prx | f973351945 | |
Solene Rapenne | 76fafe0a9d | |
Solene Rapenne | 4496e744e8 | |
Solene Rapenne | 9cca2408c3 | |
Solene Rapenne | 8bd1144178 | |
Solene Rapenne | 6743c54359 | |
solene | bbd0f05663 | |
aabacchus | aa1affb6c2 | |
solene | 8efcdb7512 | |
aabacchus | 394b86bca8 | |
Solene Rapenne | 01f2503376 | |
Solene Rapenne | ed6dc1ed12 | |
Solene Rapenne | 0d3d453498 | |
Solene Rapenne | 914a143c3f | |
Solene Rapenne | a319de23f7 | |
prx | 15d09d2c01 | |
prx | 843d1f0ab7 | |
prx | f8aff7fe05 | |
prx | 4972df5999 | |
Solene Rapenne | 365e99400a | |
Solene Rapenne | 807d1b8409 | |
prx | 02d2d1dc7d | |
prx | de52acecfc | |
Omar Polo | ee8569c6e6 | |
Omar Polo | 16a5ed7b30 | |
Omar Polo | 8454548b51 | |
Omar Polo | cbcf4ec9b6 | |
Omar Polo | 7431d3eeec | |
prx | e9be1b73a7 | |
prx | e87b36c991 | |
Solene Rapenne | e3b5fb2ab3 | |
Solene Rapenne | 9525d66afb | |
Solene Rapenne | d086262d1a | |
Solene Rapenne | 7b0686bdfa | |
prx | fbacb35170 | |
prx | efa1f639fc | |
Solene Rapenne | 458592594e | |
Solene Rapenne | 31d384833f |
|
@ -0,0 +1,2 @@
|
|||
*.o
|
||||
vger
|
15
Makefile
15
Makefile
|
@ -1,6 +1,8 @@
|
|||
include config.mk
|
||||
|
||||
PREFIX?=/usr/local/
|
||||
CFLAGS += -pedantic -Wall -Wextra -Wmissing-prototypes \
|
||||
-Wstrict-prototypes -Wwrite-strings
|
||||
-Wstrict-prototypes -Wwrite-strings ${EXTRAFLAGS}
|
||||
|
||||
.SUFFIXES: .c .o
|
||||
|
||||
|
@ -11,16 +13,21 @@ all: vger
|
|||
|
||||
clean:
|
||||
find . \( -name vger -o \
|
||||
-name unit_test -o \
|
||||
-name "*.o" -o \
|
||||
-name "*.core" \) \
|
||||
-delete
|
||||
|
||||
vger: main.o mimes.o utils.o opts.h
|
||||
${CC} ${CFLAGS} -o $@ main.o mimes.o utils.o
|
||||
vger: main.c vger.c mimes.o utils.o opts.h
|
||||
${CC} ${CFLAGS} -o $@ main.c mimes.o utils.o
|
||||
|
||||
install: vger
|
||||
install -o root -g wheel vger ${PREFIX}/bin/
|
||||
install -o root -g wheel vger.8 ${PREFIX}/man/man8/
|
||||
|
||||
test: vger
|
||||
unit_test: tests.c vger.o
|
||||
${CC} ${CFLAGS} -o $@ vger.o tests.c mimes.o utils.o
|
||||
|
||||
test: vger unit_test
|
||||
./unit_test
|
||||
cd tests && sh test.sh
|
||||
|
|
39
README.md
39
README.md
|
@ -1,12 +1,8 @@
|
|||
# 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, CGI, default language choice, redirections and MIME types detection.
|
||||
|
||||
**Vger** design is relying on inetd and a daemon to take care of
|
||||
TLS. The idea is to delegate TLS and network to daemons which
|
||||
proved doing it correctly, so vger takes its request from stdin and
|
||||
output the result to stdout.
|
||||
**Vger** design is relying on inetd and a daemon to take care of TLS. The idea is to delegate TLS and network to daemons which proved doing it correctly, so vger takes its request from stdin and output the result to stdout.
|
||||
|
||||
The average setup should look like:
|
||||
|
||||
|
@ -19,34 +15,36 @@ The average setup should look like:
|
|||
vger on inetd
|
||||
```
|
||||
|
||||
**Vger** is perfectly secure if run on **OpenBSD**, using `unveil()`
|
||||
the filesystem access is restricted to one directory (default to
|
||||
`/var/gemini/`) and with `pledge()` only systems calls related to
|
||||
reading files and reading input/output are allowed. 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. More explanations about Vger security can be found [on this link](https://dataswamp.org/~solene/2021-01-14-vger-security.html).
|
||||
|
||||
For all supported OS, it's possible to run **Vger** in a chroot
|
||||
and drop privileges to a dedicated user.
|
||||
For all supported OS, it's possible to run **Vger** in a chroot and drop privileges to a dedicated user.
|
||||
|
||||
|
||||
# Install
|
||||
|
||||
`vger` is available as a package for the following systems:
|
||||
|
||||
[![Packaging status](https://repology.org/badge/vertical-allrepos/vger.svg)](https://repology.org/project/vger/versions)
|
||||
|
||||
# Building from sources
|
||||
|
||||
```
|
||||
git clone https://tildegit.org/solene/vger.git
|
||||
cd vger
|
||||
./configure (only really useful for Linux)
|
||||
make
|
||||
doas make install
|
||||
```
|
||||
|
||||
On GNU/Linux, make sure you installed `libbsd`.
|
||||
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
|
||||
|
@ -64,8 +62,7 @@ without a `-d` parameter.
|
|||
|
||||
# How to configure Vger using relayd and inetd
|
||||
|
||||
Create directory `/var/gemini/` (I'd allow this to be configured
|
||||
later), files will be served from there.
|
||||
Create directory `/var/gemini/` (I'd allow this to be configured later), files will be served from there.
|
||||
|
||||
Create an user `gemini_user`.
|
||||
|
||||
|
@ -101,6 +98,4 @@ On OpenBSD, enable inetd and relayd and start them:
|
|||
|
||||
Don't forget to open the TCP port 1965 in your firewall.
|
||||
|
||||
Vger will serve files named `index.gmi` if no explicit filename is given.
|
||||
If this file doesn't exist and auto index is enabled, an index file
|
||||
with a link to every file in the directory will be served.
|
||||
Vger will serve files named `index.gmi` if no explicit filename is given. If this file doesn't exist and auto index is enabled, an index file with a link to every file in the directory will be served.
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
|
||||
OS="$(uname -s)"
|
||||
|
||||
case "$OS" in
|
||||
Linux)
|
||||
EXTRAFLAGS=-lbsd
|
||||
;;
|
||||
*)
|
||||
EXTRAFLAGS=""
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "EXTRAFLAGS=${EXTRAFLAGS}" > config.mk
|
495
main.c
495
main.c
|
@ -1,345 +1,47 @@
|
|||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include <dirent.h>
|
||||
#include <err.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <pwd.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <syslog.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "mimes.h"
|
||||
#include "opts.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define GEMINI_PART 9
|
||||
#define GEMINI_REQUEST_MAX 1024 /* see https://gemini.circumlunar.space/docs/specification.html */
|
||||
|
||||
|
||||
|
||||
void autoindex(const char *);
|
||||
void cgi(const char *cgicmd);
|
||||
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 *user, const char *path)
|
||||
{
|
||||
struct passwd *pw;
|
||||
int chrooted = 0;
|
||||
|
||||
/*
|
||||
* use chroot() if an user is specified requires root user to be
|
||||
* running the program to run chroot() and then drop privileges
|
||||
*/
|
||||
if (strlen(user) > 0) {
|
||||
|
||||
/* is root? */
|
||||
if (getuid() != 0) {
|
||||
errlog("chroot requires program to be run as root");
|
||||
}
|
||||
/* search user uid from name */
|
||||
if ((pw = getpwnam(user)) == NULL) {
|
||||
errlog("the user %s can't be found on the system", user);
|
||||
}
|
||||
/* chroot worked? */
|
||||
if (chroot(path) != 0) {
|
||||
errlog("the chroot_dir %s can't be used for chroot", path);
|
||||
}
|
||||
chrooted = 1;
|
||||
if (chdir("/") == -1) {
|
||||
errlog("failed to 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)) {
|
||||
errlog("dropping privileges to user %s (uid=%i) failed",
|
||||
user, pw->pw_uid);
|
||||
}
|
||||
}
|
||||
#ifdef __OpenBSD__
|
||||
/*
|
||||
* prevent access to files other than the one in path
|
||||
*/
|
||||
if (chrooted) {
|
||||
eunveil("/", "r");
|
||||
} else {
|
||||
eunveil(path, "r");
|
||||
}
|
||||
/* permission to execute what's inside cgipath */
|
||||
if (strlen(cgibin) > 0) {
|
||||
/* first, build the full path of cgi (not in chroot) */
|
||||
char cgifullpath[PATH_MAX] = {'\0'};
|
||||
estrlcpy(cgifullpath, path, sizeof(cgifullpath));
|
||||
estrlcat(cgifullpath, cgibin, sizeof(cgifullpath));
|
||||
|
||||
eunveil(cgifullpath, "rx");
|
||||
}
|
||||
/* forbid more unveil */
|
||||
eunveil(NULL, NULL);
|
||||
|
||||
/*
|
||||
* prevent system calls other parsing queryfor fread file and
|
||||
* write to stdio
|
||||
*/
|
||||
if (strlen(cgibin) > 0) {
|
||||
/* cgi need execlp() (exec) and fork() (proc) */
|
||||
epledge("stdio rpath exec proc", NULL);
|
||||
} else {
|
||||
epledge("stdio rpath", NULL);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
status(const int code, const char *file_mime)
|
||||
{
|
||||
printf("%i %s; %s\r\n",
|
||||
code, file_mime, lang);
|
||||
}
|
||||
|
||||
void
|
||||
status_redirect(const int code, const char *url)
|
||||
{
|
||||
printf("%i %s\r\n",
|
||||
code, url);
|
||||
}
|
||||
|
||||
void
|
||||
display_file(const char *uri)
|
||||
{
|
||||
FILE *fd = NULL;
|
||||
struct stat sb = {0};
|
||||
ssize_t nread = 0;
|
||||
const char *file_mime;
|
||||
char *buffer[BUFSIZ];
|
||||
char target[FILENAME_MAX] = {'\0'};
|
||||
char fp[PATH_MAX] = {'\0'};
|
||||
char tmp[PATH_MAX] = {'\0'}; /* used to build temporary path */
|
||||
|
||||
/* build file path inside chroot */
|
||||
estrlcpy(fp, chroot_dir, sizeof(fp));
|
||||
estrlcat(fp, uri, sizeof(fp));
|
||||
|
||||
/* this is to check if path exists and obtain metadata later */
|
||||
if (stat(fp, &sb) == -1) {
|
||||
|
||||
/* check if fp is a symbolic link
|
||||
* if so, redirect using its target */
|
||||
if (lstat(fp, &sb) != -1 && S_ISLNK(sb.st_mode) == 1)
|
||||
goto redirect;
|
||||
else
|
||||
goto err;
|
||||
}
|
||||
|
||||
/* check if directory */
|
||||
if (S_ISDIR(sb.st_mode) != 0) {
|
||||
if (fp[strlen(fp) -1 ] != '/') {
|
||||
/* no ending "/", redirect to "path/" */
|
||||
estrlcpy(tmp, uri, sizeof(tmp));
|
||||
estrlcat(tmp, "/", sizeof(tmp));
|
||||
status_redirect(31, tmp);
|
||||
return;
|
||||
|
||||
} else {
|
||||
/* there is a leading "/", display index.gmi */
|
||||
estrlcpy(tmp, fp, sizeof(tmp));
|
||||
estrlcat(tmp, "index.gmi", sizeof(tmp));
|
||||
|
||||
/* check if index.gmi exists or show autoindex */
|
||||
if (stat(tmp, &sb) == 0) {
|
||||
estrlcpy(fp, tmp, sizeof(fp));
|
||||
} else if (doautoidx != 0) {
|
||||
autoindex(fp);
|
||||
return;
|
||||
} else {
|
||||
goto err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* open the file requested */
|
||||
if ((fd = fopen(fp, "r")) == NULL) { goto err; }
|
||||
|
||||
file_mime = get_file_mime(fp, default_mime);
|
||||
|
||||
status(20, file_mime);
|
||||
|
||||
/* read the file byte after byte in buffer and write it to stdout */
|
||||
while ((nread = fread(buffer, 1, sizeof(buffer), fd)) != 0)
|
||||
fwrite(buffer, 1, nread, stdout);
|
||||
goto closefd;
|
||||
syslog(LOG_DAEMON, "path served %s", fp);
|
||||
|
||||
return;
|
||||
|
||||
err:
|
||||
/* return an error code and no content */
|
||||
status(51, "text/gemini");
|
||||
syslog(LOG_DAEMON, "path invalid %s", fp);
|
||||
goto closefd;
|
||||
|
||||
redirect:
|
||||
/* read symbolic link target to redirect */
|
||||
if (readlink(fp, target, FILENAME_MAX) == -1) {
|
||||
goto err;
|
||||
}
|
||||
|
||||
status_redirect(30, target);
|
||||
syslog(LOG_DAEMON, "redirection from %s to %s", fp, target);
|
||||
|
||||
closefd:
|
||||
if (S_ISREG(sb.st_mode) != 0) {
|
||||
fclose(fd);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
autoindex(const char *path)
|
||||
{
|
||||
int n = 0;
|
||||
char *pos = NULL;
|
||||
struct dirent **namelist; /* this must be freed at last */
|
||||
|
||||
|
||||
syslog(LOG_DAEMON, "autoindex: %s", path);
|
||||
|
||||
status(20, "text/gemini");
|
||||
|
||||
/* display link to parent */
|
||||
char parent[PATH_MAX] = {'\0'};
|
||||
/* parent is "path" without chroot_dir */
|
||||
estrlcpy(parent, path+strlen(chroot_dir), sizeof(parent));
|
||||
/* remove ending '/' */
|
||||
while (parent[strlen(parent)-1] == '/') {
|
||||
parent[strlen(parent)-1] = '\0';
|
||||
}
|
||||
/* remove last part after '/' */
|
||||
pos = strrchr(parent, '/');
|
||||
if (pos != NULL) {
|
||||
pos[1] = '\0'; /* at worse, parent is now "/" */
|
||||
}
|
||||
printf("=> %s ../\n", parent);
|
||||
|
||||
/* use alphasort to always have the same order on every system */
|
||||
if ((n = scandir(path, &namelist, NULL, alphasort)) < 0) {
|
||||
status(51, "text/gemini");
|
||||
errlog("Can't scan %s", path);
|
||||
} else {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
cgi(const char *cgicmd)
|
||||
{
|
||||
|
||||
int pipedes[2] = {0};
|
||||
pid_t pid;
|
||||
|
||||
/* get a pipe to get stdout */
|
||||
if (pipe(pipedes) != 0) {
|
||||
status(42, "text/gemini");
|
||||
err(1, "pipe failed");
|
||||
}
|
||||
|
||||
pid = fork();
|
||||
|
||||
if (pid < 0) {
|
||||
close(pipedes[0]);
|
||||
close(pipedes[1]);
|
||||
status(42, "text/gemini");
|
||||
err(1, "fork failed");
|
||||
}
|
||||
|
||||
if (pid > 0) { /* parent */
|
||||
char buf[3];
|
||||
size_t nread = 0;
|
||||
FILE *output = NULL;
|
||||
|
||||
close(pipedes[1]); /* make sure entry is closed so fread() gets EOF */
|
||||
|
||||
/* use fread/fwrite because are buffered */
|
||||
output = fdopen(pipedes[0], "r");
|
||||
if (output == NULL) {
|
||||
status(42, "text/gemini");
|
||||
err(1, "fdopen failed");
|
||||
}
|
||||
|
||||
/* read pipe output */
|
||||
while ((nread = fread(buf, 1, sizeof(buf), output)) != 0) {
|
||||
fwrite(buf, 1, nread, stdout);
|
||||
}
|
||||
close(pipedes[0]);
|
||||
fclose(output);
|
||||
|
||||
wait(NULL); /* wait for child to terminate */
|
||||
|
||||
exit(0);
|
||||
|
||||
} else if (pid == 0) { /* child */
|
||||
dup2(pipedes[1], STDOUT_FILENO); /* set pipe output equal to stdout */
|
||||
close(pipedes[1]); /* no need this file descriptor : it is now stdout */
|
||||
execlp(cgicmd, cgicmd, NULL);
|
||||
/* if execlp is ok, this will never be reached */
|
||||
status(42, "text/gemini");
|
||||
errlog("error when trying to execlp %s", cgicmd);
|
||||
}
|
||||
}
|
||||
#include "vger.c"
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
char request [GEMINI_REQUEST_MAX] = {'\0'};
|
||||
char hostname [GEMINI_REQUEST_MAX] = {'\0'};
|
||||
char uri [PATH_MAX] = {'\0'};
|
||||
char user [_SC_LOGIN_NAME_MAX] = "";
|
||||
char query[PATH_MAX] = {'\0'};
|
||||
int virtualhost = 0;
|
||||
int option = 0;
|
||||
char *pos = NULL;
|
||||
char request[GEMINI_REQUEST_MAX] = {'\0'};
|
||||
char user[_SC_LOGIN_NAME_MAX] = {'\0'};
|
||||
char hostname[GEMINI_REQUEST_MAX] = {'\0'};
|
||||
char path[GEMINI_REQUEST_MAX] = {'\0'};
|
||||
char query[GEMINI_REQUEST_MAX] = {'\0'};
|
||||
char cgi_dir[PATH_MAX] = {'\0'};
|
||||
char rel_cgi_dir[PATH_MAX] = {'\0'};
|
||||
char chroot_dir[PATH_MAX] = DEFAULT_CHROOT;
|
||||
char tmp[PATH_MAX] = {'\0'};
|
||||
int option = 0;
|
||||
int virtualhost = 0;
|
||||
|
||||
/*
|
||||
* request : contain the whole request from client : gemini://...\r\n
|
||||
* user : username, used in drop_privileges()
|
||||
* hostname : extracted from hostname. used with virtualhosts and cgi SERVER_NAME
|
||||
* query : after a ? in cgi : gemini://...?query
|
||||
*/
|
||||
|
||||
while ((option = getopt(argc, argv, ":d:l:m:u:c:vi")) != -1) {
|
||||
switch (option) {
|
||||
case 'd':
|
||||
estrlcpy(chroot_dir, optarg, sizeof(chroot_dir));
|
||||
esnprintf(chroot_dir, sizeof(chroot_dir), "%s", optarg);
|
||||
break;
|
||||
case 'l':
|
||||
estrlcpy(lang, "lang=", sizeof(lang));
|
||||
estrlcat(lang, optarg, sizeof(lang));
|
||||
esnprintf(lang, sizeof(lang), "lang=%s", optarg);
|
||||
break;
|
||||
case 'm':
|
||||
estrlcpy(default_mime, optarg, sizeof(default_mime));
|
||||
esnprintf(default_mime, sizeof(default_mime), "%s", optarg);
|
||||
break;
|
||||
case 'u':
|
||||
estrlcpy(user, optarg, sizeof(user));
|
||||
esnprintf(user, sizeof(user), "%s", optarg);
|
||||
break;
|
||||
case 'c':
|
||||
estrlcpy(cgibin, optarg, sizeof(cgibin));
|
||||
esnprintf(rel_cgi_dir, sizeof(rel_cgi_dir), "%s", optarg);
|
||||
/* remove leading / */
|
||||
while (*rel_cgi_dir == '/')
|
||||
memmove(rel_cgi_dir, rel_cgi_dir+1,
|
||||
strlen(rel_cgi_dir)); // strlen +1-1
|
||||
break;
|
||||
case 'v':
|
||||
virtualhost = 1;
|
||||
|
@ -350,123 +52,46 @@ main(int argc, char **argv)
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* do chroot if an user is supplied run pledge/unveil if OpenBSD
|
||||
*/
|
||||
drop_privileges(user, chroot_dir);
|
||||
read_request(request);
|
||||
split_request(request, hostname, path, query);
|
||||
|
||||
/*
|
||||
* read 1024 chars from stdin
|
||||
* to get the request
|
||||
*/
|
||||
if (fgets(request, GEMINI_REQUEST_MAX, stdin) == NULL) {
|
||||
status(59, "request is too long (1024 max)");
|
||||
errlog("request is too long (1024 max): %s", request);
|
||||
}
|
||||
/* do chroot if an user is supplied */
|
||||
if (*user)
|
||||
drop_privileges(user);
|
||||
|
||||
/* remove \r\n at the end of string */
|
||||
pos = strchr(request, '\r');
|
||||
if (pos != NULL)
|
||||
*pos = '\0';
|
||||
|
||||
/*
|
||||
* check if the beginning of the request starts with
|
||||
* gemini://
|
||||
*/
|
||||
if (strncmp(request, "gemini://", GEMINI_PART) != 0) {
|
||||
/* error code url malformed */
|
||||
errlog("request «%s» doesn't match gemini://",
|
||||
request);
|
||||
}
|
||||
syslog(LOG_DAEMON, "request %s", request);
|
||||
|
||||
/* remove the gemini:// part */
|
||||
memmove(request, request + GEMINI_PART, strlen(request) +1 - GEMINI_PART);
|
||||
|
||||
/* remove all "/.." for safety reasons */
|
||||
while ((pos = strstr(request, "/..")) != NULL ) {
|
||||
memmove(request, pos+3, strlen(pos) +1 - 3); /* "/.." = 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
* look for the first / after the hostname
|
||||
* in order to split hostname and uri
|
||||
*/
|
||||
pos = strchr(request, '/');
|
||||
|
||||
if (pos != NULL) {
|
||||
/* if there is a / found */
|
||||
/* separate hostname and uri */
|
||||
estrlcpy(uri, pos, strlen(pos)+1);
|
||||
/* just keep hostname in request */
|
||||
pos[0] = '\0';
|
||||
}
|
||||
/* check if client added :port at end of request */
|
||||
pos = strchr(request, ':');
|
||||
if (pos != NULL) {
|
||||
/* end string at :*/
|
||||
pos[0] = '\0';
|
||||
}
|
||||
/* copy hostname from request */
|
||||
estrlcpy(hostname, request, sizeof(hostname));
|
||||
|
||||
/* look for "?" if any to set query for cgi, or remove it*/
|
||||
pos = strchr(uri, '?');
|
||||
if (pos != NULL) {
|
||||
estrlcpy(query, pos+1, sizeof(query));
|
||||
esetenv("QUERY_STRING", query, 1);
|
||||
pos[0] = '\0';
|
||||
}
|
||||
|
||||
/*
|
||||
* if virtualhost feature is actived looking under the chroot_path +
|
||||
* hostname directory gemini://foobar/hello will look for
|
||||
* chroot_path/foobar/hello
|
||||
*/
|
||||
/* set actual chroot_dir */
|
||||
if (virtualhost) {
|
||||
if (strlen(uri) == 0) {
|
||||
estrlcpy(uri, "/index.gmi", sizeof(uri));
|
||||
}
|
||||
char tmp[PATH_MAX] = {'\0'};
|
||||
estrlcpy(tmp, hostname, sizeof(tmp));
|
||||
estrlcat(tmp, uri, sizeof(tmp));
|
||||
estrlcpy(uri, tmp, sizeof(uri));
|
||||
esnprintf(tmp, sizeof(tmp), "%s/%s", chroot_dir, hostname);
|
||||
esnprintf(chroot_dir, sizeof(chroot_dir), "%s", tmp);
|
||||
}
|
||||
|
||||
/* check if uri is cgibin */
|
||||
if ((strlen(cgibin) > 0) &&
|
||||
(strncmp(uri, cgibin, strlen(cgibin)) == 0)) {
|
||||
/* cgi_dir is in chroot_dir */
|
||||
if (*rel_cgi_dir)
|
||||
esnprintf(cgi_dir, sizeof(cgi_dir),
|
||||
"%s/%s", chroot_dir, rel_cgi_dir);
|
||||
|
||||
/* cgipath with chroot_dir at the beginning */
|
||||
char cgipath[PATH_MAX] = {'\0'};
|
||||
estrlcpy(cgipath, chroot_dir, sizeof(cgipath));
|
||||
estrlcat(cgipath, uri, sizeof(cgipath));
|
||||
/* 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);
|
||||
|
||||
/* look for an extension to find PATH_INFO */
|
||||
pos = strrchr(cgipath, '.');
|
||||
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", cgipath, 1);
|
||||
esetenv("SERVER_NAME", hostname, 1);
|
||||
|
||||
cgi(cgipath);
|
||||
set_rootdir(chroot_dir, cgi_dir, user);
|
||||
|
||||
if (strlen(path) == 0) { /* this is root dir */
|
||||
esnprintf(path, sizeof(path), "./");
|
||||
} else {
|
||||
//TODO: percent decoding here
|
||||
/* open file and send it to stdout */
|
||||
display_file(uri);
|
||||
uridecode(path);
|
||||
remove_double_dot(path);
|
||||
}
|
||||
|
||||
return (0);
|
||||
uridecode(query);
|
||||
|
||||
/* is it cgi ? */
|
||||
if (*cgi_dir)
|
||||
if (do_cgi(rel_cgi_dir, path, hostname, query) == 0)
|
||||
stop(EXIT_SUCCESS, NULL);
|
||||
/* *** from here, cgi didn't run *** */
|
||||
|
||||
/* check if path available */
|
||||
check_path(path, sizeof(path));
|
||||
|
||||
/* regular file to stdout */
|
||||
display_file(path);
|
||||
|
||||
stop(EXIT_SUCCESS, NULL);
|
||||
}
|
||||
|
|
20
mimes.c
20
mimes.c
|
@ -5,6 +5,7 @@
|
|||
#include "mimes.h"
|
||||
#include "opts.h"
|
||||
|
||||
/* extension to mimetype table */
|
||||
static const struct {
|
||||
const char *extension;
|
||||
const char *type;
|
||||
|
@ -124,16 +125,15 @@ get_file_mime(const char *path, const char *default_mime)
|
|||
size_t i;
|
||||
char *extension;
|
||||
|
||||
if ((extension = strrchr(path, '.')) == NULL)
|
||||
goto out;
|
||||
|
||||
/* look for the MIME in the database */
|
||||
for (i = 0; i < nitems(database); i++) {
|
||||
if (strcmp(database[i].extension, extension + 1) == 0)
|
||||
return (database[i].type);
|
||||
/* search for extension after last '.' in path */
|
||||
if ((extension = strrchr(path, '.')) != NULL) {
|
||||
/* look for the MIME in the database */
|
||||
for (i = 0; i < nitems(database); i++) {
|
||||
if (strcmp(database[i].extension, extension + 1) == 0)
|
||||
return (database[i].type);
|
||||
}
|
||||
}
|
||||
|
||||
out:
|
||||
/* if no MIME have been found, set a default one */
|
||||
return (default_mime);
|
||||
/* no MIME found, set a default one */
|
||||
return default_mime;
|
||||
}
|
||||
|
|
9
opts.h
9
opts.h
|
@ -1,13 +1,16 @@
|
|||
#include <limits.h> /* PATH_MAX */
|
||||
|
||||
/* Defaults values */
|
||||
#define DEFAULT_MIME "application/octet-stream"
|
||||
#define DEFAULT_LANG ""
|
||||
#define DEFAULT_CHROOT "/var/gemini"
|
||||
#define DEFAULT_INDEX "index.gmi"
|
||||
#define DEFAULT_AUTOIDX 0
|
||||
|
||||
/* longest is 56 so 64 should be enough */
|
||||
/*
|
||||
* Options used later
|
||||
*/
|
||||
/* longest hardcoded mimetype is 56 long so 64 should be enough */
|
||||
static char default_mime[64] = DEFAULT_MIME;
|
||||
static char chroot_dir[PATH_MAX] = DEFAULT_CHROOT;
|
||||
static char lang[16] = DEFAULT_LANG;
|
||||
static unsigned int doautoidx = DEFAULT_AUTOIDX;
|
||||
static char cgibin[PATH_MAX] = {'\0'};
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
with (import <nixpkgs> {});
|
||||
mkShell {
|
||||
buildInputs = [ gcc libbsd gnumake ];
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include "vger.h"
|
||||
|
||||
// to test
|
||||
void test_status(void);
|
||||
void test_status_error(void);
|
||||
void test_uridecode(char*, const int);
|
||||
|
||||
|
||||
void
|
||||
test_uridecode(char *str, const int result)
|
||||
{
|
||||
char reference[GEMINI_REQUEST_MAX] = {'\0'};
|
||||
strlcpy(reference, str, sizeof(reference));
|
||||
uridecode(str);
|
||||
if (strncmp(reference, str, strlen(str)) != result)
|
||||
{
|
||||
printf("uridecode error\n");
|
||||
printf("Strings should be %s\n", (result == 0) ? "identical" : "different");
|
||||
printf("passed %s\n", reference);
|
||||
printf("got %s\n", str);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
test_status(void)
|
||||
{
|
||||
status(20, "text/gemini");
|
||||
}
|
||||
|
||||
void
|
||||
test_status_error(void)
|
||||
{
|
||||
status(51, "file not found");
|
||||
status(50, "Forbidden path");
|
||||
status(50, "Internal server error");
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
test_status_error();
|
||||
test_status();
|
||||
//test_uridecode("host.name", 0);
|
||||
//test_uridecode("host.name/percent%25-encode%3.gmi", 1);
|
||||
return(0);
|
||||
}
|
|
@ -31,17 +31,21 @@ if ! [ $OUT = "fcc5a293f316e01f7b3103f97eca26b1" ] ; then echo "error" ; exit 1
|
|||
OUT=$(printf "gemini://host.name/subdir\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
|
||||
if ! [ $OUT = "84e5e7bb3eee0dfcc8db14865dc83e77" ] ; then echo "error" ; exit 1 ; fi
|
||||
|
||||
# redirect to uri with trailing / if directory and vhost enabled
|
||||
OUT=$(printf "gemini://perso.pw/cgi-bin\r\n" | ../vger -vd var/gemini | tee /dev/stderr | MD5)
|
||||
if ! [ $OUT = "e0eb3a8e31bdb30c89d92d1d2b0a1fa1" ] ; then echo "error" ; exit 1 ; fi
|
||||
|
||||
# file from local directory with lang=fr and markdown MIME type
|
||||
OUT=$(printf "gemini://perso.pw/file.md\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | MD5)
|
||||
if ! [ $OUT = "e663f17730d5ddc24010c14a238e1e78" ] ; then echo "error" ; exit 1 ; fi
|
||||
if ! [ $OUT = "09c82ffe243ce3b3cfb04c2bc4a91acb" ] ; then echo "error" ; exit 1 ; fi
|
||||
|
||||
# file from local directory with lang=fr and unknown MIME type (default to application/octet-stream)
|
||||
OUT=$(printf "gemini://perso.pw/foobar.unknown\r\n" | ../vger -d var/gemini/ -l fr | tee /dev/stderr | MD5)
|
||||
if ! [ $OUT = "a23b0053d759863a45da4afbffd847d2" ] ; then echo "error" ; exit 1 ; fi
|
||||
if ! [ $OUT = "2c73bfb33dd2d12be322ebb85e03c015" ] ; then echo "error" ; exit 1 ; fi
|
||||
|
||||
# file from local directory and unknown MIME type, default forced to text/plain
|
||||
OUT=$(printf "gemini://perso.pw/foobar.unknown\r\n" | ../vger -d var/gemini/ -m text/plain | tee /dev/stderr | MD5)
|
||||
if ! [ $OUT = "383a5a5ddb7bb30e3553ecb666378ebc" ] ; then echo "error" ; exit 1 ; fi
|
||||
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)
|
||||
|
@ -56,7 +60,7 @@ OUT=$(printf "gemini://perso.pw\r\n" | ../vger -v -d var/gemini/ | tee /dev/stde
|
|||
if ! [ $OUT = "5e5fca557e79f4521b21d4b81dc964c6" ] ; then echo "error" ; exit 1 ; fi
|
||||
|
||||
# file from local directory using virtualhosts without specifying a file using lang = fr
|
||||
OUT=$(printf "gemini://perso.pw\r\n" | ../vger -v -d var/gemini/ -l fr | tee /dev/stderr | MD5)
|
||||
OUT=$(printf "gemini://perso.pw/\r\n" | ../vger -v -d var/gemini/ -l fr | tee /dev/stderr | MD5)
|
||||
if ! [ $OUT = "7db981ce93fee268f29324912800f00d" ] ; then echo "error" ; exit 1 ; fi
|
||||
|
||||
# file from local directory using virtualhosts and IRI
|
||||
|
@ -73,28 +77,36 @@ if ! [ $OUT = "874f5e1af67eff6b93bedf8ac8033066" ] ; then echo "error" ; exit 1
|
|||
|
||||
# auto index in directory
|
||||
OUT=$(printf "gemini://host.name/autoidx/\r\n" | ../vger -d var/gemini/ -i | tee /dev/stderr | MD5)
|
||||
if ! [ $OUT = "515bcb4ba5f8869360f53afe2841e044" ] ; then echo "error" ; exit 1 ; fi
|
||||
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)
|
||||
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)
|
||||
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 = "2c88347cfac44450035283a8508a29cb" ] ; then echo "error" ; exit 1 ; fi
|
||||
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
|
||||
|
||||
# remove ?.* if any
|
||||
OUT=$(printf "gemini://host.name/main.gmi?anything-here\r\n" | ../vger -d var/gemini/ | tee /dev/stderr | MD5)
|
||||
if ! [ $OUT = "c7e352d6aae4ee7e7604548f7874fb9d" ] ; 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 perso.pw/cgi-bin | tee /dev/stderr | MD5)
|
||||
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 ]
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
solene, here is a % for you
|
||||
and a λ of course :)
|
108
utils.c
108
utils.c
|
@ -1,21 +1,36 @@
|
|||
#include <sys/types.h>
|
||||
|
||||
#include <err.h>
|
||||
#include <errno.h>
|
||||
#include <regex.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.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) {
|
||||
syslog(LOG_DAEMON, "unveil on %s failed", path);
|
||||
err(1, "unveil on %s failed", path);
|
||||
status(41, "Error when unveil(), see logs");
|
||||
stop(EXIT_FAILURE, "unveil on %s failed", path);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,33 +38,28 @@ void
|
|||
epledge(const char *promises, const char *execpromises)
|
||||
{
|
||||
if (pledge(promises, execpromises) == -1) {
|
||||
syslog(LOG_DAEMON, "pledge failed for: %s", promises);
|
||||
err(1, "pledge failed for: %s", promises);
|
||||
status(41, "Error when pledge(), see logs");
|
||||
stop(EXIT_FAILURE, "pledge failed for: %s", promises);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
size_t
|
||||
estrlcpy(char *dst, const char *src, size_t dstsize)
|
||||
esnprintf(char *str, size_t size, const char *format, ...)
|
||||
{
|
||||
size_t n = 0;
|
||||
/* usage : esnprintf(str, sizeof(str), "%s ... %s", arg1, arg2); */
|
||||
va_list ap;
|
||||
size_t ret = 0;
|
||||
|
||||
n = strlcpy(dst, src, dstsize);
|
||||
if (n >= dstsize) {
|
||||
err(1, "strlcpy failed for %s = %s", dst, src);
|
||||
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 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;
|
||||
return ret;
|
||||
}
|
||||
|
||||
int
|
||||
|
@ -59,22 +69,56 @@ esetenv(const char *name, const char *value, int overwrite)
|
|||
ret = setenv(name, value, overwrite);
|
||||
|
||||
if (ret != 0) {
|
||||
err(1, "setenv %s:%s", name, value);
|
||||
status(41, "setenv() failed, see logs");
|
||||
stop(EXIT_FAILURE, "setenv() %s:%s", name, value);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void
|
||||
errlog(const char *format, ...)
|
||||
echdir(const char *path)
|
||||
{
|
||||
char e[1024] = {'\0'};
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, format);
|
||||
vsnprintf(e, sizeof(e), format, ap);
|
||||
va_end(ap);
|
||||
|
||||
syslog(LOG_DAEMON, "%s", e);
|
||||
err(1, "%s", e);
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
|
14
utils.h
14
utils.h
|
@ -1,6 +1,8 @@
|
|||
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);
|
||||
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 *, ...);
|
||||
|
|
12
vger.8
12
vger.8
|
@ -44,17 +44,13 @@ will read the file /var/gemini/hostname.example/file.gmi
|
|||
.It Op Fl c
|
||||
Enable CGI support.
|
||||
.Ar cgi_path
|
||||
will be executed as a cgi script instead of returning its content.
|
||||
This path is relative to the directory set with
|
||||
.Fl d
|
||||
flag. If using virtualhost, you must insert the virtualhost directory in the 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/hello.cgi
|
||||
vger -c cgi-bin
|
||||
.Ed
|
||||
.Pp
|
||||
Note you can define a directory instead of a single file.
|
||||
.Pp
|
||||
In this case,
|
||||
.Xr pledge 2
|
||||
promises and unveil permission are set to enable cgi execution.
|
||||
|
@ -75,7 +71,7 @@ On
|
|||
.Nm
|
||||
will use
|
||||
.Xr unveil 2
|
||||
on this path in read-only to prevent file access outside this directory.
|
||||
on this path to only allow read-only file access within this directory.
|
||||
.It Op Fl u Ar username
|
||||
Enable
|
||||
.Xr chroot 2
|
||||
|
|
|
@ -0,0 +1,468 @@
|
|||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <err.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <pwd.h>
|
||||
#include <regex.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <syslog.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "mimes.h"
|
||||
#include "opts.h"
|
||||
#include "utils.h"
|
||||
#include "vger.h"
|
||||
|
||||
void
|
||||
stop(const int r, const char *fmt, ...)
|
||||
{
|
||||
va_list ap, ap2;
|
||||
|
||||
fflush(stdout); /* ensure all data is sent */
|
||||
|
||||
/* log the request and retcode */
|
||||
syslog(LOG_DAEMON|LOG_INFO, "\"%s\" %i %zd", _request, _retcode, _datasiz);
|
||||
|
||||
if (r != EXIT_SUCCESS) {
|
||||
/* log and print error */
|
||||
va_copy(ap2, ap);
|
||||
|
||||
va_start(ap, fmt);
|
||||
vsyslog(LOG_ERR, fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
va_start(ap2, fmt);
|
||||
vfprintf(stderr, fmt, ap2);
|
||||
va_end(ap2);
|
||||
|
||||
}
|
||||
exit(r);
|
||||
}
|
||||
|
||||
void
|
||||
status(const int code, const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
|
||||
_datasiz += fprintf(stdout, "%i ", code);
|
||||
|
||||
va_start(ap, fmt);
|
||||
_datasiz += vfprintf(stdout, fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
_datasiz += fprintf(stdout, "\r\n"); /* make sure status end correctly */
|
||||
|
||||
_retcode = code; /* store return code for logs */
|
||||
}
|
||||
|
||||
int
|
||||
uridecode(char *uri)
|
||||
{
|
||||
int n = 0;
|
||||
char c = '\0';
|
||||
long l = 0;
|
||||
char *pos = NULL;
|
||||
|
||||
if ((pos = strchr(uri, '%')) == NULL)
|
||||
return n;
|
||||
|
||||
while ((pos = strchr(pos, '%')) != NULL) {
|
||||
if (strlen(pos) < 3)
|
||||
return n;
|
||||
|
||||
char hex[3] = {'\0'};
|
||||
for (size_t i = 0; i < 2; i++)
|
||||
hex[i] = tolower(pos[i + 1]);
|
||||
|
||||
errno = 0;
|
||||
l = strtol(hex, 0, 16);
|
||||
if (errno == ERANGE && (l == LONG_MAX || l == LONG_MIN))
|
||||
continue; /* conversion failed */
|
||||
|
||||
c = (char)l;
|
||||
pos[0] = c;
|
||||
/* rewind of two char to remove %hex */
|
||||
memmove(pos + 1, pos + 3, strlen(pos + 3) + 1); /* +1 for \0 */
|
||||
n++;
|
||||
pos++; /* avoid infinite loop */
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void
|
||||
drop_privileges(const char *user)
|
||||
{
|
||||
struct passwd *pw;
|
||||
|
||||
/*
|
||||
* use chroot() if an user is specified requires root user to be
|
||||
* running the program to run chroot() and then drop privileges
|
||||
*/
|
||||
|
||||
/* is root? */
|
||||
if (getuid() != 0) {
|
||||
status(41, "privileges issue, see logs");
|
||||
stop(EXIT_FAILURE, "%s",
|
||||
"chroot requires program to be run as root");
|
||||
}
|
||||
|
||||
/* search user uid from name */
|
||||
if ((pw = getpwnam(user)) == NULL) {
|
||||
status(41, "privileges issue, see logs");
|
||||
stop(EXIT_FAILURE,
|
||||
"the user %s can't be found on the system", user);
|
||||
}
|
||||
|
||||
/* drop privileges */
|
||||
if (setgroups(1, &pw->pw_gid) ||
|
||||
setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
|
||||
setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) {
|
||||
status(41, "privileges issue, see logs");
|
||||
stop(EXIT_FAILURE,
|
||||
"dropping privileges to user %s (uid=%i) failed", \
|
||||
user, pw->pw_uid);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
set_rootdir(const char *chroot_dir, const char *cgi_dir, const char *user)
|
||||
{
|
||||
char capsule_dir[PATH_MAX] = {'\0'};
|
||||
|
||||
if (*user) {
|
||||
/* chroot worked? */
|
||||
if (chroot(chroot_dir) != 0) {
|
||||
status(41, "privileges issue, see logs");
|
||||
stop(EXIT_FAILURE,
|
||||
"the chroot_dir %s can't be used for chroot", chroot_dir);
|
||||
}
|
||||
|
||||
/* now chroot_dir is / */
|
||||
esnprintf(capsule_dir, sizeof(capsule_dir), "%s", "/");
|
||||
|
||||
} else {
|
||||
esnprintf(capsule_dir, sizeof(capsule_dir), "%s", chroot_dir);
|
||||
}
|
||||
|
||||
#ifdef __OpenBSD__
|
||||
/*
|
||||
* prevent access to files other than the one in chroot_dir
|
||||
*/
|
||||
eunveil(capsule_dir, "r");
|
||||
|
||||
/* permission to execute what's inside cgi_dir */
|
||||
if (*cgi_dir)
|
||||
eunveil(cgi_dir, "rx");
|
||||
|
||||
eunveil(NULL, NULL); /* no more call to unveil() */
|
||||
|
||||
/* promise permissions */
|
||||
if (*cgi_dir)
|
||||
epledge("stdio rpath exec proc", NULL);
|
||||
else
|
||||
epledge("stdio rpath", NULL);
|
||||
#endif
|
||||
echdir(capsule_dir); /* move to the gemini data directory */
|
||||
}
|
||||
|
||||
ssize_t
|
||||
display_file(const char *path)
|
||||
{
|
||||
FILE *fd = NULL;
|
||||
const char *file_mime;
|
||||
|
||||
/*
|
||||
* special case : path ends with "/". The user requested a dir
|
||||
*/
|
||||
if ((path[strlen(path)-1] == '/') && (doautoidx)) {
|
||||
/* no index.gmi, so display autoindex if enabled */
|
||||
_datasiz += autoindex(path);
|
||||
return _datasiz;
|
||||
}
|
||||
|
||||
/* open the file requested */
|
||||
if ((fd = fopen(path, "r")) != NULL) {
|
||||
file_mime = get_file_mime(path, default_mime);
|
||||
if (strcmp(file_mime, "text/gemini") == 0)
|
||||
status(20, "%s; %s", file_mime, lang);
|
||||
else
|
||||
status(20, "%s", file_mime);
|
||||
|
||||
_datasiz += print_file(fd);
|
||||
fclose(fd); /* close file descriptor */
|
||||
} else {
|
||||
/* return an error code and no content.
|
||||
* seems unlikely to happen unless the file vanished
|
||||
* since we checked with stat() if it exists
|
||||
*/
|
||||
status(51, "%s", "file not found and may have vanished");
|
||||
}
|
||||
|
||||
return _datasiz;
|
||||
}
|
||||
|
||||
int
|
||||
do_cgi(const char *rel_cgi_dir, const char *path, const char *hostname, const char *query)
|
||||
{
|
||||
|
||||
struct stat sbcgi = {0};
|
||||
struct stat sbpath = {0};
|
||||
char cgifp[PATH_MAX] = {'\0'}; /* cgi file to execute */
|
||||
char path_dir[PATH_MAX] = {'\0'};
|
||||
char *path_info = NULL;
|
||||
|
||||
/* get beginning of path */
|
||||
/* path_dir is initialized so there is an \0 at the end */
|
||||
memcpy(path_dir, path, strlen(rel_cgi_dir));
|
||||
|
||||
if (stat(rel_cgi_dir, &sbcgi) + stat(path_dir, &sbpath) != 0)
|
||||
goto nocgi;
|
||||
|
||||
/* compare inodes */
|
||||
if (sbcgi.st_ino != sbpath.st_ino)
|
||||
goto nocgi; /* not in cgi_dir, go to display_file */
|
||||
|
||||
/* set env variables for CGI
|
||||
* see
|
||||
* https://lists.orbitalfox.eu/archives/gemini/2020/000315.html
|
||||
*/
|
||||
esetenv("GATEWAY_INTERFACE", "CGI/1.1", 1);
|
||||
esetenv("SERVER_PROTOCOL", "GEMINI", 1);
|
||||
esetenv("SERVER_SOFTWARE", "vger/1", 1);
|
||||
|
||||
if (*query)
|
||||
esetenv("QUERY_STRING", query, 1);
|
||||
|
||||
/*
|
||||
* if in cgi_dir, only the first file after cgi_dir/FILE
|
||||
* is to be executed
|
||||
* the rest is PATH_INFO
|
||||
*/
|
||||
|
||||
/* find next item after cgi_dir in path:
|
||||
* path + strlen(rel_cgi_dir) + 1 (skip '/')
|
||||
*/
|
||||
|
||||
/* cgi file to execute */
|
||||
esnprintf(cgifp, sizeof(cgifp), "%s", path + strlen(rel_cgi_dir) + 1);
|
||||
if (!(*cgifp)) /* problem with cgi file, abort */
|
||||
goto nocgi;
|
||||
|
||||
/* check if there is something after cgi file for PATH_INFO */
|
||||
path_info = strchr(cgifp, '/');
|
||||
|
||||
if (path_info != NULL) {
|
||||
esetenv("PATH_INFO", path_info, 1);
|
||||
*path_info = '\0'; /* stop cgifp before PATH_INFO */
|
||||
}
|
||||
|
||||
esetenv("SCRIPT_NAME", cgifp, 1);
|
||||
esetenv("SERVER_NAME", hostname, 1);
|
||||
|
||||
echdir(rel_cgi_dir);
|
||||
|
||||
cgi(cgifp);
|
||||
return 0;
|
||||
|
||||
nocgi:
|
||||
return 1;
|
||||
}
|
||||
|
||||
ssize_t
|
||||
autoindex(const char *path)
|
||||
{
|
||||
/* display list of files in path + a link to parent (..) */
|
||||
|
||||
int n = 0;
|
||||
struct dirent **namelist; /* this must be freed at last */
|
||||
size_t bs = 0;
|
||||
|
||||
/* use alphasort to always have the same order on every system */
|
||||
if ((n = scandir(path, &namelist, NULL, alphasort)) < 0) {
|
||||
status(50, "Can't scan %s", path);
|
||||
} else {
|
||||
status(20, "text/gemini");
|
||||
bs += fprintf(stdout, "=> .. ../\n"); /* display link to parent */
|
||||
for (int j = 0; j < n; j++) {
|
||||
/* skip self and parent */
|
||||
if ((strcmp(namelist[j]->d_name, ".") == 0) ||
|
||||
(strcmp(namelist[j]->d_name, "..") == 0)) {
|
||||
continue;
|
||||
}
|
||||
/* add "/" at the end of a directory path */
|
||||
if (namelist[j]->d_type == DT_DIR) {
|
||||
bs += fprintf(stdout, "=> ./%s/ %s/\n",
|
||||
namelist[j]->d_name, namelist[j]->d_name);
|
||||
} else {
|
||||
bs += fprintf(stdout, "=> ./%s %s\n",
|
||||
namelist[j]->d_name, namelist[j]->d_name);
|
||||
}
|
||||
free(namelist[j]);
|
||||
}
|
||||
free(namelist);
|
||||
}
|
||||
|
||||
return bs;
|
||||
}
|
||||
|
||||
void
|
||||
cgi(const char *cgicmd)
|
||||
{
|
||||
int fildes[2] = {0};
|
||||
int retcode = 0;
|
||||
pid_t pid = 0;
|
||||
FILE *output = NULL;
|
||||
|
||||
if (pipe(fildes) != 0)
|
||||
goto cgierr;
|
||||
|
||||
if ((pid = fork()) < 0)
|
||||
goto cgierr;
|
||||
|
||||
if (pid > 0) { /* parent */
|
||||
close(fildes[1]); /* make sure entry is closed to get EOF */
|
||||
if ((output = fdopen(fildes[0], "r")) == NULL)
|
||||
goto cgierr;
|
||||
|
||||
_datasiz += print_file(output);
|
||||
close(fildes[0]);
|
||||
fclose(output);
|
||||
waitpid(pid, &retcode, 0);
|
||||
stop(EXIT_SUCCESS, "cgi ran with exit code %d", status);
|
||||
|
||||
} else { /* child */
|
||||
/* set pipe output equal to stdout & stderr */
|
||||
dup2(fildes[1], STDOUT_FILENO);
|
||||
close(fildes[1]); /* no longer required */
|
||||
execl(cgicmd, cgicmd, NULL);
|
||||
}
|
||||
|
||||
cgierr:
|
||||
/* if execl is ok, this will never be reached */
|
||||
close(fildes[0]);
|
||||
close(fildes[1]);
|
||||
status(42, "error when trying run cgi");
|
||||
stop(EXIT_FAILURE, "error when trying to execl %s", cgicmd);
|
||||
}
|
||||
|
||||
char *
|
||||
read_request(char *request)
|
||||
{
|
||||
/* read 1024 +1 chars from stdin to get the request (1024 + \0) */
|
||||
|
||||
if (fgets(request, GEMINI_REQUEST_MAX, stdin) == NULL) {
|
||||
/* EOF reached before reading anything */
|
||||
if (feof(stdin)) {
|
||||
status(59, "%s", "request is too short and probably empty");
|
||||
stop(EXIT_FAILURE, "%s", "request is too short and probably empty");
|
||||
|
||||
/* error before reading anything */
|
||||
} else if (ferror(stdin)) {
|
||||
status(59, "Error while reading request: %s", request);
|
||||
stop(EXIT_FAILURE, "Error while reading request: %s", request);
|
||||
}
|
||||
}
|
||||
|
||||
/* check if string ends with '\n', or to long */
|
||||
if (request[strnlen(request, GEMINI_REQUEST_MAX) - 1] != '\n') {
|
||||
status(59, "request is too long (1024 max): %s", request);
|
||||
stop(EXIT_FAILURE, "request is too long (1024 max): %s", request);
|
||||
}
|
||||
|
||||
/* remove \r\n at the end of string */
|
||||
request[strcspn(request, "\r\n")] = '\0';
|
||||
|
||||
/* save request for logs */
|
||||
esnprintf(_request, sizeof(_request), "%s", request);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
remove_double_dot(char *request)
|
||||
{
|
||||
char *pos = NULL;
|
||||
|
||||
/* remove all "/.." for safety reasons */
|
||||
while ((pos = strstr(request, "/..")) != NULL)
|
||||
memmove(request, pos + 3, strlen(pos) + 1 - 3); /* "/.." = 3 */
|
||||
}
|
||||
|
||||
void
|
||||
check_path(char *path, size_t pathsiz)
|
||||
{
|
||||
struct stat sb = {0};
|
||||
char tmp[PATH_MAX] = {'\0'};
|
||||
|
||||
if (stat(path, &sb) == -1) {
|
||||
if (lstat(path, &sb) != -1 && S_ISLNK(sb.st_mode) == 1) {
|
||||
if (readlink(path, tmp, sizeof(tmp)) > 0) {
|
||||
status(30, "%s", tmp);
|
||||
stop(EXIT_SUCCESS, NULL);
|
||||
}
|
||||
}
|
||||
status(51, "%s", "file not found");
|
||||
stop(EXIT_SUCCESS, NULL);
|
||||
}
|
||||
|
||||
if (S_ISDIR(sb.st_mode)) {
|
||||
/* check if dir path end with "/" */
|
||||
if (path[strlen(path) - 1] != '/') {
|
||||
esnprintf(tmp, sizeof(tmp), "/%s/", path);
|
||||
status(31, "%s", tmp);
|
||||
stop(EXIT_SUCCESS, NULL);
|
||||
}
|
||||
/* check if DEFAULT_INDEX exists in directory */
|
||||
esnprintf(tmp, sizeof(tmp), "%s/%s", path, DEFAULT_INDEX);
|
||||
if (stat(tmp, &sb) == 0)
|
||||
esnprintf(path, pathsiz, "%s", tmp);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
split_request(const char *request, char *hostname, char *path, char *query)
|
||||
{
|
||||
size_t nmatch = SE_MAX; /* 3 "()" + 1 for whole match */
|
||||
char buf[BUFSIZ] = {'\0'}; /* to handle error messages */
|
||||
int ret = 0;
|
||||
regex_t greg; /* compiled gemini regex */
|
||||
regmatch_t match[SE_MAX]; /* matches founds */
|
||||
|
||||
ret = regcomp(&greg, _gemini_regex, REG_EXTENDED);
|
||||
if (ret != 0) {
|
||||
regerror(ret, &greg, buf, sizeof(buf));
|
||||
regfree(&greg);
|
||||
status(50, "Internal server error");
|
||||
stop(EXIT_FAILURE, "%s", buf);
|
||||
}
|
||||
|
||||
ret = regexec(&greg, request, nmatch, match, 0);
|
||||
if (ret != 0) {
|
||||
regerror(ret, &greg, buf, sizeof(buf));
|
||||
regfree(&greg);
|
||||
status(59, "Malformed request");
|
||||
stop(EXIT_FAILURE, "Malformed request, error:%s", buf);
|
||||
}
|
||||
|
||||
/* one may want to check the return of getsubexp
|
||||
* and change memcpy to strlcpy
|
||||
* to make sure we didn't try to copy too long
|
||||
* and that string isn't trunkated.
|
||||
* It is unlikely to happen since dest string are as long as request
|
||||
*/
|
||||
getsubexp(request, match[1], hostname);
|
||||
getsubexp(request, match[2], path);
|
||||
getsubexp(request, match[3], query);
|
||||
|
||||
regfree(&greg);
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
#ifndef vger_h_INCLUDED
|
||||
#define vger_h_INCLUDED
|
||||
|
||||
/* length of "gemini://" */
|
||||
#define GEMINI_PART 9
|
||||
|
||||
/*
|
||||
* number of bytes to read with fgets() : 2014 + 1.
|
||||
* fgets() reads at most size-1 (1024 here).
|
||||
* See https://gemini.circumlunar.space/docs/specification.html.
|
||||
*/
|
||||
#define GEMINI_REQUEST_MAX 1025
|
||||
|
||||
/* max subexpression in regex : 3 + 1 */
|
||||
#define SE_MAX 4
|
||||
|
||||
/* gemini_regex:
|
||||
* =============
|
||||
* ^gemini://+ : in case of gemini:///
|
||||
* 1: hostname
|
||||
* ([^/|^\?|^:]*) :
|
||||
* catch everything, stop when /, ? or : is found
|
||||
* don't catch :port
|
||||
* [:[0-9]*]? : skip :1234 (port number) if any
|
||||
* / * : skip "/" if any
|
||||
* 2: path
|
||||
* ([^\?]*) :
|
||||
* catch everything and stop at ? if any
|
||||
* 3 : query
|
||||
* [\?]?(.*)$:
|
||||
* catch everything after ? if any
|
||||
*/
|
||||
static const char *_gemini_regex =
|
||||
"^gemini://+([^/|^\?|^:]*)[:[0-9]*]?/*([^\?]*)[\?]?(.*)$";
|
||||
|
||||
/* global vars */
|
||||
static int _retcode = 0;
|
||||
static ssize_t _datasiz = 0;
|
||||
static char _request[GEMINI_REQUEST_MAX] = {'\0'};
|
||||
|
||||
/* functions */
|
||||
ssize_t autoindex(const char *);
|
||||
void cgi(const char *);
|
||||
char * read_request(char *);
|
||||
void check_path(char *, size_t);
|
||||
ssize_t display_file(const char *);
|
||||
int do_cgi(const char *, const char *, const char *, const char *);
|
||||
void drop_privileges(const char *);
|
||||
void set_rootdir(const char *, const char *, const char *);
|
||||
void remove_double_dot(char *);
|
||||
void split_request(const char *, char *, char *, char *);
|
||||
void status(const int, const char *, ...);
|
||||
void stop(const int, const char *, ...);
|
||||
int uridecode (char *);
|
||||
|
||||
#endif // vger_h_INCLUDED
|
||||
|
Loading…
Reference in New Issue