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})
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue