Nweb
nweb Tiny Webserver Example
nweb Tiny Safe Webserver nweb is a 200 lines of C code example of how a webserver actually works at its core. It is a static page only server (no fancy live content) and only spepcific files types. It actually works too and a few 100's people use it on the Internet for for their own projects. Note: This code is a simple code sample and so NOT under any license or Copyrighted. Use it any way you like. |
nweb - A Tiny Safe Web Server with source code!
Have you ever:
- Wondered how a web server actually works via a simple example?
- Wanted to run a tiny safe web server (but were worried about using a fully blown web server may cause security issues)?
- Wondered how to write a program that accepts incoming messages via a network socket?
- Just wanted your own web server to experiment, extend and learn with.
Well, look no further = nweb is what you need. This is a simple web server to understand (only 200 lines of C source code), so you work out how it functions to server out files for a web browser and make changes. Add to this the fact it runs just as a regular user (if you use a high port number), it can not run any server side scripts or programs, so it can't open up any special privileges or security holes. If you want it to run with the standard web server port of 80 you tyipically need to start it as the superuser root.
nweb only transmits to the browser the following types of files
- Static web pages with extensions .html or .htm
- Graphical images .gif or .png or .jgp or .jpeg or .ico (tiny picture for the browswer tab)
- Useful file types like compressed binary files and archives .zip , gz and .tar
If your favourite static file type is not in this list, you can simply add it in the source code and recompile to allow it - a few seconds work
The file supplied includes the UNIX source code in C and a precompiled AIX and Linux on Intel versions. The source will compile with the IBM VisualAge
C compiler or the GNU C compiler and should run unchanged on AIX, Linux or any other UNIX version for which you care to compile it.
On UNIX (assuming the compiler is in your path) just type:
- cc -O3 nweb.c -o nweb
Summary of the functions within nweb
There are only four functions in the source code and these are explained below:
- logger()
- This function is a convenient way to hangle storing logging messages, report an error and (if nessesary) stop nweb completely.
nweb breaks from the terminal session so it can not output onto your screen any progress of good operations, nor any errors so these are saved in the nweb.log file. In some cases the failure can be reported to the user's web browser as a fake web page describing the issue for example if the browser can not find or access the requested file. In other cases like a fatal problem, the details written to the log and nweb stops - so if nweb seems to disappear - check the log. It is called from the main function and also from the web function if an error can not be reported back to the web server user.
- web()
- This is the function that actually deals with the HTTP browser request and returning the data to the browser and to user.
This function is called in a child process, one for each web request and so allows the main web server process to continue waiting for more connections. Checks are made to ensure the request is safe and can be completed, it then transmits the requested static file to the browser/user and exits.
- main()
- This is the main web server process function. After checking the command parameters, it creates a socket for incoming browser requests and then sit
s in a loop accepting requests and starting child processes to handle them. It should never end.
Pseudo code of the program
Below is the pseudo code for the ~200 lines of source code. It should help you to understand the flow of the program.
web() - this function returns the requested data back to the browser { read from the socket the request check it.s a simple GET command check no parent directory requested to escape the web servers home directory if no file name given assume index.html check the file extension is valid and supported check the file is readable by opening it transmit the HTTP header to the browser transmit the file contents to the browser stop } main() { check the arguments supplied are valid check the directory supplied is sensible and not a security risk create a socket, bind it to a port number and start listening for requests become a daemon process so it continues if the user logs out ignore child programs (to avoid zombies on their death) forever { accept incoming socket connection fork a child process if the child process then call the web function else close socket } }
System Calls
As you may not have come across some of these system calls before, these are explained in more detail below and in particular how they all fit together. Although, you can always look them up in the manual or on the web. It can hard from just the code or the manual to see what they do and how they together make up a web server.
In UNIX and Linux most I/O is performed by the read and write system calls. Instead of reading and writing a file or a pipe, in this case, of process to process communication, we read and write a socket between the processes.
The socket(), bind(), listen() and accept() network system calls, all work together to create a web server process. Combined they set up a socket ready for use to communicate over a network. A socket is:
- An input output stream like that of regular pipes and files.
- But it can also be used over a network to allow remote access to/from a server
- A socket is bidirectional - you read and write the same socket. Obviously write one end and read from the other.
- This means regular read and write functions are used to send and receive data.
- As it.s a stream (no natural structure), you have to decide the protocol structure to get a descrete message sent and understtod at the other end.
- For HTTP, the request message and response message header are finished with carriage return (CR or "/r" in the C code) and line feed (LF and "/n" in the C code)
but the end of the requested file is highlighted by closing the socket. There are alternatives in HTTP but this is the simplest way to do it.
The socket() function creates the socket and returns the file descriptor which can be used with any function that uses file descriptors like read, write, close. The arguments to socket() tell the operating system what type of socket and communication you need. There are dozens of permutations and combinations available. The arguments used in the nweb program are very typical for a regular general purpose socket using IP and other options are rare in the writer.s experience.
The bind() function attaches a particular port number to the socket. When a remote client process is trying to contact your server is will use the IP address (often found by using a DNS service to convert a hostname to an IP address) and the port number. The port number tells the operating system inetd process, which service you want on the server. The port details on most UNIX machines are listed in /etc/ services. Included in here will be standard port numbers for services like FTP, telnet and web servers (usually port 80 or 8080). You should check the port numbers in /etc/ services to make sure you don.t try one which is already in use. Although, if you try you should get an error message as it is normally impossible for two servers to use the same port number But in this case, the nweb program bind() function lets inetd know it wants to handle the particular port number you told nweb to use.
The listen() function call tells the operating system you are now ready to accept incoming requests. This is the final switch that makes the socket available to local and remote programs over the network.
The accept() function actually stops your nweb program until there is a socket connection request to this IP address and port number. Once the accept function call returns it means that the socket file descriptor it returns is live. If you read bytes from the socket you get characters written from the client end and if you write bytes to the socket they get tramsmitted to the client process. But it is not normal to simply read or write a single socket. Normally, you want to allow multiple clients to access your servers service and if this process does a read or write operation it could block until there are characters to be read or the written characters can be transmitted. in other words this server process should be running the accept function again to allow a new connection to start. The way to handle this is for the server process to start a child process that does the "talking" to the client and any work it needs done and for the main server process to close the socket and rerun the accept function and so await the next client connection. Note: When the child process is started it inherits the parents open sockets and so the child keeps the socket alive. Note: The alternative to forking sub-processes is using multi-threaded fucntions but that is more complicated so we avoided in this webserver example.
So it is normal in network servers for you to find code that looks like this (in pseudo code form):
socket() - create the resources bind() - to a port listen() - ready to handle data forever { accept() - the connection Start a child to handle the work }
Other system call functions used!
- chroot()
- This function changes directory and then makes that directory the new top level direction for the process. It is the top root / directory. Only files in it are possible to read and write. It is used here for security as it is then impossible for nweb to send any file from a different directory tree.
- setpgrp
- This function sets the process group. The effect is for this process to break away from the other processes started by this user so it will not be effected by what happens to the users. This means the user can logging off and nweb will carry on running. This is called becoming a new process group leader.
- signal
- This function decides what happens when software interrupts arrive for the
process. Within the nweb.c code, the main server wants to ignore the death of a child signal. Without this the main server process would have to run the wait system call for each child process or they would be forever stuck in the "zombie" state waiting to the parent to call the wait() function. Eventually, there would be to many 1000's zombie processes and the users environment would hang as they could not create further processes.
- open(), read(), write() and close()
These are the regular C library system call functions but they are used for the reading of the sockets and files sent to the client web browsers. To a UNIX or Linux process the socket and files behave teh same way and use a file descriptor to acces them. Note: the socket is "opened" with the accept() function - the open() in the code is for opening the requested file.
The nweb.c code only shows you the server side of the socket. For the record here is the code that is required for the client side. Fortunately, the client side is
much simpler are you only need to supply the IP address and the port number to make the connection to the server. In the below the servers IP address is 192.168.0
.42 and the server port is 8181.
int sockfd; static struct sockaddr_in serv_addr; if((sockfd = socket(AF_INET, SOCK_STREAM,0)) <0) pexit("Network socket system call"); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr("196.168.0.42"); serv_addr.sin_port = htons(8181); if(connect(connectfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) <0) pexit("Network connect system call"); /* now the sockfd can be used to communicate to the server */
Warning: the bind() and connect() functions take a structure as the argument that must be zero filled before the setting particular options. In the nweb.c the str ucture is made a static C variable to ensure a it is zero filled when the program is started. If this structure was on the stack as a variable within the function or in the heap of the process it could be filled with old data and so its content not be zero filled. In which case you need to ensure it is zero filled using the bzero() function can be used to do this.
Example use on nweb:
$ mkdir /home/nag/webpages $ cd /home/nag/webpages $ cp /tmp/nweb.tar . $ tar xvf nweb.tar $ ls nweb.c nweb_aix nweb_linux_intel sample_index.html nweb.gif $ cat sample_index.html <HTML> <HEAD> nweb test </HEAD> <BODY> This is a test page. <H1>Header level 1</H1> <H2>Header level 2</H2> <UL> <LI>This is an unordered list. <LI>Which means bullet points. </UL> This is a sample gif image <IMG SRC="nweb.gif"> <B>The End</B> </BODY> </HTML> $ cp sample_index.html index.html $ chmod ugo+x nweb $ chmod ugo+r *.html nweb.gif $ nweb 8181 /home/nag/webpages & $ hostname nigel.acme.com
Start your browser and access the website:
Downloads
Download File | Comments |
nweb25.c | Current version 25 - 24th July 2016 Added SIGCHLD support for operating systems that don't support the System V signal SIGCLD |
nweb24.c | Version 24 - 23rd July 2016 |
nweb24_extra.zip | Current version 24 client.c and a miniature example of a website for testing purposes only |
nweb24_Ubuntu_RaspberryPi3_ARM | Version 24 compiled for Raspberry Pi 3 ARM running Ubuntu 16.04 |
nweb_AIX6_POWER | Version 24 compiled for AIX 6 (or above) on POWER |
- - - The End - - -