- Notifications
You must be signed in to change notification settings - Fork20
C++ API: http server with local dynamic or precompiled repository containers
License
titi38/libnavajo
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
Modern C++ implementation of an HTTP/1.x and HTTP/2 server
- High-performance, multi-threaded HTTP server: Efficient handling of requests with FIFO connection queue and thread pooling for optimal concurrency.
- Simple and intuitive API: Method-based structure that focuses on handling specific HTTP requests, making it easy to learn and integrate.
- Built-in session management: Automatically handles user sessions, simplifying state management for web applications.
- Modular and reusable design: Components can be reused across different parts of an application, promoting clean organization and maintainable code.
- Type-safe headers and MIME type support: Ensures robust and secure handling of HTTP headers and content types.
Advanced Security and Protocol Support:
- Full SSL/TLS support: Provides secure connections via HTTPS.
- Client certificate authentication: Supports mutual TLS (mTLS) for enhanced security.
- PAM (Pluggable Authentication Module) integration: Enables server-side authentication using system accounts.
- HTTP authentication: Built-in support for basic authentication mechanisms.
Feature-Rich Functionality:
- WebSocket support (RFC 6455): Full-duplex communication over WebSocket protocol, with or without TLS (
wss://). - Multipart form data support: Efficient handling of file uploads and form submissions via HTTP multipart requests.
- Extensive logging system: A set of classes for robust logging to standard output, syslog, and log files, with support for configurable log levels (DEBUG, INFO, WARNING, ERROR).
- Ideal for developing high-performance REST APIs and web applications in C++.
In today’s world, where speed, accessibility, and seamless deployment are key,web interfaces andREST APIs have become essential. Yet, integrating these technologies into C++ projects often feels like navigating a maze of complex architectures and dependencies.
Enterlibnavajo—a lightweight, high-performance C++ framework that lets you quickly build robust web servers and dynamic interfaces with minimal effort. Say goodbye to cumbersome middleware and intricate setups. With just a few lines of code, libnavajo empowers you to deploy secure HTTP/HTTPS servers, serve files, handle dynamic content, and implement authentication—all within your existing C++ applications.
Whether you’re modernizing legacy software or building something entirely new, libnavajo offers a straightforward path to leveraging the latest web technologies directly in C++. Dive in and discover how this powerful framework can simplify your development process while ensuring top-notch performance.
There is undeniable value in developing web interfaces. Indeed, they are scalable, performant, and interactive. They do not impose any server load: part of the application logic and rendering is done by your browser. Moreover, they have a significant advantage: backward compatibility. Those who, like me, had to migrate Gtk2 interfaces to Gtk3, or more recently Qt4 to Qt5, will understand ;-)
Web interfaces are multi-user, and they have reliable and mature authentication mechanisms. They are easily accessible! No more configuring VNC, or users fighting over mouse control! Today, they have become powerful, efficient, and even responsive design, meaning they can adapt their appearance depending on the device used (computer, tablet, smartphone…).
It was with this in mind that I developed the libnavajo project. The idea was to offer C/C++ developers, the real ones ;-), a complete framework that includes a fast and efficient web server, with its web repositories, dynamically generated pages, and all the mechanisms to handle sessions, cookies, parameter passing (post and get), content compression, various mime types, keep-alive, SSL encryption (https), X509 authentication, etc… and the ability to generate dynamic content directly by accessing the application objects without any additional layer. Of course, nothing prevents the use of additional middleware (CORBA, web services, or others…).
Since then, we have been using this framework regularly in our developments. It has been enriched and debugged over time. The current version is stable and mature. It has few dependencies and is relatively portable. It is distributed via GitHub under the LGPLv3 license.
The packageszlib-devel,openssl-devel,pam-devel,doxygen, andgraphviz must be installed to compile the libnavajo framework.
Next, let's retrieve the source code:
$ git clone https://github.com/titi38/libnavajo.git
Then, let's compile using the provided Makefile:
$ make`Generate the Doxygen documentation:
$ make docs
Compile the examples:
$ make examples
Finally, install the framework:
$ sudo make install
The scriptsdpkgBuild.sh andrpmbuild/libnavajo.el6.spec are available to build packages for Debian- and RedHat-based distributions.
Running an example file allows you to verify that everything is working:
$cd examples/1_basic$ ./example[2015-05-07 06:57:49]> WebServer: Service is starting![2015-05-07 06:57:49]> WebServer: Listening on port 8080 [2015-05-07 06:58:09]> WebServer: Connection from IP: ::1
At this point, you can connect to the server using your preferred browser by entering the URL:http://localhost:8080,http://localhost:8080/dynpage.html, orhttp://localhost:8080/docs/.
libnavajo is a comprehensive framework that includes its own centralized and efficient log management system. If not initialized, log messages are lost.
Three types of output are available. These inherit from the abstract classLogOutput and allow the addition of new log types to the standard output, a file, or the syslog journal.
LogRecorder is implemented as a singleton. It is accessed via the methodgetInstance(), which returns the unique instance of the object, creating it if it does not already exist.
Fig. : Class Diagram of the Log Management System
A macroNVJ_LOG simplifies logging calls by acting as a shortcut forLogRecorder::getInstance(). To initialize the log system, simply add instances of objects derived fromLogOutput.
For displaying logs on the standard output, add the following at the beginning of your application:
NVJ_LOG->addLogOutput(new LogStdOutput);
For writing logs to the filenavajo.log:
NVJ_LOG->addLogOutput(new LogFile("navajo.log"));
To add entries to the syslog with the identifiernavajo:
NVJ_LOG->addLogOutput(new LogSyslog("navajo"));
To write a message to all the added outputs, use theappend() method, which takes two parameters: the message and a severity level.
There are six levels:NVJ_DEBUG,NVJ_INFO,NVJ_WARNING,NVJ_ALERT,NVJ_ERROR, andNVJ_FATAL.
Example usage:
NVJ_LOG->append(NVJ_INFO,"This is an info log message");
By default,NVJ_DEBUG messages are ignored. If you want to display them, set yourLogRecorder to "debug" mode:
`NVJ_LOG->setDebugMode(true);`
To release all resources, simply use:
LogRecorder::freeInstance();A web server implements the HTTP 1.1 protocol as defined in the RFC 2616 specifications. Its role is to respond to resource requests from clients, which are addressed using a URL (Uniform Resource Locator).
TheWebServer object in libnavajo fully manages the operation of one or more HTTP servers.
TheWebServer creates listening sockets—one per IP protocol (i.e., two for IPv4 and IPv6)—and manages the lifecycle of connections.
Each instance of theWebServer object is designed to start in its own execution thread. It then creates a connection pool (20 threads by default).
The web server receives resource requests and must tailor the response to the different protocol versions and features supported by the client.
For example, when my browser (Firefox) attempts to connect to the pagehttp://localhost:8080/dynpage.html?param1=34,
the generated HTTP request is as follows:
GET /dynpage.html?param1=34 HTTP/1.1 Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,\*/\*;q=0.8 Accept-Encoding:gzip, deflate, sdch Accept-Language:fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4 Cache-Control:max-age=0 Connection:keep-alive Host:localhost:8080 ...When a client (browser) makes a request, the server must generate a response based on the metadata contained in the request headers.
For example,Accept-Encoding: gzip will be interpreted by the server, which will automatically compress the content of responses if they are large enough. Connections will use the keep-alive mechanism, which is the default mode in HTTP 1.1. TheWebServer will therefore use persistent TCP connections, allowing it to respond to multiple requests using the same socket. Otherwise, the connection would have been closed after each response.
An instance ofWebServer has a list of repositories that reference the available resources. These areWebRepository objects. It queries them one by one until it finds the requested resource, in this case:/dynpage.html. If it does not belong to any repository, the server will respond with a standardized error message:"404 - Not Found".
In the case of our sample application, the code"200 OK" is returned, followed by a new server header and the resource, here in uncompressed HTML format.
As a picture is worth a thousand words, I invite you to refer to thisfigure:
Fig.: Sequence Diagram of a libnavajo Application
Alibnavajo application always begins by instantiating aWebServer object. It then passes its configuration parameters and adds repositories to it.
Finally, you need to activate the service by calling the methodstartService(). The server will fully initialize and start responding to incoming requests.
When the service is stopped, the server processes any pending requests before shutting down and releasing resources.
Implementing a Navajo web server within an application only requires a few lines of code:
// Include all the prototypes and predicates from libnavajo#include"libnavajo/libnavajo.hh"// Create an instance of WebServerauto webServer = std::make_unique<WebServer>();
And that's it! We have created our first web server that listens on port 8080!
We will now start our server on a different port. Port 8443 is more suitable for an HTTPS server:
webServer->listenTo(8443);
✍️ Creating a socket on a TCP port < 1024 is reserved for the root user on Unix systems.
To start the server in HTTPS mode, you will need a key/certificate pair. If you do not have one for your machine, you can generate a self-signed pair. To do this, use the following Unixopenssl commands:
$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout mykey.pem -out mycert.pem
We need to concatenate the key and certificate:
$ cat mykey.pem>> mycert.pemTo enable SSL mode on our web server, simply add:
webServer->setUseSSL(true,"mycert.pem");
This function activates SSL mode. Congratulations! You've just created an HTTPS server!
2.3.3 X509 Authentication
When SSL encryption is enabled, it is possible to activate X509 authentication based on Distinguished Names (DN).
To do this, simply provide your web server with a file containing all the concatenated public keys of the certificate authorities, followed by a list of authorized DNs:
webServer->setAuthPeerSSL(true,"ca-cnrs.pem");webServer->addAuthPeerDN("/C=FR/O=CNRS/OU=UMR5821/CN=Thierry Descombes/emailAddress=...");
The browser will then request a certificate when trying to access the page. Access to the resource is granted only if the certificate's DN is valid.
The framework also allows enabling HTTP authentication:
Either by specifying login/password pairs using the function, repeated as many times as there are accounts to authorize:
webServer->addLoginPass("login","password");
Or by using the system's PAM (Pluggable Authentication Module) authentication:
webServer->usePamAuth("/etc/pam.d/login");
And optionally limiting access to specific accounts:
webServer->addAuthPamUser("descombes"); webServer->addAuthPamUser("znets");
You can restrict access to your server to specific networks or machines. This can be done using theIpNetwork object from libnavajo, which allows easy manipulation of IPv4 and IPv6 network addresses:
webServer->addHostsAllowed(IpNetwork(string("127.0.0.0/8")));webServer->addHostsAllowed(IpNetwork(string("192.168.0.0/24")));
The server will only respond to requests from the192.168.0.0/24 network or the loopback network (localhost).
The methodslistenIpV4only andlistenIpV6only allow you to restrict access to either an IPv4 or IPv6 network by creating only one listening socket.
Adding web repositories (WebRepository objects), which we'll cover in more detail later, is done via calls to theaddRepository method:
webServer->addRepository(myRepo);TheWebServer starts responding to requests after calling thestartService method:
webServer->startService();The process starts in a new thread. You can then use thewait method, which will block as long as the web server is running.
TheWebServer stops after callingstopService:
webServer->stopService();The function does not return control until the complete shutdown of the web server. The object can then be destroyed.
AWebRepository defines a set of resources that can be returned to the client.
TheWebRepository class is abstract and only contains two pure virtual methods:getFile, which returns the resource (URL) if found, andfreeFile, which releases memory.
As shown in nextfigure, in the current version of libnavajo, there are three types ofWebRepository to manage:
- LocalRepository: For resources stored locally on disk.
- PrecompiledRepository: For pre-compiled resources.
- DynamicRepository: For dynamically generated content.
Fig. : The Three Types of WebRepository
✍️ When a requested URL ends with a/, the web server will attempt to return theindex.html file.
URLs are case-sensitive, regardless of the repository.
TheLocalRepository object allows you to add local directories from which the libnavajo server will serve files.
First, create an instance of the object:LocalRepository myLocalRepo;
Then, add a resource directory, which will be recursively traversed through all its subdirectories:
myLocalRepo.addDirectory("/docs","../docs/html");
In our example program,../docs/html corresponds to the relative path to the project's Doxygen documentation, previously generated using themake docs command. The entire repository is attached to thedocs folder located at the root of the web hierarchy.
The repository can then be added to the repositories served by the server:
webServer->addRepository(&myLocalRepo);Accessinghttp://myServer:8080/docs/ will refer to the file../docs/html/index.html.
✍️ You can, of course, add multiple directories to serve through yourLocalRepository.
ThePrecompiledRepository class allows you to include your repositories directly in the application code, producing only a single binary. This has multiple advantages: you can create compact applications that are easy to deploy while ensuring the integrity of your interface (there’s little risk of it being modified if you do not provide the source code, especially since your web files can also be compressed).
ThenavajoPrecompiler tool, which is part of the framework, generates an implementation of thePrecompiledRepository class containing your precompiled repository. It takes the root directory (relative or absolute) as an argument and generates an implementation of thePrecompiledRepository class, which you must compile with your application.
In our example program, the command:navajoPrecompiler exampleRepository > PrecompiledRepository.cc
generates a filePrecompiledRepository.cc that implements thePrecompiledRepository class.
You can now simply create an instance ofPrecompiledRepository and add it to the server's repositories:PrecompiledRepository myPrecompiledRepo("/");webServer->addRepository(&myPrecompiledRepo);
Your repository is then attached to the root/ of the server.
✍️ In the current implementation, there can be only one precompiled repository per application.
When a file is compressed (with a.gz extension) in a precompiled repository, the libnavajo framework will decompress it on the fly or return the resource as-is to the client if it supports compression. This feature allows for a smaller repository, optimizing memory usage.
Unlike the previous sections, it is possible to create a repository of dynamic pages, which consists of web pages generated on demand. Implementing these pages allows you to handle interactivity in your interface, analyze form values, or display information related to the operation of your application.
A dynamic repository is easily instantiated and requires no parameters:DynamicRepository myRepo;
It consists exclusively of dynamic pages (DynamicPage), which we will cover in another article. These pages are added with their absolute URL using theadd() method:myRepo.add("/dynpage.html", &page1);
As you may have noticed, there are no constraints on the extension or the name of a dynamic page (here,.html).
As with other repositories, adding it to the server is done with:webServer->addRepository(&myRepo);
Now, let's dive into the mechanisms and objects that enable interactivity, specifically how to dynamically generate a page using the libnavajo framework.
Each request received by ourWebServer instance generates two objects:
HttpRequest: Contains all the request parameters, accessible via various accessors such as the requested URL, request type (GET, POST, etc.), cookies, and more.HttpResponse: Manages all the parameters for the response to be sent back to the client, including response content, its size, MIME type, cookies, etc.
These objects are then passed to theWebRepository through calls to thegetFile() method. TheDynamicRepository, which handles dynamic pages, is queried. If it contains the requested resource, it passes references to theHttpRequest andHttpResponse objects to theDynamicPage object, which uses them to dynamically generate the response.
Fig. : Sequence Diagram of Dynamic Page Access
Every dynamic page must be instantiated at application startup. Let's look at an example of a dynamic libnavajo page that implements an access counter:
// Exemple : Création d'une page dynamique simpleclassMyFirstDynamicPage :publicDynamicPage {int nbUsed =0;boolgetPage(HttpRequest* request, HttpResponse* response)override { std::stringstream ss; ss << “<html><body>This page has been accessed" << ++nbUsed <<" times</body></html>"; return fromString(ss.str(), response); }} myFirstDynamicPage;
In this example, we use thefromString() function (inherited fromDynamicPage) to generate the response content from a string. The response could also be in binary format (e.g., an image or a downloadable file).
The page can then be added to our dynamic repository:
DynamicRepository myRepo;myRepo.add("/index.html", &myFirstDynamicPage);
Note that you can use any extension (.html,.txt, etc.) to serve dynamic content. The HttpResponse object determines the MIME type based on the file extension by default, but you can override it using:
response->setMimeType("text/html");
Dynamic content is automatically deallocated by theWebServer after the resource is served to the client.
There are two main ways to send parameters to the server:GET andPOST.
- GET parameters are passed directly in the URL (e.g.,
http://myserver/mypage.html?param1=value1¶m2=value2). The URL length is limited, so it is not recommended for long data. - POST parameters are sent within the HTTP request body, making them invisible in the URL. There is no theoretical size limit.
Regardless of the method, you can retrieve parameter values using theHttpRequest object:
std::vector<std::string> myParams = request->getParameterNames();std::string param1 = request->getParameter("param1");
To convert a parameter value to a different type:
int paramValue =0;try {if (!request->getParameter("param1", paramValue)) { response->setStatusCode(400);returnfromString("Bad Request: Missing 'param1'", response); } paramValue = getValue<int>(request->getParameter("param1")); }catch (...) {// Handle std::bad_cast exception}
HTTP cookies are small pieces of data stored in the browser and sent with every request. They help add statefulness to the otherwise stateless HTTP protocol (defined in RFC6265). Cookies are commonly used to manage session data, user preferences, and other browser-side information.
To create cookies, use theaddCookie() method of theHttpResponse object:
response->addCookie("username", "JohnDoe", 3600, 0, "/", "", true, true);
- Name/Value: The key-value pair for the cookie.
- maxAge: Validity period in seconds.
- expiresTime: Expiration date (in Unix timestamp).
- path: Limits the scope of the cookie to a specific path.
- domain: Limits the cookie to a specific domain.
- secure: Sends the cookie only over HTTPS.
- httpOnly: Restricts access to HTTP requests only (not accessible via JavaScript).
When receiving a request, use the following methods to retrieve cookies:
std::vector<std::string> cookieNames = request->getCookiesNames(); std::string cookieValue = request->getCookie("username");
✍️ Always treat cookies as potentially insecure and avoid storing sensitive data in them.
Cookies are essential for managing sessions, which allow the server to identify each user. Libnavajo automatically handles sessions using cookies, associating a randomly generated session key with each client.
TheHttpRequest object provides methods for session management:
std::vector<std::string> attrNames = request->getSessionAttributeNames();void* myAttr = request->getSessionAttribute("myAttr"); request->setSessionAttribute("myAttr", (void*)data); request->removeSession();
Here’s an example that counts the number of accesses for each user:
classMySecondDynamicPage:publicDynamicPage {boolgetPage(HttpRequest* request, HttpResponse* response) {int *count = (int*)request->getSessionAttribute("accessCount");if (count ==nullptr) { count =newint(0); request->setSessionAttribute("accessCount", count); } (*count)++; std::stringstream ss; ss <<"<HTML><BODY>Welcome back! You've visited this page" << *count <<" times.</BODY></HTML>"; response->setMimeType("text/html");returnfromString(ss.str(), response); }} mySecondDynamicPage;
✍️Do not store object instances as session attributes due to automatic deallocation.
Today’s powerful JavaScript and CSS frameworks enable the creation of fully autonomousWeb 2.0 applications that communicate with the server asynchronously via AJAX requests. This approach allows for a clear separation between thepresentation layer (handled by JavaScript) and thebusiness logic (written in C++).
A common approach is to useAJAX polling to repeatedly send requests to the server and update the UI based on the responses. The preferred format for data exchange is usuallyJSON due to its simplicity and efficiency.
Now that you are fully familiar with the libnavajo framework (yes, you are! 😉), let’s dive into a concrete example: the“2_gauge” project, which I’ve added to the available sources.
I’ve developed a gauge to monitor the server’s CPU usage. For rendering, I used a dedicated graphical API (there are many available). The gauge is refreshed every second using AJAX polling.
Below is the sequence diagram illustrating how the application functions.
Fig. : Sequence Diagram of the Application
<!-- We include jQuery for AJAX calls and Google Charts for drawing the gauge →<scriptsrc="https://code.jquery.com/jquery-3.6.0.min.js"></script><scriptsrc="https://www.gstatic.com/charts/loader.js"></script><script> // Load Google Charts and initialize the gauge google.charts.load('current',{packages:['gauge']}); google.charts.setOnLoadCallback(initChart);function initChart(){// Creating the chart object and setting initial dataconstchart=newgoogle.visualization.Gauge(document.getElementById('chart_div'));constdata=google.visualization.arrayToDataTable([['Label','Value'],['CPU',0]]);// Chart options for colors and sizesconstoptions={width:400,height:120,redFrom:80,redTo:100,yellowFrom:60,yellowTo:80,minorTicks:5};// Automatically refresh the gauge every second with AJAX pollingsetInterval(()=>updateAjax(chart,data,options),1000);}// Function to update the gauge with new data function updateChart(value, chart, data, options){data.setValue(0,1,value);chart.draw(data,options);}// Function to fetch CPU usage from the server via AJAX function updateAjax(chart, data, options){$.ajax({url:'cpuvalue.txt',success:function(value){// If the server returns a valid value, update the chartif(value>0)updateChart(value,chart,data,options);},error:function(){console.error("Failed to fetch CPU value");}});}</script><!-- HTML element where the gauge will be rendered →<divid="chart_div"></div>
#include<fstream> #include<sstream> #include<string>// Function to read CPU usage from /proc/stat// This function calculates the CPU load as a percentageintgetCpuLoad() { std::ifstreamfile("/proc/stat"); std::string line;std::getline(file, line); std::istringstreamiss(line);// Read the CPU statistics std::string cpu;long user, nice, system, idle; iss >> cpu >> user >> nice >> system >> idle;// Calculate the CPU load difference compared to the previous readingstaticlong prevTotal =0, prevIdle =0;long total = user + nice + system + idle;long totalDiff = total - prevTotal;long idleDiff = idle - prevIdle; prevTotal = total; prevIdle = idle;// Return CPU usage as a percentagereturn totalDiff >0 ? (100 * (totalDiff - idleDiff) / totalDiff) :0; }
#include"libnavajo/libnavajo.hh"// Base class to manage sessionsclassMyDynamicPage :publicDynamicPage {protected:// Check if the session is valid by looking for the "username" attributeboolisValidSession(HttpRequest* request) {return request->getSessionAttribute("username") !=nullptr; } };
// This page handles user authenticationclassAuth :publicMyDynamicPage {public:boolgetPage(HttpRequest* request, HttpResponse* response)override { std::string login, password;// Validate login and password from request parametersif (request->getParameter("login", login) && request->getParameter("pass", password) && ((login =="libnavajo" && password =="libnavajo") ||AuthPAM::authentificate(login.c_str(), password.c_str(),"/etc/pam.d/login"))) {// If valid, create a session attribute for the userauto username = std::make_shared<std::string>(login); request->setSessionAttribute("username", username);// Return "authOK" if successfulreturnfromString("authOK", response); }// Return "authBAD" if authentication failsreturnfromString("authBAD", response); } } auth;
// This page returns the current CPU usage as a plain text responseclassCpuValue :publicMyDynamicPage {public:boolgetPage(HttpRequest* request, HttpResponse* response)override {// Check if the session is validif (!isValidSession(request)) {returnfromString("ERR", response); }// Retrieve the current CPU load and send it as a responseint cpuLoad =getCpuLoad(); std::ostringstream ss; ss << cpuLoad;returnfromString(ss.str(), response); } } cpuValue;
// This controller handles redirection based on session statusclassController :publicMyDynamicPage {public:boolgetPage(HttpRequest* request, HttpResponse* response)override {// Handle user logout by removing the sessionif (request->hasParameter("disconnect")) { request->removeSession(); }// If the session is valid, forward to the gauge page; otherwise, to the login pageif (isValidSession(request)) { response->forwardTo("gauge.html"); }else { response->forwardTo("login.html"); }returntrue; } } controller;
// Custom dynamic repository to handle different resourcesclassMyDynamicRepository :publicDynamicRepository {public:MyDynamicRepository() {add("/auth.txt", &auth);add("/cpuvalue.txt", &cpuValue);add("/index.html", &controller); } } myRepo;
#include"libnavajo/libnavajo.hh" #include<memory>// Main function to set up and run the web serverintmain() {auto webServer = std::make_unique<WebServer>(); webServer->listenTo(8080);// Add local repository to serve static files LocalRepository myLocalRepo; myLocalRepo.addDirectory("/","./html"); webServer->addRepository(&myLocalRepo);// Add the custom dynamic repository webServer->addRepository(&myRepo);// Start the web server and wait for it to finish webServer->startService(); webServer->wait();// Clean up logging resourcesLogRecorder::freeInstance();return0; }
- gauge.html → Contains the front-end code for displaying the gauge.
- login.html → A simple login form for user authentication.
- cpuvalue.txt → Provides the CPU usage as a percentage in text format.
- AJAX Polling: The gauge is updated every second by sending an AJAX request to
cpuvalue.txt. - Authentication: A simple session-based authentication system.
- Redirection: The controller redirects the user to the appropriate page based on the session status.
To illustrate libnavajo’s capabilities, consider a real-world example: a CPU usage gauge that updates every second using AJAX polling.
This project uses jQuery for AJAX calls and Google Charts for the graphical gauge. The server provides the data via dynamically generated content, protected with session-based authentication.
Modern browsers support WebSockets. With this new protocol and its standardized API, JavaScript applications can communicate instantly with their servers.
The WebSocket protocol is defined by the IETF in RFC6455. It enables the establishment ofbi-directional andpersistent connections between a client and a server.
The server is referenced by a URI that follows the ABNF syntax:
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
The protocol also allows the use of "secure WebSockets" based on SSL encapsulation, similar to HTTPS. The URIs for secure WebSockets look like:
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
The default ports are80 and443, similar to HTTP and HTTPS.
The client initiates the connection by sending a standard HTTP request. This allows the protocol to take advantage of HTTP headers, including authentication mechanisms. It also ensures compatibility with most proxies and firewalls.
The WebSocket connection request contains the headers:Upgrade: websocket andConnection: Upgrade. Here’s an example of the handshake:
makefile
Copier le code
GET /chat HTTP/1.1Host: server.example.comUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Origin: http://example.comSec-WebSocket-Protocol: chat, superchatSec-WebSocket-Version: 13
The server responds with:
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Sec-WebSocket-Protocol: chat
The client sends a randomly generated key in theSec-WebSocket-Key header. The server responds withSec-WebSocket-Accept, which is derived from the client’s key.
Note: Managing WebSocket server resources can be challenging. Since WebSockets are persistent, their number can grow indefinitely, potentially overwhelming the server. It is crucial to implement security measures, such as:
- Only allowing authenticated clients (validated session cookies).
- Limiting each session to a single WebSocket connection.
- Using encrypted protocols (wss://).
- Validating the origin header (
Origin).
Modern JavaScript engines implement the standardized WebSocket API as defined by the W3C. It provides a persistent, bi-directional connection between a client and server.
The interface is as follows:
const ws =new WebSocket("ws://localhost/test");ws.onopen = function(evt) { console.log("Connection opened"); }; ws.onclose = function(evt) { console.log("Connection closed", evt.reason); }; ws.onmessage = function(evt) { console.log("Message received:", evt.data); }; ws.onerror = function(evt) { console.error("Error:", evt.data); };
This simple implementation demonstrates how a client connects to a WebSocket server and handles events.
Libnavajo is a C++ web server that also supports WebSockets. It can manage web pages, cookies, sessions, and WebSockets seamlessly.
To get started, download and install libnavajo:
$ git clone https://github.com/titi38/libnavajo.git$ make$ sudo make install
Libnavajo processes HTTP requests in a connection pool. When it receives a WebSocket connection request, it handles it specifically. If the connection is established, the socket becomes persistent.
Each client is referenced by anHttpRequest object, which is created from the WebSocket request. It persists as long as the WebSocket connection is active.
#include"libnavajo/WebSocket.hh" #include<iostream>// Define a WebSocket class that handles opening and receiving messagesclassMyWebSocket :publicWebSocket {public:// Handle the opening handshakeboolonOpening(HttpRequest* request)override { std::cout <<"New WebSocket from host:" << request->getPeerIpAddress().str() <<" - socketId=" << request->getClientSockData()->socketId << std::endl;returntrue; }// Handle receiving a text messagevoidonTextMessage(HttpRequest* request,const std::string& message,constbool fin)override { std::cout <<"Message received: '" << message <<"' from host" << request->getPeerIpAddress().str() <<"\n";sendTextMessage(request,"The message has been received!"); } } myWebSocket;// Adding the WebSocket to the serverintmain() {auto webServer = std::make_unique<WebServer>(); webServer->addWebSocket("/test", &myWebSocket); webServer->listenTo(8080); webServer->startService(); webServer->wait();return0; }
This minimal WebSocket server responds to incoming messages with a confirmation message.
Let’s create a chat application with authentication. The project is split into two files: an HTML page and a C++ application.
functionconnect(){`$.ajax({ `type:"POST",`url: "connect.txt", `data:{login:$("#login-username").val(),pass:$("#login-password").val()},success:function(response){ `if(response==='authOK')connectWS();elsealert("Invalid username or password.");},beforeSend:function(){console.log("Please wait...");}});}
classMyDynamicRepository :publicDynamicRepository {public:// Handle user loginclassConnect :publicDynamicPage {public:boolgetPage(HttpRequest* request, HttpResponse* response)override { std::string login, password;if (request->getParameter("login", login) && request->getParameter("pass", password) && ((login =="libnavajo" && password =="libnavajo") ||AuthPAM::authentificate(login.c_str(), password.c_str(),"/etc/pam.d/login"))) { request->setSessionAttribute("username", std::make_shared<std::string>(login)); request->setSessionAttribute("wschat", std::make_shared<bool>(false));returnfromString("authOK", response); }returnfromString("authBAD", response); } } connect;// Handle user logoutclassDisconnect :publicDynamicPage {public:boolgetPage(HttpRequest* request, HttpResponse* response)override { request->removeSession();returnnoContent(response); } } disconnect;MyDynamicRepository() {add("connect.txt", &connect);add("disconnect.txt", &disconnect); } } myDynRepo;
classChatWebSocket :publicWebSocket {public:boolonOpening(HttpRequest* request)override {if (!isValidSession(request))returnfalse;setSessionIsConnected(request,true);returntrue; }voidonClosing(HttpRequest* request)override {setSessionIsConnected(request,false); }voidonTextMessage(HttpRequest* request,const std::string& message,constbool fin)override {if (checkMessage(request, message))sendBroadcastTextMessage(message);elsesendCloseCtrlFrame(request,"Invalid message format"); }private:boolisValidSession(HttpRequest* request) {return request->getSessionAttribute("username") !=nullptr; } } chatWebSocket;
The server accepts WebSocket connections, manages sessions, and broadcasts chat messages.
8.1 Apache Configuration for WebSocket To deploy a libnavajo WebSocket application behind an Apache proxy:
$ sudo a2enmod proxy`$ sudo a2enmod proxy_http` $ sudo a2enmod proxy_wstunnel`
In the Apache configuration:
ProxyPass "/chat/wschat" "ws://localhost:8080/wschat"ProxyPass /chat http://localhost:8080/ProxyPassReverse /chat http://localhost:8080/Ensure the order of directives to avoid conflicts between WebSocket and HTTP connections.
Edit the Nginx configuration file (usually located at/etc/nginx/sites-available/default or/etc/nginx/nginx.conf) to include the following configuration:
# Nginx configuration for WebSocket and HTTP traffic server { listen 80; server_name your-domain.com; # Proxy WebSocket connections location /wschat { proxy_pass http://localhost:8080/wschat; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Proxy regular HTTP connections location /chat { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Redirect HTTP to HTTPS (optional) listen 443 ssl; ssl_certificate /etc/ssl/certs/your-cert.pem; ssl_certificate_key /etc/ssl/private/your-key.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; }- WebSocket Proxy (
/wschat):- The
proxy_passdirective forwards WebSocket connections to yourlibnavajoWebSocket server running on port 8080. - The headers
UpgradeandConnectionare set to ensure WebSocket compatibility.
- The
- HTTP Proxy (
/chat):- This location handles regular HTTP requests and forwards them to your
libnavajoserver.
- This location handles regular HTTP requests and forwards them to your
- HTTPS Setup (Optional):
- If you have an SSL certificate, you can enable HTTPS by specifying the certificate and key files.
About
C++ API: http server with local dynamic or precompiled repository containers
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Contributors8
Uh oh!
There was an error while loading.Please reload this page.




