From c05668b9c16d88bedac78dea002c91e462568891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Sverre=20Lien=20Sell=C3=A6g?= Date: Sat, 22 May 2021 16:44:52 +0200 Subject: [PATCH] created store --- .gitignore | 1 + .gitlab-ci.yml | 53 ++++++++++++++++++ CMakeLists.txt | 22 ++++++++ LICENSE | 22 ++++++++ README.md | 107 ++++++++++++++++++++++++++++++++++++ include/config.hpp | 15 +++++ include/http.hpp | 17 ++++++ include/json.hpp | 4 ++ include/json_converters.hpp | 9 +++ include/store.hpp | 18 ++++++ include/web_server.hpp | 8 +++ src/CMakeLists.txt | 11 ++++ src/config.cpp | 52 ++++++++++++++++++ src/http.cpp | 50 +++++++++++++++++ src/json_converters.cpp | 27 +++++++++ src/main.cpp | 7 +++ src/store.cpp | 102 ++++++++++++++++++++++++++++++++++ 17 files changed, 525 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 include/config.hpp create mode 100644 include/http.hpp create mode 100644 include/json.hpp create mode 100644 include/json_converters.hpp create mode 100644 include/store.hpp create mode 100644 include/web_server.hpp create mode 100644 src/CMakeLists.txt create mode 100644 src/config.cpp create mode 100644 src/http.cpp create mode 100644 src/json_converters.cpp create mode 100644 src/main.cpp create mode 100644 src/store.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..c4639c9 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,53 @@ +variables: + GIT_SUBMODULE_STRATEGY: recursive + +stages: + - lint + - test + +.script: &compile + stage: test + script: + - mkdir build && cd build + - CXXFLAGS=-Werror cmake .. + - make -j$(nproc) + +arch: + image: registry.gitlab.com/eidheim/docker-images:arch + <<: *compile + +static-analysis: + image: registry.gitlab.com/eidheim/docker-images:arch + stage: test + script: + - mkdir build && cd build + - scan-build cmake .. + - scan-build --status-bugs make -j$(nproc) + +thread-safety-analysis: + image: registry.gitlab.com/eidheim/docker-images:arch + stage: test + script: + - mkdir build && cd build + - CXX=clang++ CXXFLAGS=-Werror cmake .. + - make -j$(nproc) + +address-sanitizer: + image: registry.gitlab.com/eidheim/docker-images:arch + stage: test + script: + - mkdir build && cd build + - CXXFLAGS="-fsanitize=address" cmake .. + - make -j$(nproc) + +check-format: + image: cppit/jucipp:arch + stage: lint + script: + - 'find src -name "*.cpp" -exec clang-format --Werror --assume-filename={} {} -n 2>> lint-errors.txt \;' + - 'find include -name "*.hpp" -exec clang-format --Werror --assume-filename={} {} -n 2>> lint-errors.txt \;' + - 'find tests -name "*.cpp" -exec clang-format --Werror --assume-filename={} {} -n 2>> lint-errors.txt \;' + - 'find tests -name "*.hpp" -exec clang-format --Werror --assume-filename={} {} -n 2>> lint-errors.txt \;' + - 'HAS_ERRORS=$(cat lint-errors.txt | wc -l)' + - '[ "$HAS_ERRORS" == "0" ] || cat lint-errors.txt' + - '[ "$HAS_ERRORS" == "0" ]' diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5dac4b6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 2.8) + +project(store) + +find_program(CCACHE_FOUND ccache) +if(CCACHE_FOUND) + set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache) + set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache) +else() + message(STATUS "ccache was not found.") +endif(CCACHE_FOUND) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wunused-parameter") + +set(BUILD_TESTING OFF CACHE INTERNAL "") +add_subdirectory("${CMAKE_SOURCE_DIR}/lib/Simple-Web-Server") + +set(JSON_BuildTests OFF CACHE INTERNAL "") +add_subdirectory("${CMAKE_SOURCE_DIR}/lib/json") + +add_subdirectory("${CMAKE_SOURCE_DIR}/src") diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..acdea8a --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2021 Jørgen Sverre Lien Sellæg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..8640d96 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ +# Subscribe + +## About +Simple JSON store written in C++. + +## Dependencies +* [simple-web-server](http://gitlab.com/eidheim/Simple-Web-Server/) +* [json](https://github.com/nlohmann/json) + +## Build +```sh +git clone https://gitlab.com/zalox/store --recursive +cd store +mkdir build +cd build +cmake .. +make -j$(nproc) +``` + +## Usage + +## Configuration +NOTE: The default value will be used if a field is omitted. +Headers are on the format `["Accept", "*"]`; +The configuration is passed directly to [the webserver](https://gitlab.com/eidheim/Simple-Web-Server). + +```javascript +{ + "http": { + "port": 80, + // If io_service is not set, number of threads that the server will use when start() is called. + // Defaults to 1 thread. + "thread_pool_size": 1, + // Timeout on request completion. Defaults to 5 seconds. + "timeout_request": 5, + // Timeout on request/response content completion. Defaults to 300 seconds. + "timeout_content": 300, + // Maximum size of request stream buffer. Defaults to architecture maximum. + // Reaching this limit will result in a message_size error code. + // default: (std::numeric_limits::max)() + "max_request_streambuf_size": 18446744073709551615, + // IPv4 address in dotted decimal form or IPv6 address in hexadecimal notation. + // If empty, the address will be any address. + "address": "", + // Set to false to avoid binding the socket to an address that is already in use. Defaults to true. + "reuse_address": true, + // Make use of RFC 7413 or TCP Fast Open (TFO) + "fast_open": false + }, + { + "https": { + "port": 443, + // If io_service is not set, number of threads that the server will use when start() is called. + // Defaults to 1 thread. + "thread_pool_size": 1, + // Timeout on request completion. Defaults to 5 seconds. + "timeout_request": 5, + // Timeout on request/response content completion. Defaults to 300 seconds. + "timeout_content": 300, + // Maximum size of request stream buffer. Defaults to architecture maximum. + // Reaching this limit will result in a message_size error code. + // default: (std::numeric_limits::max)() + "max_request_streambuf_size": 18446744073709551615, + // IPv4 address in dotted decimal form or IPv6 address in hexadecimal notation. + // If empty, the address will be any address. + "address": "", + // Set to false to avoid binding the socket to an address that is already in use. Defaults to true. + "reuse_address": true, + // Make use of RFC 7413 or TCP Fast Open (TFO) + "fast_open": false + } +} + +``` +### Example configuration +A very simple https configuration (recommended): +``` javascript +{ + "https": { + "cert": "/etc/ssl/cert.crt", + "key": "/etc/ssl/key.pem" + } +} + +``` +A very simple http configuration (default): +``` javascript +{} +``` + +# Troubleshooting + +## Errors +### Client errors +```yaml +;; The server will return this error message if the server is configured with +;; https and the client attempts to access it over http. +100: Your server is configured to use https, but a request on http was recived. + +;; The server will return this error message if the body of your request is +;; invalid json. Consider using a validator to find your mistake. +101: Your request was not a valid JSON document. + +;; The server will return this error message if the root of your JSON request +;; is not {}. +102: Your document was not a valid JSON object. +``` \ No newline at end of file diff --git a/include/config.hpp b/include/config.hpp new file mode 100644 index 0000000..1f7c220 --- /dev/null +++ b/include/config.hpp @@ -0,0 +1,15 @@ +#pragma once +#include +#include + +namespace fs = std::experimental::filesystem; + +class config : public json { + static fs::path &get_config_path(); + void create_config_directory(); + void create_config_file(const fs::path &config_file_path); + void load_config_file(const fs::path &config_file_path); + +public: + config(); +}; \ No newline at end of file diff --git a/include/http.hpp b/include/http.hpp new file mode 100644 index 0000000..b3d2825 --- /dev/null +++ b/include/http.hpp @@ -0,0 +1,17 @@ +#pragma once +#include + +const auto header_application_data = + std::make_pair("Content-Type", "application/json"); +const auto header_access_control = + std::make_pair("Access-Control-Allow-Origin", "*"); + +class response { +public: + static void https_required(std::shared_ptr response); + static void https_required(std::shared_ptr response); + static void bad_json(std::shared_ptr response); + static void bad_json(std::shared_ptr response); + static void bad_object(std::shared_ptr response); + static void bad_object(std::shared_ptr response); +}; \ No newline at end of file diff --git a/include/json.hpp b/include/json.hpp new file mode 100644 index 0000000..e4ad9b5 --- /dev/null +++ b/include/json.hpp @@ -0,0 +1,4 @@ +#pragma once +#include + +using nlohmann::json; diff --git a/include/json_converters.hpp b/include/json_converters.hpp new file mode 100644 index 0000000..a5c6981 --- /dev/null +++ b/include/json_converters.hpp @@ -0,0 +1,9 @@ +#pragma once +#include +#include + +class convert { +public: + static void from_json(const json &, HttpServer::Config &); + static void from_json(const json &, HttpsServer::Config &); +}; diff --git a/include/store.hpp b/include/store.hpp new file mode 100644 index 0000000..a8ffe32 --- /dev/null +++ b/include/store.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include + +class Application { + HttpServer http_server; + + static void web_server_started(std::size_t port); + static void secure_web_server_started(std::size_t port); + Application() = delete; + Application(const json &cfg); + +public: + std::shared_ptr https_server = nullptr; + + static Application &get(const json &config); + int run(); +}; diff --git a/include/web_server.hpp b/include/web_server.hpp new file mode 100644 index 0000000..f363f2e --- /dev/null +++ b/include/web_server.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +using HttpServer = SimpleWeb::Server; +using Status = SimpleWeb::StatusCode; +using HttpsServer = SimpleWeb::Server; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..0bac30e --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,11 @@ +file(GLOB source_files "*.cpp") + +add_executable(store ${source_files}) +target_link_libraries(store + PRIVATE + nlohmann_json::nlohmann_json + PUBLIC + stdc++fs + simple-web-server) +target_include_directories(store PRIVATE "${CMAKE_SOURCE_DIR}/lib/json/include") +target_include_directories(store PRIVATE "${CMAKE_SOURCE_DIR}/include") diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 0000000..4feb71f --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,52 @@ +#include +#include +#include +#include + +void config::create_config_directory() { + std::error_code ec; + fs::create_directories(get_config_path(), ec); + if (ec) { + std::cerr << "Unable to create configuration directory:\n" << ec.message(); + } +} + +fs::path &config::get_config_path() { + static fs::path p(""); + if (p.native().length() != 0) { + return p; + } + const int uid = getuid(); + if (uid == 0) { + p = fs::path("/") / "etc" / "store"; + } else if (const auto ptr = std::getenv("HOME")) { + p = fs::path(ptr) / ".config" / "store"; + } + return p; +} + +void config::create_config_file(const fs::path &config_file_path) { + try { + std::ofstream f(config_file_path); + f << json::object(); + } catch (const std::exception &e) { + std::cerr << "Could not create config file:\n" << e.what() << std::endl; + } +} + +void config::load_config_file(const fs::path &config_file_path) { + std::ifstream i(config_file_path.c_str()); + i >> *this; +} + +config::config() : json(json::object()) { + if (!fs::exists(get_config_path())) { + create_config_directory(); + } + const auto config_file_path = get_config_path() / "config.json"; + if (!fs::exists(config_file_path)) { + create_config_file(config_file_path); + } else { + load_config_file(config_file_path); + } +} \ No newline at end of file diff --git a/src/http.cpp b/src/http.cpp new file mode 100644 index 0000000..927c756 --- /dev/null +++ b/src/http.cpp @@ -0,0 +1,50 @@ +#include +#include + +const auto https_required_msg = json::object({ + {"message", "100: Your server is configured to use https, but a " + "request on http was received."}, +}); + +const auto bad_json_msg = json::object({ + {"message", "101: Your request was not a valid JSON document."}, +}); + +const auto bad_object_msg = json::object({ + {"message", "102: Your request was not a valid store."}, +}); + +void response::https_required(std::shared_ptr response) { + return response->write(Status::client_error_bad_request, + https_required_msg.dump(), + {header_access_control, header_application_data}); +} + +void response::https_required(std::shared_ptr response) { + return response->write(Status::client_error_bad_request, + https_required_msg.dump(), + {header_access_control, header_application_data}); +} + +void response::bad_json(std::shared_ptr response) { + return response->write(Status::client_error_bad_request, bad_json_msg.dump(), + {header_access_control, header_application_data}); +} + +void response::bad_json(std::shared_ptr response) { + return response->write(Status::client_error_bad_request, bad_json_msg.dump(), + {header_access_control, header_application_data}); +} + + +void response::bad_object(std::shared_ptr response) { + return response->write(Status::client_error_bad_request, + bad_object_msg.dump(), + {header_access_control, header_application_data}); +} + +void response::bad_object(std::shared_ptr response) { + return response->write(Status::client_error_bad_request, + bad_object_msg.dump(), + {header_access_control, header_application_data}); +} diff --git a/src/json_converters.cpp b/src/json_converters.cpp new file mode 100644 index 0000000..2d8d734 --- /dev/null +++ b/src/json_converters.cpp @@ -0,0 +1,27 @@ +#include + +void convert::from_json(const json &json, HttpServer::Config &config) { + config.port = json.value("port", config.port); + config.timeout_request = + json.value("timeout_request", config.timeout_request); + config.timeout_content = + json.value("timeout_content", config.timeout_content); + config.max_request_streambuf_size = json.value( + "max_request_streambuf_size", config.max_request_streambuf_size); + config.address = json.value("address", config.address); + config.reuse_address = json.value("reuse_address", config.reuse_address); + config.fast_open = json.value("fast_open", config.fast_open); +}; + +void convert::from_json(const json &json, HttpsServer::Config &config) { + config.port = json.value("port", config.port); + config.timeout_request = + json.value("timeout_request", config.timeout_request); + config.timeout_content = + json.value("timeout_content", config.timeout_content); + config.max_request_streambuf_size = json.value( + "max_request_streambuf_size", config.max_request_streambuf_size); + config.address = json.value("address", config.address); + config.reuse_address = json.value("reuse_address", config.reuse_address); + config.fast_open = json.value("fast_open", config.fast_open); +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..ad2ef1b --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,7 @@ +#include +#include + +int main() { + config config; + return Application::get(config).run(); +} diff --git a/src/store.cpp b/src/store.cpp new file mode 100644 index 0000000..a0d2763 --- /dev/null +++ b/src/store.cpp @@ -0,0 +1,102 @@ +#include +#include +#include + +void Application::web_server_started(std::size_t port) { + std::cout << "Web server started and listening on " << port << ".\n"; +}; + +void Application::secure_web_server_started(std::size_t port) { + std::cout << "Secure web server started and listening on " << port << ".\n"; +}; + +Application::Application(const nlohmann::json &cfg) { + convert::from_json(cfg.value("http", json::object()), http_server.config); + if (cfg.contains("https")) { + const auto https = cfg["https"]; + if (https.contains("cert") && https.contains("key")) { + https_server = std::make_shared(https["cert"], https["key"]); + } + } + + if (https_server) { + convert::from_json(cfg.value("https", json::object()), + https_server->config); + } +} + +int Application::run() { + auto store = json::object(); + std::vector servers; + + if (https_server) { + https_server->default_resource["GET"] = + [&](std::shared_ptr response, + std::shared_ptr) { + response->write(Status::success_ok, store.dump(), + {header_access_control, header_application_data}); + }; + + https_server->default_resource["POST"] = + [&](std::shared_ptr response, + std::shared_ptr request) { + json data; + try { + request->content >> data; + } catch (...) { + return response::bad_json(response); + } + if (data.is_object()) { + store.update(data); + return response->write(Status::success_no_content); + } + return response->write(Status::client_error_bad_request); + }; + + servers.emplace_back( + [&]() { https_server->start(secure_web_server_started); }); + } + + http_server.default_resource["GET"] = + [&](std::shared_ptr response, ...) { + if (https_server) { + return response::https_required(response); + } + response->write(Status::success_ok, store.dump(), + {header_access_control, header_application_data}); + }; + + http_server.default_resource["POST"] = + [&](std::shared_ptr response, + std::shared_ptr request) { + if (https_server) { + return response::https_required(response); + } + + json data; + try { + request->content >> data; + } catch (...) { + return response::bad_json(response); + } + if (data.is_object()) { + store.update(data); + return response->write(Status::success_no_content); + } + return response->write(Status::client_error_bad_request); + }; + + + servers.emplace_back([&]() { http_server.start(web_server_started); }); + + for (auto &server : servers) { + server.join(); + } + + return 0; +} + +Application &Application::get(const json &config) { + static Application app(config); + return app; +}