first commit

This commit is contained in:
Solene Rapenne 2020-12-05 20:07:08 +01:00
commit 5c1958b150
8 changed files with 382 additions and 0 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) 2020 Solène Rapenne <solene@openbsd.org>
BSD 2-clause License
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

16
Makefile Normal file
View File

@ -0,0 +1,16 @@
PREFIX?=/usr/local/
all: khan
clean:
rm -f khan *.core
khan: main.c
${CC} -o khan main.c
install: khan
install -o root -g wheel khan ${PREFIX}/bin/
install -o root -g wheel khan.8 ${PREFIX}/man/man8/
test: khan
cd tests && sh test.sh

60
README.md Normal file
View File

@ -0,0 +1,60 @@
# A simplistic and secure Gopher server
**Khan** is a gopher server supporting chroot meant to be run on
inetd.
**Khan** design is relying on inetd, the idea is to delegate the
network to a daemon which proved doing it correctly, so khan takes
its request from stdin and outputs the result to stdout. This
also makes it very easy to write tests for it.
**Khan** is perfectly secure if run on **OpenBSD**, using `unveil()`
the filesystem access is restricted to one directory (default to
`/var/gopher/`) and with `pledge()` only systems calls related to
reading files and reading input/output are allowed.
In addition, it's possible to run khan into a chroot and drop
privileges to a dedicated user on every system on which **Khan**
compiles.
# Installing
For some systems, the library `libsd` may be required.
```
git clone https://tildegit.org/solene/khan.git
cd khan
make
su -c 'make install'
```
# Running tests
**Khan** comes with a test suite you can use with `make test`.
# Command line parameters
**Khan** has a few parameters you can use in inetd configuration.
- `-d PATH`: use `PATH` as the data directory to serve files from. Default is `/var/gopher`
- `-u username`: enable chroot to the data directory and drop privileges to `username`.
# How to configure Khan using inetd
Create directory `/var/gopher/`, files will be served from there,
or use `-d` parameter to choose another path.
Add this line to inetd.conf:
```
11965 stream tcp nowait gopher_user /usr/local/bin/khan khan
```
On OpenBSD, enable and start inetd:
```
# rcctl enable inetd
# rcctl start inetd
```

58
khan.8 Normal file
View File

@ -0,0 +1,58 @@
.Dd $Mdocdate: December 03 2020 $
.Dt KHAN 8
.Os
.Sh NAME
.Nm khan
.Nd inetd gopher server
.Sh SYNOPSIS
.Nm khan
.Op Fl d Ar path
.Op Fl u Ar username
.Sh DESCRIPTION
.Nm
is a secure gopher server that is meant to be run on
.Xr inetd 8 .
.Pp
If an incoming gopher query doesn't explicitly request a file,
.Nm
will serves a default gophermap file if present.
.Sh OPTIONS
.Bl -tag -width Ds
.It Op Fl d Ar path
Use
.Ar path
instead of the default "/var/gopher/" path to look for files.
On
.Ox
.Nm
will use
.Xr unveil 2
on this path in read-only to prevent file access outside this directory.
.It Op Fl u Ar username
Enable
.Xr chroot 2
on the data directory and then drop privileges to
.Ar username .
This requires
.Nm
to be run as root user.
.El
.Sh DEPLOYMENT
.Nm
is meant to be run by
.Xr inetd 8 .
.Pp
/etc/inetd.conf example using a dedicated gopher_user:
.Bd -literal -offset indent
70 stream tcp nowait gopher_user /usr/local/bin/khan khan
.Ed
.Sh EXIT STATUS
.Ex -std khan
.Sh SEE ALSO
.Xr chroot 2 ,
.Xr unveil 2 ,
.Xr inetd 8
.Sh AUTHORS
.An See the LICENSE file for the authors .
.Sh LICENSE
See the LICENSE file for the terms of redistribution.

188
main.c Normal file
View File

