Pages

Subscribe:

Ads 468x60px

Friday, November 30, 2012

How to write a Simple Web Server using C

In this post I'm going to explain how to write a simple but functional web server using sockets. First we have to set configuration for our web server and most common way of doing is to put the configurations into a .ini file. I used inih which is a simple .ini parser written in C. source 

Below is a sample .ini file
; Config file for ASK server

[Protocol]             
Version=6              ; IPv6

[Web]
http_version=HTTP/1.1
root_dir = www/
default_page = index.html
error_page = error.html
backlog = 10
max_header_size=1024

[Codes]
200 = OK
404 = Content Not Found

Below is the  askserver.c source file.  First the configurations are loaded and waiting for incoming requests. Then whenever a request is arrived the server check for the file requested. If it is available send a message code 200 and the file type server is going to send to the client. If the requested file was not fond, then send the message code 400 content not found. 
When a available file is send to the client, method is used.

sendfile(destination, source, offset,size);

//******************** askserver.c ******************/
// Aruna Sujith Karunarathna
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <netinet/tcp.h>
#include <limits.h>
#include <unistd.h>
#include <assert.h>
#include "ini.h"

#define CONFIGURATION_FILE  "configuration/ask.ini"


typedef struct{
    int version;
    int backlog;
	int max_header_size;
    const char* http_version;
    const char* root_dir;
	const char* default_page;
	const char* error_page;
} configuration;
configuration config;

void sigchld_handler(int s){
	while(waitpid(-1, NULL, WNOHANG) > 0);
}

// get sockaddr, IPv4 or IPv6:
void *get_in_addr(struct sockaddr *sa){
	if (sa->sa_family == AF_INET) {
		return &(((struct sockaddr_in*)sa)->sin_addr);
	}
	return &(((struct sockaddr_in6*)sa)->sin6_addr);
}

static int handler(void* user, const char* section, const char* name,const char* value){
    
    configuration* pconfig = (configuration*)user;

    #define MATCH(s, n) strcasecmp(section, s) == 0 && strcasecmp(name, n) == 0
    if (MATCH("protocol", "version")) {
        pconfig->version = atoi(value);   
    } else if (MATCH("web", "http_version")) {
        pconfig->http_version = strdup(value);
    } else if (MATCH("web", "root_dir")) {
        pconfig->root_dir = strdup(value);
    } else if (MATCH("web", "default_page")) {
        pconfig->default_page = strdup(value);
    } else if (MATCH("web", "error_page")) {
        pconfig->error_page = strdup(value);
    } else if (MATCH("web", "backlog")) {
        pconfig->backlog = atoi(value);
    } else if (MATCH("web", "max_header_size")) {
        pconfig->max_header_size = atoi(value);
    } else {
        return 0;  /* unknown section/name, error */
    }
    return 1;
}

char* get_extension(char* file_name){
    char* extension;
    extension = strchr(file_name,'.')+1	; 
    printf("EXTENSION %s \n",extension);
    return extension;
}

char* get_content_type(char* extension){
    char* type;
    if(strcmp(extension,"html")==0)
        type = "text/html";
    else if(strcmp(extension,"css")==0)
        type = "text/css";
    else if(strcmp(extension,"txt")==0)
        type = "application/text";
    else if(strcmp(extension,"pdf")==0)
        type = "application/pdf";
    else if(strcmp(extension,"zip")==0)
        type = "application/zip";    
    else if(strcmp(extension,"xml")==0)
        type = "application/xml";  
    else if(strcmp(extension,"js")==0)
        type = "application/javascript";                  
    else if(strcmp(extension,"jpg")==0)
        type = "image/jpg";
    else if(strcmp(extension,"png")==0)
        type = "image/png";    
    else if(strcmp(extension,"exe")==0)
        type = "application/octet-stream";
    else if(strcmp(extension,"ico")==0)
        type = "image/x-icon";
    else if(strcmp(extension,"php")==0)
        type = "text/html";
        
    printf("TYPE %s ",type);    
    return type;    

}

