/* CSC209F
 * Assignment #4 Sample Solution
 * Author: W. James MacLean
 *
 * This program is written in a single file to make it easier to put on the
 * web site, however I've marked places where it would be broken into header (.h)
 * and .c files.
 *
 * To compile on CDF, use the following command:
 *
 * gcc -o webServe webServe.c -L/local/lib -lbind -lnsl -lsocket -lpthread
 *
 */

/** webServe.h **/

/* this line prevents warning messages when using strtok_r() */
#define _REENTRANT

#define BASE_PORT           3000
#define SERVER_NAME         "myServer/0.99"

#ifndef MAXHOSTNAMELEN
#define MAXHOSTNAMELEN      256
#endif

#define STATS_INTERVAL      300

#define MAX_REQUEST_SIZE    4096
#define MAX_FILENAME_SIZE   1024
#define MAX_DATE_SIZE       128

#define HTTP_VER            "HTTP/1.0"

#define STATUS_OK           200
#define STATUS_OK_MSG       "OK"

#define STATUS_NOTFOUND     404
#define STATUS_NOTFOUND_MSG "File Not Found"

#define STATUS_RELOCATE     301
#define STATUS_RELOCATE_MSG "Moved Permanently"

#define HTML_NOTFOUND       "<HTML><HEAD><TITLE>404 File Not Found</TITLE></HEAD><BODY><H1>404 File Not Found</H1>The file you have requested was not found on this server</BODY></HTML>"
#define HTML_RELOCATE       "<HTML><HEAD><TITLE>301 Moved Permanently</TITLE></HEAD><BODY><H1>Moved Permanently</H1></BODY></HTML>"

/* Function Prototypes */
void *ServeFile   (void *p);
void  SendFile    (int fd, char *fileName);
void  SendRelocate(int fd, char *newLoc);
void  SendNotFound(int fd);

/** webServe.c **/

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/param.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <time.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <assert.h>
#include <pthread.h>
#include <signal.h>

/* #include "webServe.h" */

/***************/
/* Global Vars */
/***************/

long nRequests = 0 ; /* statistics, must be global so threads can access them */
long nFiles    = 0 ;
long nBytes    = 0 ;

/* mutexes */
pthread_mutex_t statsMutex ; /* must be global, see above */

/* server info */
char rootPath[MAX_FILENAME_SIZE] ; /* global mostly for convenience */
char hostName[MAXHOSTNAMELEN]    ;
int  portNum                     ;

int main(int argc, char *argv[])
{
    int                listenfd           ;
    int                acceptfd           ;
    int                sockopt = 1        ;
    struct sockaddr_in self,
                       peer = { AF_INET } ;
    struct timeval     timeout            ;
    time_t             nextStatsTime      ;
    fd_set             readset            ;
    int                result             ;
    int                addrLen            ;
    pthread_t          threadID           ;

    if (getwd(rootPath) == 0)
    {
      fprintf(stderr, "Unable to get working directory (%s)\n", rootPath);
      exit(1);
    }
    
    /* read command line parameters */
    portNum = BASE_PORT + getuid() ;
    {
      int i = 1;

      while (i < argc)
      {
        if (strcmp(argv[i], "-p") == 0)
        {
          if (i == (argc -1))
          {
            fprintf(stderr,"Expected parameter after '-p'\n");
            exit(1);
          }
          i++ ;
          portNum = htons(atoi(argv[i]));
          if (portNum == 0)
          {
            fprintf(stderr,"Invalid port number!\n");
            exit(1);           
          }
        }
        else
        {
          memset(rootPath, 0, MAX_FILENAME_SIZE);
          strncpy(rootPath,argv[i], MAX_FILENAME_SIZE);
        }
        i++ ;
      }
    }

    /* disable SIGPIPE  & do some initialization */
    signal(SIGPIPE, SIG_IGN);
    pthread_mutex_init(&statsMutex, (pthread_mutexattr_t *)NULL);

    /*************************/
    /* set up listening port */
    /*************************/

    /* Get our host name */
    /* The following 6 lines of code require that the libraries  */
    /* in the compile line be listed in the exact order as shown */
    /* at the top of this file. Why? Trust me, you don't want to */
    /* know ... wjm                                              */
    gethostname(hostName, MAXHOSTNAMELEN);
    {
      char **q ;
      struct hostent *he = gethostbyname(hostName);
      strcpy(hostName, he->h_name);
    }

    self.sin_family = AF_INET ;
    self.sin_port   = htons(portNum);
    fprintf(stdout, "Listening on port %d.\n", portNum);

    /* create socket */
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1)
    {
      fprintf(stderr, "Unable to create listening socket (%s)!\n", strerror(errno));
      exit(1);
    }

    /* allow reuse of socket port */
    if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
                 (char *)&sockopt, sizeof(sockopt)) == -1)
    {
      printf("Warning: unable to set socket option SO_REUSEADDR on listening socket:\n");
      printf("%s\n", strerror(errno));
      exit(1);
    }
  
    /* bind socket */
    if (bind(listenfd, (struct sockaddr *)&self, sizeof(self)) == -1)
    {
      fprintf(stderr, "Unable to bind listening socket (%s)!\n", strerror(errno));
      exit(1);
    }

    /* listen on socket, allow queue of 10 connections */
    if (listen(listenfd, 10) == -1)
    {
      fprintf(stderr, "Unable to listen on socket (%s)!\n", strerror(errno));
      exit(1);
    }

    /*****************************************/
    /* wait for connections, and print stats */
    /*****************************************/
    
    nextStatsTime = time(NULL) + STATS_INTERVAL ;
    
    for (;;)
    {
      /* calc next stats report time */
      timeout.tv_sec  = nextStatsTime - time(NULL);
      if (timeout.tv_sec < 0) timeout.tv_sec = 0 ;
      timeout.tv_usec = 0 ;

      /* set up required file descriptors, and call select()           */
      /* Note that since the call to select blocks, the server doesn't */
      /* waste CPU time while it's idle                                */
      FD_ZERO(&readset);
      FD_SET(listenfd, &readset);
      result = select(listenfd+1, &readset, NULL, NULL, &timeout);
      
      if (result == -1) /* handle error */
      {
        fprintf(stderr, "Error in select (%s)\n", strerror(errno));
        exit(1);
      }
      
      if (result == 0) /* print stats */
      {
        pthread_mutex_lock(&statsMutex);
        fprintf(stdout, "#Reqs = %5d, #files = %5d, #bytes = %9d\n",
                nRequests, nFiles, nBytes);
        pthread_mutex_unlock(&statsMutex);

        nextStatsTime = time(NULL) + STATS_INTERVAL ;
        
        continue ; /* go back to select() and wait for next connection */
      }

      /* if we get here, we must have a connection waiting */
      addrLen  = sizeof(peer);
      acceptfd = accept(listenfd, (struct sockaddr *)&peer, &addrLen);
      if (acceptfd == -1)
      {
        fprintf(stderr, "Error accepting connection (%s)!\n", strerror(errno));
        continue ;
      }

      fprintf(stdout,"Connection received ... %s:%u\n", 
                     inet_ntoa(peer.sin_addr),
                     ntohs(peer.sin_port));
                     
      result = pthread_create(&threadID, (pthread_attr_t *)NULL,
                              ServeFile, (void *)acceptfd);
      if (result != 0)
        fprintf(stderr, "Unable to create thread!! (%s)\n", strerror(result));

    }
}

/* converts string s to uppercase */
void MakeUpper(char *s)
{
  while (*s){ *s = toupper(*s); s++ ; }
}

void *ServeFile(void *p)
{
  int fd = (int)p ;
  char reqBuf  [MAX_REQUEST_SIZE ] ;
  char fileName[MAX_FILENAME_SIZE] ;
  char *s, *last ;

  int result ;

  result = read(fd, reqBuf, MAX_REQUEST_SIZE -1);
  if (result == 0) return NULL ;
  reqBuf[result] = 0 ; /* make sure buffer is NULL terminated */

  /* get file name */
  s = strtok_r(reqBuf, " ", &last);
  MakeUpper(s); assert(strcmp(s, "GET") == 0);
  
  s = strtok_r((char *)NULL, " ", &last);
  strcpy(fileName, s);
  
  s = strtok_r((char *)NULL, " \r\n", &last);
  MakeUpper(s);
  assert((strcmp(s, "HTTP/1.0") == 0) ||
         (strcmp(s, "HTTP/1.1") == 0));

  SendFile(fd, fileName);
  
  return NULL ;
}

/* writes "theTime" into "buf" in appropriate form for client,
 * and converts to GMT first
 */
void GMTime(char *buf, time_t theTime)
{
  struct tm res ;

  gmtime_r(&theTime, &res);
  strftime(buf, MAX_DATE_SIZE, "%a, %d %b %Y %Tq", &res);
}
      
void GetFileType(const char *fileName, char *fileType)
{
  const char *p = fileName + strlen(fileName) - 1 ;
  char q[MAX_FILENAME_SIZE];
  
  while ((*p != '.') && (p != fileName)) p-- ;
  strcpy(q, p);
  MakeUpper(q);
  
       if (strcmp(q,".GIF" ) == 0) strcpy(fileType,"image/gif");
  else if (strcmp(q,".JPG" ) == 0) strcpy(fileType,"image/jpeg");
  else if (strcmp(q,".HTML") == 0
        || strcmp(q,".HTM")  == 0) strcpy(fileType,"text/html");
  else strcpy(fileType,"text/plain");
}

void SendFile(int fd, char *fileName)
{
  struct stat fileStats ;
  char        fullName[MAX_FILENAME_SIZE] ;

  strcpy(fullName, rootPath);
  strcat(fullName, fileName);
  
  if (stat(fullName, &fileStats) == -1)
  {
    if (errno == ENOENT) 
      SendNotFound(fd);
  }

  if ( (fileStats.st_mode & S_IFMT) == S_IFDIR )/* is it a directory? */
  {
    if ( fileName[strlen(fileName)-1] != '/') /* need to append trailing slash */
    {
      strcat(fileName,"/");
      SendRelocate(fd, fileName);
    }
    else
    {
      char newFile[MAX_FILENAME_SIZE] ;
      strcpy(newFile, fileName);
      strcat(newFile, "index.html");

      SendFile(fd, newFile);
    }
  }
  else /* send file */
  {
    int fd1 ;
    
    fd1 = open(fullName, O_RDONLY);
    if (fd1 == -1) SendNotFound(fd);
    else
    {
      char c              ;
      long bytesMoved = 0 ;
      int  result         ;
      
      char msg     [MAX_REQUEST_SIZE]  ;
      char timeBuf [MAX_DATE_SIZE]     ;
      char lastMod [MAX_DATE_SIZE]     ;
      char fileType[MAX_FILENAME_SIZE] ;
      
      GetFileType(fileName, fileType);
      
      GMTime(timeBuf, time(NULL));
      GMTime(lastMod, fileStats.st_mtime);

      sprintf(msg,"%s %d %s\r\nDate: %s GMT\r\nServer: %s\r\nConnection: close\r\n"
                  "Content-Length: %d\r\nLast-Modified: %s GMT\r\nContent-Type: %s\r\n\r\n",
              HTTP_VER,
              STATUS_OK,
              STATUS_OK_MSG,
              timeBuf,
              SERVER_NAME,
              fileStats.st_size,
              lastMod,
              fileType);
      
      write(fd, msg, strlen(msg));
      while (read(fd1, &c, 1))
        if (write(fd, &c, 1) != 1)
          break ; /* connection broken */
        else
          bytesMoved++ ;
      
      close(fd1);
      close(fd);
      
      /* update stats */
      pthread_mutex_lock(&statsMutex);
      nRequests++ ;
      nFiles++ ;
      nBytes += bytesMoved ;
      pthread_mutex_unlock(&statsMutex);
    }
  }
}

void SendRelocate(int fd, char *newLoc)
{
  char       msg[MAX_REQUEST_SIZE] ;
  char       timeBuf[MAX_DATE_SIZE] ;

  GMTime(timeBuf, time(NULL));
  sprintf(msg, "%s %d %s\r\nDate: %s GMT\r\nServer: %s\r\nLocation: %s\r\n"
               "Connection: close\r\nContent-Type: text/html\r\n\r\n"
               "<HTML><HEAD><TITLE>301 Permanently Moved</TITLE></HEAD>"
               "<BODY><H1>Permanently Moved</H1>"
               "The webpage you are looking for has permanently moved to "
               "<A HREF='http://%s:%d%s'>http://%s:%d%s</A></BODY></HTML>",
          HTTP_VER,
          STATUS_RELOCATE,
          STATUS_RELOCATE_MSG,
          timeBuf,
          SERVER_NAME,
          newLoc, 
          hostName, portNum, newLoc,
          hostName, portNum, newLoc);
  write(fd, msg, strlen(msg));
  close(fd);
  
  /* update stats */
  pthread_mutex_lock(&statsMutex);
  nRequests++ ;
  pthread_mutex_unlock(&statsMutex);
}

void SendNotFound(fd)
{
  char msg    [MAX_REQUEST_SIZE] ;
  char timeBuf[MAX_DATE_SIZE   ] ;

  memset(timeBuf, 0, sizeof(timeBuf));
  GMTime(timeBuf, time(NULL));
  sprintf(msg, "%s %d %s\r\nDate: %s GMT\r\nServer: %s\r\nConnection: close\nContent-Type: text/html\r\n\r\n%s",
          HTTP_VER,
          STATUS_NOTFOUND,
          STATUS_NOTFOUND_MSG,
          timeBuf, SERVER_NAME,
          HTML_NOTFOUND);
  write(fd, msg, strlen(msg));
  close(fd);
  
  /* update stats */
  pthread_mutex_lock(&statsMutex);
  nRequests++ ;
  pthread_mutex_unlock(&statsMutex);
}

