spartserv

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

spartserv.c (10929B)


      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/types.h>
     18 #include <sys/stat.h>
     19 #include <stdbool.h>
     20 #include <ctype.h>
     21 #include <stdarg.h>
     22 #include <limits.h>
     23 #include <time.h>
     24 #include <fcntl.h> // access
     25 
     26 // inet_ntoa
     27 //already included #include <sys/socket.h>
     28 //already included #include <netinet/in.h>
     29 #include <arpa/inet.h>
     30 
     31 
     32 // server configuration
     33 #define HOSTNAME "localhost"
     34 #define ROOT "."
     35 #define PORT 3000
     36 
     37 bool isDir(const char *path) {
     38     struct stat st;
     39 
     40     if (stat(path, &st) == -1) {
     41         return(false);
     42     }
     43 
     44     if (!S_ISDIR(st.st_mode)) {
     45         return(false);
     46     }
     47     return(true);
     48 }
     49 
     50 int main(int ac, char **av){
     51     int sock;
     52     struct sockaddr_in server;
     53     int mysock;
     54     char buf[4096];
     55     int rval;
     56 
     57     char root[PATH_MAX] = {0};
     58     if (ROOT[0] == '/') {
     59         realpath(ROOT, root);
     60     }
     61     else {
     62         // create absolute path from relative ROOT path
     63         char p[PATH_MAX] = {0};
     64         getcwd(p, PATH_MAX);
     65         strcat(p, "/");
     66         strcat(p, ROOT);
     67         realpath(p, root);
     68         strcat(root, "/");
     69     }
     70 
     71     size_t rootLen = strlen(root);
     72     size_t slash = 0;
     73 
     74     // count slashes at the end of root
     75     // to compare paths with memcmp correctly
     76     // since realpath removes the slashes at the
     77     // end of the path from client.
     78     while(root[rootLen-1-slash] == '/') {
     79         slash++;
     80     }
     81 
     82     sock = socket(AF_INET, SOCK_STREAM, 0);
     83     if (sock < 0){
     84         perror("Failed to create socket");
     85     }
     86 
     87     server.sin_family = AF_INET;
     88     server.sin_addr.s_addr = INADDR_ANY;
     89     server.sin_port = htons(PORT);
     90 
     91     /* setsockopt: Handy debugging trick that lets
     92      * us rerun the server immediately after we kill it;
     93      * otherwise we have to wait about 20 secs.
     94      * Eliminates "ERROR on binding: Address already in use" error.
     95      */
     96     int optval = 1;
     97     setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval , sizeof(int));
     98 
     99     if (bind(sock, (struct sockaddr *) &server, sizeof(server))){
    100         perror("bind failed");
    101         exit(1);
    102     }
    103 
    104     listen(sock, SOMAXCONN);
    105 
    106     printf("Serving "HOSTNAME":%d %s\n", PORT, root);
    107 
    108     // date for request print
    109     char date[50];
    110     do {
    111         // struct for printing client ip in terminal with inet_ntoa
    112         struct sockaddr_in addr;
    113         socklen_t len = sizeof(addr);
    114         mysock = accept(sock, &addr, &len);
    115         if (mysock == -1)
    116             perror("accept failed");
    117         else {
    118             // Set 10s timeouts for receive and send (SO_RCVTIMEO and SO_SNDTIMEO)
    119             struct timeval timeout;
    120             timeout.tv_sec = 10;
    121             timeout.tv_usec = 0;
    122             // get YMD HMS date
    123             time_t clk = time(NULL);
    124             struct tm *pClk = localtime(&clk);
    125             strftime(date, sizeof(date), "%Y-%m-%d:%H:%M:%S", pClk);
    126             if (setsockopt(mysock, SOL_SOCKET, SO_RCVTIMEO, (void *) &timeout, sizeof(timeout)) < 0) {
    127             	printf("%s %s ",date, inet_ntoa(addr.sin_addr));
    128                 perror("receive timeout failed");
    129                 close(mysock);
    130                 continue;
    131             }
    132             if (setsockopt(mysock, SOL_SOCKET, SO_SNDTIMEO, (void *) &timeout, sizeof(timeout)) < 0) {
    133             	printf("%s %s ",date, inet_ntoa(addr.sin_addr));
    134                 perror("send timeout failed");
    135                 close(mysock);
    136                 continue;
    137             }
    138 
    139             // new client
    140             memset(buf, 0, sizeof(buf));
    141             if ((rval = recv(mysock, buf, sizeof(buf), 0)) < 0) {
    142             	printf("%s %s ",date, inet_ntoa(addr.sin_addr));
    143                 perror("reading message");
    144                 close(mysock);
    145                 continue;
    146             }
    147             else if (rval == 0) {
    148             	printf("%s %s ",date, inet_ntoa(addr.sin_addr));
    149                 puts("Ending connection");
    150                 close(mysock);
    151                 continue;
    152             }
    153 
    154             printf("%s %s ",date, inet_ntoa(addr.sin_addr));
    155 
    156             // validate request then scan hostname path and content length
    157 
    158             // find request end
    159             char *reqEnd = memmem(buf, sizeof(buf), "\r\n", 2);
    160             if (!reqEnd || buf[0] == ' ') {
    161                 puts("4 Invalid request");
    162                 // add MSG_NOSIGNAL flag to ignore SIGPIPE when the client closes the socket early
    163                 send(mysock, "4 Invalid request\r\n", sizeof("4 Invalid request\r\n"), MSG_NOSIGNAL);
    164                 close(mysock);
    165                 continue;
    166             }
    167 
    168             // check ascii
    169             char *cursor = buf;
    170             bool isBad = false;
    171             while (cursor < reqEnd) {
    172                 if (*cursor < 32 || *cursor == 127) {
    173                     isBad = true;
    174                     break;
    175                 }
    176                 cursor++;
    177             }
    178             if (isBad) {
    179                 puts("4 Non ASCII");
    180                 send(mysock, "4 Non ASCII\r\n", sizeof("4 Non ASCII\r\n"), MSG_NOSIGNAL);
    181                 close(mysock);
    182                 continue;
    183             }
    184 
    185             // print request in terminal
    186             *reqEnd = 0;
    187             printf("%s ", buf);
    188             *reqEnd = '\r';
    189 
    190             // parse hostname, path and content_length in request
    191             char *hostname;
    192             char *path;
    193             char *content_length;
    194 
    195             hostname = buf;
    196 
    197             // hostname must match HOSTNAME
    198             // comment out this test to accept nay hostname
    199             int c = memcmp(hostname, HOSTNAME, strlen(HOSTNAME));
    200 
    201             if (c != 0) {
    202                 puts("4 Hostname");
    203                 send(mysock, "4 Hostname\r\n", sizeof("4 Hostname\r\n"), MSG_NOSIGNAL);
    204                 close(mysock);
    205                 continue;
    206             }
    207 
    208             // get path
    209             cursor = buf;
    210             while (*cursor != ' ' && cursor < reqEnd) {
    211                 cursor++;
    212             }
    213 
    214             cursor++;
    215             if (cursor >= reqEnd || *cursor == ' ') {
    216                 puts("4 Path");
    217                 send(mysock, "4 Path\r\n", sizeof("4 Path\r\n"), MSG_NOSIGNAL);
    218                 close(mysock);
    219                 continue;
    220             }
    221 
    222             path = cursor;
    223 
    224             // get content_length
    225             while (*cursor != ' ' && cursor < reqEnd) {
    226                 cursor++;
    227             }
    228 
    229             cursor++;
    230             if (cursor >= reqEnd || *cursor == ' ') {
    231                 puts("4 Length");
    232                 send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL);
    233                 close(mysock);
    234                 continue;
    235             }
    236 
    237             content_length = cursor;
    238             while(cursor < reqEnd) {
    239                 if (!isdigit(*cursor)) {
    240                     isBad = true;
    241                     break;
    242                 }
    243                 cursor++;
    244             }
    245 
    246             // the request must not have any content
    247             // content_length = 0
    248             if (isBad || reqEnd - content_length > 1 || *content_length != '0') {
    249                 puts("4 Length");
    250                 send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL);
    251                 close(mysock);
    252                 continue;
    253             }
    254 
    255             // replace SPC with 0 at the end of path
    256             *(content_length-1) = 0;
    257 
    258             // build server path
    259             char localPath[PATH_MAX] = {0};
    260             if (rootLen + strlen(path) >= PATH_MAX) {
    261                 puts("5 Path too long");
    262                 send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL);
    263                 close(mysock);
    264                 continue;
    265             }
    266             memcpy(localPath, root, rootLen);
    267             cursor = localPath + rootLen;
    268             memcpy(cursor, path, strlen(path));
    269 
    270             // check path
    271             if (access(localPath, R_OK) == -1) {
    272                 puts("4 Not found");
    273                 send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL);
    274                 close(mysock);
    275                 continue;
    276             }
    277             char realPath[PATH_MAX] = {0};
    278             realpath(localPath, realPath);
    279             if (memcmp(realPath, root, rootLen-slash) != 0) {
    280                 puts("4 Not found");
    281                 send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL);
    282                 close(mysock);
    283                 continue;
    284             }
    285 
    286             size_t pathLen = strlen(realPath);
    287             cursor = realPath + pathLen;
    288             if (isDir(realPath)) {
    289                 if (pathLen + strlen("/index.gmi") >= PATH_MAX) {
    290                     puts("5 Path too long");
    291                     send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL);
    292                     close(mysock);
    293                     continue;
    294                 }
    295                 memcpy(cursor, "/index.gmi", strlen("/index.gmi"));
    296                 cursor += strlen("/index.gmi");
    297             }
    298 
    299             FILE *f = fopen(realPath, "r");
    300             if (!f) {
    301                 puts("4 Page not found");
    302                 send(mysock, "4 Page not found\r\n", sizeof("4 Page not found\r\n"), MSG_NOSIGNAL);
    303                 close(mysock);
    304                 continue;
    305             }
    306 
    307             // request in buf is not needed anymore, reuse buf for response
    308 
    309             // check gemini extension
    310             char *mimetype = "application/octet-stream";
    311             if (strlen(realPath) > 4 && memcmp(cursor-2, "md", 2) == 0) {
    312                 mimetype = "text/markdown";
    313             }
    314             else if (strlen(realPath) > 5 && memcmp(cursor-3, "gmi", 3) == 0) {
    315                 mimetype = "text/gemini";
    316             }
    317             else if (strlen(realPath) > 10 && memcmp(cursor-2, "markdown", 8) == 0) {
    318                 mimetype = "text/markdown";
    319             }
    320 
    321             int len = sprintf(buf, "2 %s\r\n", mimetype);
    322             if (send(mysock, buf, len, MSG_NOSIGNAL) != -1) {
    323                 // print response in terminal
    324                 // remove \r\n
    325                 buf[len-2] = 0;
    326                 puts(buf);
    327 
    328                 // send file
    329                 size_t fsz;
    330                 while (fsz = fread(buf, 1, (size_t)sizeof(buf) , f)) {
    331                     ssize_t r = send(mysock, buf, fsz, MSG_NOSIGNAL);
    332                     if (r == -1) {
    333                         perror("write failed");
    334                         puts("closed socket");
    335                         break;
    336                     }
    337                 }
    338             }
    339             else {
    340                 perror("write failed");
    341             }
    342             fclose(f);
    343             close(mysock);
    344         }
    345     } while(1);
    346 
    347     puts("Server stopped.");
    348 }