int set_header(char *header,int status_code,char *file_name,int file_length){
    if(status_code==404){
        //printf("%s %d File Not Found\n",config.http_version,status_code);
        sprintf(header, 
				"%s %d File Not Found\n"					
		     	 "\n",config.http_version,status_code);
		printf("\nHEADER MESSAGE %s \n",header);
        return -1;
    }
	if(status_code==400){
		sprintf(header, 
				"%s %d Bad Request\n"					
		     	 "\n",config.http_version,status_code);
		return -1;
	}
	if(status_code==501){	
		sprintf(header,"%s %d POST Not Implemented\n "
		"\n POST Not Implemented",config.http_version,status_code);	
		return -1;
	}
	char *extension;
	extension = get_extension(file_name);     
	char *content_type;
	content_type = get_content_type(extension);
		
	sprintf(header,"%s %d OK \n"	
	"Content-Type: %s\n"
	"Content-Length: %i\n"
		      "\n",config.http_version,status_code,content_type ,file_length);
	printf("\nHEADER MESSAGE %s \n",header);	      
}

char* check_request(char * request){
	char* ptr;
	ptr = strstr(request," HTTP/");
   // printf("CHK REQUEST %s \n",request);
	if(ptr == NULL)	{
		printf("Not HTTP request\n");
	}else{
	 /* *** HTTP request received *** */

		/* *** check for GET request *** */
		if(strncmp(request,"GET ",4) == 0)	{
			ptr="GET";
		}
		/* *** check for POST request, give 501 error if received *** */
		else if(strncmp(request,"POST ",5) == 0){
			printf("501 Method not implemented\n");
			ptr="POST";
		}
	}

	return ptr; 
}

char* get_path(char *request)
{
	int i=0;
//	printf("REQUEST %s \n",request);
	char *token = NULL;
	token = strtok(request, " ");
	token = strtok(NULL, " ");
	
	return token;
}

int send_content(char* file_name, int socket, int status_code){
    int sent_size; 	
    char header[config.max_header_size];
	int open_file;               /* file descriptor for source file */
    struct stat stat_buf;  /* hold information about input file */
    off_t offset = 0;      /* byte offset used by sendfile */
    int return_code;                /* return code from sendfile */
  
    open_file = open(file_name, O_RDONLY);/* check that source file exists and can be opened */ 
    fstat(open_file, &stat_buf);         /* get size and permissions of the source file */
   
  
	set_header(header,status_code,file_name,(int)stat_buf.st_size);
	sent_size=send(socket, header, strlen(header), 0);
	
	
    /* copy file using sendfile ####################################### */
    // sendfile(destination, source, offset,size);
    return_code = sendfile (socket, open_file, &offset, stat_buf.st_size);
    printf("file size %d Bytes \n\n",return_code);
    
    if (return_code == -1) {
        fprintf(stderr, "error from sendfile: %s\n", strerror(errno));
 	}
 	else if (return_code != stat_buf.st_size) {
        fprintf(stderr, "incomplete transfer from sendfile: %d of %d bytes\n", return_code, (int)stat_buf.st_size); 
    }
    /* clean up and exit */
    close(socket);
    close(open_file);
    return 0;

}

