💾 Archived View for gmi.noulin.net › gitRepositories › spartserv › file › spartserv.c.gmi captured on 2023-01-29 at 13:30:29. Gemini links have been rewritten to link to archived content

View Raw

More Information

➡️ Next capture (2023-07-10)

-=-=-=-=-=-=-

spartserv

Log

Files

Refs

README

spartserv.c (10681B)

     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             if (setsockopt(mysock, SOL_SOCKET, SO_RCVTIMEO, (void *) &timeout, sizeof(timeout)) < 0) {
   123                 perror("receive timeout failed");
   124                 close(mysock);
   125                 continue;
   126             }
   127             if (setsockopt(mysock, SOL_SOCKET, SO_SNDTIMEO, (void *) &timeout, sizeof(timeout)) < 0) {
   128                 perror("send timeout failed");
   129                 close(mysock);
   130                 continue;
   131             }
   132 
   133             // new client
   134             memset(buf, 0, sizeof(buf));
   135             if ((rval = recv(mysock, buf, sizeof(buf), 0)) < 0) {
   136                 perror("reading message");
   137                 close(mysock);
   138                 continue;
   139             }
   140             else if (rval == 0) {
   141                 puts("Ending connection");
   142                 close(mysock);
   143                 continue;
   144             }
   145 
   146             // get YMD HMS date
   147             time_t clk = time(NULL);
   148             struct tm *pClk = localtime(&clk);
   149             strftime(date, sizeof(date), "%Y-%m-%d:%H:%M:%S", pClk);
   150             printf("%s %s ",date, inet_ntoa(addr.sin_addr));
   151 
   152             // validate request then scan hostname path and content length
   153 
   154             // find request end
   155             char *reqEnd = memmem(buf, sizeof(buf), "\r\n", 2);
   156             if (!reqEnd || buf[0] == ' ') {
   157                 puts("4 Invalid request");
   158                 // add MSG_NOSIGNAL flag to ignore SIGPIPE when the client closes the socket early
   159                 send(mysock, "4 Invalid request\r\n", sizeof("4 Invalid request\r\n"), MSG_NOSIGNAL);
   160                 close(mysock);
   161                 continue;
   162             }
   163 
   164             // check ascii
   165             char *cursor = buf;
   166             bool isBad = false;
   167             while (cursor < reqEnd) {
   168                 if (*cursor < 32 || *cursor == 127) {
   169                     isBad = true;
   170                     break;
   171                 }
   172                 cursor++;
   173             }
   174             if (isBad) {
   175                 puts("4 Non ASCII");
   176                 send(mysock, "4 Non ASCII\r\n", sizeof("4 Non ASCII\r\n"), MSG_NOSIGNAL);
   177                 close(mysock);
   178                 continue;
   179             }
   180 
   181             // print request in terminal
   182             *reqEnd = 0;
   183             printf("%s ", buf);
   184             *reqEnd = '\r';
   185 
   186             // parse hostname, path and content_length in request
   187             char *hostname;
   188             char *path;
   189             char *content_length;
   190 
   191             hostname = buf;
   192 
   193             // hostname must match HOSTNAME
   194             // comment out this test to accept nay hostname
   195             int c = memcmp(hostname, HOSTNAME, strlen(HOSTNAME));
   196 
   197             if (c != 0) {
   198                 puts("4 Hostname");
   199                 send(mysock, "4 Hostname\r\n", sizeof("4 Hostname\r\n"), MSG_NOSIGNAL);
   200                 close(mysock);
   201                 continue;
   202             }
   203 
   204             // get path
   205             cursor = buf;
   206             while (*cursor != ' ' && cursor < reqEnd) {
   207                 cursor++;
   208             }
   209 
   210             cursor++;
   211             if (cursor >= reqEnd || *cursor == ' ') {
   212                 puts("4 Path");
   213                 send(mysock, "4 Path\r\n", sizeof("4 Path\r\n"), MSG_NOSIGNAL);
   214                 close(mysock);
   215                 continue;
   216             }
   217 
   218             path = cursor;
   219 
   220             // get content_length
   221             while (*cursor != ' ' && cursor < reqEnd) {
   222                 cursor++;
   223             }
   224 
   225             cursor++;
   226             if (cursor >= reqEnd || *cursor == ' ') {
   227                 puts("4 Length");
   228                 send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL);
   229                 close(mysock);
   230                 continue;
   231             }
   232 
   233             content_length = cursor;
   234             while(cursor < reqEnd) {
   235                 if (!isdigit(*cursor)) {
   236                     isBad = true;
   237                     break;
   238                 }
   239                 cursor++;
   240             }
   241 
   242             // the request must not have any content
   243             // content_length = 0
   244             if (isBad || reqEnd - content_length > 1 || *content_length != '0') {
   245                 puts("4 Length");
   246                 send(mysock, "4 Length\r\n", sizeof("4 Length\r\n"), MSG_NOSIGNAL);
   247                 close(mysock);
   248                 continue;
   249             }
   250 
   251             // replace SPC with 0 at the end of path
   252             *(content_length-1) = 0;
   253 
   254             // build server path
   255             char localPath[PATH_MAX] = {0};
   256             if (rootLen + strlen(path) >= PATH_MAX) {
   257                 puts("5 Path too long");
   258                 send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL);
   259                 close(mysock);
   260                 continue;
   261             }
   262             memcpy(localPath, root, rootLen);
   263             cursor = localPath + rootLen;
   264             memcpy(cursor, path, strlen(path));
   265 
   266             // check path
   267             if (access(localPath, R_OK) == -1) {
   268                 puts("4 Not found");
   269                 send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL);
   270                 close(mysock);
   271                 continue;
   272             }
   273             char realPath[PATH_MAX] = {0};
   274             realpath(localPath, realPath);
   275             if (memcmp(realPath, root, rootLen-slash) != 0) {
   276                 puts("4 Not found");
   277                 send(mysock, "4 Not found\r\n", sizeof("4 Not found\r\n"), MSG_NOSIGNAL);
   278                 close(mysock);
   279                 continue;
   280             }
   281 
   282             size_t pathLen = strlen(realPath);
   283             cursor = realPath + pathLen;
   284             if (isDir(realPath)) {
   285                 if (pathLen + strlen("/index.gmi") >= PATH_MAX) {
   286                     puts("5 Path too long");
   287                     send(mysock, "5 Path too long\r\n", sizeof("5 Path too long\r\n"), MSG_NOSIGNAL);
   288                     close(mysock);
   289                     continue;
   290                 }
   291                 memcpy(cursor, "/index.gmi", strlen("/index.gmi"));
   292                 cursor += strlen("/index.gmi");
   293             }
   294 
   295             FILE *f = fopen(realPath, "r");
   296             if (!f) {
   297                 puts("4 Page not found");
   298                 send(mysock, "4 Page not found\r\n", sizeof("4 Page not found\r\n"), MSG_NOSIGNAL);
   299                 close(mysock);
   300                 continue;
   301             }
   302 
   303             // request in buf is not needed anymore, reuse buf for response
   304 
   305             // check gemini extension
   306             char *mimetype = "application/octet-stream";
   307             if (strlen(realPath) > 4 && memcmp(cursor-2, "md", 2) == 0) {
   308                 mimetype = "text/markdown";
   309             }
   310             else if (strlen(realPath) > 5 && memcmp(cursor-3, "gmi", 3) == 0) {
   311                 mimetype = "text/gemini";
   312             }
   313             else if (strlen(realPath) > 10 && memcmp(cursor-2, "markdown", 8) == 0) {
   314                 mimetype = "text/markdown";
   315             }
   316 
   317             int len = sprintf(buf, "2 %s\r\n", mimetype);
   318             if (send(mysock, buf, len, MSG_NOSIGNAL) != -1) {
   319                 // print response in terminal
   320                 // remove \r\n
   321                 buf[len-2] = 0;
   322                 puts(buf);
   323 
   324                 // send file
   325                 size_t fsz;
   326                 while (fsz = fread(buf, 1, (size_t)sizeof(buf) , f)) {
   327                     ssize_t r = send(mysock, buf, fsz, MSG_NOSIGNAL);
   328                     if (r == -1) {
   329                         perror("write failed");
   330                         puts("closed socket");
   331                         break;
   332                     }
   333                 }
   334             }
   335             else {
   336                 perror("write failed");
   337             }
   338             fclose(f);
   339             close(mysock);
   340         }
   341     } while(1);
   342 
   343     puts("Server stopped.");
   344 }