@ -0,0 +1,188 @@
#include <sys/stat.h>
#include <err.h>
#include <errno.h>
#include <pwd.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#define BUFF_LEN_1 1000
#define BUFF_LEN_2 1025
#define BUFF_LEN_3 1024
#define DEFAULT_CHROOT "/var/gopher/"
void display_file(const char *);
void drop_privileges(const char *, const char *);
void
drop_privileges(const char *user, const char *path)
{
struct passwd *pw;
char chroot_dir[BUFF_LEN_2];
strlcpy(chroot_dir, path, sizeof(chroot_dir));
/*
* use chroot() if an user is specified requires root user to be
* running the program to run chroot() and then drop privileges
*/
if (strlen(user) > 0) {
/* is root? */
if (getuid() != 0) {
syslog(LOG_DAEMON, "chroot requires program to be run as root");
errx(1, "chroot requires root user");
}
/* search user uid from name */
if ((pw = getpwnam(user)) == NULL) {
syslog(LOG_DAEMON, "the user %s can't be found on the system", user);
err(1, "finding user");
}
/* chroot worked? */
if (chroot(chroot_dir) != 0) {
syslog(LOG_DAEMON, "the chroot_dir %s can't be used for chroot", chroot_dir);
err(1, "chroot");
}
if (chdir("/") == -1) {
syslog(LOG_DAEMON, "failed to chdir(\"/\")");
err(1, "chdir");
}
/* drop privileges */
if (setgroups(1, &pw->pw_gid) ||
setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) {
syslog(LOG_DAEMON, "dropping privileges to user %s (uid=%i) failed",
user, pw->pw_uid);
err(1, "Can't drop privileges");
}
strlcpy(chroot_dir, "/", sizeof(chroot_dir));
}
#ifdef __OpenBSD__
/*
* prevent access to files other than the one in path
*/
if (unveil(chroot_dir, "r") == -1) {
syslog(LOG_DAEMON, "unveil on %s failed", chroot_dir);
err(1, "unveil");
}
/*
* prevent system calls other parsing queryfor fread file and
* write to stdio
*/
if (pledge("stdio rpath", NULL) == -1) {
syslog(LOG_DAEMON, "pledge call failed");
err(1, "pledge");
}
#endif
}
void
display_file(const char *path)
{
size_t buflen = BUFF_LEN_1;
char *buffer[BUFF_LEN_1];
ssize_t nread;
struct stat sb;
FILE *fd;
/* this is to check if path is a directory */
if (stat(path, &sb) == -1)
goto err;
/* open the file requested */
if ((fd = fopen(path, "r")) == NULL)
goto err;
/* check if directory */
if (S_ISDIR(sb.st_mode) == 1)
goto err;
/* read the file and write it to stdout */
while ((nread = fread(buffer, sizeof(char), buflen, fd)) != 0)
fwrite(buffer, sizeof(char), nread, stdout);
fclose(fd);
syslog(LOG_DAEMON, "path served %s", path);
return;
err:
/* return an error code and no content */
printf("resource not found for %s\n", path);
syslog(LOG_DAEMON, "path invalid %s", path);
}
int
main(int argc, char **argv)
{
char buffer [BUFF_LEN_2];
char request [BUFF_LEN_2];
char path [BUFF_LEN_2] = DEFAULT_CHROOT;
char user [_SC_LOGIN_NAME_MAX] = "";
int option;
int chroot = 0;
char *pos;
while ((option = getopt(argc, argv, ":d:u:")) != -1) {
switch (option) {
case 'd':
strlcpy(path, optarg, sizeof(path));
break;
case 'u':
chroot = 1;
strlcpy(user, optarg, sizeof(user));
break;
}
}
/*
* do chroot if an user is supplied run pledge/unveil if OpenBSD
*/
drop_privileges(user, path);
if (chroot == 1)
strlcpy(path, "/", sizeof(path));
/*
* read 1024 chars from stdin
* to get the request
*/
fgets(request, BUFF_LEN_3, stdin);
/* remove \r\n at the end of string
* replace \n first and then \r
* because some client may only use
* \n instead of \r\n
*/
pos = strchr(request, '\n');
if (pos != NULL) *pos = '\n';
pos = strchr(request, '\r');
if (pos != NULL) *pos = '\0';
syslog(LOG_DAEMON, "request %s", request);
/*
* look for the first / after the hostname
* in order to split hostname and uri
*/
fprintf(stderr, "<%s %ld>\n", request, strlen(request));
if(strlen(request) == 0 || strcmp(request, "/") == 0) {
fprintf(stderr, "<%s %ld>\n", request, strlen(request));
strlcpy(request, "/gophermap", sizeof(request));
}
/* add the base dir to the file requested */
strlcat(path, request, sizeof(path));
/* open file and send it to stdout */
display_file(path);
return (0);
}

37
tests/test.sh Normal file
View File

@ -0,0 +1,37 @@
#!/bin/sh
set -x
# md5 is BSD md5 binary
# Linux uses md5sum
MD5=md5
type md5 2>/dev/null
if [ $? -ne 0 ]; then
MD5=md5sum
fi
# serving a file
OUT=$(printf "/main.gph\r\n" | ../khan -d var/gopher/ | tee /dev/stderr | $MD5)
if ! [ $OUT = "979481230b482d033c5a6e6c566c0322" ] ; then echo "error" ; exit 1 ; fi
# default gophermap file with nothing
OUT=$(printf "\r\n" | ../khan -d var/gopher/ | tee /dev/stderr | $MD5)
if ! [ $OUT = "979481230b482d033c5a6e6c566c0322" ] ; then echo "error" ; exit 1 ; fi
# default gophermap with /
OUT=$(printf "/\r\n" | ../khan -d var/gopher/ | tee /dev/stderr | $MD5)
if ! [ $OUT = "979481230b482d033c5a6e6c566c0322" ] ; then echo "error" ; exit 1 ; fi
# file not existing
OUT=$(printf "/foobar\r\n" | ../khan -v -d var/gopher/ | tee /dev/stderr | $MD5)
if ! [ $OUT = "fd5f8d5e7e0dab56056547bc1d9f2f3e" ] ; then echo "error" ; exit 1 ; fi
# must fail only on OpenBSD !
# try to escape from unveil
if [ -f /bsd ]
then
OUT=$(printf "/../../test.sh\r\n" | ../khan -d var/gopher/ | tee /dev/stderr | $MD5)
if [ $OUT = "$(cat $0 | $MD5)" ] ; then echo "error" ; exit 1 ; fi
fi
echo "SUCCESS"

View File

@ -0,0 +1 @@
1menu hostname 2345 a

View File

@ -0,0 +1 @@
1menu hostname 2345 a