17 changed files with 525 additions and 0 deletions
@ -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" ]' |
||||
@ -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") |
||||
@ -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. |
||||
|
||||
@ -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<std::size_t>::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<std::size_t>::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. |
||||
``` |
||||
@ -0,0 +1,15 @@
|
||||
#pragma once |
||||
#include <experimental/filesystem> |
||||
#include <json.hpp> |
||||
|
||||
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(); |
||||
}; |
||||
@ -0,0 +1,17 @@
|
||||
#pragma once |
||||
#include <web_server.hpp> |
||||
|
||||
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<HttpServer::Response> response); |
||||
static void https_required(std::shared_ptr<HttpsServer::Response> response); |
||||
static void bad_json(std::shared_ptr<HttpServer::Response> response); |
||||
static void bad_json(std::shared_ptr<HttpsServer::Response> response); |
||||
static void bad_object(std::shared_ptr<HttpServer::Response> response); |
||||
static void bad_object(std::shared_ptr<HttpsServer::Response> response); |
||||
}; |
||||
@ -0,0 +1,4 @@
|
||||
#pragma once |
||||
#include <nlohmann/json.hpp> |
||||
|
||||
using nlohmann::json; |
||||
@ -0,0 +1,9 @@
|
||||
#pragma once |
||||
#include <json.hpp> |
||||
#include <web_server.hpp> |
||||
|
||||
class convert { |
||||
public: |
||||
static void from_json(const json &, HttpServer::Config &); |
||||
static void from_json(const json &, HttpsServer::Config &); |
||||
}; |
||||
@ -0,0 +1,18 @@
|
||||
#pragma once |
||||
#include <json.hpp> |
||||
#include <web_server.hpp> |
||||
|
||||
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<HttpsServer> https_server = nullptr; |
||||
|
||||
static Application &get(const json &config); |
||||
int run(); |
||||
}; |
||||
@ -0,0 +1,8 @@
|
||||
#pragma once |
||||
|
||||
#include <server_http.hpp> |
||||
#include <server_https.hpp> |
||||
|
||||
using HttpServer = SimpleWeb::Server<SimpleWeb::HTTP>; |
||||
using Status = SimpleWeb::StatusCode; |
||||
using HttpsServer = SimpleWeb::Server<SimpleWeb::HTTPS>; |
||||
@ -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") |
||||
@ -0,0 +1,52 @@
|
||||
#include <config.hpp> |
||||
#include <fstream> |
||||
#include <iostream> |
||||
#include <unistd.h> |
||||
|
||||
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); |
||||
} |
||||
} |
||||
@ -0,0 +1,50 @@
|
||||
#include <http.hpp> |
||||
#include <json.hpp> |
||||
|
||||
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<HttpServer::Response> 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<HttpsServer::Response> 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<HttpServer::Response> 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<HttpsServer::Response> 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<HttpServer::Response> 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<HttpsServer::Response> response) { |
||||
return response->write(Status::client_error_bad_request, |
||||
bad_object_msg.dump(), |
||||
{header_access_control, header_application_data}); |
||||
} |
||||
@ -0,0 +1,27 @@
|
||||
#include <json_converters.hpp> |
||||
|
||||
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); |
||||
}; |
||||
@ -0,0 +1,7 @@
|
||||
#include <config.hpp> |
||||
#include <store.hpp> |
||||
|
||||
int main() { |
||||
config config; |
||||
return Application::get(config).run(); |
||||
} |
||||
@ -0,0 +1,102 @@
|
||||
#include <http.hpp> |
||||
#include <json_converters.hpp> |
||||
#include <store.hpp> |
||||
|
||||
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<HttpsServer>(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<std::thread> servers; |
||||
|
||||
if (https_server) { |
||||
https_server->default_resource["GET"] = |
||||
[&](std::shared_ptr<HttpsServer::Response> response, |
||||
std::shared_ptr<HttpsServer::Request>) { |
||||
response->write(Status::success_ok, store.dump(), |
||||
{header_access_control, header_application_data}); |
||||
}; |
||||
|
||||
https_server->default_resource["POST"] = |
||||
[&](std::shared_ptr<HttpsServer::Response> response, |
||||
std::shared_ptr<HttpsServer::Request> 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<HttpServer::Response> 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<HttpServer::Response> response, |
||||
std::shared_ptr<HttpServer::Request> 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; |
||||
} |
||||
Loading…
Reference in new issue