/*
  This program is free software: you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.
  
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
  
  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>. 
*/

/** 
 * Modifications from original nweb23.c -stg
 * 
 * Execution of external functions to generate dynamic content
 * Chunked transfers for dynamic content
 * Allow xml and json files
 * Changed incorrect \n http line breaks to \l\n
 * Made logger accessible to dynamic content generators
 * Now closing accessed files
 * Support for reading GET parameters
 * Requires dot before extensions (defaulthtml was previously accepted)
 * Use UTF-8 encoding
 * Clears log on start up
 * Log level option
 **/
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/shm.h>
#include "nweb.h"

#define BUFSIZE  8096
#define TMPSIZE  1024
#define ERROR      42
#define LOG        44
#define FORBIDDEN 403
#define NOTFOUND  404


struct {
	char *ext;
	char *filetype;
} static extensions [] = {
	{".gif", "image/gif"                       },  
	{".jpg", "image/jpg"                       }, 
	{".jpeg","image/jpeg"                      },
	{".png", "image/png"                       },  
	{".ico", "image/ico"                       },  
	{".zip", "image/zip"                       },  
	{".gz",  "image/gz"                        },  
	{".tar", "image/tar"                       },  
	{".htm", "text/html; charset=utf-8"        },  
	{".html","text/html; charset=utf-8"        },  
	{".xml", "application/xml; charset=utf-8"  },  
	{".json","application/json; charset=utf-8" },  
	{0,0} };

static unsigned char loglevel = 0;
static shared_access_t *shm;

void wlog(int type, char *s1, char *s2, int socket_fd)
{
	int fd ;
	char logbuffer[BUFSIZE*2];
	unsigned char level;

	switch (type) {
	case ERROR:
	  (void)sprintf(logbuffer,"ERROR: %s:%s Errno=%d exiting pid=%d",s1, s2, errno,getpid()); 
	  level = 1;
		break;
	case FORBIDDEN: 
		(void)write(socket_fd, "HTTP/1.1 403 Forbidden\r\nContent-Length: 185\r\nConnection: close\r\nContent-Type: text/html\r\n\r\n<html><head>\n<title>403 Forbidden</title>\n</head><body>\n<h1>Forbidden</h1>\nThe requested URL, file type or operation is not allowed on this simple static file webserver.\n</body></html>\n",271);
		(void)sprintf(logbuffer,"FORBIDDEN: %s:%s",s1, s2); 
		level = 2;
		break;
	case NOTFOUND: 
		(void)write(socket_fd, "HTTP/1.1 404 Not Found\r\nContent-Length: 136\r\nConnection: close\r\nContent-Type: text/html\r\n\r\n<html><head>\n<title>404 Not Found</title>\n</head><body>\n<h1>Not Found</h1>\nThe requested URL was not found on this server.\n</body></html>\n",224);
		(void)sprintf(logbuffer,"NOT FOUND: %s:%s",s1, s2); 
		level = 2;
		break;
	case LOG:
	  (void)sprintf(logbuffer," INFO: %s:%s:%d",s1, s2,socket_fd);
	  level = 3;
	  break;
	}	
	// No checks here, nothing can be done with a failure anyway
	if(level <= loglevel) {
  	if((fd = open("nweb.log", O_CREAT| O_WRONLY | O_APPEND,0644)) >= 0) {
  		(void)write(fd,logbuffer,strlen(logbuffer)); 
  		//sprintf(logbuffer, " %u <= %u", level, loglevel);
  		//(void)write(fd,logbuffer,strlen(logbuffer)); 
  		(void)write(fd,"\n",1);      
  		(void)close(fd);
  	}
  }
	if(type == ERROR || type == NOTFOUND || type == FORBIDDEN) exit(3);
}


void semtake() {
  struct sembuf sops;
  sops.sem_num = 0;
  sops.sem_op = -1;
  /* semaphore operation */
  sops.sem_flg = SEM_UNDO;
  if (semop(shm->semid, &sops, 1) == -1) wlog(ERROR, "system call", "semop:take", 0);
}

