#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/sendfile.h>
#include <netinet/in.h>
#include <netdb.h>
#include <dirent.h>
#include <time.h>

#define LOGFILE		"/tmp/h7log.50"	// Replace ## with your 2 digit ID:
#define PORT		47350
#define K		1024
#define CHUNK_SIZE	K*10
#define BACKLOG		25

// List of mime types for various file extensions:
struct mime_t {
  char *ext;
  char *type;
} mimetypes[] = {
  {".html", "text/html"}, {".htm", "text/html"}, {".css", "text/css"},
  {".pdf", "application/pdf"}, {".gif", "image/gif"}, {".png", "image/png"},
  {".jpg", "image/jpeg"}, {".jpeg", "image/jpeg"}, {".gz", "application/x-gzip"},
  {NULL, NULL}
};

// Weekday and Month strings for printing times:
char *wday[] = {"Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat"};
char *month[] = {
  "Jan", "Feb", "Mar", "Apr", "May", "Jun",
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};

// A big buffer for "printing" a directory listing into:
char wbuf[K*64];
// To be set to the root web path (The servers current working directory):
char *root;

/*
 * Signal handler to reap dead processes or shutdown the server on a signal:
 */
void sighandler(int sig)
{
  int status;
  // If we get a SIGCHLD signal, reap the dead child process with wait:
  if (sig == SIGCHLD) wait(&status);
  // Otherwise shutdown the server:
  else exit(0);
}

/*
 * Launches the server into the background:
 */
void daemonize(char *wd)
{
  int null, fd;

  // If the wd argument is not null, attempt to change to the directory specified
  // by it:
  if (wd) chdir(wd);
  // Set "root" to the current directory:
  root = get_current_dir_name();
  if (root == NULL) {
    fprintf(stderr,"I don't know how, but we couldn't get our current working directory, what the hell the did you do?\n");
    exit(1);
  }
  // Open /dev/null for reading:
  if ((null = open("/dev/null", O_RDONLY)) < 0) {
    perror("open /dev/null");
    exit(1);
  }

  // Open LOGFILE for writing, create it if it doesn't exist, truncate it if it
  // does:
  if ((fd = open(LOGFILE, O_WRONLY|O_TRUNC|O_CREAT, 0666)) < 0) {
    perror("open logfile");
    exit(1);
  }
  
  // Make /dev/null our stdin and the LOGFILE our stdout and stderr:
  dup2(null, 0);
  dup2(fd, 1);
  dup2(fd, 2);

  // Close the /dev/null and LOGFILE descriptors:
  close(null);
  close(fd);

  // Print a startup message to stderr, messages printed to stdout and stderr
  // will go to the LOGFILE:
  fprintf(stderr,"Server staring...\n");
  // Use the daemon(3) function to daemonize our server, don't change directory
  // and don't close the file descriptors:
  daemon(1,1);
}

/* An example HTTP request a browser might make, we are really only interested
 * in the first line, but must read the entire request which is terminated by
 * a blank line:

  GET / HTTP/1.1
  Host: localhost:8000
  User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:40.0) Gecko/20100101 Firefox/40.0
  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,* / *;q=0.8
  Accept-Language: en-US,en;q=0.5
  Accept-Encoding: gzip, deflate
  DNT: 1
  Connection: keep-alive

*/
/*
 * Reads the request header from the browser, like the above.  Returns the
 * path given by the GET request (second word, between GET and HTTP/1.1):
 * If there is any error anywhere, return a NULL instead.
 */
char *readrequest(FILE *sock)
{
  char buf[K], *s, *path;

  // Read the first line, use fgets():
  if (fgets(buf, K, sock) == NULL) return NULL;
  // Check that it begins with "GET ":
  if (strncasecmp("GET ", buf, 4) != 0) return NULL;
  // Use strtok to extract the second word:
  s = strtok(buf+4, " \r\n");
  if (s == NULL) return NULL;
  // Use strdup() to copy the second word into "path":
  path = strdup(s);
  if (path == NULL) return NULL;
  // Read out header, until a empty line is reached:
  while(fgets(buf, K, sock)) {
    s = strtok(buf, " \r\n");
    if (s == NULL) break;
  }

  return path;
}

/* Checks that the path we got from the browser lies within the root of our
 * document. Returns the full real path to the file/directory:
 */
char *checkpath(char *path)
{
  char cpath[PATH_MAX], *real;

  // Concatenate the root and path together:
  sprintf(cpath, "%s/%s", root, path);
  // Use realpath(3) to get the real path location:
  real = realpath(cpath, NULL);
  if (real == NULL) return NULL;

  // Check if the prefix of the real path above and "root" are the same,
  // If not return NULL, if so, return the real path.
  if (strncmp(real, root, strlen(root)) != 0) return NULL;
  
  return real;  
}

/* Checks what kind of file the path is, returns one of:
 * 0 = not a file/not accessible/error
 * 1 = a regular file, size of the file returned in "size"
 * 2 = a PHP script.
 * 3 = a directory.
 * date is always set to the last modified time of the file/directory.
 */
int filetype(char *path, long *size, time_t *date)
{
  struct stat st;
  int len;

  // Use stat(2) to get the inode information for "path":
  if (stat(path, &st) < 0) return 0;
  // Set date argument to the last modified time.
  *date = st.st_mtime;
  *size = st.st_size;

  // If it's a directory, return 3:
  if (S_ISDIR(st.st_mode)) return 3;
  // If it's a real file:
  if (S_ISREG(st.st_mode)) {
    // Check if we can read it using access(2), if not, return 0:
    if (access(path, R_OK)) return 0;
    // Check if the last 4 characters in the path end in .php, if so, return 2:
    len = strlen(path)-4;
    if (len > 0 && strncasecmp(path+len, ".php",4) == 0) return 2;
    // otherwise return 1:
    return 1;
  }
  // Not one of the above, so return 0:
  return 0;
}

/*
 * Returns the mime type given the path.
 */
char *mimedetect(char *path)
{
  int len = strlen(path), extlen;
  int i;
  
  // Loop through all the mimetypes[]:
  for(i=0; mimetypes[i].ext != NULL; i++) {
    // Get the length of the ext field:
    extlen = strlen(mimetypes[i].ext);
    // Check that the last "extlen" number of characterse of the path and
    // mimetypes[i].ext match.  If so return mimetypes[i].type:
    if (len < extlen) continue;
    if (strncasecmp(path+(len-extlen), mimetypes[i].ext, extlen) == 0)
      return mimetypes[i].type;
  }
  // Return a default of "text/plain" if no extension matched:
  return "text/plain";
}

/*
 * Outputs the response header to the browser:
 * All lines must be terminated by carriage-return + newline: i.e. "\r\n"
 */

void emitheader(FILE *sock, int status, char *message, int size, time_t date, char *mimetype)
{
  struct tm *lt = localtime(&date);
  // Print the HTTP/1.1 status message provided by status and message arguments:
  fprintf(sock, "HTTP/1.1 %d %s\r\n", status, message);
  fprintf(stderr, "HTTP/1.1 %d %s\r\n", status, message);
  // Print the date.  Use localtime(3) with "date" to and use the struct tm
  // structure to format a date like: Tue, 16 Feb 2016 17:35:16 GMT
  // Use the wday and month arrays define above for those strings
  fprintf(sock, "%s, %d %s %d %d:%d:%d EST\r\n",
	  wday[lt->tm_wday], lt->tm_mday, month[lt->tm_mon],
	  lt->tm_year+1900, lt->tm_hour, lt->tm_min, lt->tm_sec);
  fprintf(stderr, "%s, %d %s %d %d:%d:%d EST\r\n",
	  wday[lt->tm_wday], lt->tm_mday, month[lt->tm_mon],
	  lt->tm_year+1900, lt->tm_hour, lt->tm_min, lt->tm_sec);

  // Print a Server: string.  Make up a name for your server. ex:
  // Server: h7/1.0.0 (Linux) PHP/5.6.17
  fprintf(sock, "h7/1.0.0 (Linux) PHP/5.6.17\r\n");
  fprintf(stderr, "h7/1.0.0 (Linux) PHP/5.6.17\r\n");
  // Print "Accept-Ranges: bytes":
  fprintf(sock, "Accept-Ranges: bytes\r\n");
  fprintf(stderr, "Accept-Ranges: bytes\r\n");
  // If size < 0, print the transfer-encoding chunked header, otherwise print
  // the content-length header:
  if (size < 0) {
    fprintf(sock, "Transfer-Encoding: chunked\r\n");
    fprintf(stderr, "Transfer-Encoding: chunked\r\n");
  } else {
    fprintf(sock, "Content-Length: %d\r\n", size);
    fprintf(stderr, "Content-Length: %d\r\n", size);
  }
  // Print the Content-Type: mimetype header:
  fprintf(sock, "Content-Type: %s\r\n", mimetype);
  fprintf(stderr, "Content-Type: %s\r\n", mimetype);
  // Print a blank line:
  fprintf(sock, "\r\n");
  fprintf(stderr, "\r\n");
}


/* Outputs a 404 "Not Found" header then some HTML for the web user.
 * The size of the HTML must be known before printing the header so that
 * the Content-Length field can be set properly.  So print the HTML message
 * into wbuf, then use the strlen(wbuf) to properly fill out the emitheader()
 * function.  Call exit(2) after closing the socket.
 */
void badrequest(FILE *sock, char *path)
{
  // "Print" a error message to wbuf:
  sprintf(wbuf,
    "<!DOCTYPE html>\n"
    "<html><head>\n"
    "<title>404 Not Found</title>\n"
    "</head><body>\n"
    "<h1>Not Found</h1>\n"
    "<p>The requested URL %s was not found on this server.</p>\n"
    "</body></html>\n", path);

  emitheader(sock, 404, "Not Found", strlen(wbuf), time(0), "text/html");
  fwrite(wbuf, 1, strlen(wbuf), sock);
  fclose(sock);
  exit(0);
}

/*
 * Opens a pipe to the php-cgi program to process a PHP page.
 */
void phppipe(FILE *sock, char *path)
{
  char chunk[CHUNK_SIZE];
  int pd[2], r;
  pid_t pid;
  
  // Create a pipe:
  if (pipe(pd) < 0) {
    perror("pipe");
    exit(1);
  }
  // Fork a new process for the php-cgi program:
  pid = fork();
  if (pid < 0) return;
  if (pid == 0) {	// The child process (php-cgi):
    // Close the parents read descriptor:
    close(pd[0]);
    // Open the PHP file for reading:
    r = open(path, O_RDONLY);

    // Make the PHP file the childs stdin (descriptor 0):
    dup2(r, 0);
    // Make the write side of the pipe stdout and stderr:
    dup2(pd[1], 1);
    dup2(pd[1], 2);
    // Close the orginal PHP file and pipe write side descriptors:
    close(r);
    close(pd[1]);
    // Exec /usr/bin/php-cgi with the "-q" command line option:
    execl("/usr/bin/php-cgi", "php-cgi", "-q", NULL);
    // Print an error message if the exec failed and exit:
    fprintf(stderr,"Unable to exec php!\n");
    exit(1);
  } else {
    // Close the clients pipe (write side):
    close(pd[1]);
    // Make a loop to read CHUNK_SIZE sized data from the pipe and output
    // to the browser in Transfer-Encoding: chunked format, i.e. chunk size
    // in hex + "\r\n" + chunk data + "\r\n":
    while ((r = read(pd[0], chunk, CHUNK_SIZE)) > 0) {
      fprintf(sock, "%X\r\n", r);
      if (fwrite(chunk, 1, r, sock) != r) break;
      fprintf(sock, "\r\n");
    }

    // Print a zero length chunk at the end:
    fprintf(sock, "0\r\n");
    // Close pipe:
    close(pd[1]);
    // Use waitpid(2) to wait on the child to exit:
    waitpid(pid, &r, 0);
  }
}

// Used by scandir to filter out files beginning with "."
int filter(const struct dirent *de)
{
  // If the file begins with '.', return 0, otherwise return 1:
  if (de->d_name[0] == '.') return 0;
  return 1;
}

/* Uses sprintf() to write a directory listing to the global wbuf, then
 * sends that to the browser.
 */
void dirlist(FILE *sock, char *realpath, char *path)
{
  struct dirent **dents;
  int i, off;

  // Output the HTML for a directory listing to wbuf, you can use sprintf(3)
  // in this manner:
  //   off = sprintf(wbuf, "first part");
  //   off += sprintf(wbuf+off, "second+ parts");

  // Write out the HTML header portion of the directory listing:  
  off = sprintf(wbuf,
    "<!DOCTYPE html>\n"
    "<html>\n"
    " <head>\n"
    "  <title>Index of %s</title>\n"
    " </head>\n"
    " <body>\n"
    "<h1>Index of %s</h1>\n"
    "<ul><li><a href='..'> Parent Directory</a></li>\n",
    path, path);

  // Use scandir(3) and filter() above (to filter out all files beginning with
  // "." to read the directory:
  if (scandir(realpath, &dents, filter, versionsort) < 0) {
    fprintf(stderr,"Error performing scandir.\n");
  }

  // Loop through the directory and add a HTML link to it in the dir listing:

  for(i=0; dents[i] != NULL; i++) {
    off += sprintf(wbuf+off, "<li><a href='%s'> %s </a></li>\n",
	    dents[i]->d_name, dents[i]->d_name);
  }

  // Write out the closing portion of the HTML:
  off += sprintf(wbuf+off,
      "</ul>\n"
      "</body></html>\n");

  // Emit the header and send the data:
  emitheader(sock, 200, "OK", off, time(0), "text/html");
  fwrite(wbuf, 1, off, sock);
}


