diff --git a/toREST/include/http.hpp b/toREST/include/http.hpp index 75c4e29..b346e81 100644 --- a/toREST/include/http.hpp +++ b/toREST/include/http.hpp @@ -37,26 +37,16 @@ namespace http { std::string second; }; - 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; - virtual void set_body(const std::string &content)=0; - friend std::ostream& operator<<(std::ostream& os,const http::basic_response &rh){ return rh.do_response(os); } - }; - - class response : public basic_response { + class response { public: + friend std::ostream& operator<<(std::ostream& os,const response &rh){ return rh.do_response(os); } response():status_code(http::ok){} - void set_body(const std::string &content) override { body=content; } + void set_body(const std::string &content) { body=content; } void set_body(const nlohmann::json &json); - void add_header(const http::header &header) override { response_headers.emplace(header); } - void set_status(http::status code) override { status_code=code; } + void add_header(const http::header &header) { response_headers.emplace(header); } + void set_status(http::status code) { status_code=code; } + std::ostream& do_response(std::ostream &os) const; protected: - std::ostream& do_response(std::ostream &os) const override ; std::string body; headers response_headers; code status_code; diff --git a/toREST/include/session.hpp b/toREST/include/session.hpp index 8566089..480bcc8 100644 --- a/toREST/include/session.hpp +++ b/toREST/include/session.hpp @@ -1,182 +1,68 @@ -#ifndef _TOREST_SESSION_HPP_ -#define _TOREST_SESSION_HPP_ - -#include -#include -#include #include +#include -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) = 0; - virtual void set_listen_port() = 0; - }; - - class manager : public basic_manager { - public: - void set_session(std::shared_ptr session) override { handle=session; } - nlohmann::json get_json() const override ; - bool is_valid() const override { return handle && handle->is_valid(); } - bool patch(const nlohmann::json &data) override ; - - private: - std::shared_ptr handle; - }; - - class basic_resource { - public: - virtual ~basic_resource() {} - virtual void set_session(std::shared_ptr)=0; - }; - - class resource : public basic_resource { - public: - void set_session(std::shared_ptr session_manager) override { mgr=session_manager; } - - virtual void get(nlohmann::json arg=nullptr) { - if(mgr && mgr->is_valid()){ - response.set_status(http::ok); - response.set_body(mgr->get_json()); - }else{ - response.set_status(http::service_unavailable); +namespace tr{ + namespace session { + template + void get(torrent_session&session,response resp,request req){ + auto http_response = http::response(); + if (session.is_valid()) { + const auto settings = session.get_settings(); + http_response.set_body({ + { "paused", session.is_paused() }, + { "port", session.listen_port() }, + { "dht_enabled", session.is_dht_running() }, + { "down_limit", settings.get_int(settings.download_rate_limit) }, + { "up_limit", settings.get_int(settings.upload_rate_limit) }, + { "torrents", session.get_torrents().size() }, //TODO Optimize +// { "default_download_dir", default_download_dir.filename().string() }, +// { "root_dir", root_dir.string() }, + }); + } else { + auto response_code = http::code(http::service_unavailable); + http_response.set_body({{"code", response_code.first}, { "status", response_code.second}}); + http_response.set_status(response_code.first); } + *resp << http_response; } - /** - * 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` `listen_port` - */ - void patch(const nlohmann::json&data=nlohmann::json::object()); - - // @todo should we return service unavailable or just method not allowed? - void del(nlohmann::json arg=nullptr) { response.set_status(http::method_not_allowed); } - void put(nlohmann::json arg=nullptr) { response.set_status(http::method_not_allowed); } - virtual void post(nlohmann::json arg=nullptr) { response.set_status(http::method_not_allowed); } - protected: - std::shared_ptr mgr; - http::response response; - }; -} -#endif // _TOREST_RESOURCE_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> { -public: - torrents(libtorrent::session &session); -}; -*/ -// std::function - - - /*! 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": "", - "save_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 resp,std::shared_ptr 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 required_fields={ - request_json.value("torrent",""), - request_json.value("save_path","") - }; - std::vector 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(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; + template + void patch(torrent_session&session,response resp,request req){ + auto http_response = http::response(); + if (!session.is_valid()) { + auto response_code = http::code(http::service_unavailable); + http_response.set_body({{response_code.first, response_code.second}}); + http_response.set_status(response_code.first); + } else { + const auto json=util::json::parse(req->content); + if(json.is_object() && !json.is_null()) { + if(util::json::get("paused",json,session.is_paused())) + session.pause(); + else + session.resume(); + auto settings = session.get_settings(); + const auto new_listen_port=util::json::get("port",json,session.listen_port()); + if(new_listen_port!=session.listen_port()) + settings.set_str(settings.listen_interfaces,"0.0.0.0:"+std::to_string(new_listen_port)); + const auto new_dht_enabled=util::json::get("dht_enabled",json,session.is_dht_running()); + if(new_dht_enabled!=session.is_dht_running()) + settings.set_bool(settings.enable_dht,new_dht_enabled); + auto old_limit=settings.get_int(settings.download_rate_limit); + auto new_limit=util::json::get("down_limit",json,old_limit); + if(new_limit!=old_limit) + settings.set_int(settings.download_rate_limit,new_limit); + old_limit=settings.get_int(settings.upload_rate_limit); + new_limit=util::json::get("up_limit",json,old_limit); + if(new_limit!=old_limit) + settings.set_int(settings.upload_rate_limit,new_limit); + session.apply_settings(settings); + http_response.set_body({{200,"OK"}}); + } else { + auto response_code = http::code(http::bad_request); + http_response.set_body({{response_code.first,response_code.second}}); + http_response.set_status(response_code.first); + } + } + *resp << http_response; } - return handler.respond(nlohmann::json(http::http_code(http::accepted).second), http::accepted); - }; -}*/ + } +} \ No newline at end of file diff --git a/toREST/src/main.cxx b/toREST/src/main.cxx index 399a967..08db99a 100644 --- a/toREST/src/main.cxx +++ b/toREST/src/main.cxx @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -31,64 +32,11 @@ int main(int argc, char *argv[]) { }; http_server.resource["^/session(\\?.*)?\\/?$"]["GET"]=[&](std::shared_ptr resp, std::shared_ptr req) { - auto response = http::response(); - if (session.is_valid()) { - const auto settings = session.get_settings(); - response.set_body({ - { "paused", session.is_paused() }, - { "port", session.listen_port() }, - { "dht_enabled", session.is_dht_running() }, - { "down_limit", settings.get_int(settings.download_rate_limit) }, - { "up_limit", settings.get_int(settings.upload_rate_limit) }, - { "torrents", session.get_torrents().size() }, //TODO Optimize - { "default_download_dir", default_download_dir.filename().string() }, - { "root_dir", root_dir.string() }, - }); - } else { - auto response_code = http::code(http::service_unavailable); - response.set_body({{response_code.first, response_code.second}}); - response.set_status(response_code.first); - } - *resp << response; + tr::session::get(session,resp,req); }; http_server.resource["^/session(\\?.*)?\\/?$"]["PATCH"]=[&](std::shared_ptr resp, std::shared_ptr req) { - auto response = http::response(); - if (!session.is_valid()) { - auto response_code = http::code(http::service_unavailable); - response.set_body({{response_code.first, response_code.second}}); - response.set_status(response_code.first); - } else { - const auto json=util::json::parse(req->content); - if(json.is_object() && !json.is_null()) { - if(util::json::get("paused",json,session.is_paused())) - session.pause(); - else - session.resume(); - auto settings = session.get_settings(); - const auto new_listen_port=util::json::get("port",json,session.listen_port()); - if(new_listen_port!=session.listen_port()) - settings.set_str(settings.listen_interfaces,"0.0.0.0:"+std::to_string(new_listen_port)); - const auto new_dht_enabled=util::json::get("dht_enabled",json,session.is_dht_running()); - if(new_dht_enabled!=session.is_dht_running()) - settings.set_bool(settings.enable_dht,new_dht_enabled); - auto old_limit=settings.get_int(settings.download_rate_limit); - auto new_limit=util::json::get("down_limit",json,old_limit); - if(new_limit!=old_limit) - settings.set_int(settings.download_rate_limit,new_limit); - old_limit=settings.get_int(settings.upload_rate_limit); - new_limit=util::json::get("up_limit",json,old_limit); - if(new_limit!=old_limit) - settings.set_int(settings.upload_rate_limit,new_limit); - session.apply_settings(settings); - response.set_body({{200,"OK"}}); - } else { - auto response_code = http::code(http::bad_request); - response.set_body({{response_code.first,response_code.second}}); - response.set_status(response_code.first); - } - } - *resp << response; + tr::session::patch(session,resp,req); }; http_server.resource["^/session/torrents(\\?.*)?\\/?$"]["GET"]=[&session](std::shared_ptr resp, std::shared_ptr req) { diff --git a/toREST/src/session.cpp b/toREST/src/session.cpp index 3d098c3..e69de29 100644 --- a/toREST/src/session.cpp +++ b/toREST/src/session.cpp @@ -1,68 +0,0 @@ -#include - -namespace session { - nlohmann::json manager::get_json() const { - nlohmann::json session_json; - session_json["id"] = handle->id().to_string(); - session_json["paused"] = handle->is_paused(); - session_json["listening"] = handle->is_listening(); - session_json["port"] = handle->listen_port(); - session_json["ssl_port"] = handle->ssl_listen_port(); - return session_json; - } - - bool manager::patch(const nlohmann::json &data) { - int paused=data["paused"]; - int port=data["port"]; - - const 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; - }; - - const 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(paused) && listen(port); - }; - void resource::patch(const nlohmann::json &data){ - if(mgr && mgr->is_valid()){ - if(data.is_object() && !data.is_null()){ - int is_paused=util::json::get("is_paused",data,-1), - listen_port=util::json::get("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 response.set_status(http::ok); - }else{ - return response.set_status(http::internal_server_error); - } - }else{ - return response.set_status(http::bad_request); - } - } - return response.set_status(http::bad_request); - // auto data=data.array(); - }else{ - return response.set_status(http::service_unavailable); - } - } -} \ No newline at end of file diff --git a/toREST/tests/session_test.cpp b/toREST/tests/session_test.cpp new file mode 100644 index 0000000..af12426 --- /dev/null +++ b/toREST/tests/session_test.cpp @@ -0,0 +1,76 @@ +#include +#include +#include + +using namespace std; + +class TestTorrent {}; +class TestRequest : public std::stringstream {}; +class TestResponse : public std::stringstream {}; + +class TestSessionSettings { +public: + const int download_rate_limit = 1; + const int upload_rate_limit = 2; + int get_int(int type) const { + return 0; + } +}; + +class TestTorrentSession { +public: + bool valid = false; + bool paused = false; + bool dht_running = true; + bool is_valid() { + return valid; + } + TestSessionSettings get_settings() { + return TestSessionSettings(); + } + bool is_paused() { + return paused; + } + int listen_port() { + return 0; + } + int is_dht_running() { + return dht_running; + } + std::vector get_torrents() const { + return std::vector(); + } +}; + +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 ok_json = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 82\r\n\r\n{\"dht_enabled\":1,\"down_limit\":0,\"paused\":false,\"port\":0,\"torrents\":0,\"up_limit\":0}"; +const std::string ok_json_paused = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 81\r\n\r\n{\"dht_enabled\":1,\"down_limit\":0,\"paused\":true,\"port\":0,\"torrents\":0,\"up_limit\":0}"; + +SCENARIO("We recive a GET request to the /session resource") { + auto torrent_session = TestTorrentSession(); + auto response = std::make_shared(); + GIVEN("the session is invalid") { + auto request = std::make_shared(); + tr::session::get(torrent_session, response, request); + THEN("the server should reply with service unavailable") { + REQUIRE(response->str() == service_unavailable_json); + } + } + GIVEN("the session is valid") { + auto request = std::make_shared(); + torrent_session.valid = true; + tr::session::get(torrent_session, response, request); + THEN("the server should reply with service unavailable") { + REQUIRE(response->str() == ok_json); + } + } + GIVEN("the session is valid") { + auto request = std::make_shared(); + torrent_session.valid = true; + torrent_session.paused = true; + tr::session::get(torrent_session, response, request); + THEN("the server should reply with service unavailable") { + REQUIRE(response->str() == ok_json_paused); + } + } +} diff --git a/toREST/tests/test.cpp b/toREST/tests/test.cpp index bb365e5..790feb5 100644 --- a/toREST/tests/test.cpp +++ b/toREST/tests/test.cpp @@ -1,3 +1,2 @@ - #define CATCH_CONFIG_MAIN #include \ No newline at end of file