commit e52806bc7a9ce92d838167805ebc8fc9b85b725d Author: apio Date: Fri Oct 7 16:24:12 2022 +0200 Ready. Set. Go! diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c4ffa11 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.8..3.22) +project(webcxx LANGUAGES CXX) + +add_library(${PROJECT_NAME} STATIC ${CMAKE_CURRENT_LIST_DIR}/src/App.cpp ${CMAKE_CURRENT_LIST_DIR}/src/Socket.cpp ${CMAKE_CURRENT_LIST_DIR}/src/HTMLTemplate.cpp ${CMAKE_CURRENT_LIST_DIR}/src/MIMETypes.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/Response.cpp ${CMAKE_CURRENT_LIST_DIR}/src/Request.cpp ${CMAKE_CURRENT_LIST_DIR}/src/HTTPStatus.cpp ${CMAKE_CURRENT_LIST_DIR}/src/Map.cpp) + +target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_LIST_DIR}/include/webcxx ${CMAKE_CURRENT_LIST_DIR}/src) + +set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 17) + +set(webcxx_include ${CMAKE_CURRENT_LIST_DIR}/include) +set(webcxx_libs ${PROJECT_NAME}) \ No newline at end of file diff --git a/include/webcxx/App.h b/include/webcxx/App.h new file mode 100644 index 0000000..7440252 --- /dev/null +++ b/include/webcxx/App.h @@ -0,0 +1,70 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace webcxx { +class Response; +class Request; +class ErrorWatcher; +class App { +public: + static std::shared_ptr create(); + int run(int port); + void terminate(); + + static std::shared_ptr current_app(); + + void on(int methods, std::string path, + std::function action); + + void on_error(int error, std::function action); + + void set_static_resource_path(const std::string& path); + + void set_favicon(const std::string& favicon_path); + +private: + App() = default; + int socket_init(int); + int main_loop(); + bool m_is_running; + int m_socket_fd; + static std::shared_ptr s_instance; + + std::string static_resource_path = "/static"; + + int m_current_connection_fd; + + sockaddr_in m_addr; + + int handle_request(Request& req); + + void get_client_addr(); + std::string m_client_ip; + + struct Action { + std::function get_action; + std::function post_action; + std::function put_action; + std::function delete_action; + }; + + std::map m_action_map; + + std::map> m_error_action_map; + + static void app_terminate(int); // Terminate the current app. + + friend class ErrorWatcher; +}; + +Response text(const std::string &content); +Response json(const std::string &content); +Response html(const std::string &content, const std::string& title = ""); +Response redirect(const std::string &url, int status_code = 302); +Response error(int status_code = 500); +Response file(const std::string& item_path); +} // namespace webcxx diff --git a/include/webcxx/Map.h b/include/webcxx/Map.h new file mode 100644 index 0000000..cdbf7ea --- /dev/null +++ b/include/webcxx/Map.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include + +namespace webcxx +{ + /* Wrapper around std::map that provides a read-only interface with a contains() function */ + class Map { + public: + typedef std::map MapType; + Map(const MapType& map); + + std::string at(const std::string& key); + bool contains(const std::string& key); + std::string operator[](const std::string& key); + MapType::const_iterator begin() const noexcept; + MapType::const_iterator cbegin() const noexcept; + MapType::const_iterator end() const noexcept; + MapType::const_iterator cend() const noexcept; + private: + const MapType m_map; + }; +} \ No newline at end of file diff --git a/include/webcxx/Request.h b/include/webcxx/Request.h new file mode 100644 index 0000000..bcda196 --- /dev/null +++ b/include/webcxx/Request.h @@ -0,0 +1,46 @@ +#pragma once +#include +#include + +namespace webcxx { +enum RequestType { GET = 1 << 0, POST = 1 << 1, PUT = 1 << 2, DELETE = 1 << 3 }; + +class App; +class Map; + +class Request { +public: + RequestType method() { return m_method; } + std::string ip() { return m_ip; } + + Map headers() const; + + Map args() const; + + std::string path() const { return m_path; } + std::string content() const { return m_content; } + +private: + explicit Request(int fd); + + bool is_bad_request() { return m_bad_request; }; + + RequestType m_method; + std::string m_path; + std::string m_real_path; + std::string m_content; + std::map m_headers; + bool m_bad_request = false; + std::string m_ip; + + std::map m_args; + + std::pair split_header(std::string input, + std::string delim); + void decode_http_header(std::string header); + + void log(int status_code); + + friend class App; +}; +} // namespace webcxx \ No newline at end of file diff --git a/include/webcxx/Response.h b/include/webcxx/Response.h new file mode 100644 index 0000000..1ee9d5f --- /dev/null +++ b/include/webcxx/Response.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include + +namespace webcxx { +class App; +class Response { +public: + explicit Response(int status_code, std::string content = "", + std::string content_type = "text/html", + std::map extra_headers = {}); + std::string to_string(); + + void set_status(int status_code); + +private: + int status; + std::map headers; + std::string content; + int send(int fd); + + friend class App; +}; +} // namespace webcxx \ No newline at end of file diff --git a/src/App.cpp b/src/App.cpp new file mode 100644 index 0000000..b906c7b --- /dev/null +++ b/src/App.cpp @@ -0,0 +1,419 @@ +#include "App.h" +#include "HTTPStatus.h" +#include "Request.h" +#include "Response.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "HTMLTemplate.h" +#include +#include +#include +#include +#include "MIMETypes.h" + +namespace fs = std::filesystem; + +webcxx::Response webcxx::text(const std::string &content) { + return Response{200, content, {"text/plain"}}; +} + +webcxx::Response webcxx::json(const std::string &content) { + return Response{200, content, {"application/json"}}; +} + +webcxx::Response webcxx::html(const std::string& content, const std::string& title) { + return Response{200, _webcxx_internal::build_html_from_template(content, title)}; +} + +webcxx::Response webcxx::redirect(const std::string &url, int status_code) { + return Response{ + status_code, + _webcxx_internal::build_html_from_template("

If you aren't redirected automatically, please click here.

","Redirecting..."), + "text/html", + {{"Location", url}}}; +} + +static webcxx::Response _webcxx_default_error(int status_code) +{ + std::ostringstream ss; + ss << "

"; + ss << _webcxx_internal::http_status_codes[status_code]; + ss << "

"; + ss << "

"; + ss << "webcxx/0.1"; + ss << "

"; + std::ostringstream title; + title << status_code; + title << " "; + title << _webcxx_internal::http_status_codes[status_code]; + return webcxx::Response{status_code, _webcxx_internal::build_html_from_template(ss.str(),title.str())}; +} + +class webcxx::ErrorWatcher +{ +public: + std::map>& get_app_error_map(std::shared_ptr app) + { + return app->m_error_action_map; + } +}; + +webcxx::Response webcxx::error(int status_code) { + static bool is_recursing = false; + if(is_recursing) return _webcxx_default_error(status_code); + is_recursing = true; + ErrorWatcher e; + auto& error_map = e.get_app_error_map(webcxx::App::current_app()); + if(error_map.find(status_code) != error_map.end()) + { + Response resp = error_map.at(status_code)(); + resp.set_status(status_code); + is_recursing = false; + return resp; + } else { + is_recursing = false; + return _webcxx_default_error(status_code); + } +} + +std::shared_ptr webcxx::App::s_instance; + +std::shared_ptr webcxx::App::create() { + s_instance = std::shared_ptr(new App); + return s_instance; +} + +std::shared_ptr webcxx::App::current_app() { + if (!s_instance) { + throw std::runtime_error( + "Trying to access the current app before creating an app"); + } + return s_instance; +} + +int webcxx::App::run(int port) { + if (signal(SIGINT, app_terminate) == SIG_ERR) { + perror("signal(SIGINT)"); + exit(1); + } + + int rc = socket_init(port); + + if (rc != 0) + return rc; + + printf("webcxx - running on port %d\n", port); + fflush(stdout); + + return main_loop(); +} + +void webcxx::App::app_terminate(int __signum) { + if (!s_instance) + exit(1); + s_instance->terminate(); +} + +int webcxx::App::socket_init(int port) { + m_socket_fd = socket(AF_INET, SOCK_STREAM, 0); + if (m_socket_fd < 0) { + perror("socket"); + return 1; + } + + m_addr.sin_port = htons(port); + m_addr.sin_family = AF_INET; + m_addr.sin_addr.s_addr = INADDR_ANY; + if (bind(m_socket_fd, (struct sockaddr *)&m_addr, sizeof(sockaddr)) < 0) { + perror("bind"); + return 1; + } + + if (listen(m_socket_fd, 20) < 0) { + perror("listen"); + return 1; + } + + return 0; +} + +void webcxx::App::terminate() { m_is_running = false; } + +void webcxx::App::get_client_addr() { + char result[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &m_addr.sin_addr.s_addr, result, sizeof(result)); + m_client_ip = result; +} + +void webcxx::App::on(int methods, std::string path, + std::function action) { + Action act; + if (m_action_map.find(path) != m_action_map.end()) { + act = m_action_map.at(path); + } + if (methods & (int)RequestType::GET) { + act.get_action = action; + } + if (methods & (int)RequestType::POST) { + act.post_action = action; + } + if (methods & (int)RequestType::PUT) { + act.put_action = action; + } + if (methods & (int)RequestType::DELETE) { + act.delete_action = action; + } + m_action_map[path] = act; +} + +void webcxx::App::on_error(int error, + std::function action) { + if(error < 400) + { + throw std::runtime_error("on_error can only register handlers for status codes 400 and over"); + } + m_error_action_map[error] = action; +} + +void webcxx::App::set_favicon(const std::string& favicon_path) +{ + std::string path = favicon_path; + on(GET, "/favicon.ico", [=](Request& req){ + return file(path); + }); +} + +void webcxx::App::set_static_resource_path(const std::string& path) +{ + static_resource_path = path; +} + +int webcxx::App::main_loop() { + m_is_running = true; + struct pollfd poll_info; + poll_info.fd = m_socket_fd; + poll_info.events = POLL_IN; + while (m_is_running) { + + bool is_ready = false; + m_current_connection_fd = -1; + while (!is_ready) { + if (!m_is_running) { + break; + } + int rc = poll(&poll_info, 1, 2000); + if (rc < 0) { + if (errno == EINTR) { + close(m_socket_fd); + return 0; + } + perror("poll"); + close(m_socket_fd); + return 1; + } + if (rc == 0) { + continue; + } + if (poll_info.revents & POLL_IN) { + auto addrlen = sizeof(sockaddr); + m_current_connection_fd = accept( + m_socket_fd, (struct sockaddr *)&m_addr, (socklen_t *)&addrlen); + if (m_current_connection_fd < 0) { + perror("accept"); + close(m_socket_fd); + return 1; + } + get_client_addr(); + is_ready = true; + } + } + + if (!m_is_running) { + close(m_current_connection_fd); + break; + } + + Request req(m_current_connection_fd); + req.m_ip = m_client_ip; + + int status; + + if((status = handle_request(req))) + { + return status; + } + + close(m_current_connection_fd); + } + close(m_socket_fd); + return 0; +} + +static inline bool string_starts_with(const std::string& string, const std::string& substring) +{ + return string.rfind(substring, 0) == 0; +} + +static std::string get_current_working_directory() +{ + char buffer[PATH_MAX]; + getcwd(buffer, PATH_MAX); + return std::move(std::string(buffer)); +} + +static inline void trim_string_with_substring(std::string& string, const std::string& substring) +{ + string.assign(string.begin() + substring.length(), string.end()); +} + +static inline size_t get_file_size(std::ifstream& file) +{ + file.seekg(0, file.end); + std::size_t result = file.tellg(); + file.seekg(0, file.beg); + return result; +} + +static std::string read_whole_file(std::ifstream& file) +{ + std::string result; + while(file.good()) + { + char ch = file.get(); + if(ch != -1) result.push_back(ch); + } + return std::move(result); +} + +static std::string remove_trailing_newline(std::string str) +{ + while(str[str.size() - 1] == '\n') + { + str.pop_back(); + } + return std::move(str); +} + +webcxx::Response webcxx::file(const std::string& item_path) +{ + std::string path = fs::absolute(item_path).string(); + struct stat statbuf; + if(stat(path.c_str(), &statbuf) < 0) + { + if(errno == EACCES) + { + return error(403); + } + return error(404); + } + if(S_ISDIR(statbuf.st_mode)) + { + return error(403); + } + std::ifstream item(path, std::ios::binary); + std::string content = read_whole_file(item); + Response res = Response{200, content, remove_trailing_newline(_webcxx_internal::get_mime_type_from_filepath(path))}; + item.close(); + return res; +} + +int webcxx::App::handle_request(Request& req) +{ + Response res(200); + + if (req.is_bad_request()) { + res = error(400); + } else if (!static_resource_path.empty() && string_starts_with(req.path(), static_resource_path)) { + std::string path = req.path(); + trim_string_with_substring(path, static_resource_path); + std::string item_path = fs::path("static").concat(path); + res = file(item_path); + } else if (m_action_map.find(req.path()) != m_action_map.end()) { + Action act = m_action_map.at(req.path()); + switch (req.m_method) { + case RequestType::GET: + if (act.get_action) { + try { + res = act.get_action(req); + } catch (...) { + res = error(500); + req.log(res.status); + res.send(m_current_connection_fd); + close(m_current_connection_fd); + close(m_socket_fd); + std::rethrow_exception(std::current_exception()); + } + } else { + res = error(405); + } + break; + case RequestType::POST: + if (act.post_action) { + try { + res = act.post_action(req); + } catch (...) { + res = error(500); + req.log(res.status); + res.send(m_current_connection_fd); + close(m_current_connection_fd); + close(m_socket_fd); + std::rethrow_exception(std::current_exception()); + } + } else { + res = error(405); + } + break; + case RequestType::PUT: + if (act.put_action) { + try { + res = act.put_action(req); + } catch (...) { + res = error(500); + req.log(res.status); + res.send(m_current_connection_fd); + close(m_current_connection_fd); + close(m_socket_fd); + std::rethrow_exception(std::current_exception()); + } + } else { + res = error(405); + } + break; + case RequestType::DELETE: + if (act.delete_action) { + try { + res = act.delete_action(req); + } catch (...) { + res = error(500); + req.log(res.status); + res.send(m_current_connection_fd); + close(m_current_connection_fd); + close(m_socket_fd); + std::rethrow_exception(std::current_exception()); + } + } else { + res = error(405); + } + break; + } + } else { + res = error(404); + } + + req.log(res.status); + if (res.send(m_current_connection_fd) < 0) { + perror("send"); + close(m_current_connection_fd); + close(m_socket_fd); + return 1; + } + + return 0; +} diff --git a/src/HTMLTemplate.cpp b/src/HTMLTemplate.cpp new file mode 100644 index 0000000..82324a5 --- /dev/null +++ b/src/HTMLTemplate.cpp @@ -0,0 +1,25 @@ +#include "HTMLTemplate.h" +#include + +std::string _webcxx_internal::build_html_from_template(const std::string& content, const std::string& title) +{ + std::ostringstream ss; + ss << "" + "" + "" + "" + "" + ""; + if (!title.empty()) + { + ss << ""; + ss << title; + ss << ""; + } + ss << "" + ""; + ss << content; + ss << "" + ""; + return std::move(ss.str()); +} \ No newline at end of file diff --git a/src/HTMLTemplate.h b/src/HTMLTemplate.h new file mode 100644 index 0000000..dc57d33 --- /dev/null +++ b/src/HTMLTemplate.h @@ -0,0 +1,6 @@ +#pragma once +#include + +namespace _webcxx_internal { + std::string build_html_from_template(const std::string& content, const std::string& title = ""); +} \ No newline at end of file diff --git a/src/HTTPStatus.cpp b/src/HTTPStatus.cpp new file mode 100644 index 0000000..ac780a9 --- /dev/null +++ b/src/HTTPStatus.cpp @@ -0,0 +1,58 @@ +#include "HTTPStatus.h" + +std::map _webcxx_internal::http_status_codes = { + {100, "Continue"}, + {101, "Switching Protocols"}, + {103, "Early Hints"}, + {200, "OK"}, + {201, "Created"}, + {202, "Accepted"}, + {203, "Non-Authoritative Information"}, + {204, "No Content"}, + {205, "Reset Content"}, + {206, "Partial Content"}, + {300, "Multiple Choices"}, + {301, "Moved Permanently"}, + {302, "Found"}, + {303, "See Other"}, + {304, "Not Modified"}, + {307, "Temporary Redirect"}, + {308, "Permanent Redirect"}, + {400, "Bad Request"}, + {401, "Unauthorized"}, + {402, "Payment Required"}, + {403, "Forbidden"}, + {404, "Not Found"}, + {405, "Method Not Allowed"}, + {406, "Not Acceptable"}, + {407, "Proxy Authentication Required"}, + {408, "Request Timeout"}, + {409, "Conflict"}, + {410, "Gone"}, + {411, "Length Required"}, + {412, "Precondition Failed"}, + {413, "Payload Too Large"}, + {414, "URI Too Long"}, + {415, "Unsupported Media Type"}, + {416, "Range Not Satisfiable"}, + {417, "Expectation Failed"}, + {418, "I'm a teapot"}, + {422, "Unprocessable Entity"}, + {425, "Too Early"}, + {426, "Upgrade Required"}, + {428, "Precondition Required"}, + {429, "Too Many Requests"}, + {431, "Request Header Fields Too Large"}, + {451, "Unavailable For Legal Reasons"}, + {500, "Internal Server Error"}, + {501, "Not Implemented"}, + {502, "Bad Gateway"}, + {503, "Service Unavailable"}, + {504, "Gateway Timeout"}, + {505, "HTTP Version Not Supported"}, + {506, "Variant Also Negotiates"}, + {507, "Insufficient Storage"}, + {508, "Loop Detected"}, + {510, "Not Extended"}, + {511, "Network Authentication Required"}, +}; \ No newline at end of file diff --git a/src/HTTPStatus.h b/src/HTTPStatus.h new file mode 100644 index 0000000..d16b158 --- /dev/null +++ b/src/HTTPStatus.h @@ -0,0 +1,7 @@ +#pragma once +#include +#include + +namespace _webcxx_internal { +extern std::map http_status_codes; +} \ No newline at end of file diff --git a/src/MIMETypes.cpp b/src/MIMETypes.cpp new file mode 100644 index 0000000..de2a97d --- /dev/null +++ b/src/MIMETypes.cpp @@ -0,0 +1,69 @@ +#include "MIMETypes.h" + +#ifdef __linux__ +#define OS_UNIX_LIKE +#elif defined(__unix__) +#define OS_UNIX_LIKE +#elif defined(_WIN32) || defined(WIN32) +#define OS_WINDOWS +#endif + +#ifdef OS_UNIX_LIKE +#include +#elif defined(OS_WINDOWS) +#include +#include +#else +#error "Unsupported platform" +#endif + +#include + +#ifdef OS_UNIX_LIKE +std::string _webcxx_internal::get_mime_type_from_filepath(const std::string& filepath) +{ + std::string extension; + size_t dot = filepath.rfind('.'); + if(dot == filepath.npos) goto use_file_command; + extension = filepath.substr(dot+1); + if(extension == "js") return "text/javascript"; + if(extension == "json") return "application/json"; + if(extension == "css") return "text/css"; + +use_file_command: + FILE* pipe = popen(("file --mime-type -b " + filepath).c_str(), "r"); + if(!pipe) + { + throw std::runtime_error("Unable to determine mimetype for file " + filepath); + } + + std::string result; + int ch; + + while((ch=fgetc(pipe)) != EOF) + { + result.push_back(ch); + } + + pclose(pipe); + + return std::move(result); +} +#else +std::string _webcxx_internal::get_mime_type_from_filepath(const std::string& filepath) +{ + std::wstring str = filepath; + LPWSTR pwzMimeOut = NULL; + HRESULT hr = FindMimeFromData( NULL, str.c_str(), NULL, 0, + NULL, FMFD_URLASFILENAME, &pwzMimeOut, 0x0 ); + if ( SUCCEEDED( hr ) ) { + std::wstring strResult( pwzMimeOut ); + // Despite the documentation stating to call operator delete, the + // returned string must be cleaned up using CoTaskMemFree + CoTaskMemFree( pwzMimeOut ); + return std::move(std::string(strResult)); + } + + return "application/octet-stream"; +} +#endif \ No newline at end of file diff --git a/src/MIMETypes.h b/src/MIMETypes.h new file mode 100644 index 0000000..ccb8009 --- /dev/null +++ b/src/MIMETypes.h @@ -0,0 +1,7 @@ +#pragma once +#include + +namespace _webcxx_internal +{ + std::string get_mime_type_from_filepath(const std::string& filepath); +} \ No newline at end of file diff --git a/src/Map.cpp b/src/Map.cpp new file mode 100644 index 0000000..a8f2e55 --- /dev/null +++ b/src/Map.cpp @@ -0,0 +1,43 @@ +#include "Map.h" + +namespace webcxx { + + Map::Map(const MapType& map) : m_map(map) + { + } + + std::string Map::at(const std::string& key) + { + return m_map.at(key); + } + + std::string Map::operator[](const std::string& key) + { + return std::move(at(key)); + } + + bool Map::contains(const std::string& key) + { + return m_map.find(key) != m_map.end(); + } + + Map::MapType::const_iterator Map::begin() const noexcept + { + return m_map.cbegin(); + } + + Map::MapType::const_iterator Map::cbegin() const noexcept + { + return m_map.cbegin(); + } + + Map::MapType::const_iterator Map::end() const noexcept + { + return m_map.cend(); + } + + Map::MapType::const_iterator Map::cend() const noexcept + { + return m_map.cend(); + } +} \ No newline at end of file diff --git a/src/Request.cpp b/src/Request.cpp new file mode 100644 index 0000000..0aaa7fe --- /dev/null +++ b/src/Request.cpp @@ -0,0 +1,163 @@ +#include "Request.h" +#include "Socket.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "Map.h" + +using namespace _webcxx_internal; + +static std::pair _split_header(std::string input, + std::string delim) { + int start = 0; + int end = input.find(delim); + if (end == std::string::npos) { + return {"", ""}; + } + return {input.substr(start, end), + input.substr(end + delim.size(), input.size() - end - delim.size())}; +} + +static bool is_pair_empty(const std::pair& p) { + return (p.first.size() == 0) && (p.second.size() == 0); +} + +std::pair +webcxx::Request::split_header(std::string input, std::string delim) { + auto result = _split_header(input, delim); + if (is_pair_empty(result)) + m_bad_request = true; + return result; +} + +static std::pair +remove_newlines(std::pair header) { + if (header.first[header.first.size() - 1] == '\n') { + header.first.pop_back(); + } + if (header.second[header.second.size() - 1] == '\n') { + header.second.pop_back(); + } + return header; +} + +void webcxx::Request::decode_http_header(std::string header) { + auto rc = split_header(header, " "); + if (rc.first == "GET") { + m_method = RequestType::GET; + } else if (rc.first == "POST") { + m_method = RequestType::POST; + } else if (rc.first == "PUT") { + m_method = RequestType::PUT; + } else if (rc.first == "DELETE") { + m_method = RequestType::DELETE; + } else { + m_bad_request = true; + return; + } + rc = split_header(rc.second, " "); + m_path = rc.first; + m_real_path = rc.first; + if (m_path.find('?') != std::string::npos) { + auto path_parts = split_header(m_path, "?"); + m_path = path_parts.first; + std::vector arg_strings; + std::pair p = + _split_header(path_parts.second, "&"); + while (!is_pair_empty(p)) { + arg_strings.push_back(p.first); + if(p.second.find("&") != std::string::npos) p = split_header(p.second, "&"); + else break; + } + if (arg_strings.size() == 0) { + arg_strings.push_back(path_parts.second); + } else { + arg_strings.push_back(p.second); + } + for (auto &arg : arg_strings) { + if (arg.find('=') != std::string::npos) { + auto _p = split_header(arg, "="); + m_args[_p.first] = _p.second; + } else { + m_args[arg] = ""; + } + } + } + if (rc.second != "HTTP/1.1\n") { + m_bad_request = true; + } +} + +webcxx::Request::Request(int fd) { + std::string m_http_header_line = ""; + std::string m_request_header = ""; + char buffer[4096]; + if (sockgetline(fd, buffer, sizeof(buffer)) > 2) { + m_http_header_line = buffer; + decode_http_header(m_http_header_line); + while (sockgetline(fd, buffer, sizeof(buffer)) > 2) { + m_request_header = buffer; + m_headers.insert(remove_newlines(split_header(m_request_header, ": "))); + } + } else { + m_bad_request = true; + } + if(!(m_method == POST || m_method == PUT)) { m_content = ""; return; } + try { + std::string content_length = m_headers.at("Content-Length"); + int content_length_as_int; + int rc = sscanf(content_length.c_str(), "%d", &content_length_as_int); + if (rc != 1) { + printf("%d\n", rc); + m_bad_request = true; + return; + } + char *buf = (char *)malloc(content_length_as_int); + if (read(fd, buf, content_length_as_int) < 0) { + m_bad_request = true; + return; + } + m_content = buf; + free(buf); + } catch (std::out_of_range e) { + m_content = ""; + } +} + +static std::string method_to_string(webcxx::RequestType method) { + switch (method) { + case webcxx::RequestType::GET: + return "GET"; + case webcxx::RequestType::POST: + return "POST"; + case webcxx::RequestType::PUT: + return "PUT"; + case webcxx::RequestType::DELETE: + return "DELETE"; + default: + return "Unknown"; + } +} + +void webcxx::Request::log(int status_code) { + printf("%s - \"%s %s HTTP/1.1\" - %d\n", m_ip.c_str(), + method_to_string(m_method).c_str(), + m_real_path.c_str(), + status_code); + fflush(stdout); +} + +webcxx::Map webcxx::Request::headers() const +{ + return Map(m_headers); +} + +webcxx::Map webcxx::Request::args() const +{ + return Map(m_args); +} \ No newline at end of file diff --git a/src/Response.cpp b/src/Response.cpp new file mode 100644 index 0000000..623a818 --- /dev/null +++ b/src/Response.cpp @@ -0,0 +1,52 @@ +#include "Response.h" +#include "HTTPStatus.h" +#include +#include + +static int socket_send(int fd, const char *str, size_t size, int flags) { + return send(fd, str, size, flags); +}; + +void webcxx::Response::set_status(int status_code) +{ + status = status_code; +} + +webcxx::Response::Response(int status_code, std::string content, + std::string content_type, + std::map extra_headers) + : status(status_code), headers(extra_headers), content(content) { + headers["Content-Type"] = content_type; + if (content != "") { + std::ostringstream ss; + ss << content.size(); + headers["Content-Length"] = ss.str(); + } + headers["Server"] = "webcxx/0.1"; + headers["Connection"] = "close"; +} + +std::string webcxx::Response::to_string() { + std::ostringstream result; + result << "HTTP/1.1 "; + result << status; + result << " "; + result << _webcxx_internal::http_status_codes[status]; + result << "\r\n"; + for (auto &header : headers) { + result << header.first; + result << ": "; + result << header.second; + result << "\r\n"; + } + result << "\r\n"; + if (content != "") { + result << content; + } + return result.str(); +} + +int webcxx::Response::send(int fd) { + std::string as_string = to_string(); + return socket_send(fd, as_string.c_str(), as_string.size(), 0); +} \ No newline at end of file diff --git a/src/Socket.cpp b/src/Socket.cpp new file mode 100644 index 0000000..bd8d669 --- /dev/null +++ b/src/Socket.cpp @@ -0,0 +1,42 @@ +#include "Socket.h" +#include + +namespace _webcxx_internal { +char sockgetchar(int fd) { + char c; + if (recv(fd, &c, 1, 0) < 0) { + return -1; + } + return c; +} +char sockpeekchar(int fd) { + char c; + if (recv(fd, &c, 1, MSG_PEEK) < 0) { + return -1; + } + return c; +} +int sockgetline(int fd, char *buf, size_t n) { + int i = 0; + char c = '\0'; + + while ((i < n - 1) && (c != '\n')) { + c = sockgetchar(fd); + if (c > 0) { + if (c == '\r') { + c = sockpeekchar(fd); + if ((c > 0) && (c == '\n')) + c = sockgetchar(fd); + else + c = '\n'; + } + buf[i] = c; + i++; + } else + c = '\n'; + } + buf[i] = '\0'; + + return i; +} +} // namespace _webcxx_internal \ No newline at end of file diff --git a/src/Socket.h b/src/Socket.h new file mode 100644 index 0000000..30cb24f --- /dev/null +++ b/src/Socket.h @@ -0,0 +1,8 @@ +#pragma once +#include + +namespace _webcxx_internal { +char sockgetchar(int fd); +char sockpeekchar(int fd); +int sockgetline(int fd, char *buf, size_t n); +} // namespace _webcxx_internal \ No newline at end of file