diff --git a/toREST/include/config.hpp b/toREST/include/config.hpp new file mode 100644 index 0000000..e8bc296 --- /dev/null +++ b/toREST/include/config.hpp @@ -0,0 +1,11 @@ +#ifndef _TR_CONFIG_HPP_ +#define _TR_CONFIG_HPP_ + +#include + +struct Config { + const boost::filesystem::path root_dir = "/home/zalox/downloads"; + const boost::filesystem::path default_download_dir = root_dir / "toREST"; +}; + +#endif \ No newline at end of file diff --git a/toREST/include/torrents.hpp b/toREST/include/torrents.hpp index f60a628..30cc4f7 100644 --- a/toREST/include/torrents.hpp +++ b/toREST/include/torrents.hpp @@ -1,6 +1,8 @@ #ifndef _TR_TORRENTS_HPP_ #define _TR_TORRENTS_HPP_ #include +#include +#include namespace tr { namespace session { @@ -36,6 +38,63 @@ void get(torrent_session &session, response resp, request req) { } *resp << http_response; } +template +void post(settings opts, torrent_session &session, response resp, request req) { + auto http_response = http::response(); + const auto respond = [&](http::status status) { + const auto response_code = http::code(status); + http_response.set_body({{"code", response_code.first}, {"status", response_code.second}}); + http_response.set_status(response_code.first); + *resp << http_response; + }; + + if (!session.is_valid()) { + return respond(http::service_unavailable); + } + + auto request_object = util::json::parse(req->content); + + if (request_object.is_null()) { + return respond(http::bad_request); + } + + if (!request_object.is_object()) { + return respond(http::bad_request); + } + + if (request_object.find("magnet_uri") == request_object.end()) { + return respond(http::bad_request); + } + + std::string uri = request_object.at("magnet_uri"); + boost::system::error_code ec; + libtorrent::add_torrent_params params; + libtorrent::parse_magnet_uri(uri, params, ec); + auto magnet = util::uri::parse(uri); + if (ec) { + return respond(http::bad_request); + } + + params.save_path = opts.default_download_dir.string(); + + + http_response.add_header({"Location", "/session/torrents/hash"}); + + return respond(http::created); + + // if(ec){ + // auto response_code = http::code(http::bad_request); + // http_response.set_body({{"code", response_code.first}, {"status", response_code.second + ec.message()}}); + // http_response.set_status(response_code.first); + // } + + // if(request_object.find("save_path") != request_object.end()) + // params.save_path=request_object.at("save_path"); + // else + // params.save_path=opts.default_download_dir.string(); + // session.async_add_torrent(params); + // http_response.add_header({"Location",params.info_hash.to_string()}); +} } } } diff --git a/toREST/src/main.cxx b/toREST/src/main.cxx index 85c4151..662b48c 100644 --- a/toREST/src/main.cxx +++ b/toREST/src/main.cxx @@ -1,8 +1,9 @@ -#include #include #include #include #include + +#include #include #include #include @@ -11,18 +12,21 @@ typedef SimpleWeb::Server HttpServer; using namespace std; + int main(int argc, char *argv[]) { int http_port = 8080, http_threads = 4; - const boost::filesystem::path root_dir = "/home/zalox/downloads"; - const boost::filesystem::path default_download_dir = root_dir / "toREST"; - if (!boost::filesystem::exists(default_download_dir)) { + + Config options; + + if (!boost::filesystem::exists(options.root_dir)) { boost::system::error_code ec; - boost::filesystem::create_directories(default_download_dir, ec); + boost::filesystem::create_directories(options.default_download_dir, ec); if (ec) { std::cout << ec.message() << "\n"; return 1; } } + HttpServer http_server(http_port, http_threads); libtorrent::session session; const auto stats_metrics = libtorrent::session_stats_metrics(); @@ -43,8 +47,8 @@ int main(int argc, char *argv[]) { http_server.resource["^/session/torrents(\\?.*)?\\/?$"]["GET"] = [&](std::shared_ptr resp, std::shared_ptr req) { tr::session::torrents::get(session, resp, req); }; - http_server.resource["^/session/torrents(\\?.*)?\\/?$"]["PATCH"] = [&](std::shared_ptr resp, std::shared_ptr req) { - + http_server.resource["^/session/torrents(\\?.*)?\\/?$"]["POST"] = [&](std::shared_ptr resp, std::shared_ptr req) { + tr::session::torrents::post(options, session, resp, req); }; std::thread server_thread([&http_server]() { diff --git a/toREST/tests/include/ConfigContext.hpp b/toREST/tests/include/ConfigContext.hpp new file mode 100644 index 0000000..02e8385 --- /dev/null +++ b/toREST/tests/include/ConfigContext.hpp @@ -0,0 +1,7 @@ +#ifndef _TR_TEST_CONFIG_CONTEXT_HPP_ +#define _TR_TEST_CONFIG_CONTEXT_HPP_ + +#include + + +#endif \ No newline at end of file diff --git a/toREST/tests/include/ServerContext.hpp b/toREST/tests/include/ServerContext.hpp index 1f9c13e..8121cb6 100644 --- a/toREST/tests/include/ServerContext.hpp +++ b/toREST/tests/include/ServerContext.hpp @@ -2,7 +2,7 @@ #define _TR_TEST_SERVER_CONTEXT_HPP_ #include #include -#include +#include #include class TestRequest { @@ -13,9 +13,12 @@ public: class TestResponse : public std::stringstream { public: std::string buffer; + std::unordered_map headers_; std::string string(); std::string message(); std::string code(); + std::string content(); + const std::unordered_map& headers(); }; class CommonResponse { @@ -24,6 +27,7 @@ public: static void ok(std::shared_ptr response, const nlohmann::json &data); static void bad_request(std::shared_ptr response); static void accepted(std::shared_ptr response); + static void created(std::shared_ptr response, std::shared_ptr request, const std::string &location); }; #endif \ No newline at end of file diff --git a/toREST/tests/include/SessionContext.hpp b/toREST/tests/include/SessionContext.hpp index f636320..7c99580 100644 --- a/toREST/tests/include/SessionContext.hpp +++ b/toREST/tests/include/SessionContext.hpp @@ -4,6 +4,7 @@ #include #include #include +#include class TestSessionSettings { public: @@ -38,6 +39,7 @@ public: bool is_dht_running() const; std::vector &get_torrents(); void apply_settings(TestSessionSettings settings); + void async_add_torrent(const libtorrent::add_torrent_params ¶ms); }; #endif diff --git a/toREST/tests/stubs/ServerContext.cpp b/toREST/tests/stubs/ServerContext.cpp index 77e4896..df53232 100644 --- a/toREST/tests/stubs/ServerContext.cpp +++ b/toREST/tests/stubs/ServerContext.cpp @@ -15,29 +15,80 @@ std::string TestResponse::code() { string(); return buffer.substr(9, 3); } +std::string TestResponse::content() { + if (buffer.empty()) + string(); + auto start = buffer.find("\r\n\r\n") + 4; + return buffer.substr(buffer.find("\r\n\r\n") + 4, buffer.size() - start); +} + +const std::unordered_map &TestResponse::headers() { + size_t end = buffer.find("\r\n\r\n") + 4; + size_t start = buffer.find("\r\n") + 2; + std::string header; + std::string header_content; + for (size_t c = start; c < end; c++) { + if (buffer[c] == ':') { + header = buffer.substr(start, c - start); + c += 2; + start = c; + } + if (buffer[c] == '\r') { + header_content = buffer.substr(start, c - start); + c += 2; + start = c; + headers_.emplace(header, header_content); + header = ""; + header_content = ""; + } + } + return headers_; +} + +void header_present(std::shared_ptr response, const std::string &header, const std::string &content) { + const auto loc_itr = response->headers().find(header); + const auto end = response->headers().end(); + const auto header_found = loc_itr != end; + REQUIRE(header_found); + REQUIRE(loc_itr->first == header); + REQUIRE(loc_itr->second == content); +} + +void is_json_request(std::shared_ptr response) { + const auto content = response->content(); + auto obj = nlohmann::json::parse(content); + REQUIRE(obj.is_object()); + header_present(response, "Content-Type", "application/json"); + header_present(response, "Content-Length", std::to_string(content.length())); +} void CommonResponse::service_unavailable(std::shared_ptr response) { - 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\"}"; - REQUIRE(response->string() == service_unavailable_json); REQUIRE(response->code() == "503"); REQUIRE(response->message() == "Service Unavailable"); + is_json_request(response); }; + void CommonResponse::ok(std::shared_ptr response, const nlohmann::json &data) { - std::string str(data.dump()); - const std::string res = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " + std::to_string(str.size()) + "\r\n\r\n" + str; + std::string data_dump(data.dump()); REQUIRE(response->code() == "200"); REQUIRE(response->message() == "OK"); - REQUIRE(response->string() == res); + REQUIRE(response->content() == data_dump); + is_json_request(response); }; void CommonResponse::bad_request(std::shared_ptr response) { - const std::string bad_request_json = "HTTP/1.1 400 Bad Request\r\nContent-Type: application/json\r\nContent-Length: 35\r\n\r\n{\"code\":400,\"status\":\"Bad Request\"}"; - REQUIRE(response->string() == bad_request_json); REQUIRE(response->code() == "400"); REQUIRE(response->message() == "Bad Request"); + is_json_request(response); }; void CommonResponse::accepted(std::shared_ptr response) { - const std::string accepted = "HTTP/1.1 202 Accepted\r\nContent-Type: application/json\r\nContent-Length: 32\r\n\r\n{\"code\":202,\"status\":\"Accepted\"}"; - REQUIRE(response->string() == accepted); REQUIRE(response->code() == "202"); REQUIRE(response->message() == "Accepted"); + is_json_request(response); }; + +void CommonResponse::created(std::shared_ptr response, std::shared_ptr request, const std::string &location) { + REQUIRE(response->code() == "201"); + REQUIRE(response->message() == "Created"); + is_json_request(response); + header_present(response, "Location", "/session/torrents/hash"); +}; \ No newline at end of file diff --git a/toREST/tests/stubs/SessionContext.cpp b/toREST/tests/stubs/SessionContext.cpp index 87aed05..c2850d1 100644 --- a/toREST/tests/stubs/SessionContext.cpp +++ b/toREST/tests/stubs/SessionContext.cpp @@ -66,3 +66,8 @@ std::vector &TestSession::get_torrents() { void TestSession::apply_settings(TestSessionSettings settings) { settings_ = settings; }; + +void TestSession::async_add_torrent(const libtorrent::add_torrent_params &settings) { + TestTorrent torrent; +}; + diff --git a/toREST/tests/torrents_test.cpp b/toREST/tests/torrents_test.cpp index b3671fc..42bc2bb 100644 --- a/toREST/tests/torrents_test.cpp +++ b/toREST/tests/torrents_test.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -44,4 +45,65 @@ SCENARIO("We are running a GET /session/torrents resource") { } } } -} \ No newline at end of file +} + +SCENARIO("We are running a POST /session/torrents resource") { + auto torrent_session = TestSession(); + auto settings = Config(); + auto response = std::make_shared(); + auto request = std::make_shared(); + GIVEN("the server is not working properly") { + AND_WHEN("we recive a request") { + tr::session::torrents::post(settings, torrent_session, response, request); + THEN("the server should reply with service unavailable") { + CommonResponse::service_unavailable(response); + } + } + } + GIVEN("the server is working properly") { + torrent_session.valid = true; + WHEN("we recive a valid request") { + GIVEN("we use the default download directory") { + request->content << nlohmann::json::object({{"magnet_uri", "magnet:?xt=urn:btih:c0b0a90089710812fe8c37385a4cc2978eabf3e8&dn=Taylor Swift&tr=http://tracker.sout.no"}}); + THEN("the server should reply with created") { + tr::session::torrents::post(settings, torrent_session, response, request); + CommonResponse::created(response, request, "/session/torrents/hash"); + } + } + + + GIVEN("the torrent doesn't exist") { + // tr::session::torrents::post(settings, torrent_session, response, request); + // tr::session::torrents::get(torrent_session, response, request); + // CommonResponse::ok(response, {{"torrents", nlohmann::json::array()}}); + // } + // } + // GIVEN("we have at least one torrent") { + // auto t_torrent = TestTorrent(); + // torrent_session.get_torrents().emplace_back(t_torrent); + // THEN("the server should reply with an array of torrents") { + // tr::session::torrents::get(torrent_session, response, request); + // CommonResponse::ok(response, {{"torrents", {torrent}}}); + // }; + } + GIVEN("the torrent exists from before") { + } + } + WHEN("we recive an invalid request") { + GIVEN("the request doesn't contain valid json") { + request->content << "Not valid"; + THEN("the server should reply with bad request") { + tr::session::torrents::post(settings, torrent_session, response, request); + CommonResponse::bad_request(response); + } + } + GIVEN("it is valid json but are missing requred field") { + request->content << nlohmann::json({{"up_speed", "100"}}); + THEN("the server should reply with bad request") { + tr::session::torrents::post(settings, torrent_session, response, request); + CommonResponse::bad_request(response); + } + } + } + } +}