/* xcontents.c
   XContents is a minimal fileserver over http.
   It reads and writes files only in the directory where is is started, and its subdirectories.

   Methods
   The following methods are supported.
   GET: If the specified resource exists, it is returned.
   HEAD: Like GET, except only the headers are returned, with no content.
   PUT: The file is created or overwritten.
   POST: 
      If the resource does not exist, it is created, with the content surrounded by the tags <post>...</post>.
      If it exists, the content is appended before the last closing tag, if there is one, and otherwise at the end.
      Therefore, if you don't like the tags <post>...</post> when the file is created, 
         first PUT an empty file, or one containing only the open and closing tags, e.g. <data></data>,
         before you POST to it.
   OPTIONS: Access control allows access from anywhere.

   Errors
   404: File not found for GET or HEAD
   403: No access to directories (for reading or writing)
   403: File can't be opened for writing for a PUT or POST
   400: Bad request, if the URI contains "/../" (Most browsers pre-process ".." path steps, so this should never arise.)
   501: Not implemented, for other methods.

   Mostly not robust or secure. 
   For demonstration and test purposes only: do not use for production.
   Based originally on tiny.c by Dave O'Hallaron, Carnegie Mellon

   compile: cc xcontents.c -o xcontents
   usage:   xcontents <port>

   Advice: if you create a user for it, and make that user the owner of the binary, and set uid, 
           then it can only overwrite files it creates itself.
*/

/* Sorry, not supported:
 
  An example absolute-form of request-line would be:

     GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1

   To allow for transition to the absolute-form for all requests in some
   future version of HTTP, a server MUST accept the absolute-form in
   requests, even though HTTP/1.1 clients will only send them in
   requests to proxies.
*/

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <netdb.h>
#include <fcntl.h>
#include <sys/types.h> 
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h> /* ? */
#include <netinet/in.h> /* ? */
#include <arpa/inet.h>
#include <signal.h>

#define BUFSIZE 1024
#define SERVER "XContents Web Server"

#define LOG printf

/* For storing the incoming content header heading, should we ever need it */
char content_type[BUFSIZE];
/* For storing the value of the incoming cookie */
char cookie_value[BUFSIZE];
#define COOKIE "XContents"
int add_cookie=0;
int delete_cookie=0;

/* halt - when syscalls fail */
void halt(char *message) {
   perror(message);
   exit(1);
}

/* Annoyingly fclose() can terminate the program if the connection has been closed... */
void sigpipe_handler(int unused)
{
   LOG("Connection unexpectedly closed\n");
}

/* Set a session cookie with
      Set-Cookie: name=value;
              or  name="value";
   Cookie expires at the end of the 'session'.
   To specify an expiry date and time add
         Expires=<date>;
   Stupid date format Expires=Thu, 01 Jan 1970 00:00:00 GMT;
   Must be GMT.
   or
         Max-Age: <seconds>;
   86400 is one day.
     
   Delete a cookie with 
     Set-Cookie: name=value; Max-Age=0;
*/

void cookie_delete(FILE *stream, char *cookie) {
   fprintf(stream, "Set-Cookie: %s=0; Max-Age: 0;\n", cookie);
   delete_cookie= 0;
}

void cookie_add(FILE *stream, char *cookie, char *value, char *limit) {
   if (limit) {
      fprintf(stream, "Set-Cookie: %s=%s; %s;\n", cookie, value, limit);
   } else {
      fprintf(stream, "Set-Cookie: %s=%s;\n", cookie, value);
   }
   add_cookie= 0;
}

void cookie_read(char *buf, char *name, char *value) {
   char *place;
   char format[BUFSIZE];

   sprintf(format, " %s=", name);
   place=strstr(buf, format);
   if (place) {
      sprintf(format, " %s=%%[^ ;]", name);
      sscanf(place, format, value);
      LOG("... cookie=%s\n", value);
   }
}