int main(int argc, char* argv[]){

        if (ini_parse(CONFIGURATION_FILE , handler, &config) < 0) {
                printf("Can't load 'ask.ini'\n");
                return 1;
        }
         printf("Config loaded from 'ask.ini':\nversion=%d\nbacklog=%d\nmax_header_size=%d \nhttp_version=%s\n",
        config.version, config.backlog, config.max_header_size, config.http_version);
        
	int sockfd, new_fd;  // listen on sock_fd, new connection on new_fd
	struct addrinfo hints, *servinfo, *p;
	struct sockaddr_storage their_addr; // connector's address information
	socklen_t sin_size;
	struct sigaction sa;
	int yes=1;
	char s[INET6_ADDRSTRLEN];
	int rv;

	memset(&hints, 0, sizeof hints);
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_STREAM;
	hints.ai_flags = AI_PASSIVE; // use my IP

	if ((rv = getaddrinfo(NULL, argv[1], &hints, &servinfo)) != 0) {
		fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
		return 1;
	}

	// loop through all the results and bind to the first we can
	for(p = servinfo; p != NULL; p = p->ai_next) {
		if ((sockfd = socket(p->ai_family, p->ai_socktype,
				p->ai_protocol)) == -1) {
			perror("server: socket");
			continue;
		}

		if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &yes,
				sizeof(int)) == -1) {
			perror("setsockopt");
			exit(1);
		}

		if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
			close(sockfd);
			perror("server: bind");
			continue;
		}

		break;
	}

	if (p == NULL)  {
		fprintf(stderr, "server: failed to bind\n");
		return 2;
	}

	freeaddrinfo(servinfo); // all done with this structure

	if (listen(sockfd, config.backlog) == -1) {
		perror("listen");
		exit(1);
	}

	sa.sa_handler = sigchld_handler; // reap all dead processes
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = SA_RESTART;
	if (sigaction(SIGCHLD, &sa, NULL) == -1) {
		perror("sigaction");
		exit(1);
	}

	printf("ASK_SERVER: waiting for connections...\n");

	while(1) {  // main accept() loop
		sin_size = sizeof their_addr;
		new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
		if (new_fd == -1) {
			perror("accept");
			continue;
		}

		inet_ntop(their_addr.ss_family,get_in_addr((struct sockaddr *)&their_addr),s, sizeof s);
		printf("ASK_SERVER: got connection from %s\n", s);

		if (!fork()) { // this is the child process
			close(sockfd); // child doesn't need the listener
			int buffer_size =1024;
			char* char_buffer = malloc(buffer_size);
			//get the client request
			read(new_fd,char_buffer,buffer_size);
			char *path;
  		    //printf("REQUEST ------ %s \n",char_buffer);
			//printf("CHECK_REQUEST %s \n",check_request(char_buffer));
			char* request_type = check_request(char_buffer);
			printf("REQUEST TYPE ::::::: %s\n",request_type);
			char* header=malloc(200);
  		    memset(header,0,200);
            
            if(request_type==NULL){	  	
  			    set_header(header,400,NULL,0);
  			    send(new_fd, header, strlen(header), 0);
  			    return 0;
  			}
  		    else if(strcmp(request_type,"POST")==0){
  			    set_header(header,501,NULL,0);
  			    send(new_fd, header, strlen(header), 0);
  			    return 0;
  		    }   
  		    
  		    path = get_path(char_buffer);
  		    if(strcmp("/",path)==0){
  		        path = (char*)config.default_page;
  		        // path = config.default_page;
  		    }
  		    char *root_dir=malloc(100);
			memset(root_dir,0,sizeof root_dir);
			strcpy(root_dir,config.root_dir);			
			path=strcat(root_dir,path);
			//printf("11111111111 real %s \n", path);
			
			/* ******* REQUEST FILE FOUND ********** */
  		    if(access(path, F_OK ) != -1){
				 send_content(path,new_fd,200);
				 //printf("OKKKKKKKKK %s \n", path);
			}
  		    else{					
				memset(root_dir,0,100);
				strcpy(root_dir,config.root_dir);
				path=strcat(root_dir,"/");
				path=strcat(root_dir,config.error_page);
				//printf("ERRRRRRRRRRORRR %s \n",path);
				send_content(path,new_fd,404);	
			}
  		    
			
		//	if (send(new_fd, "Hello, world!", 13, 0) == -1)
		//		perror("send");
		    free(char_buffer);
			close(new_fd);
			exit(0);
		}
		close(new_fd);  // parent doesn't need this
	}	
	
        printf("Config loaded from 'ask.ini': version=%d, backlog=%d,max_header_size=%d ,  http_version=%s\n",
        config.version, config.backlog, config.max_header_size, config.http_version);
        return 0;
		
}

I have included a sample www folder in the same directory to simulate the web server.

Download the full project from here.... DOWNLOAD

How to Test the Sample ASK Server.

Open a Terminal and Type the Following Commands.

[aruna@ubuntu]~$ makefile

[aruna@ubuntu]~$ ./askserver 7788


The argument 7788 is the port that we bind the webserver. Now open a web browser and type the following address localhost:7788

You'll see the web server is up and running. :)