void semgive() {
  struct sembuf sops;
  sops.sem_num = 0;
  sops.sem_op = 1;
  /* semaphore operation */
  sops.sem_flg = SEM_UNDO;
  if (semop(shm->semid, &sops, 1) == -1) wlog(ERROR, "system call", "semop:give", 0);
}

// this is a child web server process, so we can exit on errors
static void web(int fd, int hit, void *shared)
{
	int j, file_fd, buflen;
	long i, ret, len;
	char * fstr;
	static char buffer[BUFSIZE+1]; // static so zero filled
	static char temp[TMPSIZE+1]; // static so zero filled
	char *params = NULL;
	dfile fp;

	ret =read(fd,buffer,BUFSIZE); 	// read Web request in one go
	if(ret == 0 || ret == -1) {	// read failure stop now
		wlog(FORBIDDEN,"failed to read browser request","",fd);
	}
	if(ret > 0 && ret < BUFSIZE)	// return code is valid chars
		buffer[ret]=0;		// terminate the buffer
	else buffer[0]=0;
	for(i=0;i<ret;i++)	// remove CF and LF characters
		if(buffer[i] == '\r' || buffer[i] == '\n')
			buffer[i]='*';
	if(strncmp(buffer,"GET ",4) && strncmp(buffer,"get ",4)) {
		wlog(FORBIDDEN,"Only simple GET operation supported",buffer,fd);
	}
	// string is "GET [url](?[parameters]) [http_version]"
	// null terminate at '?' and at second ' '
	for(i = 4; i < BUFSIZE; i++) {
		if(buffer[i] == '?') { 
		  buffer[i] = 0;
		  params = &buffer[i + 1]; // point to params
		}
		if(buffer[i] == ' ') {
			buffer[i] = 0;
			break;
		}
	}
	wlog(LOG,"request",&buffer[4],hit);
	for(j=0;j<i-1;j++) 	// check for illegal parent directory use ..
		if(buffer[j] == '.' && buffer[j+1] == '.') {
			wlog(FORBIDDEN,"Parent directory (..) path names not supported",buffer,fd);
		}
	if(!strncmp(&buffer[0],"GET /\0",6) || !strncmp(&buffer[0],"get /\0",6)) // convert no filename to index file
		(void)strcpy(buffer,"GET /index.html");

	// work out the file type and check we support it
	buflen=strlen(buffer);
	fstr = (char *)0;
	for(i=0;extensions[i].ext != 0;i++) {
		len = strlen(extensions[i].ext);
		if(!strncmp(&buffer[buflen-len], extensions[i].ext, len)) {
			fstr =extensions[i].filetype;
			break;
		}
	}
	if(fstr == 0) wlog(FORBIDDEN,"file extension type not supported",buffer,fd);

	if((file_fd = open(&buffer[5],O_RDONLY)) == -1) {  // open the file for reading
		fp = wopen(&buffer[5], fd, shared);
		if(fp) {
		  wlog(LOG,"EXECUTE",&buffer[5],hit);
  	} else {
  		wlog(NOTFOUND, "failed to open file",&buffer[5],fd);
  	}
	} else {
	  wlog(LOG,"SEND",&buffer[5],hit);
	}
	len = (long)lseek(file_fd, (off_t)0, SEEK_END); // lseek to the file end to find the length
	(void)lseek(file_fd, (off_t)0, SEEK_SET); // lseek back to the file start ready for reading
  
  (void)sprintf(temp,"HTTP/1.1 200 OK\r\nServer: nweb-stg/%d.0\r\nConnection: close\r\nContent-Type: %s\r\n", VERSION, fstr);
  if(file_fd == -1) {
    (void)sprintf(temp + strlen(temp), "Transfer-Encoding: chunked\r\n\r\n");
  } else {
    (void)sprintf(temp + strlen(temp), "Content-Length: %ld\r\n\r\n", len);
  }
	(void)write(fd,temp,strlen(temp));
	if(!params) {
	  temp[0] = 0;
	  params = temp; // no params? point to empty string
  }  
	if(file_fd == -1) {
	  (void)fp(fd, params); // execute dynamic content generator
	  (void)wclose();
    (void)write(fd,"0\r\n\r\n",5); // chunked transfer termination
  } else {
  	// send file in 8KB block - last block may be smaller
  	while (	(ret = read(file_fd, buffer, BUFSIZE)) > 0) {
  		(void)write(fd,buffer,ret);
  	}
  	close(file_fd);
  }
	sleep(1);	// allow socket to drain before signalling the socket is closed
	close(fd);
	exit(1);
}

// writed chunked dynamic content
ssize_t wwrite(int fd, const void *buf, size_t count) {
  char buffer[sizeof(size_t)*2+2]; // *2 for hex, +2 for \n and null-termination
  if(count) {
    (void)sprintf(buffer,"%x\r\n",count);
    (void)write(fd,buffer,strlen(buffer));
    (void)write(fd,buf,count);
    (void)write(fd,"\r\n",2);
  }
}

static size_t uridecode(char *buf) {
  char *prd = buf, *pwr = buf;
  char hexbyte[3] = {0, 0, 0};
  while(*prd) { // until null termination
    if(*prd == '%') {
      if((hexbyte[0] = *(++prd)) == 0) break; // copy two nibbles and
      if((hexbyte[1] = *(++prd)) == 0) break; // break if cut short
      *prd = strtoul(hexbyte, NULL, 16); // convert, ignore errors
    }
    *pwr++ = *prd++; //copy
  }
  *pwr++ = 0; // null-terminate
  return pwr - buf; // return bytes, including termination
}

size_t wgets(char *buf, size_t bufsize, char *name, char *params) {
  char *tok = NULL;
  size_t toksize = 0;
  size_t bytesout = 0;
  size_t i;
  unsigned char state = 0;
  if(bufsize) buf[0] = 0; else return 0;
  size_t length = strlen(params);
  for(i = 0; i <= length; i++) {
    switch(state) {
      case 0: // new token
        tok = params;
        state = 1;
        toksize = 1;
        bytesout = 0;
        break;
      case 1: // find end of token
        if(*params == '=') {
          state = 2;
          break;
        } else if(*params == '&' || *params == 0) {
          state = 2;
        } else {
          toksize++;
          break;
        }
      case 2: // find end of value
        if(*params == '&' || *params == 0) {
          if(strlen(name) == toksize) { // check length
            if(!memcmp(tok, name, toksize)) { // match token:name
              if(buf == NULL) return bytesout + 1; // return nof bytes needed
              if(bytesout < bufsize) {
                memcpy(buf, tok + toksize + 1, bytesout); // copy
                buf[bytesout++] = 0; // terminate
                return uridecode(buf); // decode in place, return bytes written
              } else {
                return 0;
              }
            }
          }
          state = 0;
        } else {
          bytesout++;
        }
    }
    params++;
  }
  return 0;
}

int main(int argc, char **argv)
{
	int i, port, pid, listenfd, socketfd, hit;
	FILE *f;
	socklen_t length;
	static struct sockaddr_in cli_addr; // static = initialised to zeros
	static struct sockaddr_in serv_addr; // static = initialised to zeros
  key_t key;
  int shmid;
  struct sembuf sbuf;
	
	if(argc < 3  || argc > 4 || !strcmp(argv[1], "-?")) {
		(void)printf("hint: nweb Port-Number Top-Directory\t\tversion %d\n\n"
	"\tnweb-stg is a spin-off from the mini web server nweb, originally\n"
	"\twritten by Nigel Griffiths, IBM.\n"
	"\tit had no fancy features (thus claimed safe ans secure) instead\n"
	"\tit had lots of bugs and unchecked buffers.\n"
	"\tnow it has more fancy features and less bugs, yet it should\n"
	"\tstill not be considered safe - use at your own risk.\n\n"
	"\tweb server nweb-stg by D.Taylor, original nweb by N.Griffiths.\n\n"
	"\tExample: nweb 8181 /home/nwebdir (-v(v(v)))\n\n"
	"\tOnly Supports:", VERSION);
		for(i=0;extensions[i].ext != 0;i++)
			(void)printf(" %s",extensions[i].ext);

		(void)printf("\n\tNot Supported: URLs including \"..\", Java, Javascript, CGI\n"
	"\tNot Supported: directories / /etc /bin /lib /tmp /usr /dev /sbin \n"
	"\tNo warranty given or implied\n\tD.Taylor d.taylor@arduino.cc\n\tN.Griffiths nag@uk.ibm.com\n" );
		exit(0);
	}
	if(!strncmp(argv[2],"/"   ,2) || !strncmp(argv[2],"/etc", 5) ||
	    !strncmp(argv[2],"/bin",5) || !strncmp(argv[2],"/lib", 5) ||
	    !strncmp(argv[2],"/tmp",5) || !strncmp(argv[2],"/usr", 5) ||
	    !strncmp(argv[2],"/dev",5) || !strncmp(argv[2],"/sbin",6)){
		(void)printf("ERROR: Bad top directory %s, see nweb -?\n",argv[2]);
		exit(3);
	}
	if(argc > 3) {
  	if(!strncmp(argv[3],"-vvv",5)) loglevel = 3;
  	else if(!strncmp(argv[3],"-vv",4)) loglevel = 2;
  	else if(!strncmp(argv[3],"-v",3)) loglevel = 1;
  	else {
  	  (void)printf("ERROR: Bad flag, see nweb -?\n",argv[3]);
  		exit(8);
  	}
  }
	if(chdir(argv[2]) == -1){ 
		(void)printf("ERROR: Can't Change to directory %s\n",argv[2]);
		exit(4);
	}

  // Initialize key
  key = ftok(".",'a');
  if(key == (key_t)-1 ) {
    printf("ERROR: Unable to get key, ftok, Errno=%d\n", errno);
    exit(5);
  }
  
  // Initialize shared memory
  if((shmid = shmget(key, wmem(), 0644 | IPC_CREAT)) < 0) {
    //wlog(ERROR,"system call","shmget",0);    
    printf("ERROR: Unable to get shared memory, shmget, Errno=%d\n", errno);
    exit(7);
  }
  
  // Attach to shared memory
  shm = shmat(shmid, (void *)0, 0);
  if(shm == (shared_access_t*)(-1)) {
    printf("ERROR: Unable to attach shared memory, shmget, Errno=%d\n", errno);
    exit(7);
  }
  memset(shm, 0, wmem());
  
  // Get semaphore ID associated with this key
  if((shm->semid = semget(key, 0, 0)) == -1) {
    // Semaphore does not exist - Create.
    if ((shm->semid = semget(key, 1, IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH)) == -1) {
      printf("ERROR: Unable to get/create semaphore, semget, Errno=%d\n", errno);
      exit(6);
    }
  }
  shm->lock = semtake;
  shm->unlock = semgive;
	if((f = fopen("nweb.log", "w")) >= 0) fclose(f); // clear log

	// Become deamon + unstopable and no zombies children (= no wait())
	if(fork() != 0)
		return 0; // parent returns OK to shell
	(void)signal(SIGCLD, SIG_IGN); // ignore child death
	(void)signal(SIGHUP, SIG_IGN); // ignore terminal hangups
	for(i=0;i<32;i++)
		(void)close(i);		// close open files
	(void)setpgrp();		// break away from process group
	wlog(LOG,"nweb starting",argv[1],getpid());
	// setup the network socket
	if((listenfd = socket(AF_INET, SOCK_STREAM,0)) <0)
		wlog(ERROR, "system call","socket",0);
	port = atoi(argv[1]);
	if(port < 0 || port >60000)
		wlog(ERROR,"Invalid port number (try 1->60000)",argv[1],0);
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port = htons(port);
	if(bind(listenfd, (struct sockaddr *)&serv_addr,sizeof(serv_addr)) <0)
		wlog(ERROR,"system call","bind",0);
	if(listen(listenfd,64) <0)
		wlog(ERROR,"system call","listen",0);
  shm->unlock(); // unlock shared memory (locked by default)
	for(hit=1; ;hit++) {
		length = sizeof(cli_addr);
		if((socketfd = accept(listenfd, (struct sockaddr *)&cli_addr, &length)) < 0)
			wlog(ERROR,"system call","accept",0);
		if((pid = fork()) < 0) {
			wlog(ERROR,"system call","fork",0);
		}
		else {
			if(pid == 0) { 	// child
				(void)close(listenfd);
				web(socketfd,hit,shm); // never returns
			} else { 	// parent
				(void)close(socketfd);
			}
		}
	}
}
