spartserv

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

spartservudp.c (10105B)


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