Web server (C Plus Plus)

From LiteratePrograms
Jump to: navigation, search

A simple web server library.

Contents

[edit] Class definitions

[edit] The header file

<<http.hpp>>=
#ifndef HTTP_HPP_INCLUDE_GUARD
#define HTTP_HPP_INCLUDE_GUARD

#include<string>
#include<map>

Everything is defined in namespace lp(LiteratePrograms), to avoid polluting the global namespace.

<<http.hpp>>=
namespace lp {
exceptions
http_request
server
} // namespace lp
#endif

[edit] Exceptions

Errors from the socket API are reported with exceptions.

<<exceptions>>=
struct http_error {
	virtual const char *what()=0;
	virtual ~http_error() {}
};
struct http_socket_error: http_error {
	const char *what() {return "socket()";}
};
struct http_bind_error: http_error {
	const char *what() {return "bind()";}
};
struct http_listen_error: http_error {
	const char *what() {return "listen()";}
};
struct http_accept_error: http_error {
	const char *what() {return "accept()";}
};
struct http_recv_error: http_error {
	const char *what() {return "recv()";}
};
struct http_send_error: http_error {
	const char *what() {return "send()";}
};

[edit] struct http_request

When a client connects, the http_request structure is filled with relevant data. If we can't parse a client request, the error member is filled with a message indicating that.

<<http_request>>=
struct http_request {
	std::string error;
	std::string method;
	std::string path;
	std::string version;
	std::map<std::string, std::string> fields;
	std::map<std::string, std::string> formvalues;
};

[edit] class http

This server class can only handle 1 connection at a time.

  • wait() is used to wait for client connections. It takes an optional timeout argument, which specifies the maximum wait-time in milliseconds.
  • request() will try to fetch the next client request.
  • reply() is used to send data to the client.
  • error() reports an error to the client.
<<server>>=
class http {
	int m_port;
	std::string m_path;
	int m_cs;		// Client socket
	int m_ss;		// Server socket
	std::string m_rbuf;
	http_request m_request;

	void setup();
	bool dorecv();
	std::string getline();
	void dosend(const std::string &);
public:
	http(int port): m_port(port), m_cs(0), m_ss(0) {}
	~http();
	bool wait(int timeout=0);
	const http_request &request();
	void reply(const std::string &data, const std::string &ctype);
	void error(const std::string &code, const std::string &msg);
};

[edit] Implementation

<<http.cpp>>=
#include"http.hpp"

#include<sys/socket.h>
#include<netinet/in.h>

namespace lp {
destructor
dorecv
getline
dosend
setup
wait
request
reply
error
} // namespace lp

[edit] Destructor

The destructor will close any open sockets.

<<destructor>>=
http::~http()
{
	if(m_cs>0) close(m_cs);
	if(m_ss>0) close(m_ss);
}

[edit] setup()

This function create the server socket (m_ss), and make it ready to receive connections.

<<setup>>=
void http::setup()
{
	try {
	struct sockaddr_in addr;
	if((m_ss=socket(PF_INET, SOCK_STREAM, 0))==-1) throw http_socket_error();

After creating a socket, we must bind it to a TCP port. For portability, we use htons() to convert the port number to network byte order.
The bind() function will typically fail if the TCP port is allready in use, or if the user running the program does not have permission to use that port.

<<setup>>=
	memset((void*)&addr, 0, sizeof addr);
	addr.sin_family=AF_INET;
	addr.sin_port=htons(m_port);
	addr.sin_addr.s_addr=INADDR_ANY;
	if(bind(m_ss, (struct sockaddr*)&addr, sizeof addr)==-1) throw http_bind_error();

We must tell the operating system to start listening to incoming connections.

<<setup>>=
	if(listen(m_ss, 1)==-1) throw http_listen_error();

If we get an error from bind() or listen(), the server socket should be closed.

<<setup>>=
	} catch(...) {
		if(m_ss>0) close(m_ss);
		m_ss=0;
		throw;
	}
}

[edit] dorecv()

dorecv() is a thin wrapper around the standard recv(). Everything received is put into m_rbuf.

