Browse Source

created store

master
Jørgen Sverre Lien Sellæg 5 years ago
parent
commit
c05668b9c1
  1. 1
      .gitignore
  2. 53
      .gitlab-ci.yml
  3. 22
      CMakeLists.txt
  4. 22
      LICENSE
  5. 107
      README.md
  6. 15
      include/config.hpp
  7. 17
      include/http.hpp
  8. 4
      include/json.hpp
  9. 9
      include/json_converters.hpp
  10. 18
      include/store.hpp
  11. 8
      include/web_server.hpp
  12. 11
      src/CMakeLists.txt
  13. 52
      src/config.cpp
  14. 50
      src/http.cpp
  15. 27
      src/json_converters.cpp
  16. 7
      src/main.cpp
  17. 102
      src/store.cpp

1
.gitignore vendored

@ -0,0 +1 @@
build

53
.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" ]'

22
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")

22
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.

107
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<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.
```

15
include/config.hpp

@ -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();
};

17
include/http.hpp

@ -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);
};

4
include/json.hpp

@ -0,0 +1,4 @@
#pragma once
#include <nlohmann/json.hpp>
using nlohmann::json;

9
include/json_converters.hpp

@ -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 &);
};

18
include/store.hpp

@ -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();
};

8
include/web_server.hpp

@ -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>;

11
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")

52
src/config.cpp

@ -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);
}
}

50
src/http.cpp

@ -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});
}

27
src/json_converters.cpp

@ -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);
};

7
src/main.cpp

@ -0,0 +1,7 @@
#include <config.hpp>
#include <store.hpp>
int main() {
config config;
return Application::get(config).run();
}

102
src/store.cpp

@ -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…
Cancel
Save