From eda5d9df6b4a4a2119c4b8e61313b8132ddcb9e8 Mon Sep 17 00:00:00 2001 From: Tijmen van Nesselrooij Date: Sat, 15 Jun 2019 12:00:21 +0200 Subject: [PATCH] Initial commit --- .gitignore | 7 + bin/server.cfg | 35 ++++ makefile | 37 ++++ src/constants/httprequest.hpp | 111 +++++++++++ src/constants/httpresponse.hpp | 308 ++++++++++++++++++++++++++++++ src/http/mime.cpp | 76 ++++++++ src/http/mime.hpp | 23 +++ src/http/request.cpp | 38 ++++ src/http/request.hpp | 13 ++ src/http/response.cpp | 37 ++++ src/http/response.hpp | 16 ++ src/logger.cpp | 32 ++++ src/logger.hpp | 20 ++ src/main.cpp | 21 ++ src/middleware/base.hpp | 17 ++ src/middleware/notfound.cpp | 27 +++ src/middleware/notfound.hpp | 11 ++ src/middleware/staticcontent.cpp | 72 +++++++ src/middleware/staticcontent.hpp | 19 ++ src/server/configuration.cpp | 51 +++++ src/server/configuration.hpp | 29 +++ src/server/connection.cpp | 68 +++++++ src/server/connection.hpp | 23 +++ src/server/connectionoperator.cpp | 45 +++++ src/server/connectionoperator.hpp | 17 ++ src/server/listeningsocket.cpp | 54 ++++++ src/server/listeningsocket.hpp | 20 ++ src/server/server.cpp | 31 +++ src/server/server.hpp | 18 ++ src/server/url.cpp | 33 ++++ src/server/url.hpp | 19 ++ 31 files changed, 1328 insertions(+) create mode 100644 .gitignore create mode 100755 bin/server.cfg create mode 100644 makefile create mode 100644 src/constants/httprequest.hpp create mode 100644 src/constants/httpresponse.hpp create mode 100644 src/http/mime.cpp create mode 100644 src/http/mime.hpp create mode 100644 src/http/request.cpp create mode 100644 src/http/request.hpp create mode 100644 src/http/response.cpp create mode 100644 src/http/response.hpp create mode 100644 src/logger.cpp create mode 100644 src/logger.hpp create mode 100755 src/main.cpp create mode 100644 src/middleware/base.hpp create mode 100644 src/middleware/notfound.cpp create mode 100644 src/middleware/notfound.hpp create mode 100644 src/middleware/staticcontent.cpp create mode 100644 src/middleware/staticcontent.hpp create mode 100644 src/server/configuration.cpp create mode 100644 src/server/configuration.hpp create mode 100644 src/server/connection.cpp create mode 100644 src/server/connection.hpp create mode 100644 src/server/connectionoperator.cpp create mode 100644 src/server/connectionoperator.hpp create mode 100644 src/server/listeningsocket.cpp create mode 100644 src/server/listeningsocket.hpp create mode 100755 src/server/server.cpp create mode 100755 src/server/server.hpp create mode 100644 src/server/url.cpp create mode 100644 src/server/url.hpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..521d914 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.vscode +bin/www/ +build/ + +bin/*.out +src/*.o +src/*.d \ No newline at end of file diff --git a/bin/server.cfg b/bin/server.cfg new file mode 100755 index 0000000..2881161 --- /dev/null +++ b/bin/server.cfg @@ -0,0 +1,35 @@ +# Configuration file for HTTPSoup +# Format: +# key: value (space is optional) + +server-port: 80 +www-root: /home/tijmen/project/http-server/bin/www/ + +# List of supported content per MIME type +# Supported MIME types are: text, image, audio, video, application and multipart +# Unknown types are usually shoved under application +type: text + css + html + htm + js + php + +type: image + bmp + gif + jpg + png + +type: audio + flac + mp3 + ogg + +type: video + avi + mp4 + +type: application + +type: multipart diff --git a/makefile b/makefile new file mode 100644 index 0000000..10424fc --- /dev/null +++ b/makefile @@ -0,0 +1,37 @@ +CC = g++ +CFLAGS = -std=c++17 -Wall -g +LFLAGS = -lstdc++fs + +CPPS = $(shell find ./src/ -name *.cpp) +OBJS = $(patsubst ./src/%.cpp, ./build/%.o, ${CPPS}) +DEPS = $(patsubst %.o, %.d, $(OBJS)) + +BUILDDIRS = $(patsubst ./src/%, ./build/%, $(shell find ./src/ -type d)) + +BINARY_NAME = server.out +BINARY_OUT = ./build/${BINARY_NAME} + +-include $(DEPS) + +./build/%.o: ./src/%.cpp + ${CC} ${CFLAGS} -MMD -c $< -o $@ + +${BINARY_OUT}: directories ${OBJS} + ${CC} ${CFLAGS} ${OBJS} ${LFLAGS} -o $@ + +.PHONY: all clean check syntax directories + +all: ${BINARY_OUT} + +clean: + -rm -r ./build/ + +check: ${BINARY_OUT} + cp -uv $^ ./bin/ + ./bin/${BINARY_NAME} + +syntax: ${CPPS} + ${CC} ${CFLAGS} -fsyntax-only $^ + +directories: + mkdir -p ${BUILDDIRS} \ No newline at end of file diff --git a/src/constants/httprequest.hpp b/src/constants/httprequest.hpp new file mode 100644 index 0000000..5767b56 --- /dev/null +++ b/src/constants/httprequest.hpp @@ -0,0 +1,111 @@ +#pragma once +#include +#include + +namespace HttpRequest +{ + enum class Type + { + UNKNOWN = -1, + GET, + HEAD, + POST, + PUT, + DELETE, + TRACE, + OPTIONS, + CONNECT, + PATCH + }; + + const std::vector typeStrings = { + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "TRACE", + "OPTIONS", + "CONNECT", + "PATCH", + }; + + enum class Header + { + UNKNOWN = -1, + // https://en.wikipedia.org/wiki/List_of_HTTP_header_fields + ACCEPT, + ACCEPT_CHARSET, + ACCEPT_ENCODING, + ACCEPT_LANGUAGE, + ACCEPT_DATETIME, + ACCESS_CONTROL_REQUEST_METHOD, + ACCESS_CONTROL_REQUEST_HEADERS, + AUTHORIZATION, + CACHE_CONTROL, + CONNECTION, + COOKIE, + CONTENT_LENGTH, + CONTENT_MD5, // obsolete + CONTENT_TYPE, + DATE, + EXPECT, + FORWARDED, + FROM, + HOST, + IF_MATCH, + IF_MODIFIED_SINCE, + IF_NONE_MATCH, + IF_RANGE, + IF_UNMODIFIED_SINCE, + MAX_FORWARDS, + ORIGIN, + PRAGMA, + PROXY_AUTHORIZATION, + RANGE, + REFERER, + TE, // transfer encoding + USER_AGENT, + UPGRADE, + VIA, + WARNING + }; + + const std::vector headerStrings = { + "Accept", + "Accept-Charset", + "Accept-Encoding", + "Accept-Language", + "Accept-Datetime", + "Access-Control-Request-Method", + "Access-Control-Request-Headers", + "Authorization", + "Cache-Control", + "Connection", + "Cookie", + "Content-Length", + "Content-MD5", + "Content-Type", + "Date", + "Expect", + "Forwarded", + "From", + "Host", + "If-Match", + "If-Modified-Since", + "If-None-Match", + "If-Range", + "If-Unmodified-Since", + "Max-Forwards", + "Origin", + "Pragma", + "Proxy-Authorization", + "Range", + "Referer", + "TE", + "User-Agent", + "Upgrade", + "Via", + "Warning", + }; +} \ No newline at end of file diff --git a/src/constants/httpresponse.hpp b/src/constants/httpresponse.hpp new file mode 100644 index 0000000..7d5d587 --- /dev/null +++ b/src/constants/httpresponse.hpp @@ -0,0 +1,308 @@ +#pragma once +#include +#include + +namespace HttpResponse +{ + enum class CodeRange + { + // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes + INFORMATIONAL = 100, + SUCCESS = 200, + REDIRECTION = 300, + CLIENT_ERROR = 400, + SERVER_ERROR = 500 + }; + + const std::vector codeValues + { + 100, + 101, + 102, + 200, + 201, + 202, + 203, + 204, + 205, + 206, + 207, + 208, + 226, + 300, + 301, + 302, + 303, + 304, + 305, + 306, + 307, + 308, + 400, + 401, + 402, + 403, + 404, + 405, + 406, + 407, + 408, + 409, + 410, + 411, + 412, + 413, + 414, + 415, + 416, + 417, + 418, + 421, + 422, + 423, + 424, + 426, + 428, + 429, + 431, + 451, + 500, + 501, + 502, + 503, + 504, + 505, + 506, + 507, + 508, + 510, + 511, + }; + + enum class Code : int + { + UNKNOWN = -1, + CONTINUE, + SWITCHING_PROTOCOLS, + PROCESSING, + OK, + CREATED, + ACCEPTED, + NON_AUTHORITATIVE_INFORMATION, + NO_CONTENT, + RESET_CONTENT, + PARTIAL_CONTENT, + MULTI_STATUS, + ALREADY_REPORTED, + IM_USED, + MULTIPLE_CHOICES, + MOVED_PERMANENTLY, + FOUND, + SEE_OTHER, + NOT_MODIFIED, + USE_PROXY, + UNUSED, + TEMPORARY_REDIRECT, + PERMANENT_REDIRECT, + BAD_REQUEST, + UNAUTHORIZED, + PAYMENT_REQUIRED, + FORBIDDEN, + NOT_FOUND, + METHOD_NOT_ALLOWED, + NOT_ACCEPTABLE, + PROXY_AUTHENTICATION_REQUIRED, + REQUEST_TIMEOUT, + CONFLICT, + GONE, + LENGTH_REQUIRED, + PRECONDITION_FAILED, + PAYLOAD_TOO_LARGE, + URI_TOO_LONG, + UNSUPPORTED_MEDIA_TYPE, + RANGE_NOT_SATISFIABLE, + EXPECTATION_FAILED, + I_AM_A_TEAPOT, + MISDIRECTED_REQUEST, + UNPROCESSABLE_ENTITY, + LOCKED, + FAILED_DEPENDENCY, + UPGRADE_REQUIRED, + PRECONDITION_REQUIRED, + TOO_MANY_REQUESTS, + REQUEST_HEADER_FIELDS_TOO_LARGE, + UNAVAILABLE_FOR_LEGAL_REASONS, + INTERNAL_SERVER_ERROR, + NOT_IMPLEMENTED, + BAD_GATEWAY, + SERVICE_UNAVAILABLE, + GATEWAY_TIMEOUT, + HTTP_VERSION_NOT_SUPPORTED, + VARIANT_ALSO_NEGOTIATES, + INSUFFICIENT_STORAGE, + LOOP_DETECTED, + NOT_EXTENDED, + NETWORK_AUTHENTICATION_REQUIRED + }; + + const std::vector codeStrings = { + "Continue", + "Switching Protocols", + "Processing", + "OK", + "Created", + "Accepted", + "Non-Authoritative Information", + "No Content", + "Reset Content", + "Partial Content", + "Multi-Status", + "Already Reported", + "IM Used", + "Multiple Choices", + "Moved Permanently", + "Found", + "See Other", + "Not Modified", + "Use Proxy", + "(Unused)", + "Temporary Redirect", + "Permanent Redirect", + "Bad Request", + "Unauthorized", + "Payment Required", + "Forbidden", + "Not Found", + "Method Not Allowed", + "Not Acceptable", + "Proxy Authentication Required", + "Request Timeout", + "Conflict", + "Gone", + "Length Required", + "Precondition Failed", + "Payload Too Large", + "URI Too Long", + "Unsupported Media Type", + "Range Not Satisfiable", + "Expectation Failed", + "I am a teapot", + "Misdirected Request", + "Unprocessable Entity", + "Locked", + "Failed Dependency", + "Upgrade Required", + "Precondition Required", + "Too Many Requests", + "Request Header Fields Too Large", + "Unavailable For Legal Reasons", + "Internal Server Error", + "Not Implemented", + "Bad Gateway", + "Service Unavailable", + "Gateway Timeout", + "HTTP Version Not Supported", + "Variant Also Negotiates", + "Insufficient Storage", + "Loop Detected", + "Not Extended", + "Network Authentication Required", + }; + + enum class Header + { + UNKNOWN = -1, + // https://en.wikipedia.org/wiki/List_of_HTTP_header_fields + ACCESS_CONTROL_ALLOW_ORIGIN, + ACCESS_CONTROL_ALLOW_CREDENTIALS, + ACCESS_CONTROL_EXPOSE_HEADERS, + ACCESS_CONTROL_MAX_AGE, + ACCESS_CONTROL_ALLOW_METHODS, + ACCESS_CONTROL_ALLOW_HEADERS, + ACCEPT_PATCH, + ACCEPT_RANGES, + AGE, + ALLOW, + ALT_SVC, + CACHE_CONTROL, + CONNECTION, + CONTENT_DISPOSITION, + CONTENT_ENCODING, + CONTENT_LANGUAGE, + CONTENT_LENGTH, + CONTENT_LOCATION, + CONTENT_MD5, + CONTENT_RANGE, + CONTENT_TYPE, + DATE, + ETAG, + EXPIRES, + LAST_MODIFIED, + LINK, + LOCATION, + P3P, + PRAGMA, + PROXY_AUTHENTICATE, + PUBLIC_KEY_PINS, + RETRY_AFTER, + SERVER, + SET_COOKIE, + STRICT_TRANSPORT_SECURITY, + TRAILER, + TRANSFER_ENCODING, + TK, + UPGRADE, + VARY, + VIA, + WARNING, + WWW_AUTHENTICATE, + X_FRAME_OPTION + }; + + const std::vector headerStrings = { + "Access-Control-Allow-Origin", + "Access-Control-Allow-Credentials", + "Access-Control-Expose-Headers", + "Access-Control-Max-Age", + "Access-Control-Allow-Methods", + "Access-Control-Allow-Headers", + "Accept-Patch", + "Accept-Ranges", + "Age", + "Allow", + "Alt-Svc", + "Cache-Control", + "Connection", + "Content-Disposition", + "Content-Encoding", + "Content-Language", + "Content-Length", + "Content-Location", + "Content-MD5", + "Content-Range", + "Content-Type", + "Date", + "ETag", + "Expires", + "Last-Modified", + "Link", + "Location", + "P3P", + "Pragma", + "Proxy-Authenticate", + "Public-Key-Pins", + "Retry-After", + "Server", + "Set-Cookie", + "Strict-Transport-Security", + "Trailer", + "Transfer-Encoding", + "Tk", + "Upgrade", + "Vary", + "Via", + "Warning", + "WWW-Authenticate", + "X-Frame-Option", + }; +} \ No newline at end of file diff --git a/src/http/mime.cpp b/src/http/mime.cpp new file mode 100644 index 0000000..36abae6 --- /dev/null +++ b/src/http/mime.cpp @@ -0,0 +1,76 @@ +#include "../logger.hpp" +#include "mime.hpp" +#include + +namespace Http +{ + /* Existing MIME types + text + image + audio + video + application + multipart + */ + + std::string GetMimeType(std::filesystem::path const & path) + { + auto const extension = path.extension(); + return GetMimeType(extension.string()); + } + + std::string GetMimeType(std::string const & extension) + { + static std::unordered_map fileExtensionMap + { + {".html", FileType::HTML}, + {".htm", FileType::HTML}, + {".css", FileType::CSS}, + {".js", FileType::JS}, + {".jpg", FileType::JPG}, + {".jpeg", FileType::JPG}, + {".png", FileType::PNG}, + {".mp3", FileType::MP3}, + {".mp4", FileType::MP4}, + }; + + auto const findResult = fileExtensionMap.find(extension); + if (findResult == fileExtensionMap.end()) + { + return GetMimeType(FileType::UNKNOWN); + } + + return GetMimeType(findResult->second); + } + + std::string GetMimeType(FileType const fileType) + { + switch(fileType) + { + default: + case FileType::UNKNOWN: + return "text/plain"; + + case FileType::HTML: + return "text/html"; + + case FileType::CSS: + return "text/css"; + + case FileType::JS: + return "application/javascript"; + + case FileType::JPG: + return "image/jpeg"; + + case FileType::PNG: + return "image/png"; + + case FileType::MP3: + return "audio/mpeg3"; + + case FileType::MP4: + return "video/mp4"; + } + } +} \ No newline at end of file diff --git a/src/http/mime.hpp b/src/http/mime.hpp new file mode 100644 index 0000000..64b4528 --- /dev/null +++ b/src/http/mime.hpp @@ -0,0 +1,23 @@ +#pragma once +#include +#include +#include + +namespace Http +{ + enum class FileType + { + UNKNOWN = -1, + HTML, + CSS, + JS, + PNG, + JPG, + MP3, + MP4 + }; + + std::string GetMimeType(std::filesystem::path const & path); + std::string GetMimeType(std::string const & extension); + std::string GetMimeType(FileType const fileType); +} \ No newline at end of file diff --git a/src/http/request.cpp b/src/http/request.cpp new file mode 100644 index 0000000..680a902 --- /dev/null +++ b/src/http/request.cpp @@ -0,0 +1,38 @@ +#include "request.hpp" +#include + +namespace Http +{ + template + T ToEnum(std::string const & str, std::vector const & enumStrings) + { + for(unsigned i = 0; i < enumStrings.size(); ++i) + { + if (str.compare(enumStrings[i]) == 0) + { + return static_cast(i); + } + } + + return static_cast(-1); + } + + Http::Request Request::Deserialize(std::vector const & bytes) + { + // TODO serialize more than just the start + Http::Request request; + + std::stringstream ss(std::string(bytes.begin(), bytes.end())); + + std::string requestTypeString; + ss >> requestTypeString; + request.requestType = ToEnum(requestTypeString, HttpRequest::typeStrings); + + ss >> request.path; + + std::string httpProtocolString; + ss >> httpProtocolString; + + return request; + } +} diff --git a/src/http/request.hpp b/src/http/request.hpp new file mode 100644 index 0000000..ef0f595 --- /dev/null +++ b/src/http/request.hpp @@ -0,0 +1,13 @@ +#pragma once +#include "../constants/httprequest.hpp" + +namespace Http +{ + struct Request + { + HttpRequest::Type requestType; + std::string path; + + static Request Deserialize(std::vector const & bytes); + }; +} diff --git a/src/http/response.cpp b/src/http/response.cpp new file mode 100644 index 0000000..63cb475 --- /dev/null +++ b/src/http/response.cpp @@ -0,0 +1,37 @@ +#include "response.hpp" +#include + +namespace Http +{ + std::vector Response::Serialize() + { + // TODO implement headers properly + std::stringstream ss; + ss << "HTTP/1.1"; + ss << ' ' << HttpResponse::codeValues[static_cast(code)]; + ss << ' ' << HttpResponse::codeStrings[static_cast(code)]; + ss << "\r\n"; + ss << "Server: http-server/0.1\r\n"; + + if (contentType.size() > 0) + { + ss << "Content-Type: "; + ss << contentType << "\r\n"; + } + + ss << "\r\n"; + + auto header = ss.str(); + std::vector buffer (header.begin(), header.end()); + buffer.insert(buffer.end(), content.begin(), content.end()); + + return buffer; + } + + Response::Response() + : code(HttpResponse::Code::UNKNOWN), + contentType(), + content() + { + } +} \ No newline at end of file diff --git a/src/http/response.hpp b/src/http/response.hpp new file mode 100644 index 0000000..100d7d7 --- /dev/null +++ b/src/http/response.hpp @@ -0,0 +1,16 @@ +#pragma once +#include "../constants/httpresponse.hpp" + +namespace Http +{ + struct Response + { + HttpResponse::Code code; + std::string contentType; + std::vector content; + + std::vector Serialize(); + + Response(); + }; +} \ No newline at end of file diff --git a/src/logger.cpp b/src/logger.cpp new file mode 100644 index 0000000..ffcb111 --- /dev/null +++ b/src/logger.cpp @@ -0,0 +1,32 @@ +#include +#include "logger.hpp" + +Logger::~Logger() +{ +} + +Logger::Logger() +{ +} + +void Logger::Success(const std::string & s) +{ + std::printf("[SUCCESS] %s\n", s.c_str()); +} + +void Logger::Error(const std::string & s) +{ + std::printf("[ERROR] %s\n", s.c_str()); +} + +void Logger::Info(const std::string & s) +{ + std::printf("[INFO] %s\n", s.c_str()); +} + +Logger & Logger::GetInstance() +{ + static Logger logger; + + return logger; +} \ No newline at end of file diff --git a/src/logger.hpp b/src/logger.hpp new file mode 100644 index 0000000..604d724 --- /dev/null +++ b/src/logger.hpp @@ -0,0 +1,20 @@ +#pragma once +#include + +class Logger +{ +private: + Logger(); + ~Logger(); + +public: + void Success(const std::string & s); + void Error(const std::string & s); + void Info(const std::string & s); + + static Logger & GetInstance(); + + Logger(Logger & other) = delete; + Logger(Logger && other) = delete; + Logger & operator=(Logger & other) = delete; +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100755 index 0000000..e52d810 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,21 @@ +#pragma once +#include "server/configuration.hpp" +#include "logger.hpp" +#include "server/server.hpp" + +int main(int argc, char ** argv) +{ + /* + ServerConfiguration & serverConfiguration; + if (serverConfiguration.LoadFromFile("./server.cfg") && !serverConfiguration.IsValid()) + { + Logger::GetInstance().Error("Error loading configuration file, aborting."); + return 1; + } + */ + + HttpServer httpServer; + httpServer.Execute(); + + return 0; +} \ No newline at end of file diff --git a/src/middleware/base.hpp b/src/middleware/base.hpp new file mode 100644 index 0000000..7f13afa --- /dev/null +++ b/src/middleware/base.hpp @@ -0,0 +1,17 @@ +#pragma once +#include "../http/request.hpp" +#include "../http/response.hpp" +#include +#include + +namespace Middleware +{ + class BaseMiddleware + { + public: + virtual void HandleRequest(Http::Request const & request, Http::Response & response) = 0; + + BaseMiddleware() = default; + virtual ~BaseMiddleware() = default; + }; +} \ No newline at end of file diff --git a/src/middleware/notfound.cpp b/src/middleware/notfound.cpp new file mode 100644 index 0000000..5419271 --- /dev/null +++ b/src/middleware/notfound.cpp @@ -0,0 +1,27 @@ +#include "notfound.hpp" +#include + +namespace Middleware +{ + void NotFound::HandleRequest(Http::Request const & request, Http::Response & response) + { + if (response.code != HttpResponse::Code::UNKNOWN) + { + return; + } + + response.code = HttpResponse::Code::NOT_FOUND; + + std::stringstream ss; + ss << "404 - file not found\n"; + ss << "File: "; + ss << request.path << '\n'; + + auto responseContent = ss.str(); + response.content.insert(response.content.begin(), + responseContent.begin(), + responseContent.end()); + + response.contentType = "text/plain"; + } +} \ No newline at end of file diff --git a/src/middleware/notfound.hpp b/src/middleware/notfound.hpp new file mode 100644 index 0000000..022ca84 --- /dev/null +++ b/src/middleware/notfound.hpp @@ -0,0 +1,11 @@ +#pragma once +#include "base.hpp" + +namespace Middleware +{ + class NotFound : public BaseMiddleware + { + public: + void HandleRequest(Http::Request const & request, Http::Response & Response) override; + }; +} \ No newline at end of file diff --git a/src/middleware/staticcontent.cpp b/src/middleware/staticcontent.cpp new file mode 100644 index 0000000..ab7e039 --- /dev/null +++ b/src/middleware/staticcontent.cpp @@ -0,0 +1,72 @@ +#include "../http/mime.hpp" +#include "../logger.hpp" +#include +#include +#include +#include "staticcontent.hpp" +#include + +namespace Middleware +{ + void ReadAllBytes(std::filesystem::path const & path, std::vector & buffer) + { + std::ifstream ifs(path, std::ios_base::binary | std::ios_base::ate); + std::ifstream::pos_type length = ifs.tellg(); + + auto const oldBufferSize = buffer.size(); + buffer.resize(oldBufferSize + length); + + ifs.seekg(0, std::ios_base::beg); + ifs.read(&buffer[oldBufferSize], length); + } + + void StaticContent::HandleRequest(Http::Request const & request, Http::Response & response) + { + switch(request.requestType) + { + case HttpRequest::Type::GET: + break; + + default: + { + return; + } + } + + std::filesystem::path path; + if (request.path.size() == 1) + { + // TODO make configurable? + path = root + "/index.html"; + } + else + { + path = root + request.path; + } + + if (!std::filesystem::exists(path)) + { + std::stringstream ss; + ss << "Static file <"; + ss << path.string(); + ss << "> not found"; + Logger::GetInstance().Info(ss.str()); + + return; + } + + response.code = HttpResponse::Code::OK; + ReadAllBytes(path, response.content); + response.contentType = Http::GetMimeType(path); + + return; + } + + StaticContent::StaticContent(std::string const & staticFileRoot) + : root(staticFileRoot) + { + std::stringstream ss; + ss << "Using static file root " << root; + Logger::GetInstance().Info(ss.str()); + } +} \ No newline at end of file diff --git a/src/middleware/staticcontent.hpp b/src/middleware/staticcontent.hpp new file mode 100644 index 0000000..8967eba --- /dev/null +++ b/src/middleware/staticcontent.hpp @@ -0,0 +1,19 @@ +#pragma once +#include "base.hpp" +#include +#include +#include + +namespace Middleware +{ + class StaticContent : public BaseMiddleware + { + private: + std::string root; + + public: + virtual void HandleRequest(Http::Request const & request, Http::Response & response) override; + + StaticContent(std::string const & staticFileRoot); + }; +} \ No newline at end of file diff --git a/src/server/configuration.cpp b/src/server/configuration.cpp new file mode 100644 index 0000000..8d49656 --- /dev/null +++ b/src/server/configuration.cpp @@ -0,0 +1,51 @@ +#include "configuration.hpp" + +bool ServerConfiguration::LoadFromFile(std::string const & filePath) +{ + // TODO implement + return false; +} + +ServerConfiguration::ServerConfiguration() + : wwwRoot("/home/tijmen/project/http-server/bin/www"), + serverName("http-server"), + port(8080) +{ +} + +int ServerConfiguration::GetMajorVersion() const +{ + return 0; +} + +int ServerConfiguration::GetMinorVersion() const +{ + return 1; +} + +std::string const & ServerConfiguration::GetWwwRoot() const +{ + return wwwRoot; +} + +std::string const & ServerConfiguration::GetServerName() const +{ + return serverName; +} + +int ServerConfiguration::GetPort() const +{ + return port; +} + +bool ServerConfiguration::IsValid() const +{ + return isValid; +} + +ServerConfiguration const & ServerConfiguration::GetInstance() +{ + static ServerConfiguration config; + + return config; +} \ No newline at end of file diff --git a/src/server/configuration.hpp b/src/server/configuration.hpp new file mode 100644 index 0000000..f11546a --- /dev/null +++ b/src/server/configuration.hpp @@ -0,0 +1,29 @@ +#pragma once +#include + +class ServerConfiguration +{ +private: + std::string wwwRoot; + std::string serverName; + int port; + bool isValid; + + bool LoadFromFile(std::string const & filePath); + + ServerConfiguration(); + ~ServerConfiguration() = default; + +public: + int GetMajorVersion() const; + int GetMinorVersion() const; + std::string const & GetWwwRoot() const; + std::string const & GetServerName() const; + int GetPort() const; + bool IsValid() const; + + static ServerConfiguration const & GetInstance(); + + ServerConfiguration(ServerConfiguration & other) = delete; + ServerConfiguration(ServerConfiguration && other) = delete; +}; \ No newline at end of file diff --git a/src/server/connection.cpp b/src/server/connection.cpp new file mode 100644 index 0000000..f42b457 --- /dev/null +++ b/src/server/connection.cpp @@ -0,0 +1,68 @@ +#include "connection.hpp" +#include +#include + +std::vector Connection::ReadBytes(size_t limit) const +{ + size_t const readChunkSize = 128; + + std::vector buffer; + ssize_t totalBytesRead = 0; + ssize_t bytesRead = 0; + do + { + buffer.resize(buffer.size() + readChunkSize); + bytesRead = read(fileDescriptor, &buffer[totalBytesRead], readChunkSize); + if (bytesRead < 0) + { + throw std::runtime_error("Error reading from filedescriptor"); + } + + totalBytesRead += bytesRead; + } while (bytesRead == readChunkSize && bytesRead < limit); + + buffer.resize(totalBytesRead); + return buffer; +} + +size_t Connection::WriteBytes(std::vector const & bytes) const +{ + ssize_t totalBytesWritten = 0; + size_t const sizeToWrite = bytes.size(); + while (totalBytesWritten < sizeToWrite) + { + ssize_t bytesWritten = write(fileDescriptor, &bytes[totalBytesWritten], sizeToWrite - totalBytesWritten); + if (bytesWritten <= 0) + { + throw std::runtime_error("Error writing to filedescriptor"); + } + + totalBytesWritten += bytesWritten; + } + + return totalBytesWritten; +} + +Connection::Connection(int _fileDescriptor) + : fileDescriptor(_fileDescriptor) +{ + if (_fileDescriptor < 0) + { + throw std::runtime_error("connection created with invalid file descriptor"); + } +} + +Connection::~Connection() +{ + if (fileDescriptor >= 0) + { + close(fileDescriptor); + } +} + +Connection::Connection(Connection && other) +{ + fileDescriptor = other.fileDescriptor; + + other.fileDescriptor = -1; +} \ No newline at end of file diff --git a/src/server/connection.hpp b/src/server/connection.hpp new file mode 100644 index 0000000..1fb260b --- /dev/null +++ b/src/server/connection.hpp @@ -0,0 +1,23 @@ +#pragma once +#include +#include + +class Connection +{ +private: + int fileDescriptor; + +public: + // Parameter limit is a multiple of 128 + std::vector ReadBytes(size_t limit = 512) const; + + size_t WriteBytes(std::vector const & bytes) const; + + Connection(int _fileDescriptor); + ~Connection(); + + Connection(Connection && other); + + Connection(Connection & other) = delete; + Connection & operator=(Connection & other) = delete; +}; \ No newline at end of file diff --git a/src/server/connectionoperator.cpp b/src/server/connectionoperator.cpp new file mode 100644 index 0000000..f456b4f --- /dev/null +++ b/src/server/connectionoperator.cpp @@ -0,0 +1,45 @@ +#include "../middleware/notfound.hpp" +#include "../middleware/staticcontent.hpp" +#include "../logger.hpp" +#include "configuration.hpp" +#include "connectionoperator.hpp" +#include +#include + +void ConnectionOperator::HandleNewConnection(Connection const & newConnection) +{ + auto requestBytes = newConnection.ReadBytes(); + Http::Request request = Http::Request::Deserialize(requestBytes); + + Http::Response response; + for(size_t i = 0; i < middlewares.size(); ++i) + { + middlewares[i]->HandleRequest(request, response); + } + + if (response.code == HttpResponse::Code::UNKNOWN) + { + std::stringstream ss; + ss << "Unhandled request for file <"; + ss << request.path; + ss << '>'; + Logger::GetInstance().Error(ss.str()); + return; + } + + auto bytesToSend = response.Serialize(); + newConnection.WriteBytes(bytesToSend); +} + +ConnectionOperator::ConnectionOperator() +{ + // Base static file server + auto const & staticFileRoot = ServerConfiguration::GetInstance().GetWwwRoot(); + if (staticFileRoot.size() > 0) + { + middlewares.emplace_back(std::make_unique(staticFileRoot)); + } + + // ALWAYS LAST! + middlewares.emplace_back(std::make_unique()); +} \ No newline at end of file diff --git a/src/server/connectionoperator.hpp b/src/server/connectionoperator.hpp new file mode 100644 index 0000000..ba850a7 --- /dev/null +++ b/src/server/connectionoperator.hpp @@ -0,0 +1,17 @@ +#pragma once +#include "../middleware/base.hpp" +#include "connection.hpp" +#include +#include +#include + +class ConnectionOperator +{ +private: + std::vector> middlewares; + +public: + void HandleNewConnection(Connection const & newConnection); + + ConnectionOperator(); +}; \ No newline at end of file diff --git a/src/server/listeningsocket.cpp b/src/server/listeningsocket.cpp new file mode 100644 index 0000000..0bc1822 --- /dev/null +++ b/src/server/listeningsocket.cpp @@ -0,0 +1,54 @@ +#include "listeningsocket.hpp" +#include +#include + +int const connectionLimit = 10; + +Connection ListeningSocket::AcceptNextConnection() +{ + unsigned sockaddrSize = sizeof(sockaddr_in); + int connectionFileDescriptor = accept( + socketFileDescriptor, + reinterpret_cast(&socketAddress), + &sockaddrSize); + + return Connection(connectionFileDescriptor); +} + +ListeningSocket::ListeningSocket(int const port) + : socketFileDescriptor(-1), + socketAddress() +{ + socketFileDescriptor = socket(AF_INET, SOCK_STREAM, 0); + if (socketFileDescriptor < 0) + { + throw std::runtime_error("socket creation error"); + } + + socketAddress.sin_family = AF_INET; + socketAddress.sin_addr.s_addr = INADDR_ANY; + socketAddress.sin_port = htons(port); + + int const bindResult = bind( + socketFileDescriptor, + reinterpret_cast(&socketAddress), + sizeof(sockaddr_in)); + if (bindResult < 0) + { + throw std::runtime_error("socket bind error"); + } + + int const listenResult = listen(socketFileDescriptor, connectionLimit); + if (listenResult < 0) + { + throw std::runtime_error("socket listening error"); + } +} + +ListeningSocket::~ListeningSocket() +{ + if (socketFileDescriptor >= 0) + { + close(socketFileDescriptor); + } +} \ No newline at end of file diff --git a/src/server/listeningsocket.hpp b/src/server/listeningsocket.hpp new file mode 100644 index 0000000..6a3dc79 --- /dev/null +++ b/src/server/listeningsocket.hpp @@ -0,0 +1,20 @@ +#pragma once +#include "connection.hpp" +#include +#include + +class ListeningSocket +{ +private: + int socketFileDescriptor; + sockaddr_in socketAddress; + +public: + Connection AcceptNextConnection(); + + ListeningSocket(int const port); + ~ListeningSocket(); + + ListeningSocket(ListeningSocket & other) = delete; + ListeningSocket & operator=(ListeningSocket & other) = delete; +}; \ No newline at end of file diff --git a/src/server/server.cpp b/src/server/server.cpp new file mode 100755 index 0000000..17af6e1 --- /dev/null +++ b/src/server/server.cpp @@ -0,0 +1,31 @@ +#include "../logger.hpp" +#include "configuration.hpp" +#include "server.hpp" +#include +#include + +void HttpServer::Execute() +{ + while(isOpen) + { + try + { + Connection newConnection = listeningSocket.AcceptNextConnection(); + connectionOperator.HandleNewConnection(newConnection); + } + catch (std::runtime_error & e) + { + Logger::GetInstance().Info("Connection dropped on accept"); + } + } +} + +HttpServer::HttpServer() + : listeningSocket(ServerConfiguration::GetInstance().GetPort()), + connectionOperator(), + isOpen(true) +{ + std::stringstream ss; + ss << "Listening on port " << ServerConfiguration::GetInstance().GetPort(); + Logger::GetInstance().Info(ss.str()); +} \ No newline at end of file diff --git a/src/server/server.hpp b/src/server/server.hpp new file mode 100755 index 0000000..5f13afc --- /dev/null +++ b/src/server/server.hpp @@ -0,0 +1,18 @@ +#pragma once +#include "connectionoperator.hpp" +#include "listeningsocket.hpp" + +class HttpServer +{ +private: + ListeningSocket listeningSocket; + ConnectionOperator connectionOperator; + bool isOpen; + +public: + void Execute(); + + HttpServer(); + HttpServer(HttpServer & other) = delete; + HttpServer & operator=(HttpServer & other) = delete; +}; \ No newline at end of file diff --git a/src/server/url.cpp b/src/server/url.cpp new file mode 100644 index 0000000..cbcfe9b --- /dev/null +++ b/src/server/url.cpp @@ -0,0 +1,33 @@ +#include "url.hpp" + +bool Url::HasPath() const +{ + return path.size() > 1; +} + +bool Url::HasQuery() const +{ + // TODO implement + return false; +} + +bool Url::HasFragment() const +{ + // TODO implement + return false; +} + +std::string const & Url::GetPath() const +{ + return path; +} + +std::string const & Url::GetQuery() const +{ + return query; +} + +std::string const & Url::GetFragment() const +{ + return fragment; +} \ No newline at end of file diff --git a/src/server/url.hpp b/src/server/url.hpp new file mode 100644 index 0000000..eb2cde9 --- /dev/null +++ b/src/server/url.hpp @@ -0,0 +1,19 @@ +#pragma once +#include + +class Url +{ +private: + std::string path; + std::string query; + std::string fragment; + +public: + bool HasPath() const; + bool HasQuery() const; + bool HasFragment() const; + + std::string const & GetPath() const; + std::string const & GetQuery() const; + std::string const & GetFragment() const; +}; \ No newline at end of file