diff --git a/toREST/CMakeLists.txt b/toREST/CMakeLists.txt new file mode 100644 index 0000000..fa6a261 --- /dev/null +++ b/toREST/CMakeLists.txt @@ -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_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_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) + + diff --git a/toREST/cmake_modules/FindLibTorrent.cmake b/toREST/cmake_modules/FindLibTorrent.cmake deleted file mode 100644 index b50e4f0..0000000 --- a/toREST/cmake_modules/FindLibTorrent.cmake +++ /dev/null @@ -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) diff --git a/toREST/include/http.hpp b/toREST/include/http.hpp new file mode 100644 index 0000000..95d9630 --- /dev/null +++ b/toREST/include/http.hpp @@ -0,0 +1,92 @@ +#ifndef _TOREST_HTTP_HPP_ +#define _TOREST_HTTP_HPP_ + +#include +#include + +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 code; + typedef std::pair header; + typedef std::unordered_map 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_ diff --git a/toREST/include/resource.hpp b/toREST/include/resource.hpp new file mode 100644 index 0000000..fa015fb --- /dev/null +++ b/toREST/include/resource.hpp @@ -0,0 +1,27 @@ +#ifndef _TOREST_RESOURCE_HPP_ +#define _TOREST_RESOURCE_HPP_ + +#include + +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_ diff --git a/toREST/include/session.hpp b/toREST/include/session.hpp new file mode 100644 index 0000000..e91d734 --- /dev/null +++ b/toREST/include/session.hpp @@ -0,0 +1,250 @@ +#ifndef _TOREST_RESOURCE_SESSION_HPP_ +#define _TOREST_RESOURCE_SESSION_HPP_ + +#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; + // virtual std::shared_ptr 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 get_session() override { return handle; } + void set_session(std::shared_ptr 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 handle; + }; + + class basic_resource : public resource_base { + public: + virtual ~basic_resource() {} + virtual void set_session(std::shared_ptr)=0; + }; + + class resource : public basic_resource { + public: + resource(){} //TODO remove? + void set_session(std::shared_ptr 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` `listen_port` + */ + 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("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 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 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> { +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; + } + return handler.respond(nlohmann::json(http::http_code(http::accepted).second), http::accepted); + }; +}*/ diff --git a/toREST/include/util.hpp b/toREST/include/util.hpp new file mode 100644 index 0000000..e5eab73 --- /dev/null +++ b/toREST/include/util.hpp @@ -0,0 +1,94 @@ +#ifndef _BT_HELPERS_HPP_ +#define _BT_HELPERS_HPP_ + +#include +#include +#include + +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 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 + 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_ diff --git a/toREST/src/main.cxx b/toREST/src/main.cxx new file mode 100644 index 0000000..9d91a73 --- /dev/null +++ b/toREST/src/main.cxx @@ -0,0 +1,83 @@ +#include +#include + +typedef SimpleWeb::Server 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 resp, std::shared_ptr 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 resp, std::shared_ptr req) { + session.get(); + *resp << session; + }; + + + /* + http_server.resource["^/v1/session$"]["GET"]=[&session](HttpServer::Response &response, std::shared_ptr 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 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; +}; diff --git a/toREST/tests/http_test.cpp b/toREST/tests/http_test.cpp new file mode 100644 index 0000000..a83fb99 --- /dev/null +++ b/toREST/tests/http_test.cpp @@ -0,0 +1,120 @@ +#include +#include + +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! ye"); + 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! ye"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 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 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 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 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"); +} +*/ \ 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..5355094 --- /dev/null +++ b/toREST/tests/session_test.cpp @@ -0,0 +1,118 @@ +#include +#include + +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 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(&(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); + } + } + } +} \ No newline at end of file diff --git a/toREST/tests/test.cpp b/toREST/tests/test.cpp new file mode 100644 index 0000000..bb365e5 --- /dev/null +++ b/toREST/tests/test.cpp @@ -0,0 +1,3 @@ + +#define CATCH_CONFIG_MAIN +#include \ No newline at end of file