spartserv

Simple client and server for the spartan protocol
git clone https://noulin.net/git/spartserv.git
Log | Files | Refs | README

commit 2f198dcdd76acabb0da5d47dd0ed9349e021dec0
parent 0157c674bc97f02949fa8557d2d34577448c0a4e
Author: Remy Noulin <loader2x@gmail.com>
Date:   Wed,  4 Jan 2023 12:05:43 +0200

add secure spartan server

README.md           |  22 +-
build.sh            |   6 +-
spartservPrivDrop.c | 564 ++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 588 insertions(+), 4 deletions(-)

Diffstat:
MREADME.md | 22+++++++++++++++++++++-
Mbuild.sh | 6+++---
AspartservPrivDrop.c | 564+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 588 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md @@ -11,7 +11,7 @@ apt-get install gcc ./build.sh ``` -`spartserv.c` is a server handling one request at a time. The configurations for the hostname and server root are on the top of `spartserv.c`. +`spartserv.c` is a server handling one request at a time and serves static pages. The configurations for the hostname and server root are on the top of `spartserv.c`. By default, `spartserv` opens port 3000 and serves on hostname `localhost`. @@ -40,3 +40,23 @@ The line type `=:` is not supported, to upload data run `sparline` with the URL ``` ./sparline spartan://hostname/path --infile afile.txt ``` + +# Running on port 300 +The default port for spartan is port 300 and in general only root processes can open a listening socket on port under 1024. + +`spartservPrivDrop` starts as a root and open port 300 and then chroot and drops the privileges. + +Only necessary syscalls are allowed. + +Create a user `spartserv`, the server will run as this user: +``` +# as root +adduser spartserv +``` + +In `spartservPrivDrop.c`, set the hostname and the chroot directory, this is the directory being served. +Compile and run: +``` +gcc -std=gnu11 -g3 spartservPrivDrop.c -o spartserv +./spartserv +``` diff --git a/build.sh b/build.sh @@ -1,6 +1,6 @@ -gcc -g3 spartserv.c -o spartserv -gcc -g3 spartclient.c -o spartclient -gcc -g3 sparline.c -o sparline +gcc -std=gnu11 -g3 spartserv.c -o spartserv +gcc -std=gnu11 -g3 spartclient.c -o spartclient +gcc -std=gnu11 -g3 sparline.c -o sparline echo Run to start spartan server: diff --git a/spartservPrivDrop.c b/spartservPrivDrop.c @@ -0,0 +1,564 @@ +/* +This server creates a socket listening to PORT and +starts the event loop +2 mimetypes are set depending on file extension: +text/gemini .gmi +text/markdown .md .markdown +*/ + +#define _GNU_SOURCE +#include <string.h> +#include <stdio.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <stdbool.h> +#include <ctype.h> +#include <stdarg.h> +#include <limits.h> +#include <time.h> + +// inet_ntoa +//already included #include <sys/socket.h> +//already included #include <netinet/in.h> +#include <arpa/inet.h> + +// privilege drop +// already included #include <sys/types.h> +#include <pwd.h> +#include <grp.h> + +// seccomp bpf filters +#include <sys/syscall.h> +#include <errno.h> +#include <stddef.h> +#include <linux/seccomp.h> +#include <linux/filter.h> // struct sock_filter +#include <linux/audit.h> +#include <sys/mman.h> +#include <sys/prctl.h> + +// server configuration +#define HOSTNAME "localhost" +#define ROOT "/" +#define PORT 300 + +#define RUNAS "spartserv" +#define CHROOT "." + +// ----------------- seccomp setup +// This code is copied and modified from kore http server +// https://kore.io +/* + * Copyright (c) 2013-2022 Joris Vink <joris@coders.se> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define ARGS_LO_OFFSET 0 +#define ARGS_HI_OFFSET sizeof(u_int32_t) +#elif __BYTE_ORDER == __BIG_ENDIAN +#define ARGS_LO_OFFSET sizeof(u_int32_t) +#define ARGS_HI_OFFSET 0 +#else +#error "__BYTE_ORDER unknown" +#endif + +// AUDIT_ARCH_X86_64 AUDIT_ARCH_ARM AUDIT_ARCH_AARCH64 +#define SECCOMP_AUDIT_ARCH AUDIT_ARCH_X86_64 +#define SECCOMP_KILL_POLICY SECCOMP_RET_KILL + +/* Load field of seccomp_data into accumulator. */ +#define KORE_BPF_LOAD(_field, _off) \ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, offsetof(struct seccomp_data, _field) + _off) +/* Compare the accumulator against a constant (==). */ +#define KORE_BPF_CMP(_k, _jt, _jf) \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, _k, _jt, _jf) + +/* Return a constant from a BPF program. */ +#define KORE_BPF_RET(_retval) \ + BPF_STMT(BPF_RET+BPF_K, _retval) + +/* AND operation on the accumulator. */ +#define KORE_BPF_AND(_k) \ + BPF_STMT(BPF_ALU+BPF_AND+BPF_K, _k) + +/* The length of a filter. */ +#define KORE_FILTER_LEN(x) (sizeof(x) / sizeof(x[0])) + +#define KORE_SYSCALL_FILTER(_name, _action) \ + KORE_BPF_CMP(SYS_##_name, 0, 1), \ + KORE_BPF_RET(_action) + +#define KORE_SYSCALL_WITH_FLAG(_name, _arg, _flag, _action) \ + KORE_BPF_CMP(SYS_##_name, 0, 8), \ + KORE_BPF_LOAD(args[(_arg)], ARGS_LO_OFFSET), \ + KORE_BPF_AND(((_flag) & 0xffffffff)), \ + KORE_BPF_CMP(((_flag) & 0xffffffff), 0, 4), \ + KORE_BPF_LOAD(args[(_arg)], ARGS_HI_OFFSET), \ + KORE_BPF_AND((((uint32_t)((uint64_t)(_flag) >> 32)) & 0xffffffff)), \ + KORE_BPF_CMP((((uint32_t)((uint64_t)(_flag) >> 32)) & 0xffffffff), 0, 1), \ + KORE_BPF_RET(_action), \ + KORE_BPF_LOAD(nr, 0) + +#define KORE_SYSCALL_DENY(_name, _errno) \ + KORE_SYSCALL_FILTER(_name, SECCOMP_RET_ERRNO|(_errno)) + +#define KORE_SYSCALL_DENY_WITH_FLAG(_name, _arg, _flag, _errno) \ + KORE_SYSCALL_WITH_FLAG(_name, _arg, _flag, SECCOMP_RET_ERRNO|(_errno)) + +#define KORE_SYSCALL_ALLOW(_name) \ + KORE_SYSCALL_FILTER(_name, SECCOMP_RET_ALLOW) + + +/* + * The bare minimum to be able to run kore. These are added last and can + * be overwritten by a filter program that is added before hand. + */ +static struct sock_filter filter_kore[] = { + /* Deny these, but with EACCESS instead of dying. */ + KORE_SYSCALL_DENY(ioctl, EACCES), + + /* File related. */ + KORE_SYSCALL_ALLOW(read), +#if defined(SYS_stat) + KORE_SYSCALL_ALLOW(stat), +#endif +#if defined(SYS_lstat) + KORE_SYSCALL_ALLOW(lstat), +#endif + KORE_SYSCALL_ALLOW(fstat), + KORE_SYSCALL_ALLOW(write), + KORE_SYSCALL_ALLOW(close), + KORE_SYSCALL_ALLOW(openat), +#if defined(SYS_send) + KORE_SYSCALL_ALLOW(send), +#endif + KORE_SYSCALL_ALLOW(sendto), + KORE_SYSCALL_ALLOW(accept), + KORE_SYSCALL_ALLOW(sendfile), +#if defined(SYS_recv) + KORE_SYSCALL_ALLOW(recv), +#endif + KORE_SYSCALL_ALLOW(recvfrom), + KORE_SYSCALL_ALLOW(setsockopt), +}; + +/* bpf program prologue. */ +static struct sock_filter filter_prologue[] = { + /* Load arch member into accumulator (A) (arch is __u32). */ + KORE_BPF_LOAD(arch, 0), + + /* Compare accumulator against constant, if false jump over kill. */ + KORE_BPF_CMP(SECCOMP_AUDIT_ARCH, 1, 0), + KORE_BPF_RET(SECCOMP_RET_KILL), + + /* Load the system call number into the accumulator. */ + KORE_BPF_LOAD(nr, 0), +}; + +/* bpf program epilogue. */ +static struct sock_filter filter_epilogue[] = { + /* Return hit if no system calls matched our list. */ + BPF_STMT(BPF_RET+BPF_K, SECCOMP_KILL_POLICY) +}; + +#define filter_prologue_len KORE_FILTER_LEN(filter_prologue) +#define filter_epilogue_len KORE_FILTER_LEN(filter_epilogue) + +int kore_seccomp_tracing = 0; + +void kore_seccomp_enable(void) { + struct sock_filter *sf; + struct sock_fprog prog; + size_t prog_len, off, i; + + /* + * If kore_seccomp_tracing is turned on, set the default policy to + * SECCOMP_RET_TRACE so we can log the system calls. + */ + if (kore_seccomp_tracing) { + filter_epilogue[0].k = SECCOMP_RET_TRACE; + puts("seccomp tracing enabled"); + } + + /* Start with the prologue. */ + /* Finally add the epilogue. */ + prog_len = filter_prologue_len + KORE_FILTER_LEN(filter_kore) + filter_epilogue_len ; + + /* Build the entire bpf program now. */ + if ((sf = calloc(prog_len, sizeof(*sf))) == NULL) { + puts("calloc"); + exit(1); + } + + off = 0; + for (i = 0; i < filter_prologue_len; i++) + sf[off++] = filter_prologue[i]; + + for (i = 0; i < KORE_FILTER_LEN(filter_kore); i++) + sf[off++] = filter_kore[i]; + + for (i = 0; i < filter_epilogue_len; i++) + sf[off++] = filter_epilogue[i]; + + /* Lock and load it. */ + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) { + printf("prctl: %s\n", strerror(errno)); + exit(1); + } + + prog.filter = sf; + prog.len = prog_len; + + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) { + printf("prctl: %s\n", strerror(errno)); + exit(1); + } +} + +// ----------------- seccomp end + +bool isDir(const char *path) { + struct stat st; + + if (stat(path, &st) == -1) { + return(false); + } + + if (!S_ISDIR(st.st_mode)) { + return(false); + } + return(true); +} + +int main(int ac, char **av){ + int sock; + struct sockaddr_in server; + int mysock; + char buf[4096]; + int rval; + + char root[PATH_MAX] = {0}; + if (ROOT[0] == '/') { + realpath(ROOT, root); + } + else { + // create absolute path from relative ROOT path + char p[PATH_MAX] = {0}; + getcwd(p, PATH_MAX); + strcat(p, "/"); + strcat(p, ROOT); + realpath(p, root); + strcat(root, "/"); + } + + size_t rootLen = strlen(root); + size_t slash = 0; + + // count slashes at the end of root + // to compare paths with memcmp correctly + // since realpath removes the slashes at the + // end of the path from client. + while(root[rootLen-1-slash] == '/') { + slash++; + } + + sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0){ + perror("Failed to create socket"); + } + + server.sin_family = AF_INET; + server.sin_addr.s_addr = INADDR_ANY; + server.sin_port = htons(PORT); + + /* setsockopt: Handy debugging trick that lets + * us rerun the server immediately after we kill it; + * otherwise we have to wait about 20 secs. + * Eliminates "ERROR on binding: Address already in use" error. + */ + int optval = 1; + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval , sizeof(int)); + + if (bind(sock, (struct sockaddr *) &server, sizeof(server))){ + perror("bind failed"); + exit(1); + } + + listen(sock, SOMAXCONN); + + struct passwd *pw = NULL; + pw = getpwnam(RUNAS); + if (!pw) { + perror("get user info failed"); + exit(1); + } + + if (chroot(CHROOT) == -1) { + perror("cannot chroot"); + exit(1); + } + + if (chdir("/") == -1) { + perror("cannot chdir"); + exit(1); + } + + 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)) { + printf("cannot drop privileges"); + exit(1); + } + + kore_seccomp_enable(); + + printf("Serving "HOSTNAME":%d %s\n", PORT, root); + + // date for request print + char date[50]; + do { + // struct for printing client ip in terminal with inet_ntoa + struct sockaddr_in addr; + socklen_t len = sizeof(addr); + mysock = accept(sock, &addr, &len); + if (mysock == -1) + perror("accept failed"); + else { + // Set 10s timeouts for receive and send (SO_RCVTIMEO and SO_SNDTIMEO) + struct timeval timeout; + timeout.tv_sec = 10; + timeout.tv_usec = 0; + if (setsockopt(mysock, SOL_SOCKET, SO_RCVTIMEO, (void *) &timeout, sizeof(timeout)) < 0) { + perror("receive timeout failed"); + close(mysock); + continue; + } + if (setsockopt(mysock, SOL_SOCKET, SO_SNDTIMEO, (void *) &timeout, sizeof(timeout)) < 0) { + perror("send timeout failed"); + close(mysock); + continue; + } + + // new client + memset(buf, 0, sizeof(buf)); + if ((rval = recv(mysock, buf, sizeof(buf), 0)) < 0) { + perror("reading message"); + close(mysock); + continue; + } + else if (rval == 0) { + puts("Ending connection"); + close(mysock); + continue; + } + + // get YMD HMS date + time_t clk = time(NULL); + struct tm *pClk = localtime(&clk); + strftime(date, sizeof(date), "%Y-%m-%d:%H:%M:%S", pClk); + printf("%s %s ",date, inet_ntoa(addr.sin_addr)); + + // validate request then scan hostname path and content length + + // find request end + char *reqEnd = memmem(buf, sizeof(buf), "\r\n", 2); + if (!reqEnd || buf[0] == ' ') { + puts("4 Invalid request"); + // add MSG_NOSIGNAL flag to ignore SIGPIPE when the client closes the socket early + send(mysock, "4 Invalid request\r\n", sizeof("4 Invalid request\r\n"), MSG_NOSIGNAL); + close(mysock); + continue; + } + + // check ascii + char *cursor = buf; + bool isBad = false; + while (cursor < reqEnd) { + if (*cursor < 32 || *cursor == 127) { + isBad = true; + break; + } + cursor++; + } + if (isBad) { + puts("4 Non ASCII"); + send(mysock, "4 Non ASCII\r\n", sizeof("4 Non ASCII\r\n"), MSG_NOSIGNAL); + close(mysock); + continue; + } + + // print request in terminal + *reqEnd = 0; + printf("%s ", buf); + *reqEnd = '\r'; + + // parse hostname, path and content_length in request + char *hostname; + char *path; + char *content_length; + + hostname = buf; + + // hostname must match HOSTNAME + // comment out this test to accept nay hostname + int c = memcmp(hostname, HOSTNAME, strlen(HOSTNAME)); + + if (c != 0) { + puts("4 Hostname"); + send(mysock, "4 Hostname\r\n", sizeof("4 Hostname\r\n"), MSG_NOSIGNAL); + close(mysock); + continue; + } + + // get path + cursor = buf; + while (*cursor != ' ' && cursor < reqEnd) { + cursor++; + } + + cursor++; + if (cursor >= reqEnd || *cursor == ' ') { + puts("4 Path"); + send(mysock, "4 Path\r\n", sizeof("4 Path\r\n"), MSG_NOSIGNAL); + close(mysock); + continue; + } + + path = cursor; + + // get content_length + while (*cursor != ' ' && cursor < reqEnd) { + cursor++; + } + + cursor++; + if (cursor >= reqEnd || *cursor == ' ') { + puts("4 Length"); + send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL); + close(mysock); + continue; + } + + content_length = cursor; + while(cursor < reqEnd) { + if (!isdigit(*cursor)) { + isBad = true; + break; + } + cursor++; + } + + // the request must not have any content + // content_length = 0 + if (isBad || reqEnd - content_length > 1 || *content_length != '0') { + puts("4 Length"); + send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL); + close(mysock); + continue; + } + + // replace SPC with 0 at the end of path + *(content_length-1) = 0; + + // build server path + char localPath[PATH_MAX] = {0}; + if (rootLen + strlen(path) >= PATH_MAX) { + puts("5 Path too long"); + send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL); + close(mysock); + continue; + } + memcpy(localPath, root, rootLen); + cursor = localPath + rootLen; + memcpy(cursor, path, strlen(path)); + + // check path + char realPath[PATH_MAX] = {0}; + realpath(localPath, realPath); + if (memcmp(realPath, root, rootLen-slash) != 0) { + puts("4 Not found"); + send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL); + close(mysock); + continue; + } + + size_t pathLen = strlen(realPath); + cursor = realPath + pathLen; + if (isDir(realPath)) { + if (pathLen + strlen("/index.gmi") >= PATH_MAX) { + puts("5 Path too long"); + send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL); + close(mysock); + continue; + } + memcpy(cursor, "/index.gmi", strlen("/index.gmi")); + cursor += strlen("/index.gmi"); + } + + FILE *f = fopen(realPath, "r"); + if (!f) { + puts("4 Page not found"); + send(mysock, "4 Page not found\r\n", sizeof("4 Page not found\r\n"), MSG_NOSIGNAL); + close(mysock); + continue; + } + + // request in buf is not needed anymore, reuse buf for response + + // check gemini extension + char *mimetype = "application/octet-stream"; + if (strlen(realPath) > 4 && memcmp(cursor-2, "md", 2) == 0) { + mimetype = "text/markdown"; + } + else if (strlen(realPath) > 5 && memcmp(cursor-3, "gmi", 3) == 0) { + mimetype = "text/gemini"; + } + else if (strlen(realPath) > 10 && memcmp(cursor-2, "markdown", 8) == 0) { + mimetype = "text/markdown"; + } + + int len = sprintf(buf, "2 %s\r\n", mimetype); + if (send(mysock, buf, len, MSG_NOSIGNAL) != -1) { + // print response in terminal + // remove \r\n + buf[len-2] = 0; + puts(buf); + + // send file + size_t fsz; + while (fsz = fread(buf, 1, (size_t)sizeof(buf) , f)) { + ssize_t r = send(mysock, buf, fsz, MSG_NOSIGNAL); + if (r == -1) { + perror("write failed"); + puts("closed socket"); + break; + } + } + } + else { + perror("write failed"); + } + fclose(f); + close(mysock); + } + } while(1); + + puts("Server stopped."); +}