diff --git a/README.adoc b/README.adoc index d70fcbc..c6af591 100644 --- a/README.adoc +++ b/README.adoc @@ -110,8 +110,7 @@ Not included in this list are entities. | 110 | Connection timed out | 111 | Connection refused (check http_error_code) | 113 | No route to host / Could not resolve host -| 192 | curlpp runtime error -| 193 | curlpp logic error +| 150 | Encryption error (TODO: CHANGEME!) | 255 | Unknown error |=================================================== diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cf39f14..f602aff 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,8 @@ pkg_check_modules(curlpp REQUIRED IMPORTED_TARGET curlpp) if(WITH_EASY) find_package(jsoncpp REQUIRED CONFIG) endif() +# Some distributions do not contain Poco*Config.cmake recipes. +find_package(Poco COMPONENTS Foundation Net NetSSL CONFIG) if(WITH_EASY) file(GLOB_RECURSE sources *.cpp *.hpp) @@ -24,6 +26,7 @@ set_target_properties(${PROJECT_NAME} PROPERTIES target_include_directories(${PROJECT_NAME} PRIVATE "$" + "$" PUBLIC "$" "$") @@ -36,6 +39,26 @@ else() PUBLIC pthread PkgConfig::curlpp) endif() +# If no Poco*Config.cmake recipes are found, look for headers in standard dirs. +if(PocoNetSSL_FOUND) + target_link_libraries(${PROJECT_NAME} + PRIVATE Poco::Foundation Poco::Net Poco::NetSSL) +else() + find_file(Poco_h NAMES "Poco/Poco.h" + PATHS "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}") + + if("${Poco_h}" STREQUAL "Poco_h-NOTFOUND") + message(FATAL_ERROR "Could not find POCO.") + else() + message(WARNING + "Your distribution of POCO doesn't contain the *Config.cmake recipes, " + "but the files seem to be in the standard directories. " + "Let's hope this works.") + target_link_libraries(${PROJECT_NAME} + PRIVATE PocoFoundation PocoNet PocoNetSSL) + endif() +endif() + install(TARGETS ${PROJECT_NAME} EXPORT "${PROJECT_NAME}Targets" LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} diff --git a/src/http.cpp b/src/http.cpp index 81080d2..b0c8dca 100644 --- a/src/http.cpp +++ b/src/http.cpp @@ -16,19 +16,36 @@ #include #include // std::bind -#include -#include // std::strncmp #include #include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "debug.hpp" #include "mastodon-cpp.hpp" +#include using namespace Mastodon; namespace curlopts = curlpp::options; using std::cerr; +using std::istream; +using std::make_unique; +using std::move; +using Poco::Net::HTTPSClientSession; +using Poco::Net::HTTPRequest; +using Poco::Net::HTTPResponse; +using Poco::Net::HTTPMessage; +using Poco::StreamCopier; +using Poco::Environment; API::http::http(const API &api, const string &instance, const string &access_token) @@ -38,23 +55,65 @@ API::http::http(const API &api, const string &instance, , _cancel_stream(false) { curlpp::initialize(); + + Poco::Net::initializeSSL(); + + // FIXME: rewrite set_proxy() and set proxy here. + // string proxy_host, proxy_userpw; + // parent.get_proxy(proxy_host, proxy_userpw); + + try + { + HTTPSClientSession::ProxyConfig proxy; + string proxy_env = Environment::get("http_proxy"); + size_t pos; + + // Only keep text between // and /. + if ((pos = proxy_env.find("//")) != string::npos) + { + proxy_env = proxy_env.substr(pos + 2); + } + if ((pos = proxy_env.find('/')) != string::npos) + { + proxy_env = proxy_env.substr(0, pos); + } + + if ((pos = proxy_env.find(':')) != string::npos) + { + proxy.host = proxy_env.substr(0, pos); + proxy.port = std::stoi(proxy_env.substr(pos + 1)); + } + else + { + proxy.host = proxy_env; + } + + HTTPSClientSession::setGlobalProxyConfig(proxy); + } + catch (const std::exception &) + { + // No proxy found, no problem. + } + } API::http::~http() { curlpp::terminate(); + + Poco::Net::uninitializeSSL(); } return_call API::http::request(const http_method &meth, const string &path) { - return request(meth, path, curlpp::Forms()); + return request(meth, path, make_unique()); } return_call API::http::request(const http_method &meth, const string &path, - const curlpp::Forms &formdata) + unique_ptr formdata) { string answer; - return request_common(meth, path, formdata, answer); + return request_common(meth, path, move(formdata), answer); } void API::http::request_stream(const string &path, string &stream) @@ -64,7 +123,7 @@ void API::http::request_stream(const string &path, string &stream) [&, path] // path is captured by value because it may be { // deleted before we access it. ret = request_common(http_method::GET_STREAM, path, - curlpp::Forms(), stream); + make_unique(), stream); ttdebug << "Remaining content of the stream: " << stream << '\n'; if (!ret) { @@ -78,149 +137,171 @@ void API::http::request_stream(const string &path, string &stream) return_call API::http::request_common(const http_method &meth, const string &path, - const curlpp::Forms &formdata, + unique_ptr formdata, string &answer) { - using namespace std::placeholders; // _1, _2, _3 - ttdebug << "Path is: " << path << '\n'; try { - curlpp::Easy request; - std::list headers; - - request.setOpt("https://" + _instance + path); - ttdebug << "User-Agent: " << parent.get_useragent() << "\n"; - request.setOpt(parent.get_useragent()); - - { - string proxy; - string userpw; - parent.get_proxy(proxy, userpw); - if (!proxy.empty()) - { - request.setOpt(proxy); - if (!userpw.empty()) - { - request.setOpt(userpw); - } - } - } - - if (!_access_token.empty()) - { - headers.push_back("Authorization: Bearer " + _access_token); - } - if (meth != http_method::GET_STREAM) - { - headers.push_back("Connection: close"); - // Get headers from server - request.setOpt(true); - } - - request.setOpt(headers); - request.setOpt(true); - request.setOpt - (std::bind(&http::callback_write, this, _1, _2, _3, &answer)); - request.setOpt - (std::bind(&http::callback_progress, this, _1, _2, _3, _4)); - request.setOpt(0); - if (!formdata.empty()) - { - request.setOpt(formdata); - } + string method; + // TODO: operator string on http_method? switch (meth) { case http_method::GET: case http_method::GET_STREAM: - break; - case http_method::PATCH: - request.setOpt("PATCH"); - break; - case http_method::POST: - request.setOpt("POST"); - break; - case http_method::PUT: - request.setOpt("PUT"); - break; - case http_method::DELETE: - request.setOpt("DELETE"); + { + method = HTTPRequest::HTTP_GET; break; } + case http_method::PUT: + { + method = HTTPRequest::HTTP_PUT; + break; + } + case http_method::POST: + { + method = HTTPRequest::HTTP_POST; + break; + } + case http_method::PATCH: + { + method = HTTPRequest::HTTP_PATCH; + break; + } + case http_method::DELETE: + { + method = HTTPRequest::HTTP_DELETE; + break; + } + default: + { + break; + } + } - //request.setOpt(true); + HTTPSClientSession session(_instance); + HTTPRequest request(method, path, HTTPMessage::HTTP_1_1); + request.set("User-Agent", parent.get_useragent()); + + if (!_access_token.empty()) + { + request.set("Authorization", " Bearer " + _access_token); + } + + if (!formdata->empty()) + { + // TODO: Test form submit. + // TODO: Change maptoformdata() and so on. + formdata->prepareSubmit(request); + } + + HTTPResponse response; + + session.sendRequest(request); + istream &rs = session.receiveResponse(response); + + const uint16_t http_code = response.getStatus(); + ttdebug << "Response code: " << http_code << '\n'; answer.clear(); - request.perform(); - uint16_t http_code = curlpp::infos::ResponseCode::get(request); - ttdebug << "Response code: " << http_code << '\n'; - // Work around "HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK" - size_t pos = answer.find("\r\n\r\n", 25); - _headers = answer.substr(0, pos); - // Only return body - answer = answer.substr(pos + 4); + StreamCopier::copyToString(rs, answer); - if (http_code == 200 || http_code == 302 || http_code == 307) - { // OK or Found or Temporary Redirect + switch (http_code) + { + case HTTPResponse::HTTP_OK: + { return { 0, "", http_code, answer }; } - else if (http_code == 301 || http_code == 308) - { // Moved Permanently or Permanent Redirect - // return new URL - answer = curlpp::infos::EffectiveUrl::get(request); - return { 78, "Remote address changed", http_code, answer }; - } - else if (http_code == 0) + // Not using the constants because some are too new for Debian stretch. + case 301: // HTTPResponse::HTTP_MOVED_PERMANENTLY + case 308: // HTTPResponse::HTTP_PERMANENT_REDIRECT + case 302: // HTTPResponse::HTTP_FOUND + case 303: // HTTPResponse::HTTP_SEE_OTHER + case 307: // HTTPResponse::HTTP_TEMPORARY_REDIRECT { - return { 255, "Unknown error", http_code, answer }; + string location = response.get("Location"); + + // TODO: Test this. + if (location.substr(0, 4) == "http") + { // Remove protocol and instance from path. + size_t pos1 = location.find("//") + 2; + size_t pos2 = location.find('/', pos1); + + if (location.substr(pos1, pos2) != _instance) + { // Return new location if the domain changed. + return { 78, "Remote address changed", http_code, + location }; + } + + location = location.substr(pos2); + } + + if (http_code == 301 || http_code == 308) + { // Return new location for permanent redirects. + return { 78, "Remote address changed", http_code, location }; + } + else + { + return request_common(meth, location, move(formdata), answer); + } } - else + default: { return { 111, "Connection refused", http_code, answer }; } + } } - catch (curlpp::RuntimeError &e) + catch (const Poco::Net::DNSException &e) { - const string what = e.what(); - // This error is thrown if http.cancel_stream() is used. - if ((what.compare(0, 16, "Callback aborted") == 0) || - (what.compare(0, 19, "Failed writing body") == 0)) - { - ttdebug << "Request was cancelled by user\n"; - return { 0, "Request was cancelled by user", 0, "" }; - } - else if (what.compare(what.size() - 20, 20, "Connection timed out") == 0) - { - ttdebug << what << "\n"; - return { 110, "Connection timed out", 0, "" }; - } - else if (what.compare(0, 23, "Could not resolve host:") == 0) - { - ttdebug << what << "\n"; - return { 113, "Could not resolve host", 0, "" }; - } - if (parent.exceptions()) { - std::rethrow_exception(std::current_exception()); - } - else - { - ttdebug << "curlpp::RuntimeError: " << e.what() << std::endl; - return { 192, e.what(), 0, "" }; + e.rethrow(); } + + ttdebug << e.displayText() << "\n"; + return { 113, e.displayText(), 0, "" }; } - catch (curlpp::LogicError &e) + catch (const Poco::Net::ConnectionRefusedException &e) + { + if (parent.exceptions()) + { + e.rethrow(); + } + + ttdebug << e.displayText() << "\n"; + return { 111, e.displayText(), 0, "" }; + } + catch (const Poco::Net::SSLException &e) + { + if (parent.exceptions()) + { + e.rethrow(); + } + + ttdebug << e.displayText() << "\n"; + return { 150, e.displayText(), 0, "" }; + } + catch (const Poco::Net::NetException &e) + { + if (parent.exceptions()) + { + e.rethrow(); + } + + ttdebug << "Unknown network error: " << e.displayText() << std::endl; + return { 255, e.displayText(), 0, "" }; + } + catch (const std::exception &e) { if (parent.exceptions()) { std::rethrow_exception(std::current_exception()); } - ttdebug << "curlpp::LogicError: " << e.what() << std::endl; - return { 193, e.what(), 0, "" }; + ttdebug << "Unknown error: " << e.what() << std::endl; + return { 255, e.what(), 0, "" }; } } diff --git a/src/mastodon-cpp.cpp b/src/mastodon-cpp.cpp index f5ee783..a43b1c0 100644 --- a/src/mastodon-cpp.cpp +++ b/src/mastodon-cpp.cpp @@ -14,20 +14,20 @@ * along with this program. If not, see . */ -#include #include #include #include #include #include -#include -#include #include +#include #include "version.hpp" #include "debug.hpp" #include "mastodon-cpp.hpp" using namespace Mastodon; +using std::make_unique; +using Poco::Net::FilePartSource; API::API(const string &instance, const string &access_token) : _instance(instance) @@ -111,9 +111,10 @@ const string API::maptostr(const parameters &map, const bool &firstparam) return result; } -const curlpp::Forms API::maptoformdata(const parameters &map) +unique_ptr API::maptoformdata(const parameters &map) { - curlpp::Forms formdata; + unique_ptr formdata = + make_unique(HTMLForm::ENCODING_MULTIPART); if (map.size() == 0) { @@ -122,51 +123,57 @@ const curlpp::Forms API::maptoformdata(const parameters &map) for (const auto &it : map) { + string key = it.key; + + // TODO: Test nested parameters. + if (const size_t pos = key.find('.') != string::npos) + { // Nested parameters. + key.replace(pos, 1, "["); + key += ']'; + } + if (it.values.size() == 1) { // If the file is not base64-encoded, treat as filename. - if ((it.key == "avatar" || - it.key == "header" || - it.key == "file") && + if ((key == "avatar" || + key == "header" || + key == "file") && it.values.front().substr(0, 5) != "data:") - { - ttdebug << it.key << ": Filename detected.\n"; - std::ifstream testfile(it.values.front()); - if (testfile.good()) + { + ttdebug << key << ": Filename detected.\n"; + + try { - testfile.close(); - formdata.push_back( - new curlpp::FormParts::File(it.key, it.values.front())); + formdata->addPart(key, + new FilePartSource(it.values.front())); } - else + catch (const std::exception &e) { - std::cerr << "Error: File not found: " << it.values.front() - << std::endl; + if (exceptions()) + { + std::rethrow_exception(std::current_exception()); + } + + // TODO: Proper error handling without exceptions. + std::cerr << "Error: Could not open file: " + << it.values.front() << std::endl; } - } - else - { - string key = it.key; - // Append [] to array keys. - if (key == "account_ids" + } + else if (key == "account_ids" || key == "exclude_types" || key == "media_ids" || key == "context") - { - key += "[]"; - } - formdata.push_back( - new curlpp::FormParts::Content(key, it.values.front())); + { + key += "[]"; } + + formdata->add(key, it.values.front()); } else { - std::transform(it.values.begin(), it.values.end(), - std::back_inserter(formdata), - [&it](const string &s) - { - return new curlpp::FormParts::Content - (it.key + "[]", s); - }); + for (const string &value : it.values) + { + formdata->add(key + "[]", value); + } } } diff --git a/src/mastodon-cpp.hpp b/src/mastodon-cpp.hpp index 9790aa6..46a0706 100644 --- a/src/mastodon-cpp.hpp +++ b/src/mastodon-cpp.hpp @@ -27,12 +27,15 @@ #include #include #include +#include #include "return_types.hpp" #include "types.hpp" using std::string; using std::uint8_t; +using std::unique_ptr; +using Poco::Net::HTMLForm; /*! * @example example01_get_public_timeline.cpp @@ -45,6 +48,7 @@ using std::uint8_t; */ namespace Mastodon { + // TODO: error enum, different error codes. /*! * @brief Interface to the Mastodon API. * @@ -60,8 +64,7 @@ namespace Mastodon * | 110 | Connection timed out | * | 111 | Connection refused (check http_error_code) | * | 113 | No route to host / Could not resolve host | - * | 192 | curlpp runtime error | - * | 193 | curlpp logic error | + * | 150 | Encryption error | * | 255 | Unknown error | * * @since before 0.11.0 @@ -102,7 +105,7 @@ namespace Mastodon */ return_call request(const http_method &meth, const string &path, - const curlpp::Forms &formdata); + unique_ptr formdata); /*! * @brief HTTP Request for streams. @@ -153,7 +156,7 @@ namespace Mastodon return_call request_common(const http_method &meth, const string &path, - const curlpp::Forms &formdata, + unique_ptr formdata, string &answer); size_t callback_write(char* data, size_t size, size_t nmemb, string *oss); @@ -412,8 +415,7 @@ namespace Mastodon /*! * @brief Turn exceptions on or off. Defaults to off. * - * This applies to exceptions from curlpp. curlpp::RuntimeError - * and curlpp::LogicError. + * Most exceptions will be thrown at you to handle if on. * * @param value true for on, false for off * @@ -514,7 +516,7 @@ namespace Mastodon */ void get_stream(const Mastodon::API::v1 &call, const parameters ¶meters, - std::unique_ptr &ptr, + unique_ptr &ptr, string &stream); /*! @@ -527,7 +529,7 @@ namespace Mastodon * @since 0.100.0 */ void get_stream(const Mastodon::API::v1 &call, - std::unique_ptr &ptr, + unique_ptr &ptr, string &stream); /*! @@ -540,7 +542,7 @@ namespace Mastodon * @since 0.100.0 */ void get_stream(const string &call, - std::unique_ptr &ptr, + unique_ptr &ptr, string &stream); /*! @@ -665,9 +667,9 @@ namespace Mastodon * * @param map Map of parameters * - * @return Form data as curlpp::Forms + * @return Form data as Poco::Net::HTMLForm. */ - const curlpp::Forms maptoformdata(const parameters &map); + unique_ptr maptoformdata(const parameters &map); /*! * @brief Delete Mastodon::param from Mastodon::parameters.