diff --git a/CMakeLists.txt b/CMakeLists.txt index 483987b..0fb7424 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,28 @@ -cmake_minimum_required(VERSION 3.0) +cmake_minimum_required(VERSION 3.5) +project(pica + VERSION 0.0.1 + LANGUAGES CXX +) -project(pica) +cmake_policy(SET CMP0076 NEW) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(FCGI fcgi) add_executable(pica main.cpp) +target_include_directories(pica PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + +add_subdirectory(server) +add_subdirectory(request) +add_subdirectory(response) +add_subdirectory(stream) + +target_link_libraries(pica + fcgi + fcgi++ +) install(TARGETS pica RUNTIME DESTINATION bin) diff --git a/main.cpp b/main.cpp index 8bb47f1..977ea62 100644 --- a/main.cpp +++ b/main.cpp @@ -1,6 +1,27 @@ +#include + +#include +#include + #include -int main(int argc, char **argv) { - std::cout << "Hello, world!" << std::endl; - return 0; +#include "server/server.h" + +int main() { + const char* socketPath = "/run/pica/pica.sock"; + int sockfd = FCGX_OpenSocket(socketPath, 1024); + if (sockfd < 0) { + std::cerr << "Error opening socket" << std::endl; + return 1; + } + + if (chmod(socketPath, 0770) != 0) { + std::cerr << "Couldn't set socket permissions" << std::endl; + return 2; + } + + FCGX_Init(); + + Server server; + server.run(sockfd); } diff --git a/pica.kdev4 b/pica.kdev4 deleted file mode 100644 index 18ec499..0000000 --- a/pica.kdev4 +++ /dev/null @@ -1,3 +0,0 @@ -[Project] -Name=pica -Manager=KDevCMakeManager diff --git a/request/CMakeLists.txt b/request/CMakeLists.txt new file mode 100644 index 0000000..eee9cda --- /dev/null +++ b/request/CMakeLists.txt @@ -0,0 +1,9 @@ +set(HEADERS + request.h +) + +set(SOURCES + request.cpp +) + +target_sources(pica PRIVATE ${SOURCES}) diff --git a/request/request.cpp b/request/request.cpp new file mode 100644 index 0000000..18505f9 --- /dev/null +++ b/request/request.cpp @@ -0,0 +1,93 @@ +#include "request.h" + +constexpr static const char* GET("GET"); + +constexpr static const char* REQUEST_METHOD("REQUEST_METHOD"); +constexpr static const char* SCRIPT_FILENAME("SCRIPT_FILENAME"); +constexpr static const char* SERVER_NAME("SERVER_NAME"); + +Request::Request (): + state(State::initial), + raw() +{} + +Request::~Request() { + terminate(); +} + +void Request::terminate() { + switch (state) { + case State::accepted: + case State::responded: + FCGX_Finish_r(&raw); + break; + default: + break; + } +} + +bool Request::isGet() const { + if (state != State::accepted) + throw std::runtime_error("An attempt to read request type on a wrong request state"); + + std::string_view method(FCGX_GetParam(REQUEST_METHOD, raw.envp)); + return method == GET; +} + +bool Request::wait(int socketDescriptor) { + if (state != State::initial) + throw std::runtime_error("An attempt to wait for new incomming request on a wrong request state"); + + FCGX_Request* request = &raw; + FCGX_InitRequest(request, socketDescriptor, 0); + int rc = FCGX_Accept_r(request); + + bool result = rc == 0; + if (result) + state = State::accepted; + + return result; +} + +OStream Request::getOutputStream() { + if (state != State::accepted) + throw std::runtime_error("An attempt to request output stream on a wrong request state"); + + return OStream(raw.out); +} + +OStream Request::getErrorStream() { + if (state != State::accepted) + throw std::runtime_error("An attempt to request error stream on a wrong request state"); + + return OStream(raw.err); +} + +std::string_view Request::getPath(const std::string& serverName) const { + if (state != State::accepted) + throw std::runtime_error("An attempt to request path on a wrong request state"); + + std::string_view path; + std::string_view scriptFileName(FCGX_GetParam(SCRIPT_FILENAME, raw.envp)); + std::string::size_type snLocation = scriptFileName.find(serverName); + if (snLocation != std::string::npos) { + if (snLocation + serverName.size() < scriptFileName.size()) + path = scriptFileName.substr(snLocation + serverName.size() + 1); + } + + return path; +} + +std::string Request::getServerName() const { + if (state != State::accepted) + throw std::runtime_error("An attempt to request server name on a wrong request state"); + + return FCGX_GetParam(SERVER_NAME, raw.envp);; +} + +void Request::printEnvironment(std::ostream& out) { + char **envp = raw.envp; + for (int i = 0; envp[i] != nullptr; ++i) { + out << envp[i] << "
"; + } +} diff --git a/request/request.h b/request/request.h new file mode 100644 index 0000000..bff6d4e --- /dev/null +++ b/request/request.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +#include + +#include "stream/ostream.h" + +class Request { +public: + enum class State { + initial, + accepted, + responded + }; + + Request(); + ~Request(); + Request(const Request& other) = delete; + Request(Request&& other) = delete; + Request& operator = (const Request& other) = delete; + Request& operator = (Request&& other) = delete; + + bool wait(int socketDescriptor); + void terminate(); + bool isGet() const; + + OStream getOutputStream(); + OStream getErrorStream(); + + std::string_view getPath(const std::string& serverName) const; + std::string getServerName() const; + void printEnvironment(std::ostream& out); + +private: + State state; + FCGX_Request raw; +}; diff --git a/response/CMakeLists.txt b/response/CMakeLists.txt new file mode 100644 index 0000000..6b35c08 --- /dev/null +++ b/response/CMakeLists.txt @@ -0,0 +1,9 @@ +set(HEADERS + response.h +) + +set(SOURCES + response.cpp +) + +target_sources(pica PRIVATE ${SOURCES}) diff --git a/response/response.cpp b/response/response.cpp new file mode 100644 index 0000000..262cbbf --- /dev/null +++ b/response/response.cpp @@ -0,0 +1,42 @@ +#include "response.h" + +constexpr std::array(Response::Status::__size)> statusCodes = { + "Status: 200 OK", + "Status: 404 Not Found", + "Status: 405 Method Not Allowed", + "Status: 500 Internal Error" +}; + +constexpr std::array(Response::ContentType::__size)> contentTypes = { + "Content-type: text/html" +}; + +Response::Response(): + status(Status::ok), + type(ContentType::text), + body() +{} + +Response::Response(Status status): + status(status), + type(ContentType::text), + body() +{} + +void Response::replyTo(Request& request) const { + // OStream out = status == Status::ok ? + // request.getOutputStream() : + // request.getErrorStream(); + OStream out = request.getOutputStream(); + + out << statusCodes[static_cast(status)]; + if (!body.empty()) + out << '\n' + << contentTypes[static_cast(type)] + << '\n' << '\n' + << body; +} + +void Response::setBody(const std::string& body) { + Response::body = body; +} diff --git a/response/response.h b/response/response.h new file mode 100644 index 0000000..60e3ef8 --- /dev/null +++ b/response/response.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +#include "request/request.h" +#include "stream/ostream.h" + +class Response { +public: + enum class Status { + ok, + notFound, + methodNotAllowed, + internalError, + __size + }; + + enum class ContentType { + text, + __size + }; + Response(); + Response(Status status); + + void replyTo(Request& request) const; + void setBody(const std::string& body); + +private: + Status status; + ContentType type; + std::string body; +}; diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt new file mode 100644 index 0000000..393a4f2 --- /dev/null +++ b/server/CMakeLists.txt @@ -0,0 +1,9 @@ +set(HEADERS + server.h +) + +set(SOURCES + server.cpp +) + +target_sources(pica PRIVATE ${SOURCES}) diff --git a/server/server.cpp b/server/server.cpp new file mode 100644 index 0000000..aa56860 --- /dev/null +++ b/server/server.cpp @@ -0,0 +1,78 @@ +#include "server.h" + +constexpr static const char* REQUEST_URI("REQUEST_URI"); + +constexpr static const char* DOCUMENT_URI("DOCUMENT_URI"); +constexpr static const char* DOCUMENT_ROOT("DOCUMENT_ROOT"); + +constexpr static const char* SCRIPT_NAME("SCRIPT_NAME"); +constexpr static const char* SCRIPT_FILENAME("SCRIPT_FILENAME"); + +constexpr std::string_view info("info"); +constexpr std::string_view env("env"); + +Server::Server(): + terminating(false), + requestCount(0), + serverName(std::nullopt) +{} + +Server::~Server() {} + +void Server::run(int socketDescriptor) { + while (!terminating) { + std::unique_ptr request = std::make_unique(); + bool result = request->wait(socketDescriptor); + if (!result) { + std::cerr << "Error accepting a request" << std::endl; + return; + } + handleRequest(std::move(request)); + } +} + +void Server::handleRequest(std::unique_ptr request) { + ++requestCount; + if (!serverName) { + try { + serverName = request->getServerName(); + std::cout << "received server name " << serverName.value() << std::endl; + } catch (...) { + std::cerr << "failed to read server name" << std::endl; + Response error(Response::Status::internalError); + error.replyTo(*request.get()); + return; + } + } + + if (!request->isGet()) { + static const Response methodNotAllowed(Response::Status::methodNotAllowed); + methodNotAllowed.replyTo(*request.get()); + return; + } + + try { + std::string_view sPath = request->getPath(serverName.value()); + + if (sPath == info) { + Response res; + res.setBody("Pica
\nVersion: 1.0.0
\nRequestsHandled: " + std::to_string(requestCount)); + res.replyTo(*request.get()); + } else if (sPath == env) { + std::ostringstream ss; + request->printEnvironment(ss); + Response res; + res.setBody(ss.str()); + res.replyTo(*request.get()); + } else { + Response notFound(Response::Status::notFound); + notFound.setBody(std::string("Path ") + sPath.data() + " was not found"); + notFound.replyTo(*request.get()); + std::cerr << "Not found: " << sPath.data() << std::endl; + } + } catch (const std::exception e) { + Response error(Response::Status::internalError); + error.setBody(e.what()); + error.replyTo(*request.get()); + } +} diff --git a/server/server.h b/server/server.h new file mode 100644 index 0000000..2ce2377 --- /dev/null +++ b/server/server.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "request/request.h" +#include "response/response.h" + +class Server { +public: + Server(); + ~Server(); + + void run(int socketDescriptor); + +private: + void handleRequest(std::unique_ptr request); + void router(const std::vector& path); + void printEnv(std::ostream& out, FCGX_Request& request); + std::string_view getPath(const FCGX_Request& request); + +private: + bool terminating; + uint64_t requestCount; + std::optional serverName; +}; diff --git a/stream/CMakeLists.txt b/stream/CMakeLists.txt new file mode 100644 index 0000000..6eef2a8 --- /dev/null +++ b/stream/CMakeLists.txt @@ -0,0 +1,11 @@ +set(HEADERS + stream.h + ostream.h +) + +set(SOURCES + stream.cpp + ostream.cpp +) + +target_sources(pica PRIVATE ${SOURCES}) diff --git a/stream/ostream.cpp b/stream/ostream.cpp new file mode 100644 index 0000000..50fd0ff --- /dev/null +++ b/stream/ostream.cpp @@ -0,0 +1,9 @@ +#include "ostream.h" + +OStream::OStream(FCGX_Stream* _raw): + Stream(_raw), + std(&raw) +{ +} + +OStream::~OStream() {} diff --git a/stream/ostream.h b/stream/ostream.h new file mode 100644 index 0000000..04c3a45 --- /dev/null +++ b/stream/ostream.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include "stream.h" + +class Request; + +class OStream : Stream { + friend class Request; +private: + OStream(FCGX_Stream* raw); + +public: + ~OStream(); + + template + OStream& operator << (const T& value) { + std << value; + return *this; + }; +private: + std::ostream std; +}; diff --git a/stream/stream.cpp b/stream/stream.cpp new file mode 100644 index 0000000..88c40ef --- /dev/null +++ b/stream/stream.cpp @@ -0,0 +1,8 @@ +#include "stream.h" + +Stream::Stream(FCGX_Stream* raw): + raw(raw) +{} + +Stream::~Stream() { +} diff --git a/stream/stream.h b/stream/stream.h new file mode 100644 index 0000000..435a7a0 --- /dev/null +++ b/stream/stream.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +class Stream { +protected: + Stream(FCGX_Stream* raw); + Stream(const Stream& other) = delete; + Stream(Stream&& other) = delete; + Stream& operator = (const Stream& other) = delete; + Stream& operator = (Stream&& other) = delete; + virtual ~Stream(); + +public: + +protected: + fcgi_streambuf raw; +};