Skip to content
Snippets Groups Projects
Commit 54cae8ce authored by Daniel Müller's avatar Daniel Müller :speech_balloon:
Browse files

Initial commit

parents
No related branches found
No related tags found
No related merge requests found
/build/*
.vscode/*
!**/.gitkeep
\ No newline at end of file
Makefile 0 → 100644
# Name of the executable
NAME = prog.run
# Main source code directory
SRC_DIR = src
# Build output directory
BUILD_DIR = build
TARGET = $(addprefix $(BUILD_DIR)/, $(NAME))
SRC = $(wildcard $(SRC_DIR)/*.cpp)
OBJ = $(addprefix $(BUILD_DIR)/, $(notdir $(SRC:.cpp=.o)))
LD_FLAGS = -g -pthread
COMPILE_FLAGS = -g -c -O3
# Build rule for the main target executable
$(TARGET): $(OBJ)
g++ $(LD_FLAGS) -o $@ $(OBJ)
# Build rule for normal source files
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp
g++ $(COMPILE_FLAGS) -o $@ $<
.PHONY: clean
clean:
rm $(OBJ) $(TARGET)
.PHONY: run
run: $(TARGET)
./$(TARGET)
...
\ No newline at end of file
#include "http_err.hpp"
HttpException::HttpException(const HttpException::Type &type, const std::string &message)
{
this->error_type = type;
this->message = std::string(HttpException::typeToString(type)) + ": " + message;
}
HttpException::HttpException(const HttpException::Type &type)
: HttpException(type, "?")
{ }
const char * HttpException::what()
const noexcept
{
return message.c_str();
}
const HttpException::Type & HttpException::getType()
const noexcept
{
return error_type;
}
const char * HttpException::typeToString(const HttpException::Type & type)
{
switch (type)
{
case Generic:
return "HttpException::Generic";
case SocketOpen:
return "HttpException::SocketOpen";
case InvalidIP:
return "HttpException::InvalidIP";
case SocketBind:
return "HttpException::SocketBind";
case TcpAccept:
return "HttpException::TcpAccept";
case TcpSend:
return "HttpException::TcpSend";
}
return "HttpException::NoType";
}
\ No newline at end of file
#ifndef _HTTP_ERR_HPP
#define _HTTP_ERR_HPP
#include <string>
#include <exception>
class HttpException : virtual public std::exception
{
public:
enum Type
{
Generic,
SocketOpen,
InvalidIP,
SocketBind,
TcpAccept,
TcpSend
};
protected:
std::string message;
Type error_type;
public:
HttpException(const HttpException::Type &type, const std::string &message);
HttpException(const HttpException::Type &type);
const char * what()
const noexcept override;
const HttpException::Type & getType() const noexcept;
static const char * typeToString(const HttpException::Type & type);
};
#endif // _HTTP_ERR_HPP
\ No newline at end of file
#include "http_header.hpp"
const std::string HttpHeader::Authorization = "Authorization";
const std::string HttpHeader::Connection = "Connection";
const std::string HttpHeader::KeepAlive = "Keep-Alive";
const std::string HttpHeader::Accept = "Accept";
const std::string HttpHeader::AcceptCharset = "Accept-Charset";
const std::string HttpHeader::AcceptEncoding = "Accept-Encoding";
const std::string HttpHeader::Cookie = "Cookie";
const std::string HttpHeader::SetCookie = "Set-Cookie";
const std::string HttpHeader::ContentDisposition = "Content-Disposition";
const std::string HttpHeader::ContentLength = "Content-Length";
const std::string HttpHeader::ContentType = "Content-Type";
const std::string HttpHeader::ContentEncoding = "Content-Encoding";
const std::string HttpHeader::ContentLanguage = "Content-Language";
const std::string HttpHeader::ContentLocation = "Content-Location";
const std::string HttpHeader::Location = "Location";
const std::string HttpHeader::Host = "Host";
const std::string HttpHeader::Referer = "Referer";
const std::string HttpHeader::UserAgent = "User-Agent";
const std::string HttpHeader::Allow = "Allow";
const std::string HttpHeader::Server = "Server";
const std::string HttpHeader::AcceptRanges = "Accept-Ranges";
const std::string HttpHeader::Range = "Range";
const std::string HttpHeader::ContentRange = "Content-Range";
const std::string HttpHeader::TransferEncoding = "Transfer-Encoding";
const std::string HttpHeader::Date = "Date";
HttpHeader::HttpHeader()
: _isSet{false}
{ }
HttpHeader::HttpHeader(const std::string & key, const std::string & value)
: _isSet{true}, key{key}, value{value}
{ }
const bool HttpHeader::isSet() const
{
return _isSet;
}
const std::string & HttpHeader::getKey() const
{
return key;
}
const std::string & HttpHeader::getValue() const
{
return value;
}
void HttpHeader::setKey(const std::string & _key)
{
key = _key;
// Convert key to lowercase to ensure case insensitivity
for (auto &c : key) c = tolower(c);
}
void HttpHeader::setValue(const std::string & _value)
{
value = _value;
}
///////////////////////
// Class HttpHeaders //
///////////////////////
const HttpHeader HttpHeaders::emptyHeader{};
const std::string HttpHeaders::emptyString{};
const HttpHeader & HttpHeaders::getHeader(const std::string & key) const
{
std::string key_lower = key;
for (auto &c : key_lower) c = std::tolower(c);
auto h = _headers.find(key_lower);
if (h == _headers.end())
{
return HttpHeaders::emptyHeader;
}
return (*h).second;
}
const std::string & HttpHeaders::getValueOrEmpty(const std::string & key) const
{
const HttpHeader & h = getHeader(key);
if (!h._isSet) return HttpHeaders::emptyString;
return h.getValue();
}
const std::unordered_map<std::string, HttpHeader> & HttpHeaders::getRawHeaders() const
{
return _headers;
}
const bool HttpHeaders::headerExists(const std::string & key) const
{
std::string key_lower = key;
for (auto &c : key_lower) c = std::tolower(c);
auto h = _headers.find(key_lower);
if (h == _headers.end())
{
return false;
}
return true;
}
void HttpHeaders::setHeader(const HttpHeader & header)
{
_headers[header.key] = header;
}
void HttpHeaders::setHeader(const std::string & key, const std::string & value)
{
std::string key_lower = key;
for (auto &c : key_lower) c = std::tolower(c);
_headers[key_lower].key = key;
_headers[key_lower].value = value;
}
void HttpHeaders::unsetHeader(const std::string & key)
{
std::string key_lower = key;
for (auto &c : key_lower) c = std::tolower(c);
auto h = _headers.find(key_lower);
if (h != _headers.end())
{
_headers.erase(h);
}
}
#ifndef _HTTP_HEADER_HPP
#define _HTTP_HEADER_HPP
#include <string>
#include <unordered_map>
class HttpHeader
{
private:
bool _isSet;
std::string key;
std::string value;
public:
HttpHeader();
HttpHeader(const std::string & key, const std::string & value);
const bool isSet() const;
const std::string & getKey() const;
const std::string & getValue() const;
void setKey(const std::string & key);
void setValue(const std::string & value);
friend class HttpHeaders;
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
static const std::string Authorization;
static const std::string Connection;
static const std::string KeepAlive;
static const std::string Accept;
static const std::string AcceptCharset;
static const std::string AcceptEncoding;
static const std::string Cookie;
static const std::string SetCookie;
static const std::string ContentDisposition;
static const std::string ContentLength;
static const std::string ContentType;
static const std::string ContentEncoding;
static const std::string ContentLanguage;
static const std::string ContentLocation;
static const std::string Location;
static const std::string Host;
static const std::string Referer;
static const std::string UserAgent;
static const std::string Allow;
static const std::string Server;
static const std::string AcceptRanges;
static const std::string Range;
static const std::string ContentRange;
static const std::string TransferEncoding;
static const std::string Date;
};
class HttpHeaders
{
private:
/**
* @brief Statically allocated empty string to be used for empty values without
* the need for new allocations.
*/
const static std::string emptyString;
/**
* @brief Statically allocated empty header to be used for empty values without
* the need for new allocations.
*/
const static HttpHeader emptyHeader;
std::unordered_map<std::string, HttpHeader> _headers;
public:
const HttpHeader & getHeader(const std::string & key) const;
const bool headerExists(const std::string & key) const;
const std::string & getValueOrEmpty(const std::string & key) const;
const std::unordered_map<std::string, HttpHeader> & getRawHeaders() const;
void setHeader(const HttpHeader & header);
void setHeader(const std::string & key, const std::string & value);
void unsetHeader(const std::string & key);
};
#endif // _HTTP_HEADER_HPP
\ No newline at end of file
#include "http_request.hpp"
const std::string & HttpRequest::ip() const
{
return _ip;
}
int HttpRequest::port() const
{
return _port;
}
const std::string & HttpRequest::method() const
{
return _method;
}
const std::string & HttpRequest::uri() const
{
return _uri;
}
const std::string & HttpRequest::httpver() const
{
return _httpver;
}
const HttpHeaders & HttpRequest::headers() const
{
return _headers;
}
const std::vector<std::string> & HttpRequest::regexMatches() const
{
return _regexMatches;
}
\ No newline at end of file
#ifndef _HTTP_REQUEST_HPP
#define _HTTP_REQUEST_HPP
#include <string>
#include <vector>
#include <unordered_map>
#include "http_header.hpp"
class HttpRequest
{
private:
std::string _ip;
int _port;
std::string _method;
std::string _uri;
std::string _httpver;
HttpHeaders _headers;
std::vector<std::string> _regexMatches;
public:
const std::string & ip() const;
int port() const;
const std::string & method() const;
const std::string & uri() const;
const std::string & httpver() const;
const HttpHeaders & headers() const;
const std::vector<std::string> & regexMatches() const;
friend class HttpServer;
friend bool parse_headers_into(const std::string &head_text, HttpRequest &req);
friend bool parse_requestline_into(const std::string &head_text, HttpRequest &req);
};
#endif // _HTTP_REQUEST_HPP
\ No newline at end of file
#include "http_response.hpp"
#include <unistd.h>
#include <sys/socket.h>
#include "http_err.hpp"
HttpResponse::HttpResponse(int sockfd)
: sockfd{sockfd}
{
status = 200;
statusPhrase = "OK";
httpver = "HTTP/1.1";
headers.setHeader(HttpHeader::ContentType, "text/html; charset=utf-8");
}
void HttpResponse::setStatus(uint16_t statusCode, std::string _statusPhrase)
{
status = statusCode;
statusPhrase = _statusPhrase;
}
void HttpResponse::setHttpver(std::string _httpver)
{
httpver = _httpver;
}
HttpHeaders & HttpResponse::getHeadersWritable()
{
return headers;
}
void HttpResponse::rawWriteAll(int sockfd, const uint8_t *data, size_t dataLength)
{
size_t bytes_written_total = 0;
ssize_t bytes_written = 0;
do
{
bytes_written = write(sockfd, data + bytes_written_total, dataLength-bytes_written_total);
if (bytes_written < 0)
{
throw HttpException(HttpException::TcpSend);
}
bytes_written_total += bytes_written;
} while (bytes_written_total != dataLength);
}
void HttpResponse::sendHeader()
{
std::string head = httpver + " " + std::to_string(status) + " " + statusPhrase + "\r\n";
for (auto h : headers.getRawHeaders())
{
head += h.first + ": " + h.second.getValue() + "\r\n";
}
head += "\r\n";
rawWriteAll(sockfd, (uint8_t*)head.c_str(), head.size());
}
void HttpResponse::sendAll(const uint8_t *bodyData, size_t bodyLength)
{
sendHeader();
sendBody(bodyData, bodyLength);
}
void HttpResponse::send(const uint8_t *bodyData, size_t bodyLength)
{
sendAll(bodyData, bodyLength);
}
void HttpResponse::sendBody(const uint8_t *bodyData, size_t bodyLength)
{
rawWriteAll(sockfd, bodyData, bodyLength);
}
void HttpResponse::sendDefault404()
{
status = 404;
statusPhrase = "Not found";
headers.setHeader(HttpHeader::ContentType, "text/html; charset=utf-8");
char body[] = "404 Not found";
sendAll((uint8_t*)body, sizeof(body));
}
\ No newline at end of file
#ifndef _HTTP_RESPONSE_HPP
#define _HTTP_RESPONSE_HPP
#include <string>
#include <unordered_map>
#include "http_header.hpp"
class HttpResponse
{
private:
int sockfd;
uint16_t status;
std::string statusPhrase;
std::string httpver;
HttpHeaders headers;
void rawWriteAll(int sockfd, const uint8_t *data, size_t dataLength);
public:
HttpResponse(int sockfd);
void setStatus(uint16_t statusCode, std::string statusPhrase = "");
void setHttpver(std::string httpver);
HttpHeaders & getHeadersWritable();
void sendHeader();
void sendAll(const uint8_t *bodyData, size_t bodyLength);
void send(const uint8_t *bodyData, size_t bodyLength);
void sendBody(const uint8_t *bodyData, size_t bodyLength);
void sendDefault404();
friend class HttpServer;
};
#endif // _HTTP_RESPONSE_HPP
\ No newline at end of file
#include "http_route.hpp"
HttpRoute::HttpRoute(const std::string &route, HttpHandlerFn handler,
HttpRoute::MatchType matchType)
: route{route}, route_matcher{"^" + route + "$"},
handler_fn {handler}, matchType{matchType}
{ }
const std::string & HttpRoute::getRoute() const
{
return route;
}
const HttpRoute::MatchType & HttpRoute::getMatchType() const
{
return matchType;
}
\ No newline at end of file
#ifndef _HTTP_ROUTE_HPP
#define _HTTP_ROUTE_HPP
#include <functional>
#include <string>
#include <regex>
#include "http_request.hpp"
#include "http_response.hpp"
enum class HttpRouteHandling
{
/**
* @brief End the connection after the route was handled.
*/
End,
/**
* @brief Continue trying to match the route with other handlers.
*/
Continue
};
typedef std::function<HttpRouteHandling (const HttpRequest &req, HttpResponse &res)> HttpHandlerFn;
class HttpRoute
{
public:
enum class MatchType
{
Regex,
StartsWith,
Literal,
MatchAny
};
private:
std::string route;
std::regex route_matcher;
HttpRoute::MatchType matchType;
HttpHandlerFn handler_fn;
public:
HttpRoute(const std::string &route, HttpHandlerFn handler,
HttpRoute::MatchType matchType = HttpRoute::MatchType::Regex);
const std::string & getRoute() const;
const HttpRoute::MatchType & getMatchType() const;
friend class HttpServer;
};
#endif // _HTTP_ROUTE_HPP
\ No newline at end of file
#ifndef _HTTP_SERVICE_HPP
#define _HTTP_SERVICE_HPP
#include <string>
#include <fstream>
#include "http_route.hpp"
HttpRoute serveFile(
const std::string route, const std::string & filePath, const std::string & contentType,
HttpRoute::MatchType matchType = HttpRoute::MatchType::Literal,
const std::vector<HttpHeader> setHeaders = std::vector<HttpHeader>{}
)
{
return HttpRoute (
route,
[filePath, contentType, setHeaders] (const HttpRequest &req, HttpResponse res) {
std::ifstream inputFile(filePath, std::ios::binary);
if (inputFile.fail())
{
res.sendDefault404();
return HttpRouteHandling::End;
}
// This is not a good way to get the filesize
auto fsize_beg = inputFile.tellg();
inputFile.seekg(0, std::ios::end);
auto fsize_end = inputFile.tellg();
auto filesize = fsize_end-fsize_beg;
inputFile.seekg(0, std::ios::beg);
res.getHeadersWritable().setHeader("Content-Type", contentType);
res.getHeadersWritable().setHeader("Content-Length", std::to_string(filesize));
for (const auto &h : setHeaders)
{
res.getHeadersWritable().setHeader(h.getKey(), h.getValue());
}
res.sendHeader();
uint8_t buffer[4096];
ssize_t bytes_read = 0;
while((bytes_read = inputFile.readsome((char*)buffer, 4096)) > 0)
{
res.sendBody(buffer, bytes_read);
}
return HttpRouteHandling::End;
},
matchType
);
}
HttpRoute serveFileCached(
const std::string route, const std::string & filePath, const std::string & contentType,
HttpRoute::MatchType matchType = HttpRoute::MatchType::Literal,
const std::vector<HttpHeader> setHeaders = std::vector<HttpHeader>{}
)
{
std::ifstream inputFile(filePath, std::ios::binary);
bool found = !inputFile.fail();
std::vector<uint8_t> data;
if (found)
{
uint8_t buffer[4096];
ssize_t bytes_read = 0;
while((bytes_read = inputFile.readsome((char*)buffer, 4096)) > 0)
{
data.insert(data.end(), std::begin(buffer), std::begin(buffer) + bytes_read);
}
}
return HttpRoute (
route,
[filePath, contentType, setHeaders, data, found] (const HttpRequest &req, HttpResponse res) {
if (!found)
{
res.sendDefault404();
return HttpRouteHandling::End;
}
res.getHeadersWritable().setHeader("Content-Type", contentType);
res.getHeadersWritable().setHeader("Content-Length", std::to_string(data.size()));
for (const auto &h : setHeaders)
{
res.getHeadersWritable().setHeader(h.getKey(), h.getValue());
}
res.sendHeader();
res.sendBody(data.data(), data.size());
return HttpRouteHandling::End;
},
matchType
);
}
#endif // _HTTP_SERVICE_HPP
\ No newline at end of file
#include <iostream>
#include <vector>
#include <cstring>
#include <sstream>
#include <regex>
#include <chrono>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include "threadpool.hpp"
#include "httpd.hpp"
bool parse_requestline_into(const std::string &head_text, HttpRequest &req)
{
// Find the end of the request line
auto offset_httpver_end = head_text.find("\r\n");
if (offset_httpver_end == std::string::npos)
return false;
// Find the end of the first section (METHOD) and the beginning of the next (URI)
auto offset_method_end = head_text.find(" ", 0);
if (offset_method_end == std::string::npos || offset_method_end > offset_httpver_end)
return false;
std::string method = head_text.substr(0, offset_method_end);
// Find the space after the URI and before the HTTPVER
auto offset_uri_end = head_text.find(" ", offset_method_end+1);
if (offset_uri_end == std::string::npos || offset_uri_end > offset_httpver_end)
return false;
std::string uri = head_text.substr(offset_method_end+1, offset_uri_end-offset_method_end -1);
std::string httpver = head_text.substr(offset_uri_end+1, offset_httpver_end-offset_uri_end -1);
req._method = method;
req._uri = uri;
req._httpver = httpver;
return true;
}
bool parse_headers_into(const std::string &head_text, HttpRequest &req)
{
// The first call gets the end of the request line
auto offset = head_text.find("\r\n");
// Loop as long as there are more lines to parse
while (offset < head_text.size()-1 -2)
{
// Skip the 2 characters from \r\n
offset += 2;
// Search the next ": " divider, starting from the current line beginning
auto offset_colon = head_text.find(": ", offset);
if (offset_colon == std::string::npos)
return false;
auto header_key = head_text.substr(offset, offset_colon - offset);
offset = head_text.find("\r\n", offset_colon);
if (offset == std::string::npos)
return false;
auto header_val = head_text.substr(offset_colon+2, offset - offset_colon - 2);
req._headers.setHeader(header_key, header_val);
}
return true;
}
void HttpServer::handleConnection(int sockfd, const std::string &ip, uint16_t port)
{
// HttpRequest object will be filled with the parsed request parameters
HttpRequest req;
req._ip = ip;
req._port = port;
// Temporary read buffer that is used as an immediate target for receiving data
std::vector<uint8_t> buff(tcp_read_buffer_size);
// Body buffer that will be filled with accidentally read body data
std::vector<uint8_t> body_buff;
// String buffer that will be filled with the head data (request line + headers)
std::string head_str_buff;
// Byte offset at which the head ends (the location of the head-end \r\n\r\n )
// The body content begins at offset_head_end + 4
size_t offset_head_end = std::string::npos;
// The number of bytes read with the last read command
ssize_t bytes_read;
// Loop and read from socket while the head is not finished and there is still data available
while (offset_head_end == std::string::npos && (bytes_read = read(sockfd, buff.data(), tcp_read_buffer_size)) > 0)
{
// Append the newly read data to the head string buffer
head_str_buff.append((char*)buff.data(), bytes_read);
// Look for the head end. The loop will end if it is found
offset_head_end = head_str_buff.find("\r\n\r\n");
}
// Cut off the head-end, as well as body data that was possibly read
// One line end (\r\n) is left in for easier header parsing
head_str_buff.resize(offset_head_end + 2);
// Try to parse the request line (METHOD URI HTTPVER)
if (!parse_requestline_into(head_str_buff, req))
{
char resp[] = "HTTP/1.1 400 Bad Request\r\n\r\n";
send(sockfd, resp, sizeof(resp), 0);
return;
}
// Try to parse the headers
if (!parse_headers_into(head_str_buff, req))
{
char resp[] = "HTTP/1.1 400 Bad Request\r\n\r\n";
send(sockfd, resp, sizeof(resp), 0);
return;
}
// Insert potential body data into the body buffer. Body data might accidently be read
// when the head-end (\r\n\r\n) is somewhere in the middle of the read buffer.
// This does not read the full body data, instead only data that was already read is
// inserted
if (offset_head_end < head_str_buff.size()-1)
{
body_buff.insert(body_buff.end(), buff.begin() + offset_head_end + 4, buff.begin() + bytes_read);
}
// Prepare HttpResponse
HttpResponse res(sockfd);
// Try to match routes available for the server using the dedicated matching type
// for each available route.
bool finalHandled = false;
for (const auto &route : routes)
{
bool match_found = false;
std::smatch matches;
switch (route.matchType)
{
case HttpRoute::MatchType::MatchAny:
match_found = true;
break;
case HttpRoute::MatchType::Regex:
if (std::regex_search(req._uri, matches, route.route_matcher))
{
for (auto m : matches)
{
req._regexMatches.push_back(m);
}
match_found = true;
}
break;
case HttpRoute::MatchType::Literal:
if (route.route == req._uri)
{
match_found = true;
}
break;
case HttpRoute::MatchType::StartsWith:
if (req._uri.compare(0, route.route.size(), route.route) == 0)
{
match_found = true;
}
break;
}
if (match_found)
{
auto routeType = route.handler_fn(req, res);
if (routeType == HttpRouteHandling::End)
{
finalHandled = true;
break;
}
}
}
// If none of the routes match, use the default handler. This will cause a
// 404 Not found status code by default
if (!finalHandled) defaultHandler(req, res);
}
HttpRouteHandling HttpServer::defaultHandlerFunction(const HttpRequest &req, HttpResponse &res)
{
res.sendDefault404();
return HttpRouteHandling::End;
}
HttpServer::HttpServer(uint16_t port, std::string ip)
: port{port}, ip{ip}
{
// Try to convert the given ipv4 string into the C lower level ipv4 struct
if (inet_aton(ip.c_str(), &ip_inaddr) == 0)
{
throw HttpException(HttpException::InvalidIP);
}
}
void HttpServer::serveForever()
{
// The number of connections that the server can keep on wait (not yet accepted) before
// refusing further connection requests
const int waiting_connections = pendingConnections;
// Socket File Descriptor for listening
int sockfd_listen;
// Socket Address for listening
sockaddr_in local_saddr = {0};
local_saddr.sin_family = AF_INET;
local_saddr.sin_addr.s_addr = ip_inaddr.s_addr;
local_saddr.sin_port = htons(port);
try
{
// Create a tcp network socket
sockfd_listen = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_listen < 0)
{
throw(HttpException(
HttpException::SocketOpen,
"Opening socket failed"
));
}
// Bind the socket to the listening address and port
if (bind(sockfd_listen, (sockaddr*) &local_saddr, sizeof(local_saddr)))
{
throw(HttpException(
HttpException::SocketBind,
"Binding socket failed"
));
}
// Listen to new connections
listen(sockfd_listen, waiting_connections);
// Create and run the threadpool
Threadpool tp(numberOfThreads, queuedConnections);
// Accept-Handle-Repeat loop
// This loops forever and handles new requests
while (true)
{
// Remote socket address
sockaddr_in remote_saddr = {0};
// Length of the remote socket address structure
socklen_t remote_slen = sizeof(remote_saddr);
// Accept a new tcp connection
int remote_sockfd = accept(sockfd_listen, (sockaddr*) &remote_saddr, &remote_slen);
if (remote_sockfd < 0)
{
throw(HttpException(
HttpException::TcpAccept,
"Accepting connection failed"
));
}
//std::cout << "Connection opened\n";
// Pass the tcp connection socket to the http handling function
// handleConnection(remote_sockfd);
tp.addTask([this, remote_sockfd, remote_saddr]() {
try
{
// inet_ntoa returns the string in a statically allocated buffer
// that will be overwritten on subsequent calls. So no free needed
std::string remote_ip(inet_ntoa(remote_saddr.sin_addr));
uint16_t remote_port = ntohs(remote_saddr.sin_port);
handleConnection(remote_sockfd, remote_ip, remote_port);
close(remote_sockfd);
}
catch (const HttpException& e)
{
close(remote_sockfd);
std::cerr << e.what() + '\n';
}
});
//std::cout << "Connection closing\n";
// Close the accepted tcp connection
// close(remote_sockfd);
}
}
catch (const HttpException& e)
{
close(sockfd_listen);
throw e;
}
}
\ No newline at end of file
#ifndef _HTTPD_HPP
#define _HTTPD_HPP
#include <regex>
#include <string>
#include <unordered_map>
#include <functional>
#include "http_err.hpp"
#include "http_request.hpp"
#include "http_response.hpp"
#include "http_route.hpp"
#include "threadpool.hpp"
class HttpServer
{
private:
ssize_t tcp_read_buffer_size = 4096;
int pendingConnections = 5;
int queuedConnections = 5;
int numberOfThreads = Threadpool::AUTO_NO_WORKERS;
std::string ip;
in_addr ip_inaddr;
uint16_t port;
std::vector<HttpRoute> routes;
static HttpRouteHandling defaultHandlerFunction(const HttpRequest &req, HttpResponse & res);
HttpHandlerFn defaultHandler = &defaultHandlerFunction;
void handleConnection(int sockfd, const std::string &ip, uint16_t port);
public:
HttpServer(uint16_t port, std::string ip = "0.0.0.0");
void addRoute(const HttpRoute &route)
{
routes.push_back(route);
}
void setDefaultHandler(const HttpHandlerFn &);
void serveForever();
};
#endif // _HTTPD_HPP
\ No newline at end of file
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <cstring>
#include <sstream>
#include <chrono>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include "httpd.hpp"
#include "http_service.hpp"
HttpRouteHandling handle_echo_path(const HttpRequest &req, HttpResponse &res)
{
std::string echo_text = req.regexMatches().at(1);
std::string resp = "Echo: " + echo_text;
res.send((uint8_t*) resp.c_str(), resp.size());
return HttpRouteHandling::End;
}
// Calculate the first N prime numbers where N is extracted from the request uri
HttpRouteHandling handle_prime(const HttpRequest &req, HttpResponse &res)
{
std::string result;
int number_of_primes_found = 0;
int max = std::stoi(req.regexMatches().at(1));
int num = 2;
while(number_of_primes_found < max)
{
bool is_prime = true;
for (int i = 2; i <= num/2; i++)
{
if (num % i == 0)
{
is_prime = false;
break;
}
}
if (is_prime)
{
result += std::to_string(num) + " ";
number_of_primes_found++;
}
num++;
}
res.send((uint8_t*) result.c_str(), result.size());
return HttpRouteHandling::End;
}
int main(int argc, char **argv)
{
// Create the webserver on port 8081. By default listening on 0.0.0.0
HttpServer srv(8081);
// Log Middleware example
srv.addRoute(HttpRoute(
"",
[](const HttpRequest &req, HttpResponse &res) {
std::string log = "Request: " + req.ip() + " => " + req.uri() + "\n";
log += " Agent: " + req.headers().getHeader(HttpHeader::UserAgent).getValue() + "\n";
std::cout << log;
return HttpRouteHandling::Continue;
},
HttpRoute::MatchType::MatchAny
));
// Example index route with hardcoded html response
srv.addRoute(HttpRoute(
"/(index.html)?", // match "/" or "/index.html"
[](const HttpRequest &req, HttpResponse &res) {
char body[] = R"EOT(
<!DOCTYPE html>
<html lang="en">
<head>
<title>Example HTML Document</title>
</head>
<body>
<h1><a href="/example.html">example.html</a></h1>
<h1><a href="/cached/example.html">cached example.html</a></h1>
<h1><a href="/prime/100">first 100 primes</a></h1>
<h1><a href="/echo/Hello">echo hello</a></h1>
</body>
</html>
)EOT";
res.sendAll((uint8_t*)body, sizeof(body));
return HttpRouteHandling::End;
}
));
// Static file example
srv.addRoute(serveFile(
"/example.html",
"./static/example.html",
"text/html; charset=utf-8"
));
// Cached static file example. The file is loaded into memory at launch
srv.addRoute(serveFileCached(
"/cached/example.html",
"./static/example.html",
"text/html; charset=utf-8"
));
// Example for a handler function that uses regex to extract a path segment
srv.addRoute(HttpRoute(
"/prime/(\\d+)",
&handle_prime
));
// Example for a handler function that uses regex to extract a path segment
srv.addRoute(HttpRoute(
"/echo/([a-zA-Z0-9_\\-.]+)/?",
&handle_echo_path
));
try
{
// Start serving the content
srv.serveForever();
}
catch(const HttpException& e)
{
std::cerr << e.what() << '\n';
}
return 0;
}
/**
* @author Daniel Mueller
*
* @copyright Copyright 2021 Daniel Mueller. All rights reserved.
*/
#include "semaphore.hpp"
#include <stdexcept>
#include <ctime>
Semaphore::Semaphore(int initialValue)
{
_semaphore = new sem_t;
sem_init(_semaphore, 0, initialValue);
}
Semaphore::Semaphore(Semaphore && other)
: _semaphore{other._semaphore}
{
other._semaphore = nullptr;
}
Semaphore & Semaphore::operator=(Semaphore && other)
{
if (&other != this)
{
sem_destroy(_semaphore);
delete _semaphore;
_semaphore = other._semaphore;
other._semaphore = nullptr;
}
return *this;
}
Semaphore::~Semaphore()
{
sem_destroy(_semaphore);
delete _semaphore;
}
void Semaphore::post()
{
if (_semaphore == nullptr)
throw std::runtime_error("Semaphore used after move");
sem_post(_semaphore);
}
void Semaphore::wait()
{
if (_semaphore == nullptr)
throw std::runtime_error("Semaphore used after move");
sem_wait(_semaphore);
}
bool Semaphore::tryWait()
{
if (_semaphore == nullptr)
throw std::runtime_error("Semaphore used after move");
return (sem_trywait(_semaphore) == 0);
}
bool Semaphore::timedWait(long ms)
{
if (_semaphore == nullptr)
throw std::runtime_error("Semaphore used after move");
// Split the milliseconds into full seconds and leftover nanoseconds
int secs = ms / 1000;
int nsecs = (ms % 1000) * 1000000L;
timespec ts;
// sem_timedwait need the absolute timestamp when to stop waiting, so get
// the current timestamp and add the wait time.
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += secs;
ts.tv_nsec += nsecs;
return (sem_timedwait(_semaphore, &ts) == 0);
}
int Semaphore::getValue()
{
if (_semaphore == nullptr)
throw std::runtime_error("Semaphore used after move");
int val;
sem_getvalue(_semaphore, &val);
return val;
}
/**
* @author Daniel Mueller
*
* @copyright Copyright 2021 Daniel Mueller. All rights reserved.
*/
#ifndef _CPPSEMAPHORE_HPP
#define _CPPSEMAPHORE_HPP
extern "C" {
#include <semaphore.h>
}
/**
* @brief The Semaphore class provides a simple CPP wrapper for the sem_*
* functions from the C "semaphore.h" library. This only uses the thread-shared
* semaphore, not process shared.
*/
class Semaphore
{
private:
/**
* @brief Pointer to the underlying C semaphore.
*/
sem_t * _semaphore;
public:
/**
* @brief Initialize the Semaphore and set the start value.
*
* @param initialValue The Semaphore counter value to start with.
* 0 by default.
*
* @see sem_init
*/
Semaphore(int initialValue = 0);
/**
* @brief The semaphore must not be copied, so the copy constructors is deleted.
*
* @param other The reference to copy from.
*/
Semaphore(const Semaphore & other) = delete;
/**
* @brief The semaphore must not be copied, so the copy assignment is deleted.
*
* @param other The reference to copy from.
*/
Semaphore & operator=(const Semaphore & other) = delete;
/**
* @brief Create the instance by taking over an existing Semaphore.
*
* @warning The moved-from instance is no longer valid to use after the move.
*
* @param other The reference to take over.
*/
Semaphore(Semaphore && other);
/**
* @brief Take over an existing Semaphore. The internal samaphore is destroyed
* and replace by the internal semaphore of the other instance.
*
* @warning The moved-from instance is no longer valid to use after the move.
*
* @param other The reference to take over.
*/
Semaphore & operator=(Semaphore && other);
/**
* @brief Destructor destroys the underlying semaphore and frees the memory.
*
* @see sem_destroy
*/
~Semaphore();
/**
* @brief Incremet the Semaphore.
*
* Increments the underlying semaphore using sem_post. If another thread is
* waiting, it will be woken up.
*
* @see sem_post
*/
void post();
/**
* @brief Decrement the Semaphore or wait if 0.
*
* Decrements the underlying semaphore using sem_wait. If the semaphore has
* the value 0, the function blocks until it can decrement (post was
* called).
*
* @see sem_wait
*/
void wait();
/**
* @brief Same as wait() but doesn't block if the value is 0.
*
* Tries to decrement the underlying semaphore using
* sem_trywait. If the value is 0, it instantly returns false.
*
* @return True if the semaphore was decremented, false otherwise.
*
* @see sem_trywait
*/
bool tryWait();
/**
* @brief Same as wait() but only blocks for a specified ammount of time.
*
* Tries to decrement the underlying semaphore using sem_timedwait. If the
* value is 0, it will block until it can decrement or the specified time
* in ms has passed.
*
* @param ms The time in milliseconds that will be waited if the current
* value is 0, before returning false.
*
* @return True if the value could be decremented, false if the ms time has
* passed without the possibility to decrement.
*
* @see sem_timedwait
*/
bool timedWait(long ms);
/**
*
* @brief Get the current Semaphore value.
*
* Get the current value from the underlying semaphore using sem_getvalue.
*
* @return The current semaphore value. (Due to the nature of semaphores
* and threads, the real value might have already changed when parsing this
* result)
*
* @see sem_getvalue
*/
int getValue();
};
#endif // _SEMAPHORE_HPP
#include "threadpool.hpp"
Threadpool::Threadpool(int _numberOfWorkers, int _maxQueueBacklog)
: numberOfWorkers{_numberOfWorkers}, maxQueueBacklog{_maxQueueBacklog}
{
if (numberOfWorkers < 0)
{
throw std::runtime_error("Threadpool number of threads can't be less than 0");
}
if (maxQueueBacklog < 0)
{
throw std::runtime_error("Threadpool maxQueueBacklog can't be less than 0");
}
if (numberOfWorkers == AUTO_NO_WORKERS)
{
// Get the hardware concurrency (number of logical cpu cores) and create half
// as many worker threads. If there is hyperthreading, the number of logical cores
// is double the amount of physical cores. So on a modern machine with hyperthreading
// the number of worker threads will be equivalent to the number of "real" cpu cores
numberOfWorkers = std::thread::hardware_concurrency() / 2;
}
if (maxQueueBacklog != DISABLE_MAX_QUEUE_BACKLOG)
{
// Change the initial semaphore value if the max queue backlog is not unlimited
semTaskQueueLimit = Semaphore(maxQueueBacklog);
}
// Create the requested number of worker threads
for (auto i = 0; i < numberOfWorkers; i++)
{
workerThreads.push_back(
std::thread(&Threadpool::workLoop, this, i)
);
}
}
void Threadpool::workLoop(int workerId)
{
// The task function that is received from the task queue
std::function<void ()> taskFunction;
while (true)
{
// Wait until a task is available in the task queue
semWaitForTask.wait();
// synchronize taskQueue
{
std::unique_lock<std::mutex> lock(mtxTaskQueue);
// Graceful shutdown is initiated by posting the sem without new tasks.
// This will cause remaining tasks to be processed and then shuts down
// the threads.
if (taskQueue.empty())
{
return;
}
// Get the next task
taskFunction = taskQueue.front();
// And pop it from the queue
taskQueue.pop();
}
// If there is a queue backlog limit, notify to addTask that a task has
// been removed from the queue and therefore one more slot is available
if (maxQueueBacklog != DISABLE_MAX_QUEUE_BACKLOG)
{
semTaskQueueLimit.post();
}
// Execute the task blocking
taskFunction();
}
}
void Threadpool::addTask(std::function<void ()> task)
{
if (shutdownInitiated)
{
throw std::runtime_error("Can't add task to threadpool after shutdown");
}
// If there is a queue backlog limit, wait until the queue is no longer full
if (maxQueueBacklog != DISABLE_MAX_QUEUE_BACKLOG)
{
semTaskQueueLimit.wait();
}
// synchronize taskQueue
{
std::unique_lock<std::mutex> lock(mtxTaskQueue);
taskQueue.push(task);
}
// Notify worker pool that a new task is available
semWaitForTask.post();
}
void Threadpool::shutdown()
{
shutdownInitiated = true;
// Post once for every worker thread. This will cause all tasks to be processed
// and after that the workers will terminate
for (int i = 0; i < workerThreads.size(); i++)
semWaitForTask.post();
}
void Threadpool::joinAll()
{
for (auto &t : workerThreads)
t.join();
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment