###############################################################################
############### ###############
############### Webrahmen - C++ Embedded Web Server Framework ###############
############### ###############
###############################################################################
Copyright (C) 2019-2023 Dr. Matthias Meixner - meixner@gmx-topmail.de
Installation
============
To install to /usr just type:
make
sudo make install
To install to a different prefix and build root location, these can be passed as
make variables, e.g.:
make install DESTDIR=buildroot PREFIX=/usr/local
To create a Debian package for installation use:
make deb
Quickstart
==========
To create your own server you need to derive HttpServer and overload the methods
that you want to implement with your server. Overloading of the following
methods is supported:
- get
- head
- post
- options
- put
- del(ete)
When not overloaded they return "HTTP 405 Method not allowed".
All methods have the same interface: They are invoked with two objects for
receiving the request and sending a reply to the http client.
For example the API for get() looks like this:
void get(HttpRequest &request, HttpResponse &response)
The URL of the request can be found in request.url. URL-decoding and checks have
been applied so that it can safely be used as part of a path name.
Header fields can be accessed via request.header and query options are found in
request.option.
The response object is used for sending the reply. It offers several ways of
sending it.
- sendDocument - send a block of data as reply
- sendBegin/sendData/sendEnd - send a reply in several chunks
- sendString - send a string as reply
- sendFile - send a file as reply
- requestBasicAuth - send a username/password request
Additional headers can be passed in variable response.header.
The response object takes care to comply with the HTTP protocol: It prevents to
send more than one reply, it takes care to send out only headers in case of
HEAD and it sends a default "404 Not found" if no other response has been sent.
License
=======
Copyright (C) 2019-2023 Dr. Matthias Meixner - meixner@gmx-topmail.de
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
###############################################################################
################################## #################################
################################## Tutorial #################################
################################## #################################
###############################################################################
All files of the tutorial can be found in the tutorial folder.
For the HTTPS examples you will need a server certificate. For quick tests you
can use a self signed certificate, which can be generated by openssl as follows
(replace yourserver.com as appropriate):
openssl req -x509 -newkey rsa:2048 -keyout server.pem -out server.pem\
-nodes -days 365 -subj "/CN=yourserver.com"
Tutorial 1: Web server in 20 lines
==================================
Let's create a small test server that sends "Hello World!"
First we need to derive our server from HttpServer, and overload get.
#include <webrahmen/httpserver.h>
using namespace webrahmen;
class TestServer: public HttpServer {
public:
TestServer() {}
void get(HttpRequest &request, HttpResponse &response);
};
Now we implement get() and send "Hello World!", if the URL is empty.
void TestServer::get(HttpRequest &request, HttpResponse &response)
{
if(request.path=="/") response.sendString(HTTP_OK,"Hello World!","text/plain");
}
In the end we need to create the server object and start it to listen on
port 8080:
int main()
{
TestServer server;
server.httpServe("8080");
}
httpServe() starts listening on the port and handles all requests. It only
returns in case of error.
Tutorial 1a: Web server listening on a specific interface
=======================================================
If the server should not listen on all interfaces but e.g. only on localhost
an address can be given for httpServe:
int main()
{
TestServer server;
server.httpServe("8080","localhost");
}
Tutorial 2: Log transmitted headers
===================================
For printing the transmitted headers, just set a logging function before
invoking httpServe. The function needs to have a fprintf-like interface,
for example using fprintf with stderr:
int main()
{
TestServer server;
server.enableLog(fprintf,stderr);
server.httpServe("8080");
}
Tutorial 3: Deliver HTML files from disk
========================================
For this we add a path name to the constructor that tells where to find the
files. Then sendFile() is used instead of sendString() to send the data.
The mime type is determined based on the suffix of the file.
class TestServer: public HttpServer {
std::string docdir;
public:
TestServer(const std::string &d) :docdir(d) {}
void get(HttpRequest &request, HttpResponse &response);
};
void TestServer::get(HttpRequest &request, HttpResponse &response)
{
if(request.path=="/") request.path="/index.html";
response.sendFile(HTTP_OK,docdir+request.path);
}
int main()
{
TestServer server("htdocs");;
server.httpServe("8080");
}
Tutorial 4: HTTPS support
=========================
To provide HTTPS support just use httpsServe() instead of httpServe() and
provide a key and certificate file in PEM format. In this example we assume
that server.pem contains both the key and certificate. The CA is left to the
default settings.
int main()
{
TestServer server("htdocs");;
server.httpsServe("8443","server.pem","server.pem");
}
Tutorial 5: Providing both HTTP and HTTPS support
=================================================
To provide HTTP and HTTPS support, run one of httpServe or httpsServe in
another a second thread, e.g.:
int main()
{
TestServer server("htdocs");;
std::thread t([&]{ server.httpsServe("8443","server.pem","server.pem"); });
server.httpServe("8080");
t.join();
}
Tutorial 6: Dropping privileges after setting up listening ports using HTTP
===========================================================================
To use privileged ports, the application needs to be started as root and
later on drop root privileges.
For this setting up the listening port needs to be separated from starting
the server and in between privileges need to be dropped.
int main()
{
// Set up the listen port as long as we run with higher privileges
TCPListen listenport;
if(listenport.listen("80")<0) {
fprintf(stderr,"Failed to listen\n");
return 1;
}
// drop the privileges by switching to some other user and group, e.g. www-data
// first switch the group, since we can no longer switch it after switching the user
group *g=getgrnam("www-data");
if(!g || setgid(g->gr_gid)==-1) {
fprintf(stderr,"Failed to set group 'www-data'\n");
return 1;
}
// then switch the user
passwd *p=getpwnam("www-data");
if(!p || setuid(p->pw_uid)==-1) {
fprintf(stderr,"Failed to set user 'www-data'\n");
return 1;
}
// Set up the server using the listen port set up above
TestServer server("htdocs");;
server.httpServe(listenport);
}
Tutorial 7: Dropping privileges after setting up listening ports using HTTPS
============================================================================
This works similar to the HTTP case, just the setup of the TLS listen port
takes some additional configuration for TLS:
int main()
{
// Set up TLS configuration parameters
TLSConfig config;
if(!config.setSystemCA()) fprintf(stderr,"Failed to set default CAs\n");
if(!config.setCertKeyFile("server.pem","server.pem")) fprintf(stderr,"Failed to set certificate and key\n");
//Leave the CA setting at the default, if you want to add a CA, uncomment the following line
//if(!config.setCAFile("CA.pem")) fprintf(stderr,"Failed to set CA file %s\n",CA);
// Set up the listen port as long as we run with higher privileges, use 10s timeout for TLS handshake
TLSListen listenport(config,10000);
if(listenport.listen("443")<0) {
fprintf(stderr,"Failed to listen\n");
return 1;
}
// drop the privileges by switching to some other user and group, e.g. www-data
// first switch the group, since we can no longer switch it after switching the user
group *g=getgrnam("www-data");
if(!g || setgid(g->gr_gid)==-1) {
fprintf(stderr,"Failed to set group 'www-data'\n");
return 1;
}
// then switch the user
passwd *p=getpwnam("www-data");
if(!p || setuid(p->pw_uid)==-1) {
fprintf(stderr,"Failed to set user 'www-data'\n");
return 1;
}
// Set up the server using the listen port set up above
TestServer server("htdocs");;
server.httpServe(listenport);
}
Tutorial 8: Redirect HTTP requests to HTTPS
===========================================
For this we use two different servers, the server listening on the HTTP
port just sends a redirect.
class RedirectServer: public HttpServer {
public:
void get(HttpRequest &request, HttpResponse &response) {
response.setLocation("https://",request.hostname,request.path,request.option);
response.sendDocument(HTTP_MOVED,"Moved",5,"text/plain");
}
};
and in main():
RedirectServer redirect;
std::thread t([&]{redirect.httpServe(tcplistenport);});
Tutorial 9: Using cookies
=========================
Received cookies can be found in the cookie map of the request.
Cookies are set using HttpResponse::addCookie(). This returns an index that can
be used to add additional attributes to this cookie like domain, path, max-age,
httponly or secure option.
It works like this:
void TestServer::get(HttpRequest &request, HttpResponse &response)
{
if(request.path=="/") {
// set session cookie for this page
response.addCookie("first","firstvalue");
// set persistent cookie for this domain and all subpages
auto idx=response.addCookie("second","secondvalue");
response.setCookieDomain(idx,request.hostname);
response.setCookiePath(idx,"/");
response.setCookieMaxAge(idx,30);
// print cookies from the request
std::string doc="Cookies found:\n";
for(auto &&i: request.cookie) {
doc+=i.first+"="+i.second+"\n";
}
response.sendString(HTTP_OK,doc,"text/plain");
}
}
On first invocation the list of printed cookies is empty, on a reload
the cookies are displayed that have been set on the previous invocation.
Tutorial 10: Using authentication
================================
Basic authentication can be used to request a user name and password. The
checking of the user and password is up to the application. getBasicAuth()
extracts authentication provided by the browser, requestBasicAuth() is used to
request the user for a user name and password.
It works like this:
void TestServer::get(HttpRequest &request, HttpResponse &response)
{
.....
if(request.path=="/secure.html") {
std::string user,pwd;
if(!request.getBasicAuth(user,pwd) || user!="foo" || pwd!="bar") {
response.requestBasicAuth("Realm");
return;
}
response.sendString(HTTP_OK,"You have access","text/plain");
}
}
Tutorial 11: File upload
========================
For file upload we add the post() method to our server. For simplicity
it just writes the file content to posted.data in htdocs.
void TestServer::post(HttpRequest &request, HttpResponse &response)
{
if(request.path=="/post.html") {
FILE *fp=fopen((docdir+"/posted.data").c_str(),"wb");
if(!fp) {
response.sendString(HTTP_UNAVAILABLE,"Cannot create","text/plain");
return;
}
char buffer[4096];
ssize_t r;
do {
r=request.readData(buffer,sizeof(buffer));
if(r<0 || (r>0 && fwrite(buffer,1,r,fp)!=(size_t)r)) {
fclose(fp);
response.sendString(HTTP_UNAVAILABLE,"Write error","text/plain");
return;
}
} while(r==sizeof(buffer));
fclose(fp);
response.sendString(HTTP_OK,"","text/plain");
}
}
You can test the upload using wget:
wget --post-data="hello" http://localhost:8080/post.html
Tutorial 12: File upload from an HTML form
==========================================
First we need to display an HTML form, which displays the input form.
For this we overload the get method to deliver the desired document:
void TestServer::get(HttpRequest &request, HttpResponse &response)
{
if(request.path=="/") request.path="/upload.html";
response.sendFile(HTTP_OK,docdir+request.path);
}
The result is posted as multipart/form-data, which we have to split
into the separate parts of the input form. This is done using MultipartReader.
We use nextPart() to skip the optional preamble and start with the first part.
It returns the type, input field name and in case of a file upload the filename.
Data is read using readData(). When it indicates a EOF by a short read, we
use nextPart() to switch to the next input. Data is written to "posted.data" with
a consecutive number postfix. The status of the upload is sent back as HTML
document.
void TestServer::post(HttpRequest &request, HttpResponse &response)
{
if(request.path!="/post.html") return;
int cnt=0;
MultipartReader rd(request);
std::string type;
std::string name;
std::string filename;
std::string result="<!DOCTYPE><html><body>";
while(rd.nextPart(type,name,filename)==NetOK) {
char str[32];
snprintf(str,sizeof(str),"%d",cnt++);
result+="Uploading: "+type+" name="+name+" filename="+filename+" as "+upload+str;
FILE *fp=fopen((upload+str).c_str(),"wb");
if(fp) {
char buffer[4096];
ssize_t r;
do {
r=rd.readData(buffer,sizeof(buffer));
if(r<0 || (r>0 && fwrite(buffer,r,1,fp)!=1)) {
result+=" failed<br>";
goto fail;
}
} while(r==sizeof(buffer));
result+=" OK<br>";
fail:
fclose(fp);
} else result+=" failed to create<br>";
}
result+="</body></html>";
response.sendString(HTTP_OK,result,"text/html");
}