/* error - returns an error message to the client */
void error(FILE *stream, char *shortmsg, char *longmsg, char *cause) {
   fprintf(stream, "HTTP/1.0 %s\n", shortmsg);
   fprintf(stream, "Content-type: text/html\n\n");
   fprintf(stream, "<html><head><title>Server Error</title></head><body>");
   fprintf(stream, "<h1>%s</h1>", shortmsg);
   fprintf(stream, "<p>%s: %s</p>", longmsg, cause);
   fprintf(stream, "<hr><p><em>%s</em></p></body></html>\n", SERVER);
   LOG("=> %s %s\n\n", shortmsg, cause);
}

void respond(FILE *stream, char *response, char *h1, char *h2, char *h3, char *h4) {
   fprintf(stream, "HTTP/1.0 %s\n", response);
   fprintf(stream, "Server: %s\n", SERVER);
   if (add_cookie) cookie_add(stream, COOKIE, cookie_value, NULL);
   else if (delete_cookie) cookie_delete(stream, COOKIE);
   if (h1) fprintf(stream, "%s\n", h1);
   if (h2) fprintf(stream, "%s\n", h2);
   if (h3) fprintf(stream, "%s\n", h3);
   if (h4) fprintf(stream, "%s\n", h4);
   fprintf(stream, "\r\n"); 
   fflush(stream);
   LOG("=> %s\n\n", response);
}

void respondOK(FILE *stream, int content_length, char *content_type) {
   fprintf(stream, "HTTP/1.0 200 OK\n");
   fprintf(stream, "Server: %s\n", SERVER);
   if (add_cookie) cookie_add(stream, COOKIE, cookie_value, NULL);
   else if (delete_cookie) cookie_delete(stream, COOKIE);
   fprintf(stream, "Content-length: %d\n", content_length);
   fprintf(stream, "Content-type: %s\n", content_type);
   fprintf(stream, "\r\n"); 
   fflush(stream);
   LOG("=> 200 %s %d\n\n", content_type, content_length);
}

/* These are the default mediatypes. 
   You can override them with a mediatypes.txt file in the server root directory. */

char *mediatypes= "\
   .txt text/plain\n\
   .html text/html\n\
   .css text/css\n\
   .xml text/xml\n\
   .xsd text/xml\n\
   .xsl text/xsl\n\
   .xhtml application/xhtml+xml\n\
   .js application/javascript\n\
   .gif image/gif\n\
   .jpg image/jpg\n\
   .png image/png\n\
   .svg image/svg+xml\n";

void read_mediatypes() {
   int mt;
   struct stat mtbuf;
   
   if (stat("mediatypes.txt", &mtbuf) < 0 || (mt= open("mediatypes.txt", O_RDONLY)) == -1) {
      LOG("Can't open mediatypes.txt; using defaults\n");
   } else {
      mediatypes= mmap(0, mtbuf.st_size, PROT_READ, MAP_PRIVATE, mt, 0);
   }
}

char *mediatype(char *filename) {
   char *found, *x, *y, *p;
   char *ext= rindex(filename, '.');
   static char mtype[BUFSIZE];

   if (ext!=NULL) found= strstr(mediatypes, ext);
   if (ext==NULL || found == NULL) found= mediatypes; /* The first entry in the mediatypes file is used as default */
   x= strchr(found, ' '); x++;
   p= mtype;
   while (*x!='\n') {
      *p= *x;
      p++; x++;
   }
   *p='\0';
   return mtype;
}

