spartserv

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

spartservPrivDrop.c (17750B)


      1 /*
      2 This server creates a socket listening to PORT and
      3 starts the event loop
      4 2 mimetypes are set depending on file extension:
      5 text/gemini   .gmi
      6 text/markdown .md .markdown
      7 */
      8 
      9 #define _GNU_SOURCE
     10 #include <string.h>
     11 #include <stdio.h>
     12 #include <sys/types.h>
     13 #include <sys/socket.h>
     14 #include <netinet/in.h>
     15 #include <stdlib.h>
     16 #include <unistd.h>
     17 #include <sys/stat.h>
     18 #include <stdbool.h>
     19 #include <ctype.h>
     20 #include <stdarg.h>
     21 #include <limits.h>
     22 #include <time.h>
     23 #include <fcntl.h> // access
     24 
     25 // inet_ntoa
     26 //already included #include <sys/socket.h>
     27 //already included #include <netinet/in.h>
     28 #include <arpa/inet.h>
     29 
     30 // privilege drop
     31 // already included #include <sys/types.h>
     32 #include <pwd.h>
     33 #include <grp.h>
     34 
     35 // seccomp bpf filters
     36 #include <sys/syscall.h>
     37 #include <errno.h>
     38 #include <stddef.h>
     39 #include <linux/seccomp.h>
     40 #include <linux/filter.h> // struct sock_filter
     41 #include <linux/audit.h>
     42 #include <sys/mman.h>
     43 #include <sys/prctl.h>
     44 
     45 // server configuration
     46 #define HOSTNAME "localhost"
     47 #define ROOT "/"
     48 #define PORT 300
     49 
     50 #define RUNAS "spartserv"
     51 #define CHROOT "."
     52 
     53 // ----------------- seccomp setup
     54 // This code is copied and modified from kore http server
     55 // https://kore.io
     56 /*
     57  * Copyright (c) 2013-2022 Joris Vink <joris@coders.se>
     58  *
     59  * Permission to use, copy, modify, and distribute this software for any
     60  * purpose with or without fee is hereby granted, provided that the above
     61  * copyright notice and this permission notice appear in all copies.
     62  *
     63  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
     64  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     65  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     66  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     67  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     68  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     69  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     70  */
     71 
     72 #if __BYTE_ORDER == __LITTLE_ENDIAN
     73 #define ARGS_LO_OFFSET		0
     74 #define ARGS_HI_OFFSET		sizeof(u_int32_t)
     75 #elif __BYTE_ORDER == __BIG_ENDIAN
     76 #define ARGS_LO_OFFSET		sizeof(u_int32_t)
     77 #define ARGS_HI_OFFSET		0
     78 #else
     79 #error "__BYTE_ORDER unknown"
     80 #endif
     81 
     82 // AUDIT_ARCH_X86_64 AUDIT_ARCH_ARM AUDIT_ARCH_AARCH64
     83 #define SECCOMP_AUDIT_ARCH AUDIT_ARCH_X86_64
     84 #define SECCOMP_KILL_POLICY		SECCOMP_RET_KILL
     85 
     86 /* Load field of seccomp_data into accumulator. */
     87 #define KORE_BPF_LOAD(_field, _off)				\
     88     BPF_STMT(BPF_LD+BPF_W+BPF_ABS, offsetof(struct seccomp_data, _field) + _off)
     89 /* Compare the accumulator against a constant (==). */
     90 #define KORE_BPF_CMP(_k, _jt, _jf)			\
     91     BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, _k, _jt, _jf)
     92 
     93 /* Return a constant from a BPF program. */
     94 #define KORE_BPF_RET(_retval)				\
     95     BPF_STMT(BPF_RET+BPF_K, _retval)
     96 
     97 /* AND operation on the accumulator. */
     98 #define KORE_BPF_AND(_k)				\
     99     BPF_STMT(BPF_ALU+BPF_AND+BPF_K, _k)
    100 
    101 /* The length of a filter. */
    102 #define KORE_FILTER_LEN(x)		(sizeof(x) / sizeof(x[0]))
    103 
    104 #define KORE_SYSCALL_FILTER(_name, _action)		\
    105     KORE_BPF_CMP(SYS_##_name, 0, 1),			\
    106     KORE_BPF_RET(_action)
    107 
    108 #define KORE_SYSCALL_WITH_FLAG(_name, _arg, _flag, _action)	\
    109     KORE_BPF_CMP(SYS_##_name, 0, 8),			\
    110     KORE_BPF_LOAD(args[(_arg)], ARGS_LO_OFFSET),	\
    111     KORE_BPF_AND(((_flag) & 0xffffffff)),		\
    112     KORE_BPF_CMP(((_flag) & 0xffffffff), 0, 4),		\
    113     KORE_BPF_LOAD(args[(_arg)], ARGS_HI_OFFSET),	\
    114     KORE_BPF_AND((((uint32_t)((uint64_t)(_flag) >> 32)) & 0xffffffff)),        \
    115     KORE_BPF_CMP((((uint32_t)((uint64_t)(_flag) >> 32)) & 0xffffffff), 0, 1),  \
    116     KORE_BPF_RET(_action),				\
    117     KORE_BPF_LOAD(nr, 0)
    118 
    119 #define KORE_SYSCALL_DENY(_name, _errno)		\
    120     KORE_SYSCALL_FILTER(_name, SECCOMP_RET_ERRNO|(_errno))
    121 
    122 #define KORE_SYSCALL_DENY_WITH_FLAG(_name, _arg, _flag, _errno)	\
    123     KORE_SYSCALL_WITH_FLAG(_name, _arg, _flag, SECCOMP_RET_ERRNO|(_errno))
    124 
    125 #define KORE_SYSCALL_ALLOW(_name)			\
    126     KORE_SYSCALL_FILTER(_name, SECCOMP_RET_ALLOW)
    127 
    128 
    129 /*
    130  * The bare minimum to be able to run kore. These are added last and can
    131  * be overwritten by a filter program that is added before hand.
    132  */
    133 static struct sock_filter filter_kore[] = {
    134 	/* Deny these, but with EACCESS instead of dying. */
    135 	KORE_SYSCALL_DENY(ioctl, EACCES),
    136 
    137 	/* File related. */
    138 	KORE_SYSCALL_ALLOW(read),
    139 #if defined(SYS_stat)
    140 	KORE_SYSCALL_ALLOW(stat),
    141 #endif
    142 #if defined(SYS_lstat)
    143 	KORE_SYSCALL_ALLOW(lstat),
    144 #endif
    145 	KORE_SYSCALL_ALLOW(fstat),
    146 	KORE_SYSCALL_ALLOW(write),
    147 	KORE_SYSCALL_ALLOW(close),
    148 	KORE_SYSCALL_ALLOW(openat),
    149 #if defined(SYS_access)
    150 	KORE_SYSCALL_ALLOW(access),
    151 #endif
    152 #if defined(SYS_send)
    153 	KORE_SYSCALL_ALLOW(send),
    154 #endif
    155 	KORE_SYSCALL_ALLOW(sendto),
    156 	KORE_SYSCALL_ALLOW(accept),
    157 	KORE_SYSCALL_ALLOW(sendfile),
    158 #if defined(SYS_recv)
    159 	KORE_SYSCALL_ALLOW(recv),
    160 #endif
    161 	KORE_SYSCALL_ALLOW(recvfrom),
    162 	KORE_SYSCALL_ALLOW(setsockopt),
    163     // for perror
    164 #if defined(SYS_dup)
    165         KORE_SYSCALL_ALLOW(dup),
    166 #endif
    167 #if defined(SYS_fcntl)
    168         KORE_SYSCALL_ALLOW(fcntl),
    169 #endif
    170 };
    171 
    172 /* bpf program prologue. */
    173 static struct sock_filter filter_prologue[] = {
    174 	/* Load arch member into accumulator (A) (arch is __u32). */
    175 	KORE_BPF_LOAD(arch, 0),
    176 
    177 	/* Compare accumulator against constant, if false jump over kill. */
    178 	KORE_BPF_CMP(SECCOMP_AUDIT_ARCH, 1, 0),
    179 	KORE_BPF_RET(SECCOMP_RET_KILL),
    180 
    181 	/* Load the system call number into the accumulator. */
    182 	KORE_BPF_LOAD(nr, 0),
    183 };
    184 
    185 /* bpf program epilogue. */
    186 static struct sock_filter filter_epilogue[] = {
    187 	/* Return hit if no system calls matched our list. */
    188 	BPF_STMT(BPF_RET+BPF_K, SECCOMP_KILL_POLICY)
    189 };
    190 
    191 #define filter_prologue_len	KORE_FILTER_LEN(filter_prologue)
    192 #define filter_epilogue_len	KORE_FILTER_LEN(filter_epilogue)
    193 
    194 int kore_seccomp_tracing = 0;
    195 
    196 void kore_seccomp_enable(void) {
    197 	struct sock_filter		*sf;
    198 	struct sock_fprog		prog;
    199 	size_t				prog_len, off, i;
    200 
    201 	/*
    202 	 * If kore_seccomp_tracing is turned on, set the default policy to
    203 	 * SECCOMP_RET_TRACE so we can log the system calls.
    204 	 */
    205 	if (kore_seccomp_tracing) {
    206 		filter_epilogue[0].k = SECCOMP_RET_TRACE;
    207 		puts("seccomp tracing enabled");
    208 	}
    209 
    210 	/* Start with the prologue. */
    211 	/* Finally add the epilogue. */
    212 	prog_len = filter_prologue_len + KORE_FILTER_LEN(filter_kore) + filter_epilogue_len ;
    213 
    214 	/* Build the entire bpf program now. */
    215 	if ((sf = calloc(prog_len, sizeof(*sf))) == NULL) {
    216 		puts("calloc");
    217         exit(1);
    218     }
    219 
    220 	off = 0;
    221 	for (i = 0; i < filter_prologue_len; i++)
    222 		sf[off++] = filter_prologue[i];
    223 
    224 	for (i = 0; i < KORE_FILTER_LEN(filter_kore); i++)
    225 		sf[off++] = filter_kore[i];
    226 
    227 	for (i = 0; i < filter_epilogue_len; i++)
    228 		sf[off++] = filter_epilogue[i];
    229 
    230 	/* Lock and load it. */
    231 	if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) {
    232 		printf("prctl: %s\n", strerror(errno));
    233         exit(1);
    234     }
    235 
    236 	prog.filter = sf;
    237 	prog.len = prog_len;
    238 
    239 	if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) {
    240 		printf("prctl: %s\n", strerror(errno));
    241         exit(1);
    242     }
    243 }
    244 
    245 // ----------------- seccomp end
    246 
    247 bool isDir(const char *path) {
    248     struct stat st;
    249 
    250     if (stat(path, &st) == -1) {
    251         return(false);
    252     }
    253 
    254     if (!S_ISDIR(st.st_mode)) {
    255         return(false);
    256     }
    257     return(true);
    258 }
    259 
    260 int main(int ac, char **av){
    261     int sock;
    262     struct sockaddr_in server;
    263     int mysock;
    264     char buf[4096];
    265     int rval;
    266 
    267     char root[PATH_MAX] = {0};
    268     if (ROOT[0] == '/') {
    269         realpath(ROOT, root);
    270     }
    271     else {
    272         // create absolute path from relative ROOT path
    273         char p[PATH_MAX] = {0};
    274         getcwd(p, PATH_MAX);
    275         strcat(p, "/");
    276         strcat(p, ROOT);
    277         realpath(p, root);
    278         strcat(root, "/");
    279     }
    280 
    281     size_t rootLen = strlen(root);
    282     size_t slash = 0;
    283 
    284     // count slashes at the end of root
    285     // to compare paths with memcmp correctly
    286     // since realpath removes the slashes at the
    287     // end of the path from client.
    288     while(root[rootLen-1-slash] == '/') {
    289         slash++;
    290     }
    291 
    292     sock = socket(AF_INET, SOCK_STREAM, 0);
    293     if (sock < 0){
    294         perror("Failed to create socket");
    295     }
    296 
    297     server.sin_family = AF_INET;
    298     server.sin_addr.s_addr = INADDR_ANY;
    299     server.sin_port = htons(PORT);
    300 
    301     /* setsockopt: Handy debugging trick that lets
    302      * us rerun the server immediately after we kill it;
    303      * otherwise we have to wait about 20 secs.
    304      * Eliminates "ERROR on binding: Address already in use" error.
    305      */
    306     int optval = 1;
    307     setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval , sizeof(int));
    308 
    309     if (bind(sock, (struct sockaddr *) &server, sizeof(server))){
    310         perror("bind failed");
    311         exit(1);
    312     }
    313 
    314     listen(sock, SOMAXCONN);
    315 
    316     struct passwd *pw = NULL;
    317     pw = getpwnam(RUNAS);
    318     if (!pw) {
    319         perror("get user info failed");
    320         exit(1);
    321     }
    322 
    323     if (chroot(CHROOT) == -1) {
    324         perror("cannot chroot");
    325         exit(1);
    326     }
    327 
    328     if (chdir("/") == -1) {
    329         perror("cannot chdir");
    330         exit(1);
    331     }
    332 
    333     if (setgroups(1, &pw->pw_gid) ||
    334             setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
    335             setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) {
    336         printf("cannot drop privileges");
    337         exit(1);
    338     }
    339 
    340     kore_seccomp_enable();
    341 
    342     printf("Serving "HOSTNAME":%d %s\n", PORT, root);
    343 
    344     // date for request print
    345     char date[50];
    346     do {
    347         // struct for printing client ip in terminal with inet_ntoa
    348         struct sockaddr_in addr;
    349         socklen_t len = sizeof(addr);
    350         mysock = accept(sock, &addr, &len);
    351         if (mysock == -1)
    352             perror("accept failed");
    353         else {
    354             // Set 10s timeouts for receive and send (SO_RCVTIMEO and SO_SNDTIMEO)
    355             struct timeval timeout;
    356             timeout.tv_sec = 10;
    357             timeout.tv_usec = 0;
    358             // get YMD HMS date
    359             time_t clk = time(NULL);
    360             struct tm *pClk = localtime(&clk);
    361             strftime(date, sizeof(date), "%Y-%m-%d:%H:%M:%S", pClk);
    362             if (setsockopt(mysock, SOL_SOCKET, SO_RCVTIMEO, (void *) &timeout, sizeof(timeout)) < 0) {
    363             	printf("%s %s ",date, inet_ntoa(addr.sin_addr));
    364                 perror("receive timeout failed");
    365                 close(mysock);
    366                 continue;
    367             }
    368             if (setsockopt(mysock, SOL_SOCKET, SO_SNDTIMEO, (void *) &timeout, sizeof(timeout)) < 0) {
    369             	printf("%s %s ",date, inet_ntoa(addr.sin_addr));
    370                 perror("send timeout failed");
    371                 close(mysock);
    372                 continue;
    373             }
    374 
    375             // new client
    376             memset(buf, 0, sizeof(buf));
    377             if ((rval = recv(mysock, buf, sizeof(buf), 0)) < 0) {
    378             	printf("%s %s ",date, inet_ntoa(addr.sin_addr));
    379                 perror("reading message");
    380                 close(mysock);
    381                 continue;
    382             }
    383             else if (rval == 0) {
    384             	printf("%s %s ",date, inet_ntoa(addr.sin_addr));
    385                 puts("Ending connection");
    386                 close(mysock);
    387                 continue;
    388             }
    389 
    390             printf("%s %s ",date, inet_ntoa(addr.sin_addr));
    391 
    392             // validate request then scan hostname path and content length
    393 
    394             // find request end
    395             char *reqEnd = memmem(buf, sizeof(buf), "\r\n", 2);
    396             if (!reqEnd || buf[0] == ' ') {
    397                 puts("4 Invalid request");
    398                 // add MSG_NOSIGNAL flag to ignore SIGPIPE when the client closes the socket early
    399                 send(mysock, "4 Invalid request\r\n", sizeof("4 Invalid request\r\n"), MSG_NOSIGNAL);
    400                 close(mysock);
    401                 continue;
    402             }
    403 
    404             // check ascii
    405             char *cursor = buf;
    406             bool isBad = false;
    407             while (cursor < reqEnd) {
    408                 if (*cursor < 32 || *cursor == 127) {
    409                     isBad = true;
    410                     break;
    411                 }
    412                 cursor++;
    413             }
    414             if (isBad) {
    415                 puts("4 Non ASCII");
    416                 send(mysock, "4 Non ASCII\r\n", sizeof("4 Non ASCII\r\n"), MSG_NOSIGNAL);
    417                 close(mysock);
    418                 continue;
    419             }
    420 
    421             // print request in terminal
    422             *reqEnd = 0;
    423             printf("%s ", buf);
    424             *reqEnd = '\r';
    425 
    426             // parse hostname, path and content_length in request
    427             char *hostname;
    428             char *path;
    429             char *content_length;
    430 
    431             hostname = buf;
    432 
    433             // hostname must match HOSTNAME
    434             // comment out this test to accept nay hostname
    435             int c = memcmp(hostname, HOSTNAME, strlen(HOSTNAME));
    436 
    437             if (c != 0) {
    438                 puts("4 Hostname");
    439                 send(mysock, "4 Hostname\r\n", sizeof("4 Hostname\r\n"), MSG_NOSIGNAL);
    440                 close(mysock);
    441                 continue;
    442             }
    443 
    444             // get path
    445             cursor = buf;
    446             while (*cursor != ' ' && cursor < reqEnd) {
    447                 cursor++;
    448             }
    449 
    450             cursor++;
    451             if (cursor >= reqEnd || *cursor == ' ') {
    452                 puts("4 Path");
    453                 send(mysock, "4 Path\r\n", sizeof("4 Path\r\n"), MSG_NOSIGNAL);
    454                 close(mysock);
    455                 continue;
    456             }
    457 
    458             path = cursor;
    459 
    460             // get content_length
    461             while (*cursor != ' ' && cursor < reqEnd) {
    462                 cursor++;
    463             }
    464 
    465             cursor++;
    466             if (cursor >= reqEnd || *cursor == ' ') {
    467                 puts("4 Length");
    468                 send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL);
    469                 close(mysock);
    470                 continue;
    471             }
    472 
    473             content_length = cursor;
    474             while(cursor < reqEnd) {
    475                 if (!isdigit(*cursor)) {
    476                     isBad = true;
    477                     break;
    478                 }
    479                 cursor++;
    480             }
    481 
    482             // the request must not have any content
    483             // content_length = 0
    484             if (isBad || reqEnd - content_length > 1 || *content_length != '0') {
    485                 puts("4 Length");
    486                 send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL);
    487                 close(mysock);
    488                 continue;
    489             }
    490 
    491             // replace SPC with 0 at the end of path
    492             *(content_length-1) = 0;
    493 
    494             // build server path
    495             char localPath[PATH_MAX] = {0};
    496             if (rootLen + strlen(path) >= PATH_MAX) {
    497                 puts("5 Path too long");
    498                 send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL);
    499                 close(mysock);
    500                 continue;
    501             }
    502             memcpy(localPath, root, rootLen);
    503             cursor = localPath + rootLen;
    504             memcpy(cursor, path, strlen(path));
    505 
    506             // check path
    507             if (access(localPath, R_OK) == -1) {
    508                 puts("4 Not found");
    509                 send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL);
    510                 close(mysock);
    511                 continue;
    512             }
    513             char realPath[PATH_MAX] = {0};
    514             realpath(localPath, realPath);
    515             if (memcmp(realPath, root, rootLen-slash) != 0) {
    516                 puts("4 Not found");
    517                 send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL);
    518                 close(mysock);
    519                 continue;
    520             }
    521 
    522             size_t pathLen = strlen(realPath);
    523             cursor = realPath + pathLen;
    524             if (isDir(realPath)) {
    525                 if (pathLen + strlen("/index.gmi") >= PATH_MAX) {
    526                     puts("5 Path too long");
    527                     send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL);
    528                     close(mysock);
    529                     continue;
    530                 }
    531                 memcpy(cursor, "/index.gmi", strlen("/index.gmi"));
    532                 cursor += strlen("/index.gmi");
    533             }
    534 
    535             FILE *f = fopen(realPath, "r");
    536             if (!f) {
    537                 puts("4 Page not found");
    538                 send(mysock, "4 Page not found\r\n", sizeof("4 Page not found\r\n"), MSG_NOSIGNAL);
    539                 close(mysock);
    540                 continue;
    541             }
    542 
    543             // request in buf is not needed anymore, reuse buf for response
    544 
    545             // check gemini extension
    546             char *mimetype = "application/octet-stream";
    547             if (strlen(realPath) > 4 && memcmp(cursor-2, "md", 2) == 0) {
    548                 mimetype = "text/markdown";
    549             }
    550             else if (strlen(realPath) > 5 && memcmp(cursor-3, "gmi", 3) == 0) {
    551                 mimetype = "text/gemini";
    552             }
    553             else if (strlen(realPath) > 10 && memcmp(cursor-2, "markdown", 8) == 0) {
    554                 mimetype = "text/markdown";
    555             }
    556 
    557             int len = sprintf(buf, "2 %s\r\n", mimetype);
    558             if (send(mysock, buf, len, MSG_NOSIGNAL) != -1) {
    559                 // print response in terminal
    560                 // remove \r\n
    561                 buf[len-2] = 0;
    562                 puts(buf);
    563 
    564                 // send file
    565                 size_t fsz;
    566                 while (fsz = fread(buf, 1, (size_t)sizeof(buf) , f)) {
    567                     ssize_t r = send(mysock, buf, fsz, MSG_NOSIGNAL);
    568                     if (r == -1) {
    569                         perror("write failed");
    570                         puts("closed socket");
    571                         break;
    572                     }
    573                 }
    574             }
    575             else {
    576                 perror("write failed");
    577             }
    578             fclose(f);
    579             close(mysock);
    580         }
    581     } while(1);
    582 
    583     puts("Server stopped.");
    584 }