6 changed files with 148 additions and 317 deletions
@ -1,182 +1,68 @@ |
|||||||
#ifndef _TOREST_SESSION_HPP_ |
|
||||||
#define _TOREST_SESSION_HPP_ |
|
||||||
|
|
||||||
#include <resource.hpp> |
|
||||||
#include <util.hpp> |
|
||||||
#include <libtorrent/session.hpp> |
|
||||||
#include <http.hpp> |
#include <http.hpp> |
||||||
|
#include <util.hpp> |
||||||
|
|
||||||
namespace session { |
namespace tr{ |
||||||
class basic_manager { |
namespace session { |
||||||
public: |
template<class torrent_session, class request, class response> |
||||||
virtual ~basic_manager()=0; |
void get(torrent_session&session,response resp,request req){ |
||||||
virtual nlohmann::json get_json() const = 0; |
auto http_response = http::response(); |
||||||
virtual bool patch(const nlohmann::json &data) = 0; |
if (session.is_valid()) { |
||||||
virtual bool is_valid() const = 0; |
const auto settings = session.get_settings(); |
||||||
virtual int listen_port() const = 0; |
http_response.set_body({ |
||||||
virtual void set_session(std::shared_ptr<libtorrent::session>) = 0; |
{ "paused", session.is_paused() }, |
||||||
virtual void set_listen_port() = 0; |
{ "port", session.listen_port() }, |
||||||
}; |
{ "dht_enabled", session.is_dht_running() }, |
||||||
|
{ "down_limit", settings.get_int(settings.download_rate_limit) }, |
||||||
class manager : public basic_manager { |
{ "up_limit", settings.get_int(settings.upload_rate_limit) }, |
||||||
public: |
{ "torrents", session.get_torrents().size() }, //TODO Optimize
|
||||||
void set_session(std::shared_ptr<libtorrent::session> session) override { handle=session; } |
// { "default_download_dir", default_download_dir.filename().string() },
|
||||||
nlohmann::json get_json() const override ; |
// { "root_dir", root_dir.string() },
|
||||||
bool is_valid() const override { return handle && handle->is_valid(); } |
}); |
||||||
bool patch(const nlohmann::json &data) override ; |
} else { |
||||||
|
auto response_code = http::code(http::service_unavailable); |
||||||
private: |
http_response.set_body({{"code", response_code.first}, { "status", response_code.second}}); |
||||||
std::shared_ptr<libtorrent::session> handle; |
http_response.set_status(response_code.first); |
||||||
}; |
|
||||||
|
|
||||||
class basic_resource { |
|
||||||
public: |
|
||||||
virtual ~basic_resource() {} |
|
||||||
virtual void set_session(std::shared_ptr<basic_manager>)=0; |
|
||||||
}; |
|
||||||
|
|
||||||
class resource : public basic_resource { |
|
||||||
public: |
|
||||||
void set_session(std::shared_ptr<basic_manager> 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); |
|
||||||
} |
} |
||||||
|
*resp << http_response; |
||||||
} |
} |
||||||
/**
|
template<class torrent_session, class request, class response> |
||||||
* PATCH /session |
void patch(torrent_session&session,response resp,request req){ |
||||||
* -------------- |
auto http_response = http::response(); |
||||||
* \brief Change values on the session. |
if (!session.is_valid()) { |
||||||
* @param[in] data takes an JSON object with at least one `action` set. |
auto response_code = http::code(http::service_unavailable); |
||||||
* available `action`s are `is_paused<bool>` `listen_port<bool>` |
http_response.set_body({{response_code.first, response_code.second}}); |
||||||
*/ |
http_response.set_status(response_code.first); |
||||||
void patch(const nlohmann::json&data=nlohmann::json::object()); |
} else {
|
||||||
|
const auto json=util::json::parse(req->content); |
||||||
// @todo should we return service unavailable or just method not allowed?
|
if(json.is_object() && !json.is_null()) { |
||||||
void del(nlohmann::json arg=nullptr) { response.set_status(http::method_not_allowed); } |
if(util::json::get("paused",json,session.is_paused())) |
||||||
void put(nlohmann::json arg=nullptr) { response.set_status(http::method_not_allowed); } |
session.pause(); |
||||||
virtual void post(nlohmann::json arg=nullptr) { response.set_status(http::method_not_allowed); } |
else |
||||||
protected: |
session.resume(); |
||||||
std::shared_ptr<basic_manager> mgr; |
auto settings = session.get_settings(); |
||||||
http::response response; |
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));
|
||||||
#endif // _TOREST_RESOURCE_HPP_
|
const auto new_dht_enabled=util::json::get("dht_enabled",json,session.is_dht_running()); |
||||||
|
if(new_dht_enabled!=session.is_dht_running()) |
||||||
/*! Resource: torrents
|
settings.set_bool(settings.enable_dht,new_dht_enabled); |
||||||
Other: |
auto old_limit=settings.get_int(settings.download_rate_limit); |
||||||
- If multiple headers with the same key is posted, the server uses only the last. |
auto new_limit=util::json::get("down_limit",json,old_limit); |
||||||
- Server can return 503 Service Unavailable if for some reason a service is down */ |
if(new_limit!=old_limit) |
||||||
/*
|
settings.set_int(settings.download_rate_limit,new_limit); |
||||||
class torrents : public Resource<std::shared_ptr<HttpServer::Response>,std::shared_ptr<HttpServer::Request>> { |
old_limit=settings.get_int(settings.upload_rate_limit); |
||||||
public: |
new_limit=util::json::get("up_limit",json,old_limit); |
||||||
torrents(libtorrent::session &session); |
if(new_limit!=old_limit) |
||||||
}; |
settings.set_int(settings.upload_rate_limit,new_limit); |
||||||
*/ |
session.apply_settings(settings); |
||||||
// std::function<void(StreamType,StreamType)>
|
http_response.set_body({{200,"OK"}}); |
||||||
|
} else { |
||||||
|
auto response_code = http::code(http::bad_request); |
||||||
/*! API-End-point: [POST] /v1/session/torrents
|
http_response.set_body({{response_code.first,response_code.second}}); |
||||||
Requirements: |
http_response.set_status(response_code.first); |
||||||
- Content-Type: application/json |
} |
||||||
* This header must be set, otherwise toREST will return 403 Forbidden. |
} |
||||||
- Data: |
*resp << http_response; |
||||||
* 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); |
} |
||||||
}; |
} |
||||||
}*/ |
|
||||||
@ -1,68 +0,0 @@ |
|||||||
#include <session.hpp> |
|
||||||
|
|
||||||
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<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 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); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,76 @@ |
|||||||
|
#include <Catch/fakeit.hpp> |
||||||
|
#include <fakeserver.hpp> |
||||||
|
#include <session.hpp> |
||||||
|
|
||||||
|
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<TestTorrent> get_torrents() const { |
||||||
|
return std::vector<TestTorrent>(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
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<TestResponse>(); |
||||||
|
GIVEN("the session is invalid") { |
||||||
|
auto request = std::make_shared<TestResponse>(); |
||||||
|
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<TestResponse>(); |
||||||
|
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<TestResponse>(); |
||||||
|
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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue