10 changed files with 852 additions and 13 deletions
@ -0,0 +1,65 @@
|
||||
cmake_minimum_required (VERSION 3.0.2) |
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/share/cmake_modules/") |
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14") |
||||
|
||||
set(project_name toREST) |
||||
project (${project_name}) |
||||
|
||||
message(${CMAKE_MODULE_PATH}) |
||||
|
||||
find_package(Boost COMPONENTS regex system thread coroutine context filesystem date_time REQUIRED) |
||||
find_package(LibTorrent REQUIRED) |
||||
find_package(OpenSSL REQUIRED) |
||||
|
||||
set(BT_INCLUDE_DIR ./include) |
||||
set(LIB_INCLUDE_DIR ./lib) |
||||
|
||||
file(GLOB source_files "./src/*.cpp") |
||||
|
||||
include_directories( |
||||
${Boost_INCLUDE_DIRS} |
||||
${OPENSSL_INCLUDE_DIR} |
||||
${LIBTORRENT_INCLUDE_DIR} |
||||
${BT_INCLUDE_DIR} |
||||
${LIB_INCLUDE_DIR} |
||||
) |
||||
|
||||
set(global_libraries |
||||
${Boost_LIBRARIES} |
||||
${CMAKE_THREAD_LIBS_INIT} |
||||
${LIBTORRENT_LIBRARY} |
||||
${OPENSSL_CRYPTO_LIBRARY} |
||||
) |
||||
|
||||
add_library(project_shared OBJECT ${source_files}) |
||||
|
||||
add_executable(${project_name} ./src/main.cxx $<TARGET_OBJECTS:project_shared>) |
||||
target_link_libraries(${project_name} ${global_libraries}) |
||||
|
||||
enable_testing() |
||||
|
||||
file(GLOB test_files "./tests/*.cpp") |
||||
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -Wall -fprofile-arcs -ftest-coverage ") |
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0 -Wall -fprofile-arcs -ftest-coverage ") |
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage ") |
||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage ") |
||||
|
||||
set(test yea) |
||||
add_executable(${test} ${test_files} $<TARGET_OBJECTS:project_shared>) |
||||
target_include_directories(${test} PUBLIC ../lib/Catch) |
||||
target_link_libraries(${test} ${global_libraries}) |
||||
add_test(${test} ${test}) |
||||
|
||||
find_package(Doxygen) |
||||
|
||||
if(DOXYGEN_FOUND) |
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/docs/Doxyfile.in ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile @ONLY) |
||||
add_custom_target(doc |
||||
${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile |
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../include |
||||
COMMENT "Generating API documentation with Doxygen to ${CMAKE_CURRENT_BINARY_DIR}" VERBATIM |
||||
) |
||||
endif(DOXYGEN_FOUND) |
||||
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
# Find libtorrent-rasterbar |
||||
find_path(LIBTORRENT_INCLUDE_DIR libtorrent /usr/include /usr/local/include) |
||||
find_library(LIBTORRENT_LIBRARY torrent-rasterbar /usr/lib /usr/local/lib) |
||||
|
||||
if(LIBTORRENT_INCLUDE_DIR AND LIBTORRENT_LIBRARY) |
||||
set(LIBTORRENT_FOUND TRUE) |
||||
endif(LIBTORRENT_INCLUDE_DIR AND LIBTORRENT_LIBRARY) |
||||
|
||||
if(LIBTORRENT_FOUND) |
||||
message(STATUS "Found libtorrent: ${LIBTORRENT_LIBRARY}") |
||||
else(LIBTORRENT_FOUND) |
||||
message(FATAL_ERROR "libtorrent not found!") |
||||
endif(LIBTORRENT_FOUND) |
||||
@ -0,0 +1,92 @@
|
||||
#ifndef _TOREST_HTTP_HPP_ |
||||
#define _TOREST_HTTP_HPP_ |
||||
|
||||
#include <json.hpp> |
||||
#include <unordered_map> |
||||
|
||||
class http { |
||||
public: |
||||
/// http codes
|
||||
enum status { |
||||
/*! Standard response for successful HTTP requests. */ ok=200, |
||||
/*! The request has been fulfilled, resulting in the creation of a new resource. */ created=201, |
||||
/*! The request has been accepted for processing, but the processing has not been completed. */ accepted=202, |
||||
/*! The server successfully processed the request and is not returning any content. */ no_content=204, |
||||
/*! The server cannot or will not process the request due to an apparent client error */ bad_request=400, |
||||
/*! Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. */ unauthorized=401, |
||||
/*! Reserved for future use. The */ payment_required=402, |
||||
/*! the request was a valid request, but the server is refusing to respond to it. */ forbidden=403, |
||||
/*! The requested resource could not be found but may be available in the future. */ not_found=404, |
||||
/*! Allowed A request method is not supported for the requested resource */ method_not_allowed=405, |
||||
/*! The resource identified by the request is only capable of generating response entities which have content characteristics not acceptable according to the accept headers sent in the request. */ not_acceptable=406, |
||||
/*! A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. */ internal_server_error=500, |
||||
/*! The server either does not recognize the request method, or it lacks the ability to fulfill the request. */ not_implemented=501, |
||||
/*! The server was acting as a gateway or proxy and received an invalid response from the upstream server. */ bad_gateway=502, |
||||
/*! The server is currently unavailable (because it is overloaded or down for maintenance). Generally, this is a temporary state. */ service_unavailable=503, |
||||
/*! Gateway timed out */ gateway_timeout=504, |
||||
/*! http-version not supported */ http_version_not_supported=505 |
||||
}; |
||||
typedef std::pair<status,std::string> code; |
||||
typedef std::pair<std::string,std::string> header; |
||||
typedef std::unordered_map<std::string,std::string> headers; |
||||
|
||||
/// creates a http::code object out of a http::status code
|
||||
static code http_code(status status){switch(status){case accepted:return{status,"Accepted"};case bad_gateway:return{status,"Bad gateway"};case bad_request:return{status,"Bad Request"};case created:return{status,"Created"};case forbidden:return{status,"Forbidden"};case gateway_timeout:return{status,"Gateway Timeout"};case http_version_not_supported:return{status,"HTTP Version Not Supported"};case internal_server_error:return{status,"Internal Server Error"};case method_not_allowed:return{status,"Method Not Allowed"};case not_acceptable:return{status,"Not Acceptable"};case no_content:return{status,"No Content"};case not_found:return{status,"Not Found"};case not_implemented:return{status,"Not Implemented"};case ok:return{status,"OK"};case payment_required:return{status,"Payment Required"};case service_unavailable:return{status,"Service Unavailable"};case unauthorized:return{status,"Unauthorized"};default: return {status,"UNKNOWN"};}} |
||||
|
||||
class basic_response { |
||||
protected: |
||||
virtual std::ostream& do_response(std::ostream& os)const=0; |
||||
public: |
||||
virtual ~basic_response(){} |
||||
virtual void add_header(const http::header &header)=0; |
||||
virtual void set_status(http::status code)=0; |
||||
friend std::ostream& operator<<(std::ostream& os,const http::basic_response &rh){ return rh.do_response(os); } |
||||
}; |
||||
|
||||
class response : public basic_response { |
||||
public: |
||||
response():status_code(http::http_code(http::ok)){} |
||||
void add_header(const http::header &header) override { response_headers.emplace(header); } |
||||
void set_status(http::status code) override { status_code=http_code(code); } |
||||
void set_body(std::string content) { body=content; } |
||||
protected: |
||||
std::ostream& do_response(std::ostream &os) const override { |
||||
os << "HTTP/1.1" << " " << status_code.first << " " << status_code.second << "\r\n"; |
||||
for(auto &header:response_headers) |
||||
os << header.first << ": " << header.second << "\r\n"; |
||||
if(status_code.first!=http::no_content){ |
||||
os << "Content-Length: "; |
||||
if(!body.empty()){ |
||||
return os << body.size() << "\r\n\r\n" << body; |
||||
} else { |
||||
auto body_replace=std::to_string(status_code.first) + " " + status_code.second; |
||||
return os << body_replace.size() << "\r\n\r\n" << body_replace; |
||||
} |
||||
} |
||||
return os << "\r\n"; |
||||
} |
||||
protected: |
||||
headers response_headers; |
||||
std::string body; |
||||
code status_code; |
||||
}; |
||||
|
||||
/// A JSON response. The default constructor sets the Content-Type header to
|
||||
/// application/json, the response defaults to 200 OK.
|
||||
class json_response : public response { |
||||
public: |
||||
json_response(){ |
||||
add_header({"Content-Type","application/json"}); |
||||
set_body({{"code",status_code.first},{"status",status_code.second}}); |
||||
} |
||||
/// Method updates the http status code. set_statups also updates the body to correspond to the new status.
|
||||
/// Notice: if you previously set a body, it will be overwritten by the status code JSON representation.
|
||||
void set_status(http::status code) override { |
||||
response::set_status(code); |
||||
set_body({{"code",status_code.first},{"status",status_code.second}}); |
||||
} |
||||
void set_body(nlohmann::json json){ response::set_body(json.dump()); } |
||||
}; |
||||
};;; |
||||
|
||||
#endif // _TOREST_HTTP_HPP_
|
||||
@ -0,0 +1,27 @@
|
||||
#ifndef _TOREST_RESOURCE_HPP_ |
||||
#define _TOREST_RESOURCE_HPP_ |
||||
|
||||
#include <http.hpp> |
||||
|
||||
class resource_base { |
||||
friend std::ostream& operator<<(std::ostream& os,const resource_base& rs) |
||||
{ os << rs.json_response; return os; } |
||||
public: |
||||
virtual ~resource_base(){} |
||||
/// GET Read 200 (OK), list of customers. Use pagination, sorting and filtering to navigate big lists. 200 (OK), single customer. 404 (Not Found), if ID not found or invalid.
|
||||
virtual void get(nlohmann::json data=nullptr){} |
||||
/// PATCH Update/Modify 404 (Not Found), unless you want to modify the collection itself. 200 (OK) or 204 (No Content). 404 (Not Found), if ID not found or invalid.
|
||||
virtual void patch(nlohmann::json data=nullptr){} |
||||
/// POST Create 201 (Created), 'Location' header with link to /customers/{id} containing new ID. 404 (Not Found), 409 (Conflict) if resource already exists..
|
||||
virtual void post(nlohmann::json data=nullptr){} |
||||
/// DELETE Delete 404 (Not Found), unless you want to delete the whole collection—not often desirable. 200 (OK). 404 (Not Found), if ID not found or invalid.
|
||||
virtual void del(nlohmann::json data=nullptr){} |
||||
/// PUT Update/Replace 404 (Not Found), unless you want to update/replace every resource in the entire collection. 200 (OK) or 204 (No Content). 404 (Not Found), if ID not found or invalid.
|
||||
virtual void put(nlohmann::json data=nullptr){} |
||||
/// get a reference to the underlying JSON response
|
||||
http::json_response& get_response(){ return json_response; } |
||||
protected: |
||||
http::json_response json_response; |
||||
}; |
||||
|
||||
#endif // _TOREST_RESOURCE_HPP_
|
||||
@ -0,0 +1,250 @@
|
||||
#ifndef _TOREST_RESOURCE_SESSION_HPP_ |
||||
#define _TOREST_RESOURCE_SESSION_HPP_ |
||||
|
||||
#include <resource.hpp> |
||||
#include <util.hpp> |
||||
#include <libtorrent/session.hpp> |
||||
|
||||
namespace session { |
||||
class basic_manager { |
||||
public: |
||||
virtual ~basic_manager()=0; |
||||
virtual nlohmann::json get_json() const = 0; |
||||
virtual bool patch(const nlohmann::json &data) = 0; |
||||
virtual bool is_valid() const = 0; |
||||
virtual int listen_port() const = 0; |
||||
virtual void set_session(std::shared_ptr<libtorrent::session>) = 0; |
||||
virtual void set_listen_port() = 0; |
||||
// virtual std::shared_ptr<libtorrent::session> get_session() = 0;
|
||||
}; |
||||
|
||||
class translate { |
||||
public: |
||||
static nlohmann::json to_json(const libtorrent::session_handle &handle){ |
||||
nlohmann::json session_json; |
||||
return session_json; |
||||
} |
||||
}; |
||||
|
||||
class manager : public basic_manager { |
||||
public: |
||||
// std::shared_ptr<libtorrent::session> get_session() override { return handle; }
|
||||
void set_session(std::shared_ptr<libtorrent::session> session) override { handle=session; } |
||||
nlohmann::json get_json() const override { |
||||
nlohmann::json session_json; |
||||
session_json["peer_id"] = handle->id().to_string(); |
||||
session_json["is_paused"] = handle->is_paused(); |
||||
session_json["is_listening"] = handle->is_listening(); |
||||
session_json["listen_port"] = handle->listen_port(); |
||||
session_json["ssl_listen_port"] = handle->ssl_listen_port(); |
||||
return session_json; |
||||
}; |
||||
bool is_valid() const override { return handle && handle->is_valid(); } |
||||
bool patch(const nlohmann::json &data) override { |
||||
int is_paused=data["is_paued"]; |
||||
int listen_port=data["listen_port"]; |
||||
|
||||
auto pause = [this](int is_paused){ |
||||
if(is_paused!=-1){ |
||||
if(is_paused==handle->is_paused()){ |
||||
return true; |
||||
}else if(is_paused==true){ |
||||
handle->resume(); |
||||
return true; |
||||
}else if(is_paused==false){ |
||||
handle->pause(); |
||||
return true; |
||||
} |
||||
} |
||||
return false; |
||||
}; |
||||
|
||||
auto listen = [this](int listen_port){ |
||||
if(listen_port!=-1&&listen_port>6880){ |
||||
libtorrent::settings_pack sp; |
||||
sp.set_str(libtorrent::settings_pack::listen_interfaces,"0.0.0.0:"+std::to_string(listen_port)); |
||||
handle->apply_settings(sp); |
||||
return true; |
||||
} |
||||
return false; |
||||
}; |
||||
|
||||
return pause(is_paused) && listen(listen_port); |
||||
}; |
||||
private: |
||||
std::shared_ptr<libtorrent::session> handle; |
||||
}; |
||||
|
||||
class basic_resource : public resource_base { |
||||
public: |
||||
virtual ~basic_resource() {} |
||||
virtual void set_session(std::shared_ptr<basic_manager>)=0; |
||||
}; |
||||
|
||||
class resource : public basic_resource { |
||||
public: |
||||
resource(){} //TODO remove?
|
||||
void set_session(std::shared_ptr<basic_manager> session_manager) override { mgr=session_manager; } |
||||
|
||||
virtual void get(nlohmann::json arg=nullptr) override { |
||||
if(mgr && mgr->is_valid()){ |
||||
get_response().set_status(http::ok); |
||||
get_response().set_body(mgr->get_json()); |
||||
}else{ |
||||
get_response().set_status(http::service_unavailable); |
||||
} |
||||
} |
||||
/**
|
||||
* PATCH /session |
||||
* -------------- |
||||
* \brief Change values on the session. |
||||
* @param[in] data takes an JSON object with at least one `action` set. |
||||
* available `action`s are `is_paused<bool>` `listen_port<bool>` |
||||
*/ |
||||
virtual void patch(const nlohmann::json data=nlohmann::json::object()) override { |
||||
if(mgr && mgr->is_valid()){ |
||||
if(data.is_object() && !data.is_null()){ |
||||
int is_paused=util::json::get<int>("is_paused",data,-1), |
||||
listen_port=util::json::get<int>("listen_port",data,-1); |
||||
bool at_at_least_one_option_is_set=is_paused!=-1||listen_port!=-1; |
||||
if(at_at_least_one_option_is_set){ |
||||
nlohmann::json patch={{"is_paused",is_paused},{"listen_port",listen_port}}; |
||||
if(mgr->patch(patch)){ |
||||
return get_response().set_status(http::ok); |
||||
}else{ |
||||
return get_response().set_status(http::internal_server_error); |
||||
} |
||||
}else{ |
||||
return get_response().set_status(http::bad_request); |
||||
} |
||||
} |
||||
return get_response().set_status(http::bad_request); |
||||
// auto data=data.array();
|
||||
}else{ |
||||
return get_response().set_status(http::service_unavailable); |
||||
} |
||||
} |
||||
|
||||
// @todo should we return service unavailable or just method not allowed?
|
||||
virtual void del(nlohmann::json arg=nullptr) override { get_response().set_status(http::method_not_allowed); } |
||||
virtual void put(nlohmann::json arg=nullptr) override { get_response().set_status(http::method_not_allowed); } |
||||
virtual void post(nlohmann::json arg=nullptr) override { get_response().set_status(http::method_not_allowed); } |
||||
protected: |
||||
std::shared_ptr<basic_manager> mgr; |
||||
}; |
||||
} |
||||
#endif // _TOREST_RESOURCE_SESSION_HPP_
|
||||
|
||||
/*! Resource: torrents
|
||||
Other: |
||||
- If multiple headers with the same key is posted, the server uses only the last. |
||||
- Server can return 503 Service Unavailable if for some reason a service is down */ |
||||
/*
|
||||
class torrents : public Resource<std::shared_ptr<HttpServer::Response>,std::shared_ptr<HttpServer::Request>> { |
||||
public: |
||||
torrents(libtorrent::session &session); |
||||
}; |
||||
*/ |
||||
// std::function<void(StreamType,StreamType)>
|
||||
|
||||
|
||||
/*! API-End-point: [POST] /v1/session/torrents
|
||||
Requirements: |
||||
- Content-Type: application/json |
||||
* This header must be set, otherwise toREST will return 403 Forbidden. |
||||
- Data: |
||||
* A valid JSON-object is required. |
||||
* A valid format: |
||||
```json |
||||
{ |
||||
"torrent": "<torrent_url>", |
||||
"save_path": "<path>" |
||||
} |
||||
``` |
||||
* Fields `torrent` and `save_path` can not be empty. toREST will return 400 Bad Request |
||||
* The `torrent` field can be a `local torrent file`, a `magnet-uri` or a HTTP-resource. |
||||
toREST will Accept every file with a valid url, uri or path, but will not download |
||||
or create torrents if the path is a invalid torrent file. |
||||
* The `save_path` field can be any location on the server machine, in which the invoking |
||||
user has write permissions. If the save_path is a directory, the torrent will download |
||||
into the directory. If the save_path is a file, toREST will return 400 Bad Request.
|
||||
|
||||
Side-Effects: |
||||
- 202 Accepted, if the request is valid. |
||||
* The `Location` header is set if the torrent info_hash is available |
||||
* If a WebSocket is open a TORRENT_ADDED action is posted when the torrent resource is available. |
||||
* |
||||
POST=[&session](std::shared_ptr<HttpServer::Response> resp,std::shared_ptr<HttpServer::Request> req){ |
||||
http::request_handler handler(req,resp); |
||||
|
||||
// If session is invalid there is no point hanging around.
|
||||
if(!session.is_valid()) |
||||
return handler.respond(i18N::session_unavailable,http::service_unavailable); |
||||
|
||||
// At least one header named with Content-Type, then check if the last one set is application/json
|
||||
if(handler.header_equals("Content-Type","application/json")) |
||||
return handler.respond(i18N::content_type_not_set,http::forbidden); |
||||
|
||||
// If the supplied JSON is invalid, bad request
|
||||
auto request_json=util::json::parse(req->content); |
||||
if(!request_json.is_object() || request_json.is_null()) |
||||
return handler.respond(nlohmann::json(i18N::unable_to_parse_json),http::bad_request); |
||||
|
||||
// handle required fields
|
||||
std::vector<std::string> required_fields={ |
||||
request_json.value("torrent",""), |
||||
request_json.value("save_path","") |
||||
}; |
||||
std::vector<std::string> empty_fields; |
||||
std::copy_if(required_fields.begin(),required_fields.end(),empty_fields.begin(),[](const std::string &field){return field.empty();}); |
||||
if(!empty_fields.empty()){ |
||||
std::string message; |
||||
for(auto &field:empty_fields) |
||||
message+=" The required field `"+field+"` is empty"; |
||||
return handler.respond(nlohmann::json(i18N::wrong_format + message),http::bad_request); |
||||
} |
||||
auto torrent_field=required_fields.front(); |
||||
libtorrent::add_torrent_params torrent_options; |
||||
boost::system::error_code ec; |
||||
if(torrent_field.substr(0,6)=="magnet"){ |
||||
libtorrent::parse_magnet_uri(torrent_field,torrent_options,ec); |
||||
ECERROR(ec); |
||||
} |
||||
else if(boost::filesystem::exists(torrent_field,ec) && !boost::filesystem::is_directory(torrent_field,ec)) |
||||
torrent_options.ti=boost::make_shared<libtorrent::torrent_info>(libtorrent::torrent_info(torrent_field,ec)); |
||||
else if(boost::regex_match(torrent_field,util::regex)) |
||||
torrent_options.url=torrent_field; |
||||
else { |
||||
ECERROR(ec); |
||||
return handler.respond(nlohmann::json(i18N::unable_to_parse_torrent_uri),http::bad_request); |
||||
} |
||||
boost::filesystem::path save_path(required_fields.back()); |
||||
auto status=boost::filesystem::status(save_path,ec); |
||||
if(!ec){ |
||||
if(boost::filesystem::exists(status) && !boost::filesystem::is_directory(status)) |
||||
return handler.respond(nlohmann::json(i18N::wrong_format + " The save path is invalid"),http::bad_request); |
||||
else { |
||||
// TODO chroot
|
||||
try { |
||||
boost::filesystem::create_directories(save_path); |
||||
} catch(const std::exception &ex){ |
||||
return handler.respond(nlohmann::json(i18N::write_error),http::forbidden); |
||||
} |
||||
}
|
||||
} else { |
||||
// invalid file status
|
||||
ECERROR(ec); |
||||
} |
||||
torrent_options.save_path=save_path.generic_string(); |
||||
session.async_add_torrent(torrent_options); |
||||
// TODO add websocket event
|
||||
std::string info_hash(torrent_options.info_hash.data()); |
||||
if(info_hash!="undefined"){ |
||||
std::stringstream ss; |
||||
ss << torrent_options.info_hash; |
||||
info_hash=ss.str(); |
||||
handler.headers["Location:"]= "/torrents/" + info_hash; |
||||
} |
||||
return handler.respond(nlohmann::json(http::http_code(http::accepted).second), http::accepted); |
||||
}; |
||||
}*/ |
||||
@ -0,0 +1,94 @@
|
||||
#ifndef _BT_HELPERS_HPP_ |
||||
#define _BT_HELPERS_HPP_ |
||||
|
||||
#include <unordered_map> |
||||
#include <boost/regex.hpp> |
||||
#include <json.hpp> |
||||
|
||||
namespace i18N { |
||||
using namespace std; |
||||
static const auto content_type_not_set= "A Content-Type header set to application/json is required."s; |
||||
static const auto unable_to_parse_json= "Unable to parse JSON in body into a JSON-object."s; |
||||
static const auto session_unavailable= "A server service is down, please try again later."s; |
||||
static const auto wrong_format= "A valid JSON request was posted, but the JSON format was wrong."s; |
||||
static const auto unable_to_parse_torrent_uri= "The torrent location was not accepted, the format was wrong."s; |
||||
static const auto write_error= "Unable to write to "; |
||||
}; |
||||
|
||||
namespace util { |
||||
class uri { |
||||
public: |
||||
auto static parse(const std::string &request_path){ |
||||
std::unordered_map<std::string,std::string> options; |
||||
std::string option, value; |
||||
bool collect=false; |
||||
for(auto &c:request_path){ |
||||
switch(c){ |
||||
case '&': |
||||
options.insert(std::make_pair(option,value)); |
||||
option=""; |
||||
collect=true; |
||||
break; |
||||
case '?': |
||||
collect=true; |
||||
break; |
||||
case '=': |
||||
collect=false; |
||||
value=""; |
||||
break; |
||||
default: |
||||
if(collect) |
||||
option+=c; |
||||
else |
||||
value+=c; |
||||
break; |
||||
} |
||||
} |
||||
if(!option.empty() || !value.empty()) |
||||
options.insert(std::make_pair(option,value)); |
||||
return options; |
||||
} |
||||
}; |
||||
const boost::regex regex("@(https?|ftp)://(-\\.)?([^\\s/?\\.#-]+\\.?)+(/[^\\s]*)?$@", boost::regex_constants::perl | boost::regex_constants::icase); |
||||
|
||||
class json { |
||||
public: |
||||
/*! @brief Wrapper for nlohmann::json::parse. If the parse fails, result.is_null() will be */ |
||||
static nlohmann::json parse(std::istream &istream) { |
||||
try { |
||||
return nlohmann::json::parse(istream); |
||||
} catch(const std::invalid_argument &exp) { |
||||
|
||||
} |
||||
return nlohmann::json(nullptr); |
||||
} |
||||
/*! @brief wrapper around nlohmann::json::parse, but object returns null on throw */ |
||||
static nlohmann::json parse(const std::string &string){ |
||||
try { |
||||
return nlohmann::json::parse(string); |
||||
} catch(const std::invalid_argument &exp) { |
||||
|
||||
} |
||||
return nlohmann::json(nullptr); |
||||
} |
||||
static bool has_property(const std::string &key, const nlohmann::json &object){ |
||||
auto it=object.find(key); |
||||
return it!=object.end(); |
||||
} |
||||
template<class T> |
||||
static auto get(const std::string &key, const nlohmann::json &object, const T &default_value){ |
||||
if(has_property(key,object)){ |
||||
T r; |
||||
try{ |
||||
r=object[key]; //TODO fix use json parser where a non-throwable exist
|
||||
} catch(const std::exception&){ |
||||
return default_value; |
||||
} |
||||
return r; |
||||
} |
||||
return default_value; |
||||
} |
||||
}; |
||||
} |
||||
|
||||
#endif // _BT_HELPERS_HPP_
|
||||
@ -0,0 +1,83 @@
|
||||
#include <server_http.hpp> |
||||
#include <session.hpp> |
||||
|
||||
typedef SimpleWeb::Server<SimpleWeb::HTTP> HttpServer; |
||||
|
||||
using namespace std; |
||||
|
||||
int main(int argc, char *argv[]) { |
||||
//auto config=filesystem::get_config();
|
||||
//auto wsc=config["webserver"];
|
||||
int http_port=8080, http_threads=4; |
||||
HttpServer http_server(http_port,http_threads); |
||||
http_server.default_resource["GET"]=[](std::shared_ptr<HttpServer::Response> resp, std::shared_ptr<HttpServer::Request> req) { |
||||
auto response = http::response(); |
||||
response.set_status(http::not_found); |
||||
*resp << response; |
||||
}; |
||||
session::resource session; |
||||
http_server.resource["^/v1/session$"]["GET"]=[&session](std::shared_ptr<HttpServer::Response> resp, std::shared_ptr<HttpServer::Request> req) { |
||||
session.get(); |
||||
*resp << session; |
||||
}; |
||||
|
||||
|
||||
/*
|
||||
http_server.resource["^/v1/session$"]["GET"]=[&session](HttpServer::Response &response, std::shared_ptr<HttpServer::Request> request) { |
||||
http http(&response, request.get()); |
||||
if(!session.is_valid()){ |
||||
return http.internal_server_error(); |
||||
} |
||||
auto json=translate::session::to_json(session); |
||||
http.json(json); |
||||
}; |
||||
http_server.resource["^/v1/session$"]["POST"]=[&session](HttpServer::Response &response, std::shared_ptr<HttpServer::Request> request) { |
||||
http http(&response, request.get()); |
||||
if(!http.is_json_request()){ |
||||
return http.not_found(); |
||||
} |
||||
if(!session.is_valid()){ |
||||
return http.internal_server_error(); |
||||
} |
||||
nlohmann::json json(request->content); |
||||
std::string action = json.value("action",""); |
||||
if(action==""){ |
||||
return http.bad_request("Invalid JSON request"); |
||||
} |
||||
if(action=="TORRENTS_PAUSE"){ |
||||
session.pause(); |
||||
} else if (action=="TORRENTS_START"){ |
||||
session.resume(); |
||||
} |
||||
return http.json(json); |
||||
}; |
||||
*/ |
||||
|
||||
/*
|
||||
std::thread websocketserver_thread([&websocket_server]() { |
||||
websocket_server.start(); |
||||
std::cout << "WebSocketServer listening on port " << config["websocket.port"] << std::endl; |
||||
}); |
||||
*/ |
||||
std::thread server_thread([&http_server](){ |
||||
http_server.start(); |
||||
}); |
||||
|
||||
std::cout << "HttpServer listening on port " << 8080 << std::endl; |
||||
|
||||
std::string input; |
||||
while(input != "q") { |
||||
std::getline(std::cin, input); |
||||
if(input == "q") { |
||||
std::cout << "Stopping server..." << std::endl; |
||||
http_server.stop(); // TODO check if throws
|
||||
std::cout << "Server stopped" << std::endl; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
server_thread.join(); |
||||
// websocketserver_thread.join();
|
||||
|
||||
return 0; |
||||
}; |
||||
@ -0,0 +1,120 @@
|
||||
#include <Catch/fakeit.hpp> |
||||
#include <http.hpp> |
||||
|
||||
using namespace std; |
||||
|
||||
TEST_CASE("Check http status"){ |
||||
auto out=http::http_code(http::ok); |
||||
REQUIRE(out.first == 200); |
||||
REQUIRE(out.second == "OK"); |
||||
// TODO test all
|
||||
} |
||||
|
||||
SCENARIO("http response"){ |
||||
auto response=http::response(); |
||||
std::stringstream ss; |
||||
GIVEN("we stream our response without setting given any data"){ |
||||
ss << response; |
||||
THEN("the response will default to 200") |
||||
REQUIRE(ss.str()=="HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\n200 OK"s);
|
||||
} |
||||
GIVEN("we stream our data with the response status set to 404"){ |
||||
response.set_status(http::not_found); |
||||
ss << response; |
||||
THEN("the default 404 response will be sent instead"){ |
||||
REQUIRE(ss.str()=="HTTP/1.1 404 Not Found\r\nContent-Length: 13\r\n\r\n404 Not Found"s); |
||||
} |
||||
} |
||||
GIVEN("we stream our response with the status set to 204 No Content"){ |
||||
response.set_status(http::no_content); |
||||
ss << response; |
||||
THEN("the response has no content header set") |
||||
REQUIRE(ss.str()=="HTTP/1.1 204 No Content\r\n\r\n"s); |
||||
} |
||||
GIVEN("we stream our response with the body set"){ |
||||
response.set_body("Easter eggs, everywhere! <a href=\"http://stream1.gifsoup.com/view4/4086928/x-x-everywhere-o.gif\">ye</a>"); |
||||
ss << response; |
||||
THEN("our response contains the appropriate headers") |
||||
REQUIRE(ss.str()=="HTTP/1.1 200 OK\r\nContent-Length: 103\r\n\r\nEaster eggs, everywhere! <a href=\"http://stream1.gifsoup.com/view4/4086928/x-x-everywhere-o.gif\">ye</a>"s); |
||||
} |
||||
} |
||||
|
||||
SCENARIO("Stream http responses with a json response"){ |
||||
auto response=http::json_response(); |
||||
std::stringstream ss; |
||||
GIVEN("we stream our response without setting any options"){ |
||||
ss << response; |
||||
THEN("our response is valid json and the response code is 200") |
||||
REQUIRE(ss.str()=="HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 26\r\n\r\n{\"code\":200,\"status\":\"OK\"}"s); |
||||
} |
||||
GIVEN("we set the response code to 404 and stream our response"){ |
||||
response.set_status(http::not_found); |
||||
ss << response; |
||||
THEN("our response is valid json and the resoinse code is 404") |
||||
REQUIRE(ss.str()=="HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nContent-Length: 33\r\n\r\n{\"code\":404,\"status\":\"Not Found\"}"s); |
||||
} |
||||
GIVEN("we stream our response with the status set to 204 No Content"){ |
||||
response.set_status(http::no_content); |
||||
ss << response; |
||||
THEN("the response has no content header set") |
||||
REQUIRE(ss.str()=="HTTP/1.1 204 No Content\r\nContent-Type: application/json\r\n\r\n"s); |
||||
} |
||||
GIVEN("we stream our response with the body set"){ |
||||
response.set_body(nlohmann::json::object()); |
||||
ss << response; |
||||
THEN("our response contains the appropriate headers") |
||||
REQUIRE(ss.str()=="HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\n\r\n{}"s); |
||||
} |
||||
} |
||||
|
||||
/*
|
||||
TEST(http_response_handler, header){ |
||||
ServerTest s; |
||||
auto req=s.create_request("GET /v1/session/torrents HTTP/1.1\r\nHost: localhost:8080\r\nFoo: bar\r\nFoo: bars\r\nContent-Type: application/json\r\n\r\n"); |
||||
std::shared_ptr<ServerTest::Response> resp(new ServerTest::Response(nullptr)); |
||||
http::request_handler out(req,resp); |
||||
ASSERT_TRUE(out.header_set("Content-Type")); |
||||
ASSERT_EQ(out.find_last_header_value("Content-Type"),"application/json"); |
||||
ASSERT_EQ(out.find_last_header_value("Host"),"localhost:8080"); |
||||
ASSERT_EQ(out.find_last_header_value("Foo"),"bars"); |
||||
ASSERT_NE(out.find_last_header_value("Foo"), "bar"); |
||||
ASSERT_FALSE(out.header_set("Content-Length")); |
||||
ASSERT_TRUE(out.header_equals("Content-Type","application/json")); |
||||
} |
||||
|
||||
TEST(http_response_handler, json_response){ |
||||
ServerTest s; |
||||
auto req=s.create_request("GET /v1/session/torrents HTTP/1.1\r\nHost: localhost:8080\r\nContent-Type: application/json\r\n\r\n"); |
||||
std::shared_ptr<ServerTest::Response> resp(new ServerTest::Response(nullptr)); |
||||
http::request_handler out(req,resp); |
||||
out.respond(nlohmann::json("{}")); |
||||
std::stringstream ss; |
||||
ss << resp->rdbuf(); |
||||
std::string string_response(ss.str()); |
||||
ASSERT_EQ(string_response,"HTTP/1.1 200 OK\r\nContent-Length: 4\r\nContent-Type: application/json\r\n\r\n\"{}\""); |
||||
} |
||||
|
||||
TEST(http_response_handler, string_response){ |
||||
ServerTest s; |
||||
auto req=s.create_request("GET /v1/session/torrents HTTP/1.1\r\nHost: localhost:8080\r\nContent-Type: application/json\r\n\r\n"); |
||||
std::shared_ptr<ServerTest::Response> resp(new ServerTest::Response(nullptr)); |
||||
http::request_handler out(req,resp); |
||||
out.respond(std::string("{}")); |
||||
std::stringstream ss; |
||||
ss << resp->rdbuf(); |
||||
std::string string_response(ss.str()); |
||||
ASSERT_EQ(string_response,"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n{}"); |
||||
} |
||||
|
||||
TEST(http_response_handler, empty_string){ |
||||
ServerTest s; |
||||
auto req=s.create_request("GET /v1/session/torrents HTTP/1.1\r\nHost: localhost:8080\r\nContent-Type: application/json\r\n\r\n"); |
||||
std::shared_ptr<ServerTest::Response> resp(new ServerTest::Response(nullptr)); |
||||
http::request_handler out(req,resp); |
||||
out.respond(std::string("")); |
||||
std::stringstream ss; |
||||
ss << resp->rdbuf(); |
||||
std::string string_response(ss.str()); |
||||
ASSERT_EQ(string_response,"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); |
||||
} |
||||
*/ |
||||
@ -0,0 +1,118 @@
|
||||
#include <Catch/fakeit.hpp> |
||||
#include <session.hpp> |
||||
|
||||
using namespace fakeit; |
||||
|
||||
const std::string method_not_allowed_json ="HTTP/1.1 405 Method Not Allowed\r\nContent-Type: application/json\r\nContent-Length: 42\r\n\r\n{\"code\":405,\"status\":\"Method Not Allowed\"}"; |
||||
const std::string service_unavailable_json="HTTP/1.1 503 Service Unavailable\r\nContent-Type: application/json\r\nContent-Length: 43\r\n\r\n{\"code\":503,\"status\":\"Service Unavailable\"}"; |
||||
const std::string internal_server_error_json="HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\nContent-Length: 45\r\n\r\n{\"code\":500,\"status\":\"Internal Server Error\"}"; |
||||
const std::string not_found ="HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nContent-Length: 36\r\n\r\n{\"code\":404,\"status\":\"Not Found\"}"; |
||||
const std::string ok_nullptr_json ="HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 4\r\n\r\nnull"; |
||||
const std::string bad_request ="HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: 35\r\n\r\n{\"code\":400,\"status\":\"Bad Request\"}"; |
||||
|
||||
using namespace session; |
||||
|
||||
SCENARIO("test of responses by session_manager on /v1/session"){ |
||||
session::resource sr; |
||||
std::stringstream ss; |
||||
GIVEN("the session is invalid"){ |
||||
WHEN("we receive a GET request"){ |
||||
sr.get(); |
||||
ss << sr; |
||||
THEN("the response is a 503 Service Unavailable JSON object") |
||||
REQUIRE(ss.str()==service_unavailable_json); |
||||
} |
||||
WHEN("we receive a PATCH request"){ |
||||
sr.patch(); |
||||
ss << sr; |
||||
THEN("the response is a 503 Service Unavailable JSON object") |
||||
REQUIRE(ss.str()==service_unavailable_json); |
||||
} |
||||
WHEN("we receive a POST request"){ |
||||
sr.post(); |
||||
ss << sr; |
||||
THEN("the response is a 405 Method Not Allowed JSON object") |
||||
REQUIRE(ss.str()==method_not_allowed_json); |
||||
} |
||||
WHEN("we receive a DELETE request"){ |
||||
sr.del(); |
||||
ss << sr; |
||||
THEN("the response is a 405 Method Not Allowed JSON object") |
||||
REQUIRE(ss.str()==method_not_allowed_json); |
||||
} |
||||
WHEN("we receive a PUT request"){ |
||||
sr.put(); |
||||
ss << sr; |
||||
THEN("the response is a 405 Method Not Allowed JSON object") |
||||
REQUIRE(ss.str()==method_not_allowed_json); |
||||
} |
||||
} |
||||
GIVEN("the session is valid"){ |
||||
Mock<basic_manager> mock_session; |
||||
When(Method(mock_session,basic_manager::is_valid)).AlwaysReturn(true); |
||||
When(Method(mock_session,basic_manager::get_json)).AlwaysReturn(nullptr); |
||||
auto session=std::shared_ptr<basic_manager>(&(mock_session.get()),[](...){}); |
||||
sr.set_session(session); |
||||
|
||||
|
||||
WHEN("we receive a GET request"){ |
||||
sr.get(); |
||||
ss << sr; |
||||
THEN("the response is a 200 OK JSON object ") |
||||
REQUIRE(ss.str()==ok_nullptr_json); |
||||
} |
||||
|
||||
|
||||
WHEN("we receive a PATCH request with invalid data"){ |
||||
GIVEN("the JSON is invalid"){ |
||||
sr.patch(nullptr); |
||||
ss << sr; |
||||
THEN("the response is a 400 Bad Request JSON object") |
||||
REQUIRE(ss.str()==bad_request); |
||||
} |
||||
GIVEN("the JSON is valid, but contains no data"){ |
||||
sr.patch({}); |
||||
ss << sr; |
||||
THEN("the response is a 400 Bad Request JSON object") |
||||
REQUIRE(ss.str()==bad_request); |
||||
} |
||||
GIVEN("the JSON is valid, but the port number is to low"){ |
||||
When(Method(mock_session,basic_manager::patch)).AlwaysReturn(false); |
||||
sr.patch({{"listen_port",1}}); |
||||
ss << sr; |
||||
THEN("the response is a 500 Internal Server Error JSON object") |
||||
REQUIRE(ss.str()==internal_server_error_json); |
||||
} |
||||
GIVEN("the JSON is valid, but listen_port is a string"){ |
||||
sr.patch({{"listen_port","1"}}); |
||||
ss << sr; |
||||
THEN("the response is a 400 Bad Request JSON object") |
||||
REQUIRE(ss.str()==bad_request); |
||||
} |
||||
GIVEN("the JSON is valid, but is_paused is a string"){ |
||||
sr.patch({{"is_paused","1"}}); |
||||
ss << sr; |
||||
THEN("the response is a 400 Bad Request JSON object") |
||||
REQUIRE(ss.str()==bad_request); |
||||
} |
||||
WHEN("we receive a POST request"){ |
||||
sr.post(); |
||||
ss << sr; |
||||
THEN("the response is a 405 Method Not Allowed JSON object") |
||||
REQUIRE(ss.str()==method_not_allowed_json); |
||||
} |
||||
WHEN("we receive a DELETE request"){ |
||||
sr.del(); |
||||
ss << sr; |
||||
THEN("the response is a 405 Method Not Allowed JSON object") |
||||
REQUIRE(ss.str()==method_not_allowed_json); |
||||
} |
||||
WHEN("we receive a PUT request"){ |
||||
sr.put(); |
||||
ss << sr; |
||||
THEN("the response is a 405 Method Not Allowed JSON object") |
||||
REQUIRE(ss.str()==method_not_allowed_json); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue