spartserv

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 }