/*
 * Routine to handle a connection:
 */
void handle(int fd)
{
  char *path, *realpath, npath[PATH_MAX], *mimetype;
  int ftype, reloc = 0, inf;
  long size;
  time_t date;
  FILE *sock;
  
  // Use fdopen(3) to make a stream from the socket file descriptor fd:
  if ((sock = fdopen(fd, "r+")) == NULL) {
    perror("fdopen");
    return;
  }
  // Get the path via readrequest:
  path = readrequest(sock);
  if (path == NULL) return badrequest(sock, "");
  if ((realpath = checkpath(path)) == NULL) return badrequest(sock, path);
  ftype = filetype(realpath, &size, &date);
  if (ftype == 0) return badrequest(sock, path);
  if (ftype == 3) {
    sprintf(npath,"%s/index.html", realpath);
    ftype = filetype(npath, &size, &date);
    if (ftype == 0) {
      sprintf(npath,"%s/index.php", realpath);
      ftype = filetype(npath, &size, &date);
      if (ftype != 0) reloc = 1;
    } else reloc = 1;
    if (reloc) {
      free(realpath);
      realpath = strdup(npath);
    } else ftype = 3;
  }
  fprintf(stderr,"GET %s [%s] %d\n", path, realpath, ftype);
  switch(ftype) {
    case 1:
      mimetype = mimedetect(realpath);
      inf = open(realpath, O_RDONLY);
      if (inf < 0) return badrequest(sock, path);
      emitheader(sock, 200, "OK", size, date, mimetype);
      // Must flush buffered I/O in sock, before sendfile() which does not use
      // stream I/O:
      fflush(sock);
      sendfile(fd, inf, 0, size);
      close(inf);
      break;
    case 2:
      emitheader(sock, 200, "OK", -1, time(0), "text/html");
      phppipe(sock, realpath);
      break;
    case 3:
      dirlist(sock, realpath, path);
      break;
    default:
      badrequest(sock, path);
      break;
  }
  fclose(sock);
  exit(0);
}

int main(int argc, char *argv[])
{
  struct sigaction sa;
  struct sockaddr_in server;
  struct epoll_event epv;
  int epfd, sock, fd, r;
  pid_t pid;

  // Daemonize the server first, then setup signal handlers:
  daemonize(argv[1]);
  // Setup signal handlers:
  sa.sa_handler = sighandler;
  sigaction(SIGCHLD, &sa, NULL);
  // Create a socket for the server:
  if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
    perror("socket");
    exit(1);
  }

  // Bind it to any interface on PORT, then begin listening for connections:
  server.sin_family = AF_INET;
  server.sin_addr.s_addr = htonl(INADDR_ANY);
  server.sin_port = htons(PORT);
  
  r = 1;
  setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &r, sizeof(r));
  
  if (bind(sock, (struct sockaddr *)&server, sizeof(struct sockaddr_in)) < 0) {
    perror("bind");
    exit(1);
  }

  if (listen(sock, 20) < 0) {
    perror("listen");
    exit(1);
  }
  // Create an epoll instance, then add the server socket to it:
  if ((epfd = epoll_create(10)) < 0) {
    perror("epoll");
    exit(1);
  }
  epv.events = EPOLLIN;
  if (epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &epv) < 0) {
    perror("epoll_ctl");
    exit(1);
  }

  // Loop forever:
  for(;;) {
    // Wait for an event to occur on the epoll instance:
    r = epoll_wait(epfd, &epv, 1, -1);
    if (r <= 0) continue;
    
    // If there was an error on the socket, break out of the loop:
    if (epv.events & (EPOLLERR | EPOLLHUP)) break;

    // Accept a new connection:
    fd = accept(sock, NULL, NULL);
    if (fd < 0) continue;
    
    // Fork a new process to handle the connection:
    pid = fork();
    if (pid < 0) {
      perror("fork");
      continue;
    } else if (pid == 0) {
      close(sock);
      handle(fd);
    } else {
      close(fd);
    }
  }

  fprintf(stderr,"Error on socket -- server shutting down.\n");
  return 0;
}