int openport(int portno) {
   int parentfd;
   int optval;        /* flag value for setsockopt */
   static struct sockaddr_in serveraddr; /* server's addr */

   /* open socket descriptor */
   parentfd = socket(AF_INET, SOCK_STREAM, 0);
   if (parentfd < 0) halt("ERROR opening socket");

   /* allows us to restart server immediately */
   optval = 1;
   setsockopt(parentfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&optval, sizeof(int));

   /* bind port to socket */
   bzero((char *) &serveraddr, sizeof(serveraddr));
   serveraddr.sin_family = AF_INET;
   serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
   serveraddr.sin_port = htons((unsigned short)portno);
   if (bind(parentfd, (struct sockaddr *) &serveraddr, sizeof(serveraddr)) < 0) 
      halt("ERROR on binding");

   /* get us ready to accept connection requests */
   /* allow 5 requests to queue up */ 
   if (listen(parentfd, 5) < 0) halt("ERROR on listen");

   return parentfd;
}

int connection(int parentfd) {
   int childfd;                  /* child socket */
   static struct sockaddr clientaddr; /* client addr */
   socklen_t clientlen;          /* byte size of client's address */
   static struct hostent *hostp; /* client host info */
   char *hostaddrp;              /* dotted decimal host addr string */
   char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];

   /* wait for a connection request */
   clientlen = sizeof(clientaddr);
   childfd = accept(parentfd, (struct sockaddr *) &clientaddr, &clientlen);
   if (childfd < 0) halt("ERROR on accept");
   if (getnameinfo(&clientaddr, clientlen, hbuf, sizeof(hbuf), sbuf,
		   sizeof(sbuf), NI_NUMERICHOST | NI_NUMERICSERV) == 0)
     LOG("... host=%s, serv=%s\n", hbuf, sbuf);
   if (getnameinfo(&clientaddr, clientlen, hbuf, sizeof(hbuf), NULL, 0, NI_NAMEREQD))
     LOG("... %s\n", "could not resolve hostname");
   else
     LOG("... host=%s\n", hbuf);

   return childfd;
}

/* read (and mostly ignore) the HTTP headers; content length is needed for PUT/POST */
int headers(FILE *stream, int is_putpost) {
   char buf[BUFSIZE];
   int content_length;
   char *p;
   p=buf;
   buf[0]='\0'; content_type[0]= '\0'; cookie_value[0]='\0'; content_length=0;
   while (strcmp(buf, "\r\n") != 0 && p != NULL) {
      p= fgets(buf, BUFSIZE, stream);
      LOG(" | %s", buf);
      if (is_putpost && strncmp(buf, "Content-Length: ", 16)==0) {
	 sscanf(buf, "Content-Length: %d\n", &content_length);
      } else if (strncmp(buf, "Cookie: ", 8)==0) {
	cookie_read(buf, COOKIE, cookie_value);
      } else if (strncmp(buf, "Content-Type: ", 14)) {
	 sscanf(buf, "Content-Type: %s\n", content_type);
      }
   }
   return content_length;
}

/* Extract the fields from the initial HTTP line, like: GET filename HTTP/1.1 */
void parse_input(char *buf, char *method, char *filename, char *params) {
   char version[BUFSIZE]; /* HTTP version (ignored) */
   char uri[BUFSIZE];     /* request uri */
   char *p;

   sscanf(buf, "%s %s %s\n", method, uri, version);	 
   /* Ignore all parameters; preserve them if we ever decide to use them in a future version */
   p = index(uri, '?');
   if (p) {
      strcpy(params, p+1);
      *p = '\0';
   } else {
      strcpy(params, "");
   }
   
   /* construct the filename; the URI should start with /, but we protect against bad actors. 
      We could send an error response but choose not to. */
   strcpy(filename, ".");
   if (uri[0] != '/') strcat(filename, "/");
   strcat(filename, uri);
}

void do_options(FILE *stream) {
   respond(stream, "204 No Content\n",
	   "Access-Control-Allow-Origin: *",
	   "Access-Control-Allow-Methods: PUT, GET, POST, HEAD, OPTIONS", 
	   "Access-Control-Allow-Headers: *",
	   "Allow: GET, PUT, POST, HEAD, OPTIONS");
}

