First Running Version
This commit is contained in:
commit
792abbee93
21 changed files with 1632 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
build/
|
||||||
|
debug-build/
|
||||||
|
release-build/
|
||||||
|
cmake-build-debug/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
example/build
|
29
CMakeLists.txt
Normal file
29
CMakeLists.txt
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
cmake_minimum_required(VERSION 3.23)
|
||||||
|
project(VWeb)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUGSOFT")
|
||||||
|
set(THREADS_PREFER_PTHREAD_FLAG ON)
|
||||||
|
find_package(Threads REQUIRED)
|
||||||
|
set(SOURCE_FILES
|
||||||
|
Source/EPollManager.cpp
|
||||||
|
Source/RequestHandler.cpp
|
||||||
|
Source/Server.cpp
|
||||||
|
Source/SocketManager.cpp
|
||||||
|
Source/ThreadPool.cpp
|
||||||
|
Source/Router.cpp
|
||||||
|
Source/MiddleWare.cpp
|
||||||
|
Source/Route.cpp
|
||||||
|
Source/StringUtils.cpp
|
||||||
|
Source/Cookie.cpp
|
||||||
|
Source/Session.cpp
|
||||||
|
Source/Response.cpp)
|
||||||
|
include_directories(${CMAKE_SOURCE_DIR}/)
|
||||||
|
add_library(VWeb ${SOURCE_FILES})
|
||||||
|
|
||||||
|
set(mode release)
|
||||||
|
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||||
|
set(mode debug)
|
||||||
|
endif ()
|
||||||
|
set(target_file ${CMAKE_SOURCE_DIR}/dist/libVWeb.${mode}.a)
|
||||||
|
add_custom_command(TARGET VWeb POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:VWeb> ${target_file})
|
107
Source/Cookie.cpp
Normal file
107
Source/Cookie.cpp
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
#include "StringUtils.h"
|
||||||
|
|
||||||
|
#include <VWeb.h>
|
||||||
|
|
||||||
|
namespace VWeb {
|
||||||
|
void Cookies::Remove(const std::string &name) {
|
||||||
|
if (Data.contains(name))
|
||||||
|
Data.erase(name);
|
||||||
|
}
|
||||||
|
bool Cookies::Has(const std::string &name) const { return Data.contains(name); }
|
||||||
|
std::string Cookies::TransformToHeader() {
|
||||||
|
std::stringstream view{};
|
||||||
|
for (auto &cookie : Data) {
|
||||||
|
auto &realCookie = cookie.second;
|
||||||
|
if (!realCookie.IsOld && !realCookie.Name.empty() &&
|
||||||
|
!realCookie.Value.empty())
|
||||||
|
CookieToString(view, realCookie);
|
||||||
|
}
|
||||||
|
return view.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cookies::CreateCookieFromString(const std::string &data) {
|
||||||
|
auto params = String::Split(data, ";");
|
||||||
|
Cookie cookie{};
|
||||||
|
int i = 0;
|
||||||
|
for (auto ¶m : params) {
|
||||||
|
auto split = String::Split(param, "=");
|
||||||
|
if (split.size() == 1) {
|
||||||
|
// we have a bool parameter
|
||||||
|
if (split[0] == "HttpOnly")
|
||||||
|
cookie.HttpOnly = true;
|
||||||
|
else if (split[0] == "Secure")
|
||||||
|
cookie.Secure = true;
|
||||||
|
} else {
|
||||||
|
if (i == 0) {
|
||||||
|
cookie.Name = split[0];
|
||||||
|
cookie.Value = split[1];
|
||||||
|
} else {
|
||||||
|
if (split[0] == "SameSite") {
|
||||||
|
if (split[1] == "None")
|
||||||
|
cookie.SameSite = CookieSameSite::None;
|
||||||
|
else if (split[1] == "Lax")
|
||||||
|
cookie.SameSite = CookieSameSite::Lax;
|
||||||
|
else if (split[1] == "Strict")
|
||||||
|
cookie.SameSite = CookieSameSite::Strict;
|
||||||
|
} else if (split[0] == "Path")
|
||||||
|
cookie.Path = split[1];
|
||||||
|
else if (split[0] == "Domain")
|
||||||
|
cookie.Domain = split[1];
|
||||||
|
else if (split[0] == "Max-Age")
|
||||||
|
cookie.MaxAge = String::ToNumber(split[1], 0);
|
||||||
|
else if (split[0] == "Expires")
|
||||||
|
cookie.Expires = split[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (!cookie.Name.empty() && !cookie.Value.empty())
|
||||||
|
Data[cookie.Name] = std::move(cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cookies::CreateOld(const std::string &name, const std::string &value) {
|
||||||
|
auto& cookie = Data[name];
|
||||||
|
cookie.Name = name;
|
||||||
|
cookie.Value = value;
|
||||||
|
cookie.IsOld = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Cookies::CookieToString(std::stringstream &stream, Cookie &cookie) {
|
||||||
|
if (cookie.Name.empty() || cookie.Value.empty())
|
||||||
|
return;
|
||||||
|
std::stringstream view{};
|
||||||
|
// Cookie names that are starting with __Secure or __Host are Secure only
|
||||||
|
// Cookies!
|
||||||
|
if (cookie.Name.starts_with("__Secure"))
|
||||||
|
cookie.Secure = true;
|
||||||
|
if (cookie.Name.starts_with("__Host")) {
|
||||||
|
cookie.Secure = true;
|
||||||
|
if (cookie.Path.empty())
|
||||||
|
cookie.Path = "/";
|
||||||
|
}
|
||||||
|
view << "Set-Cookie: " << cookie.Name << "=" << cookie.Value;
|
||||||
|
if (!cookie.Expires.empty())
|
||||||
|
view << "; Expires=" << cookie.Expires;
|
||||||
|
if (cookie.MaxAge)
|
||||||
|
view << "; Max-Age=" << cookie.MaxAge;
|
||||||
|
if (!cookie.Domain.empty())
|
||||||
|
view << "; Domain=" << cookie.Domain;
|
||||||
|
if (!cookie.Path.empty())
|
||||||
|
view << "; Path=" << cookie.Path;
|
||||||
|
if (cookie.Secure || cookie.SameSite == CookieSameSite::None)
|
||||||
|
view << "; Secure";
|
||||||
|
if (cookie.HttpOnly)
|
||||||
|
view << "; HttpOnly";
|
||||||
|
if (cookie.SameSite != CookieSameSite::Invalid) {
|
||||||
|
std::string sameSiteValue;
|
||||||
|
switch (cookie.SameSite) {
|
||||||
|
case CookieSameSite::None: sameSiteValue = "None"; break;
|
||||||
|
case CookieSameSite::Strict: sameSiteValue = "Strict"; break;
|
||||||
|
case CookieSameSite::Lax: sameSiteValue = "Lax"; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
view << "; SameSite=" << sameSiteValue;
|
||||||
|
}
|
||||||
|
view << "\n";
|
||||||
|
}
|
||||||
|
} // namespace VWeb
|
35
Source/EPollManager.cpp
Normal file
35
Source/EPollManager.cpp
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
#include <VWeb.h>
|
||||||
|
namespace VWeb {
|
||||||
|
EPollManager::EPollManager() {
|
||||||
|
m_EpollID = epoll_create1(0);
|
||||||
|
if (m_EpollID < 0)
|
||||||
|
exit(49);
|
||||||
|
unsigned int flags = fcntl(m_EpollID, F_GETFL, 0);
|
||||||
|
flags |= O_NONBLOCK;
|
||||||
|
fcntl(m_EpollID, F_SETFL, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
EPollManager::~EPollManager() { close(m_EpollID); }
|
||||||
|
|
||||||
|
bool EPollManager::Dispatch(int sock, uint32_t eventType) const {
|
||||||
|
struct epoll_event event {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
event.data.fd = sock;
|
||||||
|
event.events = eventType;
|
||||||
|
return epoll_ctl(m_EpollID, EPOLL_CTL_ADD, sock, &event) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EPollManager::UpdateEvents(int sock, uint32_t eventType) const {
|
||||||
|
struct epoll_event event {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
event.data.fd = sock;
|
||||||
|
event.events = eventType;
|
||||||
|
return epoll_ctl(m_EpollID, EPOLL_CTL_MOD, sock, &event) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EPollManager::Wait(int maxEvents, epoll_event *events) const {
|
||||||
|
return epoll_wait(m_EpollID, events, maxEvents, 10);
|
||||||
|
}
|
||||||
|
} // namespace VWeb
|
33
Source/MiddleWare.cpp
Normal file
33
Source/MiddleWare.cpp
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
#include <VWeb.h>
|
||||||
|
|
||||||
|
namespace VWeb {
|
||||||
|
void MiddleWareHandler::HandlePre(Ref<Request> &request) {
|
||||||
|
for (auto &[key, middleWare] : m_MiddleWares) {
|
||||||
|
middleWare->PreHandle(*request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void MiddleWareHandler::HandlePost(Ref<Request> &request,
|
||||||
|
Ref<Response> &response) {
|
||||||
|
for (auto &[key, middleWare] : m_MiddleWares) {
|
||||||
|
middleWare->PostHandle(*request, *response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void MiddleWareHandler::Shutdown(Ref<Request> &request,
|
||||||
|
Ref<Response> &response) {
|
||||||
|
for (auto &[key, middleWare] : m_MiddleWares) {
|
||||||
|
middleWare->Shutdown(*request, *response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ref<MiddleWare> MiddleWareHandler::GetById(const char *id) {
|
||||||
|
return m_MiddleWares[id];
|
||||||
|
}
|
||||||
|
Ref<MiddleWare> MiddleWareHandler::SetById(const char *id,
|
||||||
|
const Ref<MiddleWare> &middleWare) {
|
||||||
|
m_MiddleWares[id] = middleWare;
|
||||||
|
return middleWare;
|
||||||
|
}
|
||||||
|
void MiddleWareHandler::RemoveById(const char *id) {
|
||||||
|
if (m_MiddleWares.contains(id))
|
||||||
|
m_MiddleWares.erase(id);
|
||||||
|
}
|
||||||
|
} // namespace VWeb
|
99
Source/RequestHandler.cpp
Normal file
99
Source/RequestHandler.cpp
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
#include "StringUtils.h"
|
||||||
|
|
||||||
|
#include <VWeb.h>
|
||||||
|
|
||||||
|
namespace VWeb {
|
||||||
|
|
||||||
|
HttpMethod StringToHTTPMethod(std::string &method) {
|
||||||
|
static std::unordered_map<std::string, HttpMethod> s_StringToMethodMap{
|
||||||
|
{"get", HttpMethod::GET}, {"head", HttpMethod::HEAD},
|
||||||
|
{"options", HttpMethod::OPTIONS}, {"post", HttpMethod::POST},
|
||||||
|
{"put", HttpMethod::PUT}, {"patch", HttpMethod::PATCH},
|
||||||
|
{"delete", HttpMethod::DELETE}, {"fallback", HttpMethod::FALLBACK},
|
||||||
|
};
|
||||||
|
String::ToLowerCase(method);
|
||||||
|
if (s_StringToMethodMap.contains(method))
|
||||||
|
return s_StringToMethodMap[method];
|
||||||
|
return s_StringToMethodMap["fallback"];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ParseRequest(Ref<Request> &request) {
|
||||||
|
std::istringstream resp(request->Body);
|
||||||
|
std::string line;
|
||||||
|
std::string::size_type index;
|
||||||
|
|
||||||
|
while (std::getline(resp, line) && line != "\r") {
|
||||||
|
index = line.find(':', 0);
|
||||||
|
if (index != std::string::npos) {
|
||||||
|
auto key = line.substr(0, index);
|
||||||
|
String::Trim(key);
|
||||||
|
request->Header(key).Add(String::TrimCopy(line.substr(index + 1)));
|
||||||
|
} else if (line.find("HTTP")) {
|
||||||
|
auto headers = String::Split(line, " ");
|
||||||
|
if (headers.size() != 3)
|
||||||
|
return false;
|
||||||
|
request->Method = StringToHTTPMethod(headers[0]);
|
||||||
|
request->URI = String::UrlDecode(headers[1]);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RequestJob : public WorkerJob {
|
||||||
|
Ref<Request> MRequest{};
|
||||||
|
Ref<Router> MRouter{};
|
||||||
|
Ref<MiddleWareHandler> MMiddleWareHandler{};
|
||||||
|
RequestHandler *MRequestHandler{nullptr};
|
||||||
|
|
||||||
|
void Execute() override {
|
||||||
|
if (!ParseRequest(MRequest)) {
|
||||||
|
fprintf(stderr, "[Request] >> Request failed to parse\n");
|
||||||
|
SocketUtils::Close(MRequest->ID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MRequest->CookieData = CreateRef<Cookies>();
|
||||||
|
MRequest->SessionData = CreateRef<Session>();
|
||||||
|
MMiddleWareHandler->HandlePre(MRequest);
|
||||||
|
auto response = MRouter->HandleRoute(MRequest);
|
||||||
|
MMiddleWareHandler->HandlePost(MRequest, response);
|
||||||
|
auto content = response->GetResponse();
|
||||||
|
MRequestHandler->AddSendResponse(
|
||||||
|
{0, (ssize_t)content.size(), MRequest->ID, content});
|
||||||
|
MMiddleWareHandler->Shutdown(MRequest, response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
RequestHandler::RequestHandler(const Ref<SocketManager> &manager)
|
||||||
|
: m_SocketManager(manager), m_MiddleWare(CreateRef<MiddleWareHandler>()) {
|
||||||
|
m_MiddleWare = CreateRef<MiddleWareHandler>();
|
||||||
|
}
|
||||||
|
void RequestHandler::InitThreads(int count) {
|
||||||
|
m_Pool.SetThreadCount(count);
|
||||||
|
m_Pool.Create();
|
||||||
|
}
|
||||||
|
void RequestHandler::AddRequest(Ref<Request> &request) {
|
||||||
|
if (m_Router) {
|
||||||
|
auto job = CreateRef<RequestJob>();
|
||||||
|
job->MRequest = request;
|
||||||
|
job->MMiddleWareHandler = m_MiddleWare;
|
||||||
|
job->MRequestHandler = this;
|
||||||
|
job->MRouter = m_Router;
|
||||||
|
m_Pool.Dispatch(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void RequestHandler::Stop() {
|
||||||
|
m_Pool.Stop();
|
||||||
|
}
|
||||||
|
void RequestHandler::SetRouter(Ref<Router> &router) {
|
||||||
|
m_Router = router;
|
||||||
|
}
|
||||||
|
void RequestHandler::AddSendResponse(SendData sendData) {
|
||||||
|
auto id = sendData.SocketID;
|
||||||
|
m_Server->m_OutRequest.Add(id, sendData);
|
||||||
|
if (!m_SocketManager->SetSendListen(id)) {
|
||||||
|
SocketUtils::Close(id);
|
||||||
|
m_Server->m_OutRequest.Remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace VWeb
|
112
Source/Response.cpp
Normal file
112
Source/Response.cpp
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
#include <VWeb.h>
|
||||||
|
|
||||||
|
namespace VWeb {
|
||||||
|
// clang-format off
|
||||||
|
static std::unordered_map<HttpStatusCode, std::string> s_HTTPCodeToString = {
|
||||||
|
{HttpStatusCode::Continue, "100 Continue"},
|
||||||
|
{HttpStatusCode::SwitchingProtocols, "101 Switching Protocols"},
|
||||||
|
{HttpStatusCode::Processing, "102 Processing"},
|
||||||
|
{HttpStatusCode::EarlyHints, "103 Early Hints"},
|
||||||
|
{HttpStatusCode::OK, "200 OK"},
|
||||||
|
{HttpStatusCode::Created, "201 Created"},
|
||||||
|
{HttpStatusCode::Accepted, "202 Accepted"},
|
||||||
|
{HttpStatusCode::NonAuthoritativeInformation,"203 Non-Authoritative Information"},
|
||||||
|
{HttpStatusCode::NoContent, "204 No Content"},
|
||||||
|
{HttpStatusCode::ResetContent, "205 Reset Content"},
|
||||||
|
{HttpStatusCode::PartialContent, "206 Partial Content"},
|
||||||
|
{HttpStatusCode::MultiStatus, "207 Multi-Status"},
|
||||||
|
{HttpStatusCode::AlreadyReported, "208 Already Reported"},
|
||||||
|
{HttpStatusCode::IMUsed, "226 IM Used"},
|
||||||
|
{HttpStatusCode::MultipleChoices, "300 Multiple Choices"},
|
||||||
|
{HttpStatusCode::MovedPermanently, "301 Moved Permanently"},
|
||||||
|
{HttpStatusCode::Found, "302 Found"},
|
||||||
|
{HttpStatusCode::SeeOther, "303 See Other"},
|
||||||
|
{HttpStatusCode::NotModified, "304 Not Modified"},
|
||||||
|
{HttpStatusCode::UseProxy, "305 Use Proxy"},
|
||||||
|
{HttpStatusCode::TemporaryRedirect, "307 Temporary Redirect"},
|
||||||
|
{HttpStatusCode::PermanentRedirect, "308 Permanent Redirect"},
|
||||||
|
{HttpStatusCode::BadRequest, "400 Bad Request"},
|
||||||
|
{HttpStatusCode::Unauthorized, "401 Unauthorized"},
|
||||||
|
{HttpStatusCode::PaymentRequired, "402 Payment Required"},
|
||||||
|
{HttpStatusCode::Forbidden, "403 Forbidden"},
|
||||||
|
{HttpStatusCode::NotFound, "404 Not Found"},
|
||||||
|
{HttpStatusCode::MethodNotAllowed, "405 Method Not Allowed"},
|
||||||
|
{HttpStatusCode::NotAcceptable, "406 Not Acceptable"},
|
||||||
|
{HttpStatusCode::ProxyAuthenticationRequired,"407 Proxy Authentication Required"},
|
||||||
|
{HttpStatusCode::RequestTimeout, "408 Request Timeout"},
|
||||||
|
{HttpStatusCode::Conflict, "409 Conflict"},
|
||||||
|
{HttpStatusCode::Gone, "410 Gone"},
|
||||||
|
{HttpStatusCode::LengthRequired, "411 Length Required"},
|
||||||
|
{HttpStatusCode::PreconditionFailed, "412 Precondition Failed"},
|
||||||
|
{HttpStatusCode::PayloadTooLarge, "413 Payload Too Large"},
|
||||||
|
{HttpStatusCode::URITooLong, "414 URI Too Long"},
|
||||||
|
{HttpStatusCode::UnsupportedMediaType, "415 Unsupported Media Type"},
|
||||||
|
{HttpStatusCode::RangeNotSatisfiable, "416 Range Not Satisfiable"},
|
||||||
|
{HttpStatusCode::ExpectationFailed, "417 Expectation Failed"},
|
||||||
|
{HttpStatusCode::ImATeapot, "418 I'm a teapot"},
|
||||||
|
{HttpStatusCode::UnprocessableEntity, "422 Unprocessable Entity"},
|
||||||
|
{HttpStatusCode::Locked, "423 Locked"},
|
||||||
|
{HttpStatusCode::FailedDependency, "424 Failed Dependency"},
|
||||||
|
{HttpStatusCode::UpgradeRequired, "426 Upgrade Required"},
|
||||||
|
{HttpStatusCode::PreconditionRequired, "428 Precondition Required"},
|
||||||
|
{HttpStatusCode::TooManyRequests, "429 Too Many Requests"},
|
||||||
|
{HttpStatusCode::RequestHeaderFieldsTooLarge,"431 Request Header Fields Too Large"},
|
||||||
|
{HttpStatusCode::UnavailableForLegalReasons,"451 Unavailable For Legal Reasons"},
|
||||||
|
{HttpStatusCode::InternalServerError, "500 Internal Server Error"},
|
||||||
|
{HttpStatusCode::NotImplemented, "501 Not Implemented"},
|
||||||
|
{HttpStatusCode::BadGateway, "502 Bad Gateway"},
|
||||||
|
{HttpStatusCode::ServiceUnavailable, "503 Service Unavailable"},
|
||||||
|
{HttpStatusCode::GatewayTimeout, "504 Gateway Time-out"},
|
||||||
|
{HttpStatusCode::HTTPVersionNotSupported, "505 HTTP Version Not Supported"},
|
||||||
|
{HttpStatusCode::VariantAlsoNegotiates, "506 Variant Also Negotiates"},
|
||||||
|
{HttpStatusCode::InsufficientStorage, "507 Insufficient Storage"},
|
||||||
|
{HttpStatusCode::LoopDetected, "508 Loop Detected"},
|
||||||
|
{HttpStatusCode::NotExtended, "510 Not Extended"},
|
||||||
|
{HttpStatusCode::NetworkAuthenticationRequired,"511 Network Authentication Required"}
|
||||||
|
};
|
||||||
|
// clang-format on
|
||||||
|
std::string Response::GetResponse() {
|
||||||
|
std::string content = m_Content.str();
|
||||||
|
auto headData = TransformHeaders(content);
|
||||||
|
|
||||||
|
auto outContent = std::string(headData + content);
|
||||||
|
Length = content.length();
|
||||||
|
return outContent;
|
||||||
|
}
|
||||||
|
void Response::SetHeader(const std::string &key, const std::string &value) {
|
||||||
|
m_Headers[key].Set(value);
|
||||||
|
}
|
||||||
|
void Response::SetHeader(const std::string &key, ParameterValue &value) {
|
||||||
|
m_Headers[key] = value;
|
||||||
|
}
|
||||||
|
void Response::AddHeaders(const std::string &key,
|
||||||
|
const Vector<std::string> &values) {
|
||||||
|
auto &element = m_Headers[key];
|
||||||
|
for (const auto &value : values)
|
||||||
|
element.Add(value);
|
||||||
|
}
|
||||||
|
void Response::AddHeader(const std::string &key, const std::string &value) {
|
||||||
|
m_Headers[key].Add(value);
|
||||||
|
}
|
||||||
|
void Response::SetType(const std::string &type) { m_Type = type; }
|
||||||
|
void Response::AddContent(const std::string &data) { m_Content << data; }
|
||||||
|
void Response::SetStatus(HttpStatusCode statusCode) { Status = statusCode; }
|
||||||
|
void Response::SetMethod(HttpMethod method) { Method = method; }
|
||||||
|
|
||||||
|
void Response::Reset() { m_Content.clear(); }
|
||||||
|
|
||||||
|
std::string Response::TransformHeaders(const std::string &content) {
|
||||||
|
std::ostringstream stream;
|
||||||
|
stream << "HTTP/1.1 " << s_HTTPCodeToString[Status] << "\n";
|
||||||
|
stream << "content-type: " << m_Type << "\n";
|
||||||
|
stream << "content-length: " << content.length() << "\n";
|
||||||
|
for (auto &[key, value] : m_Headers) {
|
||||||
|
for (auto &val : value.Values()) {
|
||||||
|
stream << key << ": " << val << "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream << CookieData->TransformToHeader();
|
||||||
|
stream << "\n";
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
} // namespace VWeb
|
83
Source/Route.cpp
Normal file
83
Source/Route.cpp
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
#include <VWeb.h>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
|
||||||
|
namespace VWeb {
|
||||||
|
|
||||||
|
#define stringify(name) {name, std::string(#name).replace(0,12,"")}
|
||||||
|
static std::unordered_map<HttpMethod, std::string> s_HttpMethodToString = {
|
||||||
|
stringify(HttpMethod::HEAD),
|
||||||
|
stringify(HttpMethod::GET),
|
||||||
|
stringify(HttpMethod::OPTIONS),
|
||||||
|
stringify(HttpMethod::POST),
|
||||||
|
stringify(HttpMethod::PUT),
|
||||||
|
stringify(HttpMethod::PATCH),
|
||||||
|
stringify(HttpMethod::DELETE),
|
||||||
|
stringify(HttpMethod::FALLBACK)
|
||||||
|
};
|
||||||
|
#undef stringify
|
||||||
|
|
||||||
|
Route::Route(std::initializer_list<HttpMethod> methods) {
|
||||||
|
m_AllowedMethods = methods;
|
||||||
|
m_AllowAll = false;
|
||||||
|
m_AllowedMethods.push_back(HttpMethod::HEAD);
|
||||||
|
m_AllowedMethods.push_back(HttpMethod::OPTIONS);
|
||||||
|
}
|
||||||
|
bool Route::Execute(const Request &request, Response &response) {
|
||||||
|
switch (request.Method) {
|
||||||
|
case HttpMethod::GET:
|
||||||
|
case HttpMethod::HEAD: return Get(request, response);
|
||||||
|
case HttpMethod::POST: return Post(request, response);
|
||||||
|
case HttpMethod::PUT: return Put(request, response);
|
||||||
|
case HttpMethod::OPTIONS: return Options(request, response);
|
||||||
|
case HttpMethod::PATCH: return Patch(request, response);
|
||||||
|
case HttpMethod::DELETE: return Delete(request, response);
|
||||||
|
default: return Fallback(request, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool Route::Get(const Request &request, Response &response) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool Route::Post(const Request &request, Response &response) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool Route::Put(const Request &request, Response &response) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool Route::Patch(const Request &request, Response &response) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool Route::Delete(const Request &request, Response &response) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool Route::Options(const Request &request, Response &response) {
|
||||||
|
std::stringstream str{};
|
||||||
|
bool isFirst = true;
|
||||||
|
if (m_AllowAll) {
|
||||||
|
for (auto &[key, value] : s_HttpMethodToString) {
|
||||||
|
if (!isFirst)
|
||||||
|
str << ", ";
|
||||||
|
str << value;
|
||||||
|
isFirst = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (auto &method : m_AllowedMethods) {
|
||||||
|
if (!isFirst)
|
||||||
|
str << ", ";
|
||||||
|
str << s_HttpMethodToString[method];
|
||||||
|
isFirst = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.SetHeader("Allow", str.str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool Route::Fallback(const Request &request, Response &response) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool Route::IsAllowed(const Request &request) { return true; }
|
||||||
|
bool Route::SupportsMethod(const Request &request) {
|
||||||
|
return m_AllowAll ||
|
||||||
|
std::find(m_AllowedMethods.begin(), m_AllowedMethods.end(),
|
||||||
|
request.Method) != m_AllowedMethods.end();
|
||||||
|
}
|
||||||
|
} // namespace VWeb
|
97
Source/Router.cpp
Normal file
97
Source/Router.cpp
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
#include "StringUtils.h"
|
||||||
|
|
||||||
|
#include <VWeb.h>
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
namespace VWeb {
|
||||||
|
|
||||||
|
template <typename E> constexpr auto to_underlying(E e) noexcept {
|
||||||
|
return static_cast<std::underlying_type_t<E>>(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorRoute : public Route {
|
||||||
|
public:
|
||||||
|
bool Execute(const Request &request, Response &response) override {
|
||||||
|
response.Reset();
|
||||||
|
response << "Unhandled Error: Status "
|
||||||
|
<< std::to_string(to_underlying(response.Status));
|
||||||
|
response.SetType("text/plain");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Router::Router() { m_Routes["@"] = CreateRef<ErrorRoute>(); }
|
||||||
|
|
||||||
|
void Router::AddRoute(const std::string &name, const Ref<Route> &route) {
|
||||||
|
m_Routes[name] = route;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ref<Route> &Router::GetRoute(const std::string &name) { return m_Routes[name]; }
|
||||||
|
|
||||||
|
void Router::DeleteRoute(const std::string &name) {
|
||||||
|
if (m_Routes.contains(name)) {
|
||||||
|
m_Routes.erase(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ref<Response> Router::HandleRoute(Ref<Request> &request) {
|
||||||
|
auto response = CreateRef<Response>();
|
||||||
|
auto route = FindRoute(request);
|
||||||
|
response->CookieData = request->CookieData;
|
||||||
|
response->Method = request->Method;
|
||||||
|
if (!route) {
|
||||||
|
response->SetStatus(HttpStatusCode::NotFound);
|
||||||
|
m_Routes["@"]->Execute(*request, *response);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!route->IsAllowed(*request)) {
|
||||||
|
response->SetStatus(HttpStatusCode::Forbidden);
|
||||||
|
m_Routes["@"]->Execute(*request, *response);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!route->Execute(*request, *response)) {
|
||||||
|
std::string rKey = "@" + std::to_string(to_underlying(response->Status));
|
||||||
|
m_Routes.contains(rKey) ? m_Routes[rKey]->Execute(*request, *response)
|
||||||
|
: m_Routes["@"]->Execute(*request, *response);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ref<Route> Router::FindRoute(Ref<Request> &request) {
|
||||||
|
auto &url = request->URI;
|
||||||
|
if (m_Routes.contains(url.data())) {
|
||||||
|
auto &route = m_Routes.at(url.data());
|
||||||
|
if (route->SupportsMethod(*request))
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
if (url.starts_with("@"))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
auto split = String::Split(url.data(), "/");
|
||||||
|
if (split.size() > 1) {
|
||||||
|
AddToArgs(request, split);
|
||||||
|
while (split.size() > 1) {
|
||||||
|
std::string nUrl = String::Join(split, "/");
|
||||||
|
if (m_Routes.contains(nUrl)) {
|
||||||
|
auto &route = m_Routes[nUrl];
|
||||||
|
if (route->SupportsMethod(*request))
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
AddToArgs(request, split);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_Routes.contains("/")) {
|
||||||
|
auto &route = m_Routes["/"];
|
||||||
|
if (route->SupportsMethod(*request))
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Router::AddToArgs(Ref<Request> &request, Vector<std::string> &items) {
|
||||||
|
request->URLParameters.push_back(items[items.size() - 1]);
|
||||||
|
items.pop_back();
|
||||||
|
}
|
||||||
|
} // namespace VWeb
|
135
Source/Server.cpp
Normal file
135
Source/Server.cpp
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
#include <VWeb.h>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace VWeb {
|
||||||
|
Server::Server() {
|
||||||
|
m_Router = CreateRef<Router>();
|
||||||
|
m_ServerConfig = CreateRef<ServerConfig>();
|
||||||
|
m_ServerConfig->EPoll = CreateRef<EPollManager>();
|
||||||
|
m_ServerConfig->Socket = CreateRef<SocketManager>(m_ServerConfig);
|
||||||
|
m_RequestHandler = CreateRef<RequestHandler>(m_ServerConfig->Socket);
|
||||||
|
m_RequestHandler->m_Server = this;
|
||||||
|
};
|
||||||
|
void Server::LoadSharedLibs() {
|
||||||
|
// @TODO: LOAD .so files inside sub-folder
|
||||||
|
}
|
||||||
|
void Server::Start() {
|
||||||
|
m_ServerConfig->Socket->Init();
|
||||||
|
if (m_ServerConfig->Socket->IsErrored())
|
||||||
|
return;
|
||||||
|
m_RequestHandler->SetRouter(m_Router);
|
||||||
|
m_RequestHandler->InitThreads(m_ServerConfig->WorkerThreads);
|
||||||
|
m_WorkerThread = CreateRef<std::thread>(&Server::Execute, this);
|
||||||
|
fprintf(stdout, "[VWeb] Running Server On: 0.0.0.0:%d\n",
|
||||||
|
m_ServerConfig->Port);
|
||||||
|
}
|
||||||
|
void Server::AddRoute(const std::string &path, const Ref<Route> &route) {
|
||||||
|
m_Router->AddRoute(path, route);
|
||||||
|
}
|
||||||
|
void Server::RemoveRoute(const std::string &path) {
|
||||||
|
m_Router->DeleteRoute(path);
|
||||||
|
}
|
||||||
|
void Server::Execute() {
|
||||||
|
constexpr size_t MAX_EVENTS = 5000;
|
||||||
|
struct epoll_event events[MAX_EVENTS];
|
||||||
|
int sockID = m_ServerConfig->Socket->ID();
|
||||||
|
for (;;) {
|
||||||
|
if (m_IsExit)
|
||||||
|
break;
|
||||||
|
int eventCount = m_ServerConfig->EPoll->Wait(MAX_EVENTS, events);
|
||||||
|
for (int i = 0; i < eventCount; ++i) {
|
||||||
|
auto &event = events[i];
|
||||||
|
if ((event.events & EPOLLERR) || (event.events & EPOLLHUP)) {
|
||||||
|
m_RawRequest.Remove(event.data.fd);
|
||||||
|
SocketUtils::Close(event.data.fd);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (event.events & EPOLLOUT) {
|
||||||
|
OutgoingExecute(events[i]);
|
||||||
|
}
|
||||||
|
if (event.data.fd == sockID) {
|
||||||
|
IncomingExecute(event);
|
||||||
|
} else if (event.events & EPOLLIN) {
|
||||||
|
HandleRequestReading(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Server::OutgoingExecute(epoll_event &event) {
|
||||||
|
if (!m_OutRequest.Has(event.data.fd)) {
|
||||||
|
m_ServerConfig->Socket->SetReadListen(event.data.fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto &sendData = m_OutRequest.Get(event.data.fd);
|
||||||
|
auto returnVal = SocketUtils::Write(sendData);
|
||||||
|
if (returnVal == WriteState::ERRORED || returnVal == WriteState::OK) {
|
||||||
|
SocketUtils::WriteDone(sendData.SocketID);
|
||||||
|
m_OutRequest.Remove(event.data.fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Server::IncomingExecute(epoll_event &event) {
|
||||||
|
for (;;) {
|
||||||
|
auto req = m_ServerConfig->Socket->Handle();
|
||||||
|
if (req.ReturnValue == EPollReturns::BREAK ||
|
||||||
|
req.ReturnValue == EPollReturns::FAILURE)
|
||||||
|
break;
|
||||||
|
else if (req.ReturnValue == EPollReturns::OK) {
|
||||||
|
if (!m_RawRequest.Add(req.SockId, req))
|
||||||
|
SocketUtils::Close(req.SockId);
|
||||||
|
} else
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void Server::HandleRequestReading(epoll_event &event) {
|
||||||
|
int fd = event.data.fd;
|
||||||
|
if (!m_RawRequest.Has(fd)) {
|
||||||
|
SocketUtils::Close(fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto &data = m_RawRequest.Get(fd);
|
||||||
|
char buf[m_ServerConfig->BufferSize];
|
||||||
|
bool isFinished = false;
|
||||||
|
for (;;) {
|
||||||
|
memset(buf, 0, sizeof(buf));
|
||||||
|
ssize_t nBytes = recv(fd, buf, m_ServerConfig->BufferSize, 0);
|
||||||
|
if (nBytes < 0) {
|
||||||
|
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||||
|
if (isFinished)
|
||||||
|
CreateRequest(fd);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
m_RawRequest.Remove(fd);
|
||||||
|
SocketUtils::Close(fd);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (nBytes == 0) {
|
||||||
|
m_RawRequest.Remove(fd);
|
||||||
|
SocketUtils::Close(fd);
|
||||||
|
} else {
|
||||||
|
if (m_ServerConfig->MaxBufferSize != -1 &&
|
||||||
|
data.CurrentBytes + nBytes > m_ServerConfig->MaxBufferSize) {
|
||||||
|
data.Data.clear();
|
||||||
|
data.Data << "GET @431 HTTP/1.1";
|
||||||
|
CreateRequest(fd);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
data.Data << buf;
|
||||||
|
data.CurrentBytes += nBytes;
|
||||||
|
if (nBytes < m_ServerConfig->BufferSize - 1) {
|
||||||
|
isFinished = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void Server::CreateRequest(int sockID) {
|
||||||
|
Accept &meAccept = m_RawRequest.Get(sockID);
|
||||||
|
auto request = CreateRef<Request>();
|
||||||
|
request->ID = meAccept.SockId;
|
||||||
|
request->Body = meAccept.Data.str();
|
||||||
|
m_RawRequest.Remove(sockID);
|
||||||
|
m_RequestHandler->AddRequest(request);
|
||||||
|
}
|
||||||
|
} // namespace VWeb
|
20
Source/Session.cpp
Normal file
20
Source/Session.cpp
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#include <VWeb.h>
|
||||||
|
|
||||||
|
namespace VWeb {
|
||||||
|
bool Session::IsValid() {
|
||||||
|
auto call = std::chrono::system_clock::now();
|
||||||
|
auto time =
|
||||||
|
std::chrono::duration_cast<std::chrono::seconds>(call - m_LastCall)
|
||||||
|
.count();
|
||||||
|
return time < TTLSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Session::Update() { m_LastCall = std::chrono::system_clock::now(); }
|
||||||
|
|
||||||
|
void Session::Remove(const std::string &key) {
|
||||||
|
if (m_Data.contains(key))
|
||||||
|
m_Data.erase(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Session::Has(const std::string &key) { return m_Data.contains(key); }
|
||||||
|
} // namespace VWeb
|
102
Source/SocketManager.cpp
Normal file
102
Source/SocketManager.cpp
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
#include <VWeb.h>
|
||||||
|
|
||||||
|
namespace VWeb {
|
||||||
|
#pragma region VWebSocketUtils
|
||||||
|
bool SocketUtils::MakeAsync(int socketId) {
|
||||||
|
int flags = fcntl(socketId, F_GETFL, 0);
|
||||||
|
if (flags == -1)
|
||||||
|
return false;
|
||||||
|
if (fcntl(socketId, F_SETFL, flags | O_NONBLOCK) == -1)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool SocketUtils::Add(int socketId) { return MakeAsync(socketId); }
|
||||||
|
bool SocketUtils::Close(int socketId) { return close(socketId) >= 0; }
|
||||||
|
bool SocketUtils::WriteDone(int socketId) {
|
||||||
|
return shutdown(socketId, SHUT_WR) != -1;
|
||||||
|
}
|
||||||
|
WriteState SocketUtils::Write(VWeb::SendData &sendData) {
|
||||||
|
const char *buf = sendData.Content.c_str();
|
||||||
|
ssize_t n;
|
||||||
|
while (sendData.Size > 0) {
|
||||||
|
n = send(sendData.SocketID, buf + sendData.Offset, sendData.Size, 0);
|
||||||
|
if (n < 0) {
|
||||||
|
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||||
|
return WriteState::EPOLL;
|
||||||
|
fprintf(stderr, "[SocketUtils::Write] >> Im Errored\n");
|
||||||
|
return WriteState::ERRORED;
|
||||||
|
}
|
||||||
|
sendData.Offset += n;
|
||||||
|
sendData.Size -= n;
|
||||||
|
}
|
||||||
|
return WriteState::OK;
|
||||||
|
}
|
||||||
|
#pragma endregion VWebSocketUtils
|
||||||
|
|
||||||
|
#pragma region VWebSocketManager
|
||||||
|
SocketManager::SocketManager(const Ref<VWeb::ServerConfig> &serverConfig)
|
||||||
|
: m_ServerConfig(serverConfig) {}
|
||||||
|
SocketManager::~SocketManager() { close(m_SocketID); }
|
||||||
|
Accept SocketManager::Handle() {
|
||||||
|
Accept requestReturn{};
|
||||||
|
int client =
|
||||||
|
accept(m_SocketID, (sockaddr *)&m_Address, (socklen_t *)&m_AddressLength);
|
||||||
|
if (client < 0) {
|
||||||
|
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||||
|
requestReturn.ReturnValue = EPollReturns::BREAK;
|
||||||
|
else
|
||||||
|
requestReturn.ReturnValue = EPollReturns::FAILURE;
|
||||||
|
} else {
|
||||||
|
requestReturn.SockId = client;
|
||||||
|
requestReturn.ReturnValue = EPollReturns::FAILURE;
|
||||||
|
if (SocketUtils::Add(client) &&
|
||||||
|
m_ServerConfig->EPoll->Dispatch(client, EPOLLIN | EPOLLET)) {
|
||||||
|
requestReturn.ReturnValue = EPollReturns::OK;
|
||||||
|
} else {
|
||||||
|
SocketUtils::Close(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requestReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SocketManager::SetSendListen(int socketID) {
|
||||||
|
return m_ServerConfig->EPoll->UpdateEvents(socketID, EPOLLOUT | EPOLLET);
|
||||||
|
}
|
||||||
|
bool SocketManager::SetReadListen(int socketID) {
|
||||||
|
return m_ServerConfig->EPoll->UpdateEvents(socketID, EPOLLIN | EPOLLET);
|
||||||
|
}
|
||||||
|
void SocketManager::Init() {
|
||||||
|
if ((m_SocketID = socket(AF_INET, SOCK_STREAM, 0)) == 0)
|
||||||
|
return Errored("Cannot Create Socket");
|
||||||
|
|
||||||
|
auto trues = 1;
|
||||||
|
struct timeval tv {
|
||||||
|
1, 0
|
||||||
|
};
|
||||||
|
setsockopt(m_SocketID, SOL_SOCKET, SO_REUSEADDR, &trues, sizeof(int));
|
||||||
|
setsockopt(m_SocketID, SOL_SOCKET, SO_REUSEPORT, &trues, sizeof(int));
|
||||||
|
setsockopt(m_SocketID, SOL_SOCKET, SOCK_CLOEXEC, &trues, sizeof(int));
|
||||||
|
setsockopt(m_SocketID, SOL_SOCKET, SOCK_NONBLOCK, &trues, sizeof(int));
|
||||||
|
setsockopt(m_SocketID, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tv, sizeof tv);
|
||||||
|
|
||||||
|
m_Address.sin_family = AF_INET;
|
||||||
|
m_Address.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||||
|
m_Address.sin_port = htons(m_ServerConfig->Port);
|
||||||
|
|
||||||
|
if (bind(m_SocketID, (struct sockaddr *)&m_Address, m_AddressLength) < 0)
|
||||||
|
return Errored("Cannot Bind Socket");
|
||||||
|
SocketUtils::MakeAsync(m_SocketID);
|
||||||
|
if (listen(m_SocketID, SOMAXCONN) < 0)
|
||||||
|
return Errored("Socket Failed to Listen");
|
||||||
|
|
||||||
|
if (!m_ServerConfig->EPoll->Dispatch(m_SocketID,EPOLLIN | EPOLLET | EPOLLOUT))
|
||||||
|
return Errored("Cannot Add Event");
|
||||||
|
}
|
||||||
|
void SocketManager::Errored(const std::string &data) {
|
||||||
|
if (m_SocketID > 0)
|
||||||
|
close(m_SocketID);
|
||||||
|
fprintf(stderr, "%s\n", data.c_str());
|
||||||
|
m_IsErrored = true;
|
||||||
|
}
|
||||||
|
#pragma endregion VWebSocketManager
|
||||||
|
} // namespace VWeb
|
86
Source/StringUtils.cpp
Normal file
86
Source/StringUtils.cpp
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
#include "StringUtils.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace VWeb {
|
||||||
|
|
||||||
|
static void StringLeftTrim(std::string &s) {
|
||||||
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void StringRightTrim(std::string &s) {
|
||||||
|
s.erase(std::find_if(s.rbegin(), s.rend(),
|
||||||
|
[](unsigned char ch) { return !std::isspace(ch); })
|
||||||
|
.base(),
|
||||||
|
s.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> String::Split(const std::string &s,
|
||||||
|
const std::string &delimiter) {
|
||||||
|
size_t pos_start = 0, pos_end, delim_len = delimiter.length();
|
||||||
|
std::string token;
|
||||||
|
std::vector<std::string> res;
|
||||||
|
|
||||||
|
while ((pos_end = s.find(delimiter, pos_start)) != std::string::npos) {
|
||||||
|
token = s.substr(pos_start, pos_end - pos_start);
|
||||||
|
pos_start = pos_end + delim_len;
|
||||||
|
res.push_back(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.push_back(s.substr(pos_start));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
int String::ToNumber(std::string &string, int def) {
|
||||||
|
char *pEnd;
|
||||||
|
long val = std::strtol(string.c_str(), &pEnd, 10);
|
||||||
|
if (pEnd == string)
|
||||||
|
return def;
|
||||||
|
return (int)val;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string String::Join(const std::vector<std::string> &items,
|
||||||
|
const std::string &delimiter) {
|
||||||
|
std::stringstream string;
|
||||||
|
for (size_t i = 0; i < items.size(); ++i) {
|
||||||
|
if (i != 0)
|
||||||
|
string << delimiter;
|
||||||
|
string << items[i];
|
||||||
|
}
|
||||||
|
return string.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
void String::Trim(std::string &value) {
|
||||||
|
StringLeftTrim(value);
|
||||||
|
StringRightTrim(value);
|
||||||
|
}
|
||||||
|
std::string String::TrimCopy(const std::string &value) {
|
||||||
|
auto val = value;
|
||||||
|
Trim(val);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
void String::ToLowerCase(std::string &value) {
|
||||||
|
std::transform(value.begin(), value.end(), value.begin(),
|
||||||
|
[](unsigned char c) { return std::tolower(c); });
|
||||||
|
}
|
||||||
|
std::string String::UrlDecode(const std::string &val) {
|
||||||
|
std::string ret;
|
||||||
|
char ch;
|
||||||
|
size_t i;
|
||||||
|
unsigned int ii;
|
||||||
|
for (i = 0; i < val.length(); i++) {
|
||||||
|
if (int(val[i]) == 37) {
|
||||||
|
char *pEnd;
|
||||||
|
ii = std::strtoul(val.substr(i + 1, 2).c_str(), &pEnd, 16);
|
||||||
|
ch = static_cast<char>(ii);
|
||||||
|
ret += ch;
|
||||||
|
i = i + 2;
|
||||||
|
} else
|
||||||
|
ret += val[i];
|
||||||
|
}
|
||||||
|
return (ret);
|
||||||
|
}
|
||||||
|
} // namespace VWeb
|
18
Source/StringUtils.h
Normal file
18
Source/StringUtils.h
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace VWeb {
|
||||||
|
class String {
|
||||||
|
public:
|
||||||
|
static std::vector<std::string> Split(const std::string& s,
|
||||||
|
const std::string& delimiter);
|
||||||
|
static std::string Join(const std::vector<std::string>& items, const std::string& delimiter);
|
||||||
|
static int ToNumber(std::string& string, int def);
|
||||||
|
static void ToLowerCase(std::string& value);
|
||||||
|
static void Trim(std::string& value);
|
||||||
|
static std::string TrimCopy(const std::string& value);
|
||||||
|
static std::string UrlDecode(const std::string& value);
|
||||||
|
};
|
||||||
|
}
|
43
Source/ThreadPool.cpp
Normal file
43
Source/ThreadPool.cpp
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#include <VWeb.h>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace VWeb {
|
||||||
|
ThreadPool::ThreadPool(const std::string &name) : m_Name(name) {}
|
||||||
|
void ThreadPool::Create() {
|
||||||
|
if (m_IsCreated)
|
||||||
|
return;
|
||||||
|
m_IsDone = false;
|
||||||
|
m_IsCreated = true;
|
||||||
|
m_Queue.Open();
|
||||||
|
for (int i = 0; i < m_ThreadCount; ++i) {
|
||||||
|
m_Threads.push_back(std::thread(&ThreadPool::Execute, this));
|
||||||
|
};
|
||||||
|
printf("[ThreadPool] >> Created %d Threads for Pool \"%s\"\n", m_ThreadCount, m_Name.c_str());
|
||||||
|
}
|
||||||
|
void ThreadPool::Stop() {
|
||||||
|
m_IsDone = true;
|
||||||
|
m_Queue.Flush();
|
||||||
|
for (int i = 0; i < m_ThreadCount; ++i)
|
||||||
|
m_Threads[i].join();
|
||||||
|
}
|
||||||
|
void ThreadPool::Dispatch(const Ref<WorkerJob> &job) { m_Queue.Push(job); }
|
||||||
|
void ThreadPool::SetThreadCount(int count) {
|
||||||
|
if (m_IsCreated)
|
||||||
|
return;
|
||||||
|
m_ThreadCount =
|
||||||
|
count == -1 ? (int)std::thread::hardware_concurrency() : count;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadPool::Execute() {
|
||||||
|
if (!m_IsCreated)
|
||||||
|
return;
|
||||||
|
while (auto queueItem = m_Queue.WaitAndPop()) {
|
||||||
|
if (!queueItem.has_value() || m_Queue.IsClosed())
|
||||||
|
continue;
|
||||||
|
auto &item = queueItem.value();
|
||||||
|
if (item == nullptr)
|
||||||
|
continue;
|
||||||
|
item->Execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace VWeb
|
573
VWeb.h
Normal file
573
VWeb.h
Normal file
|
@ -0,0 +1,573 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <optional>
|
||||||
|
#include <queue>
|
||||||
|
#include <sstream>
|
||||||
|
#include <sys/epoll.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <thread>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace VWeb {
|
||||||
|
|
||||||
|
template <typename T> using Ref = std::shared_ptr<T>;
|
||||||
|
template <typename T> using Vector = std::vector<T>;
|
||||||
|
template <typename T, typename... Args>
|
||||||
|
constexpr Ref<T> CreateRef(Args &&...args) {
|
||||||
|
return std::make_shared<T>(std::forward<Args>(args)...);
|
||||||
|
}
|
||||||
|
typedef std::mutex Mutex;
|
||||||
|
|
||||||
|
#pragma region SAFESTRUCTS
|
||||||
|
template <typename T, typename S> struct SafeMap {
|
||||||
|
explicit SafeMap(size_t maxSize = -1UL) : m_MaxSize(maxSize), m_End(false){};
|
||||||
|
bool Add(const T &t, S &x) {
|
||||||
|
std::unique_lock<std::mutex> lck(m_Mutex);
|
||||||
|
while (m_Data.size() == m_MaxSize && !m_End)
|
||||||
|
return false;
|
||||||
|
assert(!m_End);
|
||||||
|
m_Data.emplace(t, std::move(x));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
bool Add(T &&t, S &&x) {
|
||||||
|
std::unique_lock<std::mutex> lck(m_Mutex);
|
||||||
|
while (m_Data.size() == m_MaxSize && !m_End)
|
||||||
|
return false;
|
||||||
|
assert(!m_End);
|
||||||
|
m_Data.push(std::move(t));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
void Remove(T t) {
|
||||||
|
std::unique_lock<std::mutex> lck(m_Mutex);
|
||||||
|
if (m_Data.empty() || m_End)
|
||||||
|
return;
|
||||||
|
if (m_Data.contains(t))
|
||||||
|
m_Data.erase(t);
|
||||||
|
m_CVFull.notify_one();
|
||||||
|
};
|
||||||
|
void Clear() {
|
||||||
|
std::unique_lock<std::mutex> lck(m_Mutex);
|
||||||
|
std::unordered_map<T, S> empty;
|
||||||
|
std::swap(m_Data, empty);
|
||||||
|
m_CVFull.notify_all();
|
||||||
|
};
|
||||||
|
bool Has(T t) {
|
||||||
|
std::unique_lock<std::mutex> lck(m_Mutex);
|
||||||
|
return m_Data.contains(t);
|
||||||
|
};
|
||||||
|
S &Get(T t) {
|
||||||
|
std::unique_lock<std::mutex> lck(m_Mutex);
|
||||||
|
return m_Data[t];
|
||||||
|
};
|
||||||
|
int Size() { return m_Data.size(); };
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<T, S> m_Data{};
|
||||||
|
std::mutex m_Mutex{};
|
||||||
|
std::condition_variable m_CVFull{};
|
||||||
|
const size_t m_MaxSize{};
|
||||||
|
std::atomic<bool> m_End{};
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename T> struct SafeQueue {
|
||||||
|
explicit SafeQueue(size_t maxSize = -1UL)
|
||||||
|
: m_MaxSize(maxSize),
|
||||||
|
m_End(false){};
|
||||||
|
void Push(const T &t) {
|
||||||
|
std::unique_lock<std::mutex> lck(m_Mutex);
|
||||||
|
while (m_Queue.size() == m_MaxSize && !m_End)
|
||||||
|
m_CVFull.wait(lck);
|
||||||
|
assert(!m_End);
|
||||||
|
m_Queue.push(std::move(t));
|
||||||
|
m_CVEmpty.notify_one();
|
||||||
|
};
|
||||||
|
void Push(T &&t) {
|
||||||
|
std::unique_lock<std::mutex> lck(m_Mutex);
|
||||||
|
while (m_Queue.size() == m_MaxSize && !m_End)
|
||||||
|
m_CVFull.wait(lck);
|
||||||
|
assert(!m_End);
|
||||||
|
m_Queue.push(std::move(t));
|
||||||
|
m_CVEmpty.notify_one();
|
||||||
|
};
|
||||||
|
void Open() {
|
||||||
|
m_End = false;
|
||||||
|
std::lock_guard<std::mutex> lck(m_Mutex);
|
||||||
|
m_CVEmpty.notify_all();
|
||||||
|
m_CVFull.notify_all();
|
||||||
|
};
|
||||||
|
void Close() {
|
||||||
|
m_End = true;
|
||||||
|
std::lock_guard<std::mutex> lck(m_Mutex);
|
||||||
|
m_CVEmpty.notify_all();
|
||||||
|
m_CVFull.notify_all();
|
||||||
|
};
|
||||||
|
void Clear() {
|
||||||
|
std::unique_lock<std::mutex> lck(m_Mutex);
|
||||||
|
std::queue<T> empty;
|
||||||
|
std::swap(m_Queue, empty);
|
||||||
|
m_CVEmpty.notify_all();
|
||||||
|
m_CVFull.notify_all();
|
||||||
|
};
|
||||||
|
std::optional<T> Pop() {
|
||||||
|
std::unique_lock<std::mutex> lck(m_Mutex);
|
||||||
|
if (m_Queue.empty() || m_End)
|
||||||
|
return {};
|
||||||
|
T t = std::move(m_Queue.front());
|
||||||
|
m_Queue.pop();
|
||||||
|
m_CVFull.notify_one();
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
std::optional<T> WaitAndPop() {
|
||||||
|
std::unique_lock<std::mutex> lck(m_Mutex);
|
||||||
|
while (m_Queue.empty() && !m_End)
|
||||||
|
m_CVEmpty.wait(lck);
|
||||||
|
if (m_Queue.empty() || m_End)
|
||||||
|
return {};
|
||||||
|
T t = std::move(m_Queue.front());
|
||||||
|
m_Queue.pop();
|
||||||
|
m_CVFull.notify_one();
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
bool IsClosed() { return m_End; }
|
||||||
|
int Size() { return m_Queue.size(); }
|
||||||
|
std::queue<T> &GetQueue() { return m_Queue; }
|
||||||
|
void Flush() {
|
||||||
|
while (!m_Queue.empty())
|
||||||
|
m_CVEmpty.notify_one();
|
||||||
|
m_End = true;
|
||||||
|
m_CVEmpty.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::queue<T> m_Queue;
|
||||||
|
std::mutex m_Mutex;
|
||||||
|
std::condition_variable m_CVEmpty, m_CVFull;
|
||||||
|
const size_t m_MaxSize;
|
||||||
|
std::atomic<bool> m_End;
|
||||||
|
};
|
||||||
|
#pragma endregion SAFESTRUCTS
|
||||||
|
#pragma region THREADING
|
||||||
|
struct WorkerJob {
|
||||||
|
virtual ~WorkerJob() = default;
|
||||||
|
virtual void Execute() = 0;
|
||||||
|
};
|
||||||
|
class ThreadPool {
|
||||||
|
public:
|
||||||
|
explicit ThreadPool(const std::string &name);
|
||||||
|
void SetThreadCount(int);
|
||||||
|
void Dispatch(const Ref<WorkerJob> &);
|
||||||
|
void Create();
|
||||||
|
void Stop();
|
||||||
|
void Execute();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::string m_Name;
|
||||||
|
int m_ThreadCount{1};
|
||||||
|
bool m_IsCreated{false};
|
||||||
|
Vector<std::thread> m_Threads{};
|
||||||
|
SafeQueue<Ref<WorkerJob>> m_Queue;
|
||||||
|
bool m_IsDone{false};
|
||||||
|
};
|
||||||
|
#pragma endregion THREADING
|
||||||
|
#pragma region VWEB
|
||||||
|
enum class EPollReturns { OK = 0, BREAK, FAILURE };
|
||||||
|
|
||||||
|
class EPollManager;
|
||||||
|
class SocketManager;
|
||||||
|
struct ServerConfig {
|
||||||
|
int Port{9020};
|
||||||
|
int MaxBufferSize{-1};
|
||||||
|
int BufferSize{16384};
|
||||||
|
int WorkerThreads{-1};
|
||||||
|
int MaxEvents{5000};
|
||||||
|
bool AllowSharedLibs{true};
|
||||||
|
Ref<EPollManager> EPoll{nullptr};
|
||||||
|
Ref<SocketManager> Socket{nullptr};
|
||||||
|
std::string ErrorDir;
|
||||||
|
std::string MimeFiles;
|
||||||
|
};
|
||||||
|
struct Accept {
|
||||||
|
EPollReturns ReturnValue{EPollReturns::OK};
|
||||||
|
std::stringstream Data{};
|
||||||
|
ssize_t CurrentBytes;
|
||||||
|
int SockId{-1};
|
||||||
|
};
|
||||||
|
class EPollManager {
|
||||||
|
public:
|
||||||
|
EPollManager();
|
||||||
|
~EPollManager();
|
||||||
|
[[nodiscard]] bool Dispatch(int sock, uint32_t eventType = EPOLLIN) const;
|
||||||
|
[[nodiscard]] bool UpdateEvents(int sock, uint32_t eventType = EPOLLIN) const;
|
||||||
|
int Wait(int idx, epoll_event *events) const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int m_EpollID{-1};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SendData {
|
||||||
|
ssize_t Offset{0};
|
||||||
|
ssize_t Size{0};
|
||||||
|
int SocketID{0};
|
||||||
|
std::string Content;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class WriteState { OK = 0, EPOLL, ERRORED };
|
||||||
|
|
||||||
|
class SocketUtils {
|
||||||
|
public:
|
||||||
|
static bool MakeAsync(int socketId);
|
||||||
|
static bool Add(int socketId);
|
||||||
|
static bool Close(int socketId);
|
||||||
|
static bool WriteDone(int socketId);
|
||||||
|
static WriteState Write(SendData &);
|
||||||
|
};
|
||||||
|
class SocketManager {
|
||||||
|
public:
|
||||||
|
explicit SocketManager(const Ref<ServerConfig> &);
|
||||||
|
~SocketManager();
|
||||||
|
Accept Handle();
|
||||||
|
bool SetSendListen(int socketID);
|
||||||
|
bool SetReadListen(int socketID);
|
||||||
|
Ref<ServerConfig> &GetServerConfig() { return m_ServerConfig; }
|
||||||
|
[[nodiscard]] int ID() const { return m_SocketID; }
|
||||||
|
[[nodiscard]] bool IsErrored() const { return m_IsErrored; }
|
||||||
|
void Init();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void Errored(const std::string &data);
|
||||||
|
|
||||||
|
int m_SocketID{};
|
||||||
|
Ref<ServerConfig> m_ServerConfig{};
|
||||||
|
struct sockaddr_in m_Address {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
int m_AddressLength{sizeof(m_Address)};
|
||||||
|
bool m_IsErrored{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
class RequestHandler;
|
||||||
|
class Response;
|
||||||
|
class Router;
|
||||||
|
class Route;
|
||||||
|
class Server {
|
||||||
|
public:
|
||||||
|
Server();
|
||||||
|
// Will Load SharedLibs from subdirectory
|
||||||
|
// This will allow that VWeb will run as Standalone and will load .so files
|
||||||
|
// without recompiling itself
|
||||||
|
void LoadSharedLibs();
|
||||||
|
void Start();
|
||||||
|
void Join() { m_WorkerThread->join(); }
|
||||||
|
void Stop() { m_IsExit = true; }
|
||||||
|
Ref<Router> &GetRouter() { return m_Router; }
|
||||||
|
Ref<ServerConfig> &GetServerConfig() { return m_ServerConfig; }
|
||||||
|
void AddRoute(const std::string &path, const Ref<Route> &route);
|
||||||
|
void RemoveRoute(const std::string &path);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void Execute();
|
||||||
|
void OutgoingExecute(epoll_event &event);
|
||||||
|
void IncomingExecute(epoll_event &event);
|
||||||
|
void HandleRequestReading(epoll_event &event);
|
||||||
|
void CreateRequest(int sockID);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Ref<Router> m_Router;
|
||||||
|
Ref<ServerConfig> m_ServerConfig;
|
||||||
|
Ref<RequestHandler> m_RequestHandler;
|
||||||
|
SafeMap<int, Accept> m_RawRequest{60000};
|
||||||
|
SafeMap<int, SendData> m_OutRequest{60000};
|
||||||
|
Ref<std::thread> m_WorkerThread;
|
||||||
|
Mutex m_Mutex;
|
||||||
|
bool m_IsExit{false};
|
||||||
|
|
||||||
|
protected:
|
||||||
|
friend RequestHandler;
|
||||||
|
};
|
||||||
|
#pragma region VWEB_ROUTING
|
||||||
|
using ms = std::chrono::duration<double, std::milli>;
|
||||||
|
struct SessionData {
|
||||||
|
virtual ~SessionData() = default;
|
||||||
|
};
|
||||||
|
struct Session {
|
||||||
|
std::string Id;
|
||||||
|
long TTLSeconds = 1440; // 24 minutes 1440 seconds
|
||||||
|
bool IsValid();
|
||||||
|
void Update();
|
||||||
|
void Remove(const std::string &key);
|
||||||
|
bool Has(const std::string &key);
|
||||||
|
Ref<SessionData> &operator[](const std::string &key) { return m_Data[key]; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::chrono::time_point<std::chrono::system_clock, ms> m_LastCall =
|
||||||
|
std::chrono::system_clock::now();
|
||||||
|
std::unordered_map<std::string, Ref<SessionData>> m_Data;
|
||||||
|
};
|
||||||
|
enum class HttpStatusCode : int {
|
||||||
|
Continue = 100,
|
||||||
|
SwitchingProtocols = 101,
|
||||||
|
Processing = 102,
|
||||||
|
EarlyHints = 103,
|
||||||
|
OK = 200,
|
||||||
|
Created = 201,
|
||||||
|
Accepted = 202,
|
||||||
|
NonAuthoritativeInformation = 203,
|
||||||
|
NoContent = 204,
|
||||||
|
ResetContent = 205,
|
||||||
|
PartialContent = 206,
|
||||||
|
MultiStatus = 207,
|
||||||
|
AlreadyReported = 208,
|
||||||
|
IMUsed = 226,
|
||||||
|
MultipleChoices = 300,
|
||||||
|
MovedPermanently = 301,
|
||||||
|
Found = 302,
|
||||||
|
SeeOther = 303,
|
||||||
|
NotModified = 304,
|
||||||
|
UseProxy = 305,
|
||||||
|
TemporaryRedirect = 307,
|
||||||
|
PermanentRedirect = 308,
|
||||||
|
BadRequest = 400,
|
||||||
|
Unauthorized = 401,
|
||||||
|
PaymentRequired = 402,
|
||||||
|
Forbidden = 403,
|
||||||
|
NotFound = 404,
|
||||||
|
MethodNotAllowed = 405,
|
||||||
|
NotAcceptable = 406,
|
||||||
|
ProxyAuthenticationRequired = 407,
|
||||||
|
RequestTimeout = 408,
|
||||||
|
Conflict = 409,
|
||||||
|
Gone = 410,
|
||||||
|
LengthRequired = 411,
|
||||||
|
PreconditionFailed = 412,
|
||||||
|
PayloadTooLarge = 413,
|
||||||
|
URITooLong = 414,
|
||||||
|
UnsupportedMediaType = 415,
|
||||||
|
RangeNotSatisfiable = 416,
|
||||||
|
ExpectationFailed = 417,
|
||||||
|
ImATeapot = 418,
|
||||||
|
UnprocessableEntity = 422,
|
||||||
|
Locked = 423,
|
||||||
|
FailedDependency = 424,
|
||||||
|
UpgradeRequired = 426,
|
||||||
|
PreconditionRequired = 428,
|
||||||
|
TooManyRequests = 429,
|
||||||
|
RequestHeaderFieldsTooLarge = 431,
|
||||||
|
UnavailableForLegalReasons = 451,
|
||||||
|
InternalServerError = 500,
|
||||||
|
NotImplemented = 501,
|
||||||
|
BadGateway = 502,
|
||||||
|
ServiceUnavailable = 503,
|
||||||
|
GatewayTimeout = 504,
|
||||||
|
HTTPVersionNotSupported = 505,
|
||||||
|
VariantAlsoNegotiates = 506,
|
||||||
|
InsufficientStorage = 507,
|
||||||
|
LoopDetected = 508,
|
||||||
|
NotExtended = 510,
|
||||||
|
NetworkAuthenticationRequired = 511
|
||||||
|
};
|
||||||
|
enum class HttpMethod {
|
||||||
|
GET = 0,
|
||||||
|
HEAD,
|
||||||
|
OPTIONS,
|
||||||
|
POST,
|
||||||
|
PUT,
|
||||||
|
PATCH,
|
||||||
|
DELETE,
|
||||||
|
FALLBACK
|
||||||
|
};
|
||||||
|
enum class CookieSameSite { Invalid = 0, Strict, Lax, None };
|
||||||
|
struct Cookie {
|
||||||
|
std::string Name;
|
||||||
|
std::string Value;
|
||||||
|
std::string Expires;
|
||||||
|
int MaxAge{};
|
||||||
|
std::string Domain;
|
||||||
|
std::string Path;
|
||||||
|
bool Secure{};
|
||||||
|
bool HttpOnly{};
|
||||||
|
CookieSameSite SameSite{CookieSameSite::Invalid};
|
||||||
|
bool IsOld = false;
|
||||||
|
};
|
||||||
|
struct Cookies {
|
||||||
|
public:
|
||||||
|
void Remove(const std::string &name);
|
||||||
|
bool Has(const std::string &name) const;
|
||||||
|
Cookie &Get(const std::string &name) { return Data[name]; };
|
||||||
|
std::string TransformToHeader();
|
||||||
|
void CreateCookieFromString(const std::string &data);
|
||||||
|
void CreateOld(const std::string &name, const std::string &value);
|
||||||
|
|
||||||
|
std::unordered_map<std::string, Cookie> Data;
|
||||||
|
Cookie &operator[](const std::string &name) { return Data[name]; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
static void CookieToString(std::stringstream &stream, Cookie &cookie);
|
||||||
|
};
|
||||||
|
struct ParameterValue {
|
||||||
|
void Add(const std::string &item) { m_Data.push_back(item); }
|
||||||
|
void Set(const std::string &item) {
|
||||||
|
m_Data.clear();
|
||||||
|
m_Data.push_back(item);
|
||||||
|
}
|
||||||
|
std::string &Get(int item) { return m_Data[item]; }
|
||||||
|
std::string &GetFirst() { return Get(0); }
|
||||||
|
size_t Size() { return m_Data.size(); }
|
||||||
|
Vector<std::string> &Values() { return m_Data; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Vector<std::string> m_Data{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Request {
|
||||||
|
public:
|
||||||
|
int ID;
|
||||||
|
std::string Body;
|
||||||
|
HttpMethod Method{HttpMethod::GET};
|
||||||
|
std::string URI;
|
||||||
|
Ref<Session> SessionData;
|
||||||
|
Ref<Cookies> CookieData;
|
||||||
|
Cookie &GetCookie(const std::string &key) { return CookieData->Get(key); };
|
||||||
|
ParameterValue &Parameter(const std::string &key) { return Parameters[key]; }
|
||||||
|
bool HasParameter(const std::string &key) const {
|
||||||
|
return Parameters.contains(key);
|
||||||
|
}
|
||||||
|
std::string &FirstOf(const std::string &key) {
|
||||||
|
return Parameters[key].GetFirst();
|
||||||
|
}
|
||||||
|
ParameterValue &Header(const std::string &key) { return Parameters[key]; }
|
||||||
|
std::unordered_map<std::string, ParameterValue> Parameters;
|
||||||
|
std::unordered_map<std::string, ParameterValue> Headers;
|
||||||
|
Vector<std::string> URLParameters;
|
||||||
|
};
|
||||||
|
class Response {
|
||||||
|
public:
|
||||||
|
size_t Length{0};
|
||||||
|
Ref<Cookies> CookieData{nullptr};
|
||||||
|
Ref<Session> SessionData{nullptr};
|
||||||
|
HttpStatusCode Status{HttpStatusCode::OK};
|
||||||
|
HttpMethod Method{HttpMethod::GET};
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::string GetResponse();
|
||||||
|
void SetHeader(const std::string &key, ParameterValue &value);
|
||||||
|
void SetHeader(const std::string &key, const std::string &value);
|
||||||
|
void AddHeaders(const std::string &key, const Vector<std::string> &values);
|
||||||
|
void AddHeader(const std::string &key, const std::string &value);
|
||||||
|
void SetType(const std::string &type);
|
||||||
|
void AddContent(const std::string &data);
|
||||||
|
void SetStatus(HttpStatusCode);
|
||||||
|
void SetMethod(HttpMethod);
|
||||||
|
void Reset();
|
||||||
|
Response &operator<<(const std::string &data) {
|
||||||
|
m_Content << data;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::string TransformHeaders(const std::string &content);
|
||||||
|
std::string m_Type{"text/html"};
|
||||||
|
std::stringstream m_Content;
|
||||||
|
std::unordered_map<std::string, ParameterValue> m_Headers;
|
||||||
|
};
|
||||||
|
class MiddleWareHandler;
|
||||||
|
class RequestHandler {
|
||||||
|
public:
|
||||||
|
explicit RequestHandler(const Ref<SocketManager> &manager);
|
||||||
|
void InitThreads(int count);
|
||||||
|
void AddRequest(Ref<Request> &request);
|
||||||
|
void Stop();
|
||||||
|
void SetRouter(Ref<Router> &router);
|
||||||
|
void AddSendResponse(SendData);
|
||||||
|
Ref<MiddleWareHandler> &Middleware() { return m_MiddleWare; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Ref<Router> m_Router{nullptr};
|
||||||
|
Ref<SocketManager> m_SocketManager{nullptr};
|
||||||
|
Server *m_Server{nullptr};
|
||||||
|
SafeQueue<Ref<Request>> m_Requests{};
|
||||||
|
ThreadPool m_Pool{"RequestHandler"};
|
||||||
|
Ref<MiddleWareHandler> m_MiddleWare{nullptr};
|
||||||
|
friend Server;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Route {
|
||||||
|
public:
|
||||||
|
Route() = default;
|
||||||
|
virtual ~Route() = default;
|
||||||
|
Route(std::initializer_list<HttpMethod>);
|
||||||
|
virtual bool Execute(const Request &request, Response &response);
|
||||||
|
virtual bool Get(const Request &request, Response &response);
|
||||||
|
virtual bool Post(const Request &request, Response &response);
|
||||||
|
virtual bool Put(const Request &request, Response &response);
|
||||||
|
virtual bool Patch(const Request &request, Response &response);
|
||||||
|
virtual bool Delete(const Request &request, Response &response);
|
||||||
|
bool Options(const Request &request, Response &response);
|
||||||
|
virtual bool Fallback(const Request &request, Response &response);
|
||||||
|
bool SupportsMethod(const Request &request);
|
||||||
|
virtual bool IsAllowed(const Request &request);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool m_AllowAll{true};
|
||||||
|
Vector<HttpMethod> m_AllowedMethods;
|
||||||
|
friend Router;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Router {
|
||||||
|
public:
|
||||||
|
Router();
|
||||||
|
void AddRoute(const std::string &name, const Ref<Route> &route);
|
||||||
|
Ref<Route> &GetRoute(const std::string &name);
|
||||||
|
void DeleteRoute(const std::string &name);
|
||||||
|
|
||||||
|
Ref<Response> HandleRoute(Ref<Request> &request);
|
||||||
|
Ref<Route> FindRoute(Ref<Request> &request);
|
||||||
|
static void AddToArgs(Ref<Request> &request, Vector<std::string> &items);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::unordered_map<std::string, Ref<Route>> m_Routes;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MiddleWare {
|
||||||
|
int Pos{0};
|
||||||
|
virtual void PreHandle(Request &){};
|
||||||
|
virtual void PostHandle(Request &, Response &){};
|
||||||
|
virtual void Shutdown(Request &, Response &){};
|
||||||
|
bool operator<(const MiddleWare *rhs) const { return Pos < rhs->Pos; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class MiddleWareHandler {
|
||||||
|
public:
|
||||||
|
void HandlePre(Ref<Request> &);
|
||||||
|
void HandlePost(Ref<Request> &, Ref<Response> &);
|
||||||
|
void Shutdown(Ref<Request> &, Ref<Response> &);
|
||||||
|
|
||||||
|
public:
|
||||||
|
template <class T> Ref<MiddleWare> Get() { return GetById(typeid(T).name()); }
|
||||||
|
template <class T> void Set(Ref<MiddleWare> &instance) {
|
||||||
|
auto &type = typeid(T);
|
||||||
|
if (type.before(typeid(MiddleWare)))
|
||||||
|
SetById(type.name(), instance);
|
||||||
|
}
|
||||||
|
template <class T> Ref<MiddleWare> Create() {
|
||||||
|
return SetById(typeid(T).name(), CreateRef<T>());
|
||||||
|
}
|
||||||
|
template <class T> void Remove() { RemoveById(typeid(T).name()); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Ref<MiddleWare> GetById(const char *id);
|
||||||
|
Ref<MiddleWare> SetById(const char *id, const Ref<MiddleWare> &);
|
||||||
|
void RemoveById(const char *id);
|
||||||
|
std::map<std::string, Ref<MiddleWare>> m_MiddleWares;
|
||||||
|
};
|
||||||
|
|
||||||
|
#pragma endregion VWEB_ROUTING
|
||||||
|
#pragma endregion VWEB
|
||||||
|
} // namespace VWeb
|
18
_clang-format
Normal file
18
_clang-format
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
BasedOnStyle: LLVM
|
||||||
|
AlignAfterOpenBracket: Align
|
||||||
|
AlignEscapedNewlines: Right
|
||||||
|
AlignOperands: true
|
||||||
|
AlignTrailingComments: true
|
||||||
|
AllowAllArgumentsOnNextLine: true
|
||||||
|
AllowAllConstructorInitializersOnNextLine: false
|
||||||
|
AllowAllParametersOfDeclarationOnNextLine: false
|
||||||
|
AllowShortBlocksOnASingleLine: Always
|
||||||
|
AllowShortCaseLabelsOnASingleLine: true
|
||||||
|
AllowShortIfStatementsOnASingleLine: Never
|
||||||
|
BinPackArguments: true
|
||||||
|
BinPackParameters: true
|
||||||
|
CompactNamespaces: false
|
||||||
|
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||||
|
FixNamespaceComments: true
|
||||||
|
IncludeBlocks: Regroup
|
BIN
dist/libVWeb.debug.a
vendored
Normal file
BIN
dist/libVWeb.debug.a
vendored
Normal file
Binary file not shown.
BIN
dist/libVWeb.release.a
vendored
Normal file
BIN
dist/libVWeb.release.a
vendored
Normal file
Binary file not shown.
15
example/CMakeLists.txt
Normal file
15
example/CMakeLists.txt
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
cmake_minimum_required(VERSION 3.17)
|
||||||
|
project(VWeb_Example)
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(THREADS_PREFER_PTHREAD_FLAG ON)
|
||||||
|
find_package(Threads REQUIRED)
|
||||||
|
add_executable(VWeb_Example main.cpp)
|
||||||
|
|
||||||
|
include_directories(${CMAKE_SOURCE_DIR}/..)
|
||||||
|
|
||||||
|
set(mode release)
|
||||||
|
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||||
|
set(mode debug)
|
||||||
|
endif ()
|
||||||
|
set(vweb_lib ${CMAKE_SOURCE_DIR}/../dist/libVWeb.${mode}.a)
|
||||||
|
target_link_libraries(VWeb_Example Threads::Threads ${vweb_lib})
|
20
example/main.cpp
Normal file
20
example/main.cpp
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#include <iostream>
|
||||||
|
#include <VWeb.h>
|
||||||
|
|
||||||
|
class BigDataRoute : public VWeb::Route {
|
||||||
|
public:
|
||||||
|
bool Execute(const VWeb::Request &request, VWeb::Response &response) override {
|
||||||
|
response << "<pre>\n";
|
||||||
|
for (int i = 0; i < 100; ++i)
|
||||||
|
response << "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.\n";
|
||||||
|
response << "</pre>";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
int main() {
|
||||||
|
VWeb::Server server;
|
||||||
|
server.AddRoute("/test", VWeb::CreateRef<BigDataRoute>());
|
||||||
|
server.Start();
|
||||||
|
server.Join();
|
||||||
|
return 0;
|
||||||
|
}
|
Loading…
Reference in a new issue