<<dorecv>>=
bool http::dorecv()
{

We use a fixed buffer of size 1024. This should be enough for most GET requests, and dorecv() can be called repeatedly to receive more data.

<<dorecv>>=
	char b[1024];
	int nrecv;

We receive up to 1023 chars, leaving 1 char for a terminating '\0'.
An error from recv() means something is wrong with the TCP connection, so we throw an exception.

<<dorecv>>=
	if((nrecv=recv(m_cs, b, 1023, 0))<0) throw http_recv_error();

If nothing is received, the client won't be sending more data. We return false to signal this.

<<dorecv>>=
	if(!nrecv) return false;

We '\0'-terminate the local buffer, and append the content to m_rbuf.

<<dorecv>>=
	b[nrecv]='\0';
	m_rbuf+=b;
	return true;
}

[edit] getline()

This function tries to fetch a line from the client connection, calling dorecv() if neccessary.

<<getline>>=
std::string http::getline()
{
	std::string line;
	size_t ix;

If there is a newline character stored in m_rbuf, we allready have receive a line of data. If not, we repeatedly call dorecv() until either we have that newline or there is no more data (dorecv() returning false), in which case we just return the rest of the data.

<<getline>>=
	while((ix=m_rbuf.find('\n'))==std::string::npos) {
		if(!dorecv()) {
			line=m_rbuf;
			m_rbuf.erase();
			return line;
		}
	}

If we found a '\n', everything in m_rbuf up to it is moved into line. The '\n' itself is discarded.

<<getline>>=
	line=m_rbuf.substr(0, ix);
	m_rbuf.erase(0, ix+1);

The client is supposed to send '\r' before '\n', but we don't thrust that, so we only remove the last character if it happens to be '\r'.

<<getline>>=
	if(!line.empty() && line[line.size()-1]=='\r') 
		line.erase(line.size()-1);	// Remove trailing '\r'
	return line;
}

[edit] dosend()

dosend() is a thin wrapper around send(). It fails if it can't send all in one call (To be fixed).

<<dosend>>=
void http::dosend(const std::string &s)
{
	if(send(m_cs, s.c_str(), s.size(), 0)!=(int)s.size()) throw http_send_error();
}

[edit] wait()

wait() will return true when a client connection is ready, or false if more than timeout milliseconds has elapsed.
If timeout is 0, it will block forever.

<<wait>>=
bool http::wait(int timeout)
{

We must make sure that we have a server socket.

<<wait>>=
	if(!m_ss) setup();

The select() function uses a pointer to a timeval structure to set maximum wait time.

<<wait>>=
	struct timeval tmout;
	struct timeval *ptmout;

We have to convert our timeout value, which is in millisecond, to the members of struct timeval(tv_sec and tv_usec), which are in seconds and microseconds.

<<wait>>=
	if(timeout) {
		tmout.tv_sec=timeout/1000;
		tmout.tv_usec=(timeout%1000)*1000;
		ptmout=&tmout;

A timeout value of 0 means to wait forever. To tell this to select(), we must set the struct timeval pointer to NULL.

<<wait>>=
	} else ptmout=NULL;

The fd_set contains a list of file descriptors to listen to. We use the macro FD_ZERO() to initialize it, and FD_SET() to add our server socket to it.

<<wait>>=
	fd_set rset;
	FD_ZERO(&rset);
	FD_SET(m_ss, &rset);

select() expects a lot of arguments

  • The first is, for some reason, the highest numbered file descriptor in any of it's sets, plus 1.
  • We want to check if our socket is readable, so we pass rset as the second argument.
  • We don't have any use for the third and fourth arguments to select()', so we provide NULL.
  • Our struct timeval pointer is the last argument.

The return value is the number of ready file descriptors. Since we only provided one, we know that a return value greater than 0 means m_ss is readable.

<<wait>>=
	if(select(m_ss+1, &rset, 0, 0, ptmout)>0) {

We call accept() to tell the operating system that we are ready to talk to the client.
accept() expects a pointer to struct sockaddr, where it will store the address of the client. We are not interested in that address, but must provide such a pointer anyway.

<<wait>>=
		struct sockaddr_in client_addr;
		socklen_t addrsize(sizeof client_addr);

		if((m_cs=accept(m_ss, (struct sockaddr*)&client_addr, &addrsize))==-1)
			throw http_accept_error();
	
		return true;

If select() failed, or we had timeout, we return false.

<<wait>>=
	} else return false;
}

[edit] request()

request() tries to parse a HTTP request, and return a http_request object. If it fails, the error member will be filled with an explanation.

<<request>>=

readformdata

const http_request &http::request()
{
	size_t ix;

	m_request.error.clear();
	m_request.method.clear();
	m_request.path.clear();
	m_request.version.clear();
	m_request.fields.clear();
	m_request.formvalues.clear();

	std::string line;

We read in the first line of the client request. This is of the form "cmd path [version]". If version is lacking, this is an old-style (pre-HTTP/1.0) request.

<<request>>=
	if((line=getline()).empty()) m_request.error="Empty line";

Here, we move the first field to the method member of m_request. We accept 1 or more ' ' or '\t' as field separator.

<<request>>=

	// Method
	if((ix=line.find_first_of(" \t"))!=std::string::npos) {
		m_request.method=line.substr(0, ix);
		line.erase(0, ix);
		while(!line.empty() && isspace(line[0])) line.erase(0, 1);

The second field is moved to m_request.path. If the path includes a ? character, the rest of the string is handed over to readformdata() to extract the name/value pairs.

<<request>>=

		// Path
		if((ix=line.find_first_of(" \t"))!=std::string::npos) {
			m_request.path=line.substr(0, ix);
			line.erase(0, ix);
			while(!line.empty() && isspace(line[0])) line.erase(0, 1);

			// Form data in URL
			if((ix=m_request.path.find('?'))!=std::string::npos) {
				std::string formdata(m_request.path.substr(ix+1));
				m_request.path=m_request.path.substr(0, ix);

				readformdata(formdata, m_request.formvalues);
			}

The third field is optional. If it exists, it is copied to m_request.version.

<<request>>=

			// Version
			m_request.version=line;
		} else m_request.path=line;
	} else m_request.error="Syntax error";

If version is lacking there will be no more data, from the client, so we just return what we have.

<<request>>=
	if(m_request.version.empty()) return m_request;	// Old style HTTP request

The following lines contain one colon-separated name/value pair each.
An empty line, means end of data.

<<request>>=

	// Fields
	while(!(line=getline()).empty()) {
		if((ix=line.find(':'))==std::string::npos) {
			m_request.error="Syntax error";
			return m_request;
		}
		std::string &value=m_request.fields[line.substr(0, ix)]=line.substr(ix+1);
		while(!value.empty() && isspace(value[0])) value.erase(0, 1);
	}

	return m_request;
}

[edit] Handling form data in URL

The readformdata() function takes a string containing name/value pairs in the name=value format, separated by an & character. These values are stored in the formvalues reference. formvalue() is used to decode special characters.

<<readformdata>>=
std::string formvalue(const std::string &raw)
{
	std::string ret;
	for(size_t ix=0; ix<raw.size(); ++ix) {
		if(raw[ix]=='+') ret+=' ';
		else if(raw[ix]=='%') {
			int val(0);
			if(++ix<raw.size()) 
				val=16*(isalpha(raw[ix]) ? toupper(raw[ix])+10-'A' : raw[ix]-'0');
			if(++ix<raw.size()) 
				val+=isalpha(raw[ix]) ? toupper(raw[ix])+10-'A' : raw[ix]-'0';
			ret+=(char)val;
		} else ret+=raw[ix];
	}
	return ret;
}

void readformdata(const std::string &str, std::map<std::string, std::string> &formvalues)
{
	std::string name, value;
	bool reading_name(true);
	for(size_t ix=0; ix<str.size(); ++ix) {
		if(str[ix]=='=') {
			reading_name=false;
		} else if(str[ix]=='&') {
			formvalues[name]=formvalue(value);
			name.clear();
			value.clear();
			reading_name=true;
		} else if(reading_name) {
			name+=str[ix];
		} else {
			value+=str[ix];
		}
	}
	if(!name.empty()) formvalues[name]=formvalue(value);
}

[edit] reply()

This function should be used to send replies to the client. The ctype argument is a MIME-type (Ex.: text/html).

<<reply>>=
void http::reply(const std::string &msg, const std::string &ctype)
{
	std::string header("HTTP/1.1 200 Ok\r\nContent-Type: "+ctype+"\r\n\r\n");
	dosend(header+msg);
	close(m_cs);
	m_cs=0;
}

[edit] error()

The code argument should contain a standard HTTP error code (Ex.: 404). msg is a single line explanation.

<<error>>=
void http::error(const std::string &code, const std::string &msg)
{
	std::string header("HTTP/1.1 "+code+" "+msg+"\r\nContent-type: text/plain\r\n\r\n");
	std::string data(msg+"\r\n");
	dosend(header+data);
	close(m_cs);
	m_cs=0;
}

[edit] Example server

This is an example web server providing basic chat functionality. To keep things simple, some important features, like access control and input data validation, are ignored.

<<main.cpp>>=

#include"http.hpp"

#include<iostream>
#include<string>
#include<vector>

std::string frameset(
"<frameset rows=\"50%, 50%\">\n\
<frame src=\"top\" /> <frame src=\"bottom\" /> </frameset>\n"
);

std::string root("<html><head><title>Chat</title></head>"+frameset+"</html>");

struct chat_t {
	std::vector<std::string> msgs;
	std::string top(size_t howmany=10) {
		std::string ret(
"<html><head><meta http-equiv=\"refresh\" content=1 /></head>\n\
<body><table border=1><colgroup span=2><col width=\"10%\" /><col width=\"90%\"></colgroup>\n\
<tr><th>User id</th><th>Message</th></tr>\n"
);

		for(int n=(int)msgs.size()-howmany; n<(int)msgs.size(); ++n) {
			ret+=" <tr>";
			if(n>=0) ret+=msgs[n];
			else ret+="<td> </td><td> </td>";
			ret+="</tr>\n";
		}

		return ret+"</table></body></html>";
	}

	std::string bottom(std::map<std::string, std::string> formvalues) {
		std::string userid(formvalues["userid"]);
		if(userid.empty()) userid="anonymous";

		std::string msg(formvalues["msg"]);
		if(!msg.empty()) {
			msgs.push_back("<td>"+userid+"</td><td>"+msg+"</td>\n");
		}

		return std::string(
"<html><head></head><body>\n\
<form action=bottom method=\"get\" value=\""+userid+"\">\n\
<label for=userid>User id: </label><input type=text id=userid name=userid value=\""+userid+"\" />\n\
<label for=msg>Text: </label><input type=text size=100 id=msg name=msg />\n\
<input type=submit value=\"Send\" />\n\
</form>\n\
</body></html>\n"
);
	}
};

int main(int argc, char *argv[])
{
	try {
	int port(argc>1?atoi(argv[1]):80);
	chat_t chat;
	
	lp::http server(port);

	while(server.wait()) {
		const lp::http_request &request(server.request());
		if(!request.error.empty()) std::cerr<<"ERROR: "<<request.error<<'\n';

		if(request.method=="GET") {
			if(request.path=="/") server.reply(root, "text/html");
			else if(request.path.substr(0, 4)=="/top") 
				server.reply(chat.top(), "text/html");
			else if(request.path=="/bottom") 
				server.reply(chat.bottom(request.formvalues), "text/html");
			else server.error("404", "Not found "+request.path);
				
		} else {
			server.error("501", "Not implemented");
		}
	}
	} catch(lp::http_error &e) {
		std::cerr<<"Exception: "<<e.what()<<'\n';
	}

	return 0;
}



<<Makefile>>=
all: lphttpd

lphttpd: main.o http.o
	c++ -o lphttpd -Wall main.o http.o

main.o: main.cpp http.hpp
	c++ -o main.o -Wall -c main.cpp

http.o: http.cpp http.hpp
	c++ -o http.o -Wall -c http.cpp

Download code
hijacker
hijacker
hijacker
hijacker