void do_get_head(FILE *stream, char *filename, int is_get) {
   struct stat sbuf;      /* file status */
   char *p;               /* temporary pointer */
   int fd;                /* static content filedes */   
   
   if (filename[strlen(filename)-1] == '/') 
      strcat(filename, "index.html");
   if (stat(filename, &sbuf) < 0) { /* make sure the file exists */
      error(stream, "404 Not found", "Server couldn't find this file", filename);
   } else if (!S_ISREG(sbuf.st_mode)) {
      error(stream, "403 Forbidden", "No access to directory", filename);
   } else { /* Handle regular file */
      
      respondOK(stream, (int)sbuf.st_size, mediatype(filename));
      
      if (is_get) {
	 /* Memory map the file and copy it out */
	 fd = open(filename, O_RDONLY);
	 p = mmap(0, sbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
	 fwrite(p, 1, sbuf.st_size, stream);
	 munmap(p, sbuf.st_size);
	 close(fd);
      }
   }
}

void do_put (FILE* stream, char *filename, int content_length) {
   struct stat sbuf;      /* file status */
   FILE *putfile;         /* Output for PUT method */
   int ch;
   int created=0;

   if (stat(filename, &sbuf) < 0) { created=1; }
   putfile = fopen(filename, "w");
   if (putfile) {
      if (created)
	 LOG("=> create %d %s\n\n", content_length, filename);
      else
	 LOG("=> overwrite %d %s\n\n", content_length, filename);
      ch=' ';
      while (content_length-- && ch!=EOF) {
	 ch= fgetc(stream);
	 fputc(ch, putfile);
	 putchar(ch);
      }
      fclose(putfile);
      if (content_length != -1) LOG("Content-length mismatch?\n");
      LOG("\n\n");
      if (created) {
	 respond(stream, "201 Created", "Content-length: 0", NULL, NULL, NULL);
      } else {
	 respond(stream, "204 No Content", NULL, NULL, NULL, NULL);
      }
   } else { 
      error(stream, "403 Forbidden", "Couldn't write file", filename);
   }
}

/* POST: if the file doesn't exist, creates "<post>...data...</post>"
   If the file exists, appends the new data before the closing tag of the file.
   If no ending tag is found, the content is just appended.
 */

void do_post (FILE* stream, char *filename, int content_length) {
   struct stat sbuf;       /* file status */
   FILE *putfile;          /* Output for PUT method */
   char closetag[BUFSIZE]; /* Closing tag */
   int ch;
   int err=0;
   int created=0;
   long seek;

   if (stat(filename, &sbuf) < 0) {
      created=1;
      putfile = fopen(filename, "w");
   } else {
      putfile = fopen(filename, "r+");
   }
   if (putfile) {
      if (created) {
	 LOG("=> create %d %s\n\n", content_length, filename);
	 fputs("<post>\n", putfile);
	 strcpy(closetag, "post");
      } else {
	 /* search backwards for the closing tag */
	 ch=' '; seek=-3L; /* The smallest tag is </a> */
	 while (ch!='<' && err!=-1) {
	    seek-=1L;
	    err=fseek(putfile, seek, SEEK_END);
	    if (err != -1) ch= fgetc(putfile);
	 }
	 if (err==-1) {
	    /* If no closing tag found, just append the content */
	    LOG("... no closing tag at %ld\n", seek);
	    fseek(putfile, 0L, SEEK_END);
	 } else { /* TO DO: make sure that there really is a closing tag here */
	    fscanf(putfile, "/%[^>]>", closetag);
	    LOG("... tag=%s at %ld\n", closetag, seek);
	    /* position just before the closing tag */
	    fseek(putfile, seek, SEEK_END);
	 }
	 LOG("=> append %d %s\n\n", content_length, filename);
      }
      ch=' ';
      while (content_length-- && ch!=EOF) {
	 ch= fgetc(stream);
	 fputc(ch, putfile);
	 putchar(ch);
      }
      if (err!=-1) fprintf(putfile, "\n</%s>\n", closetag);
      fclose(putfile);
      if (content_length != -1) LOG("Content-length mismatch?\n");
      LOG("\n\n");
      if (created) {
	 respond(stream, "201 Created", "Content-length: 0", NULL, NULL, NULL);
      } else {
	 respond(stream, "204 No Content", NULL, NULL, NULL, NULL);
      }
   } else { 
      error(stream, "403 Forbidden", "Couldn't write file", filename);
   }
}

int main(int argc, char **argv) {
   /* connection management */
   int parentfd;          /* parent socket */
   int childfd;           /* child socket */
   int portno;            /* port to listen on */
   
   /* connection I/O */
   FILE *stream;          /* stream version of childfd */
   char buf[BUFSIZE];     /* message buffer */
   char method[BUFSIZE];  /* request method */
   char params[BUFSIZE];  /* everything after # or ? in uri */
   char filename[BUFSIZE];/* path derived from uri */
   char *p;               /* temporary pointer */
   int is_get;            /* GET */
   int is_head;           /* HEAD */
   int is_put;            /* PUT */
   int is_post;           /* POST */
   int is_options;        /* OPTIONS */
   int content_length;    /* Content length for PUT */
   time_t now;
   char timenow[BUFSIZE];
   
   /* check command line args */
   if (argc != 2) {
      fprintf(stderr, "usage: %s <port>\n", argv[0]);
      exit(1);
   }
   portno = atoi(argv[1]);

   /* Initialise mediatypes */
   read_mediatypes();

   /* Stop early connection closes killing the program */
   sigaction(SIGPIPE, &(struct sigaction){sigpipe_handler}, NULL);

   parentfd= openport(portno);

   /* main loop: wait for a connection request, parse HTTP,
    * serve requested content, close connection.
    */
   while (1) {
      now= time(NULL);
      strftime(timenow, sizeof timenow, "%F %T %Z", localtime(&now));
      LOG("WAITING %s\n", timenow); fflush(stdout);

      /* open the child socket descriptor as a stream */
      childfd= connection(parentfd);   
      if ((stream = fdopen(childfd, "r+")) == NULL) halt("ERROR on fdopen");

      /* get the HTTP request line */
      p= fgets(buf, BUFSIZE, stream);
      if (p == NULL) { /* Don't know why it would be, but it is sometimes */
	perror("fgets returned NULL");
	/* fgets returned NULL: Bad file descriptor */
	/* fgets returned NULL: Success (!) */
      } else {
	 LOG("%s", buf);
	 parse_input(buf, method, filename, params);

	 /* Most browsers seem to do ".." processing first, but just in case... */
	 if (strstr(filename, "/../")) {
	    error(stream, "400 Bad Request", "URI contains '/../'", filename);
	 } else {
	    /* only support GET, PUT, HEAD, POST, and OPTIONS */
	    is_get= is_head= is_put= is_post= is_options=0;
	    if (strcasecmp(method, "GET") == 0) { is_get=1; }
	    else if (strcasecmp(method, "HEAD") == 0) { is_head=1; }
	    else if (strcasecmp(method, "PUT") == 0) { is_put=1; }
	    else if (strcasecmp(method, "POST") == 0) { is_post=1; }
	    else if (strcasecmp(method, "OPTIONS") == 0) { is_options=1; }
	    
	    content_length=headers(stream, is_put||is_post);
	    
	    if (is_get || is_head) {
	       do_get_head(stream, filename, is_get);
	    } else if (is_put) {
	       do_put(stream, filename, content_length);
	    } else if (is_post) {
	       do_post(stream, filename, content_length);
	    } else if (is_options) {
	       do_options(stream);
	    } else {
	       error(stream, "501 Not Implemented", "Server does not implement this method", method);
	    }
	 }
      }
      /* clean up */
      fclose(stream);
      close(childfd);
   } /* while(1) */
} /* main */
