commit 792abbee93974162e993a2471e85abed4eeb2c5e Author: Maurice Grönwoldt Date: Tue Aug 23 14:13:21 2022 +0200 First Running Version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4cfb38d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build/ +debug-build/ +release-build/ +cmake-build-debug/ +.idea/ + +example/build \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..354df8e --- /dev/null +++ b/CMakeLists.txt @@ -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}) \ No newline at end of file diff --git a/Source/Cookie.cpp b/Source/Cookie.cpp new file mode 100644 index 0000000..a849d93 --- /dev/null +++ b/Source/Cookie.cpp @@ -0,0 +1,107 @@ +#include "StringUtils.h" + +#include + +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 \ No newline at end of file diff --git a/Source/EPollManager.cpp b/Source/EPollManager.cpp new file mode 100644 index 0000000..a8b6f19 --- /dev/null +++ b/Source/EPollManager.cpp @@ -0,0 +1,35 @@ +#include +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 \ No newline at end of file diff --git a/Source/MiddleWare.cpp b/Source/MiddleWare.cpp new file mode 100644 index 0000000..c25bae2 --- /dev/null +++ b/Source/MiddleWare.cpp @@ -0,0 +1,33 @@ +#include + +namespace VWeb { +void MiddleWareHandler::HandlePre(Ref &request) { + for (auto &[key, middleWare] : m_MiddleWares) { + middleWare->PreHandle(*request); + } +} +void MiddleWareHandler::HandlePost(Ref &request, + Ref &response) { + for (auto &[key, middleWare] : m_MiddleWares) { + middleWare->PostHandle(*request, *response); + } +} +void MiddleWareHandler::Shutdown(Ref &request, + Ref &response) { + for (auto &[key, middleWare] : m_MiddleWares) { + middleWare->Shutdown(*request, *response); + } +} +Ref MiddleWareHandler::GetById(const char *id) { + return m_MiddleWares[id]; +} +Ref MiddleWareHandler::SetById(const char *id, + const Ref &middleWare) { + m_MiddleWares[id] = middleWare; + return middleWare; +} +void MiddleWareHandler::RemoveById(const char *id) { + if (m_MiddleWares.contains(id)) + m_MiddleWares.erase(id); +} +} // namespace VWeb \ No newline at end of file diff --git a/Source/RequestHandler.cpp b/Source/RequestHandler.cpp new file mode 100644 index 0000000..d555e6e --- /dev/null +++ b/Source/RequestHandler.cpp @@ -0,0 +1,99 @@ +#include "StringUtils.h" + +#include + +namespace VWeb { + +HttpMethod StringToHTTPMethod(std::string &method) { + static std::unordered_map 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) { + 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 MRequest{}; + Ref MRouter{}; + Ref 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(); + MRequest->SessionData = CreateRef(); + 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 &manager) + : m_SocketManager(manager), m_MiddleWare(CreateRef()) { + m_MiddleWare = CreateRef(); +} +void RequestHandler::InitThreads(int count) { + m_Pool.SetThreadCount(count); + m_Pool.Create(); +} +void RequestHandler::AddRequest(Ref &request) { + if (m_Router) { + auto job = CreateRef(); + 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) { + 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 \ No newline at end of file diff --git a/Source/Response.cpp b/Source/Response.cpp new file mode 100644 index 0000000..7250b9b --- /dev/null +++ b/Source/Response.cpp @@ -0,0 +1,112 @@ +#include + +namespace VWeb { +// clang-format off +static std::unordered_map 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 &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 \ No newline at end of file diff --git a/Source/Route.cpp b/Source/Route.cpp new file mode 100644 index 0000000..11f5458 --- /dev/null +++ b/Source/Route.cpp @@ -0,0 +1,83 @@ +#include +#include + + +namespace VWeb { + +#define stringify(name) {name, std::string(#name).replace(0,12,"")} +static std::unordered_map 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 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 \ No newline at end of file diff --git a/Source/Router.cpp b/Source/Router.cpp new file mode 100644 index 0000000..4d38247 --- /dev/null +++ b/Source/Router.cpp @@ -0,0 +1,97 @@ +#include "StringUtils.h" + +#include +#include + +namespace VWeb { + +template constexpr auto to_underlying(E e) noexcept { + return static_cast>(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(); } + +void Router::AddRoute(const std::string &name, const Ref &route) { + m_Routes[name] = route; +} + +Ref &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 Router::HandleRoute(Ref &request) { + auto response = CreateRef(); + 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 Router::FindRoute(Ref &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, Vector &items) { + request->URLParameters.push_back(items[items.size() - 1]); + items.pop_back(); +} +} // namespace VWeb \ No newline at end of file diff --git a/Source/Server.cpp b/Source/Server.cpp new file mode 100644 index 0000000..5bb8719 --- /dev/null +++ b/Source/Server.cpp @@ -0,0 +1,135 @@ +#include +#include + +namespace VWeb { +Server::Server() { + m_Router = CreateRef(); + m_ServerConfig = CreateRef(); + m_ServerConfig->EPoll = CreateRef(); + m_ServerConfig->Socket = CreateRef(m_ServerConfig); + m_RequestHandler = CreateRef(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(&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) { + 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->ID = meAccept.SockId; + request->Body = meAccept.Data.str(); + m_RawRequest.Remove(sockID); + m_RequestHandler->AddRequest(request); +} +} // namespace VWeb \ No newline at end of file diff --git a/Source/Session.cpp b/Source/Session.cpp new file mode 100644 index 0000000..1e4b983 --- /dev/null +++ b/Source/Session.cpp @@ -0,0 +1,20 @@ +#include + +namespace VWeb { +bool Session::IsValid() { + auto call = std::chrono::system_clock::now(); + auto time = + std::chrono::duration_cast(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 \ No newline at end of file diff --git a/Source/SocketManager.cpp b/Source/SocketManager.cpp new file mode 100644 index 0000000..cd70a4a --- /dev/null +++ b/Source/SocketManager.cpp @@ -0,0 +1,102 @@ +#include + +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 &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 \ No newline at end of file diff --git a/Source/StringUtils.cpp b/Source/StringUtils.cpp new file mode 100644 index 0000000..215ba85 --- /dev/null +++ b/Source/StringUtils.cpp @@ -0,0 +1,86 @@ +#include "StringUtils.h" + +#include +#include + +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 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 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 &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(ii); + ret += ch; + i = i + 2; + } else + ret += val[i]; + } + return (ret); +} +} // namespace VWeb \ No newline at end of file diff --git a/Source/StringUtils.h b/Source/StringUtils.h new file mode 100644 index 0000000..5319915 --- /dev/null +++ b/Source/StringUtils.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace VWeb { +class String { +public: + static std::vector Split(const std::string& s, + const std::string& delimiter); + static std::string Join(const std::vector& 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); +}; +} \ No newline at end of file diff --git a/Source/ThreadPool.cpp b/Source/ThreadPool.cpp new file mode 100644 index 0000000..7748058 --- /dev/null +++ b/Source/ThreadPool.cpp @@ -0,0 +1,43 @@ +#include +#include + +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 &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 \ No newline at end of file diff --git a/VWeb.h b/VWeb.h new file mode 100644 index 0000000..2ec23ef --- /dev/null +++ b/VWeb.h @@ -0,0 +1,573 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace VWeb { + +template using Ref = std::shared_ptr; +template using Vector = std::vector; +template +constexpr Ref CreateRef(Args &&...args) { + return std::make_shared(std::forward(args)...); +} +typedef std::mutex Mutex; + +#pragma region SAFESTRUCTS +template struct SafeMap { + explicit SafeMap(size_t maxSize = -1UL) : m_MaxSize(maxSize), m_End(false){}; + bool Add(const T &t, S &x) { + std::unique_lock 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 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 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 lck(m_Mutex); + std::unordered_map empty; + std::swap(m_Data, empty); + m_CVFull.notify_all(); + }; + bool Has(T t) { + std::unique_lock lck(m_Mutex); + return m_Data.contains(t); + }; + S &Get(T t) { + std::unique_lock lck(m_Mutex); + return m_Data[t]; + }; + int Size() { return m_Data.size(); }; + +private: + std::unordered_map m_Data{}; + std::mutex m_Mutex{}; + std::condition_variable m_CVFull{}; + const size_t m_MaxSize{}; + std::atomic m_End{}; +}; + +template struct SafeQueue { + explicit SafeQueue(size_t maxSize = -1UL) + : m_MaxSize(maxSize), + m_End(false){}; + void Push(const T &t) { + std::unique_lock 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 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 lck(m_Mutex); + m_CVEmpty.notify_all(); + m_CVFull.notify_all(); + }; + void Close() { + m_End = true; + std::lock_guard lck(m_Mutex); + m_CVEmpty.notify_all(); + m_CVFull.notify_all(); + }; + void Clear() { + std::unique_lock lck(m_Mutex); + std::queue empty; + std::swap(m_Queue, empty); + m_CVEmpty.notify_all(); + m_CVFull.notify_all(); + }; + std::optional Pop() { + std::unique_lock 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 WaitAndPop() { + std::unique_lock 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 &GetQueue() { return m_Queue; } + void Flush() { + while (!m_Queue.empty()) + m_CVEmpty.notify_one(); + m_End = true; + m_CVEmpty.notify_all(); + } + +private: + std::queue m_Queue; + std::mutex m_Mutex; + std::condition_variable m_CVEmpty, m_CVFull; + const size_t m_MaxSize; + std::atomic 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 &); + void Create(); + void Stop(); + void Execute(); + +protected: + std::string m_Name; + int m_ThreadCount{1}; + bool m_IsCreated{false}; + Vector m_Threads{}; + SafeQueue> 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 EPoll{nullptr}; + Ref 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 &); + ~SocketManager(); + Accept Handle(); + bool SetSendListen(int socketID); + bool SetReadListen(int socketID); + Ref &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 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 &GetRouter() { return m_Router; } + Ref &GetServerConfig() { return m_ServerConfig; } + void AddRoute(const std::string &path, const Ref &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 m_Router; + Ref m_ServerConfig; + Ref m_RequestHandler; + SafeMap m_RawRequest{60000}; + SafeMap m_OutRequest{60000}; + Ref m_WorkerThread; + Mutex m_Mutex; + bool m_IsExit{false}; + +protected: + friend RequestHandler; +}; +#pragma region VWEB_ROUTING +using ms = std::chrono::duration; +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 &operator[](const std::string &key) { return m_Data[key]; } + +protected: + std::chrono::time_point m_LastCall = + std::chrono::system_clock::now(); + std::unordered_map> 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 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 &Values() { return m_Data; } + +protected: + Vector m_Data{}; +}; + +struct Request { +public: + int ID; + std::string Body; + HttpMethod Method{HttpMethod::GET}; + std::string URI; + Ref SessionData; + Ref 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 Parameters; + std::unordered_map Headers; + Vector URLParameters; +}; +class Response { +public: + size_t Length{0}; + Ref CookieData{nullptr}; + Ref 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 &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 m_Headers; +}; +class MiddleWareHandler; +class RequestHandler { +public: + explicit RequestHandler(const Ref &manager); + void InitThreads(int count); + void AddRequest(Ref &request); + void Stop(); + void SetRouter(Ref &router); + void AddSendResponse(SendData); + Ref &Middleware() { return m_MiddleWare; } + +protected: + Ref m_Router{nullptr}; + Ref m_SocketManager{nullptr}; + Server *m_Server{nullptr}; + SafeQueue> m_Requests{}; + ThreadPool m_Pool{"RequestHandler"}; + Ref m_MiddleWare{nullptr}; + friend Server; +}; + +class Route { +public: + Route() = default; + virtual ~Route() = default; + Route(std::initializer_list); + 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 m_AllowedMethods; + friend Router; +}; + +class Router { +public: + Router(); + void AddRoute(const std::string &name, const Ref &route); + Ref &GetRoute(const std::string &name); + void DeleteRoute(const std::string &name); + + Ref HandleRoute(Ref &request); + Ref FindRoute(Ref &request); + static void AddToArgs(Ref &request, Vector &items); + +protected: + std::unordered_map> 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 &); + void HandlePost(Ref &, Ref &); + void Shutdown(Ref &, Ref &); + +public: + template Ref Get() { return GetById(typeid(T).name()); } + template void Set(Ref &instance) { + auto &type = typeid(T); + if (type.before(typeid(MiddleWare))) + SetById(type.name(), instance); + } + template Ref Create() { + return SetById(typeid(T).name(), CreateRef()); + } + template void Remove() { RemoveById(typeid(T).name()); } + +protected: + Ref GetById(const char *id); + Ref SetById(const char *id, const Ref &); + void RemoveById(const char *id); + std::map> m_MiddleWares; +}; + +#pragma endregion VWEB_ROUTING +#pragma endregion VWEB +} // namespace VWeb \ No newline at end of file diff --git a/_clang-format b/_clang-format new file mode 100644 index 0000000..ed90334 --- /dev/null +++ b/_clang-format @@ -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 \ No newline at end of file diff --git a/dist/libVWeb.debug.a b/dist/libVWeb.debug.a new file mode 100644 index 0000000..999123b Binary files /dev/null and b/dist/libVWeb.debug.a differ diff --git a/dist/libVWeb.release.a b/dist/libVWeb.release.a new file mode 100644 index 0000000..79fbe2c Binary files /dev/null and b/dist/libVWeb.release.a differ diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt new file mode 100644 index 0000000..b14eeff --- /dev/null +++ b/example/CMakeLists.txt @@ -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}) diff --git a/example/main.cpp b/example/main.cpp new file mode 100644 index 0000000..76c259c --- /dev/null +++ b/example/main.cpp @@ -0,0 +1,20 @@ +#include +#include + +class BigDataRoute : public VWeb::Route { +public: + bool Execute(const VWeb::Request &request, VWeb::Response &response) override { + response << "
\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 << "
"; + return true; + } +}; +int main() { + VWeb::Server server; + server.AddRoute("/test", VWeb::CreateRef()); + server.Start(); + server.Join(); + return 0; +}