[WIP] Switch from curlpp to POCO.

Compilable, but untested and unfinished.
This commit is contained in:
tastytea 2019-08-20 18:02:45 +02:00
parent bd07dc6f9c
commit f97608ecfa
Signed by: tastytea
GPG Key ID: CFC39497F1B26E07
5 changed files with 274 additions and 162 deletions

View File

@ -110,8 +110,7 @@ Not included in this list are entities.
| 110 | Connection timed out | 110 | Connection timed out
| 111 | Connection refused (check http_error_code) | 111 | Connection refused (check http_error_code)
| 113 | No route to host / Could not resolve host | 113 | No route to host / Could not resolve host
| 192 | curlpp runtime error | 150 | Encryption error (TODO: CHANGEME!)
| 193 | curlpp logic error
| 255 | Unknown error | 255 | Unknown error
|=================================================== |===================================================

View File

@ -5,6 +5,8 @@ pkg_check_modules(curlpp REQUIRED IMPORTED_TARGET curlpp)
if(WITH_EASY) if(WITH_EASY)
find_package(jsoncpp REQUIRED CONFIG) find_package(jsoncpp REQUIRED CONFIG)
endif() endif()
# Some distributions do not contain Poco*Config.cmake recipes.
find_package(Poco COMPONENTS Foundation Net NetSSL CONFIG)
if(WITH_EASY) if(WITH_EASY)
file(GLOB_RECURSE sources *.cpp *.hpp) file(GLOB_RECURSE sources *.cpp *.hpp)
@ -24,6 +26,7 @@ set_target_properties(${PROJECT_NAME} PROPERTIES
target_include_directories(${PROJECT_NAME} target_include_directories(${PROJECT_NAME}
PRIVATE PRIVATE
"$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}>" "$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}>"
"$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>"
PUBLIC PUBLIC
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src>" "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src>"
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>") "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>")
@ -36,6 +39,26 @@ else()
PUBLIC pthread PkgConfig::curlpp) PUBLIC pthread PkgConfig::curlpp)
endif() 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} install(TARGETS ${PROJECT_NAME}
EXPORT "${PROJECT_NAME}Targets" EXPORT "${PROJECT_NAME}Targets"
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}

View File

@ -16,19 +16,36 @@
#include <iostream> #include <iostream>
#include <functional> // std::bind #include <functional> // std::bind
#include <list>
#include <cstring> // std::strncmp
#include <exception> #include <exception>
#include <thread> #include <thread>
#include <curlpp/Options.hpp> #include <curlpp/Options.hpp>
#include <curlpp/Exception.hpp> #include <curlpp/Exception.hpp>
#include <curlpp/Infos.hpp> #include <curlpp/Infos.hpp>
#include <Poco/Net/HTTPSClientSession.h>
#include <Poco/Net/HTTPRequest.h>
#include <Poco/Net/HTTPResponse.h>
#include <Poco/StreamCopier.h>
#include <Poco/URI.h>
#include <Poco/Environment.h>
#include <Poco/Exception.h>
#include <Poco/Net/NetException.h>
#include <Poco/Net/SSLException.h>
#include "debug.hpp" #include "debug.hpp"
#include "mastodon-cpp.hpp" #include "mastodon-cpp.hpp"
#include <string>
using namespace Mastodon; using namespace Mastodon;
namespace curlopts = curlpp::options; namespace curlopts = curlpp::options;
using std::cerr; 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, API::http::http(const API &api, const string &instance,
const string &access_token) const string &access_token)
@ -38,23 +55,65 @@ API::http::http(const API &api, const string &instance,
, _cancel_stream(false) , _cancel_stream(false)
{ {
curlpp::initialize(); 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() API::http::~http()
{ {
curlpp::terminate(); curlpp::terminate();
Poco::Net::uninitializeSSL();
} }
return_call API::http::request(const http_method &meth, const string &path) return_call API::http::request(const http_method &meth, const string &path)
{ {
return request(meth, path, curlpp::Forms()); return request(meth, path, make_unique<HTMLForm>());
} }
return_call API::http::request(const http_method &meth, const string &path, return_call API::http::request(const http_method &meth, const string &path,
const curlpp::Forms &formdata) unique_ptr<HTMLForm> formdata)
{ {
string answer; 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) 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 [&, path] // path is captured by value because it may be
{ // deleted before we access it. { // deleted before we access it.
ret = request_common(http_method::GET_STREAM, path, ret = request_common(http_method::GET_STREAM, path,
curlpp::Forms(), stream); make_unique<HTMLForm>(), stream);
ttdebug << "Remaining content of the stream: " << stream << '\n'; ttdebug << "Remaining content of the stream: " << stream << '\n';
if (!ret) 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, return_call API::http::request_common(const http_method &meth,
const string &path, const string &path,
const curlpp::Forms &formdata, unique_ptr<HTMLForm> formdata,
string &answer) string &answer)
{ {
using namespace std::placeholders; // _1, _2, _3
ttdebug << "Path is: " << path << '\n'; ttdebug << "Path is: " << path << '\n';
try try
{ {
curlpp::Easy request; string method;
std::list<string> headers;
request.setOpt<curlopts::Url>("https://" + _instance + path);
ttdebug << "User-Agent: " << parent.get_useragent() << "\n";
request.setOpt<curlopts::UserAgent>(parent.get_useragent());
{
string proxy;
string userpw;
parent.get_proxy(proxy, userpw);
if (!proxy.empty())
{
request.setOpt<curlopts::Proxy>(proxy);
if (!userpw.empty())
{
request.setOpt<curlopts::ProxyUserPwd>(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<curlpp::options::Header>(true);
}
request.setOpt<curlopts::HttpHeader>(headers);
request.setOpt<curlopts::FollowLocation>(true);
request.setOpt<curlopts::WriteFunction>
(std::bind(&http::callback_write, this, _1, _2, _3, &answer));
request.setOpt<curlopts::ProgressFunction>
(std::bind(&http::callback_progress, this, _1, _2, _3, _4));
request.setOpt<curlopts::NoProgress>(0);
if (!formdata.empty())
{
request.setOpt<curlopts::HttpPost>(formdata);
}
// TODO: operator string on http_method?
switch (meth) switch (meth)
{ {
case http_method::GET: case http_method::GET:
case http_method::GET_STREAM: case http_method::GET_STREAM:
break; {
case http_method::PATCH: method = HTTPRequest::HTTP_GET;
request.setOpt<curlopts::CustomRequest>("PATCH");
break;
case http_method::POST:
request.setOpt<curlopts::CustomRequest>("POST");
break;
case http_method::PUT:
request.setOpt<curlopts::CustomRequest>("PUT");
break;
case http_method::DELETE:
request.setOpt<curlopts::CustomRequest>("DELETE");
break; 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<curlopts::Verbose>(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(); answer.clear();
request.perform(); StreamCopier::copyToString(rs, answer);
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);
if (http_code == 200 || http_code == 302 || http_code == 307) switch (http_code)
{ // OK or Found or Temporary Redirect {
case HTTPResponse::HTTP_OK:
{
return { 0, "", http_code, answer }; return { 0, "", http_code, answer };
} }
else if (http_code == 301 || http_code == 308) // Not using the constants because some are too new for Debian stretch.
{ // Moved Permanently or Permanent Redirect case 301: // HTTPResponse::HTTP_MOVED_PERMANENTLY
// return new URL case 308: // HTTPResponse::HTTP_PERMANENT_REDIRECT
answer = curlpp::infos::EffectiveUrl::get(request); case 302: // HTTPResponse::HTTP_FOUND
return { 78, "Remote address changed", http_code, answer }; case 303: // HTTPResponse::HTTP_SEE_OTHER
} case 307: // HTTPResponse::HTTP_TEMPORARY_REDIRECT
else if (http_code == 0)
{ {
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 }; 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()) if (parent.exceptions())
{ {
std::rethrow_exception(std::current_exception()); e.rethrow();
}
else
{
ttdebug << "curlpp::RuntimeError: " << e.what() << std::endl;
return { 192, e.what(), 0, "" };
} }
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()) if (parent.exceptions())
{ {
std::rethrow_exception(std::current_exception()); std::rethrow_exception(std::current_exception());
} }
ttdebug << "curlpp::LogicError: " << e.what() << std::endl; ttdebug << "Unknown error: " << e.what() << std::endl;
return { 193, e.what(), 0, "" }; return { 255, e.what(), 0, "" };
} }
} }

View File

@ -14,20 +14,20 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <sstream>
#include <regex> #include <regex>
#include <locale> #include <locale>
#include <codecvt> #include <codecvt>
#include <utility> #include <utility>
#include <iostream> #include <iostream>
#include <algorithm>
#include <fstream>
#include <exception> #include <exception>
#include <Poco/Net/FilePartSource.h>
#include "version.hpp" #include "version.hpp"
#include "debug.hpp" #include "debug.hpp"
#include "mastodon-cpp.hpp" #include "mastodon-cpp.hpp"
using namespace Mastodon; using namespace Mastodon;
using std::make_unique;
using Poco::Net::FilePartSource;
API::API(const string &instance, const string &access_token) API::API(const string &instance, const string &access_token)
: _instance(instance) : _instance(instance)
@ -111,9 +111,10 @@ const string API::maptostr(const parameters &map, const bool &firstparam)
return result; return result;
} }
const curlpp::Forms API::maptoformdata(const parameters &map) unique_ptr<HTMLForm> API::maptoformdata(const parameters &map)
{ {
curlpp::Forms formdata; unique_ptr<HTMLForm> formdata =
make_unique<HTMLForm>(HTMLForm::ENCODING_MULTIPART);
if (map.size() == 0) if (map.size() == 0)
{ {
@ -122,51 +123,57 @@ const curlpp::Forms API::maptoformdata(const parameters &map)
for (const auto &it : 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 (it.values.size() == 1)
{ // If the file is not base64-encoded, treat as filename. { // If the file is not base64-encoded, treat as filename.
if ((it.key == "avatar" || if ((key == "avatar" ||
it.key == "header" || key == "header" ||
it.key == "file") && key == "file") &&
it.values.front().substr(0, 5) != "data:") it.values.front().substr(0, 5) != "data:")
{ {
ttdebug << it.key << ": Filename detected.\n"; ttdebug << key << ": Filename detected.\n";
std::ifstream testfile(it.values.front());
if (testfile.good()) try
{ {
testfile.close(); formdata->addPart(key,
formdata.push_back( new FilePartSource(it.values.front()));
new curlpp::FormParts::File(it.key, it.values.front()));
} }
else catch (const std::exception &e)
{ {
std::cerr << "Error: File not found: " << it.values.front() if (exceptions())
<< std::endl; {
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 else if (key == "account_ids"
{
string key = it.key;
// Append [] to array keys.
if (key == "account_ids"
|| key == "exclude_types" || key == "exclude_types"
|| key == "media_ids" || key == "media_ids"
|| key == "context") || key == "context")
{ {
key += "[]"; key += "[]";
}
formdata.push_back(
new curlpp::FormParts::Content(key, it.values.front()));
} }
formdata->add(key, it.values.front());
} }
else else
{ {
std::transform(it.values.begin(), it.values.end(), for (const string &value : it.values)
std::back_inserter(formdata), {
[&it](const string &s) formdata->add(key + "[]", value);
{ }
return new curlpp::FormParts::Content
(it.key + "[]", s);
});
} }
} }

View File

@ -27,12 +27,15 @@
#include <cstdint> #include <cstdint>
#include <curlpp/cURLpp.hpp> #include <curlpp/cURLpp.hpp>
#include <curlpp/Easy.hpp> #include <curlpp/Easy.hpp>
#include <Poco/Net/HTMLForm.h>
#include "return_types.hpp" #include "return_types.hpp"
#include "types.hpp" #include "types.hpp"
using std::string; using std::string;
using std::uint8_t; using std::uint8_t;
using std::unique_ptr;
using Poco::Net::HTMLForm;
/*! /*!
* @example example01_get_public_timeline.cpp * @example example01_get_public_timeline.cpp
@ -45,6 +48,7 @@ using std::uint8_t;
*/ */
namespace Mastodon namespace Mastodon
{ {
// TODO: error enum, different error codes.
/*! /*!
* @brief Interface to the Mastodon API. * @brief Interface to the Mastodon API.
* *
@ -60,8 +64,7 @@ namespace Mastodon
* | 110 | Connection timed out | * | 110 | Connection timed out |
* | 111 | Connection refused (check http_error_code) | * | 111 | Connection refused (check http_error_code) |
* | 113 | No route to host / Could not resolve host | * | 113 | No route to host / Could not resolve host |
* | 192 | curlpp runtime error | * | 150 | Encryption error |
* | 193 | curlpp logic error |
* | 255 | Unknown error | * | 255 | Unknown error |
* *
* @since before 0.11.0 * @since before 0.11.0
@ -102,7 +105,7 @@ namespace Mastodon
*/ */
return_call request(const http_method &meth, return_call request(const http_method &meth,
const string &path, const string &path,
const curlpp::Forms &formdata); unique_ptr<HTMLForm> formdata);
/*! /*!
* @brief HTTP Request for streams. * @brief HTTP Request for streams.
@ -153,7 +156,7 @@ namespace Mastodon
return_call request_common(const http_method &meth, return_call request_common(const http_method &meth,
const string &path, const string &path,
const curlpp::Forms &formdata, unique_ptr<HTMLForm> formdata,
string &answer); string &answer);
size_t callback_write(char* data, size_t size, size_t nmemb, size_t callback_write(char* data, size_t size, size_t nmemb,
string *oss); string *oss);
@ -412,8 +415,7 @@ namespace Mastodon
/*! /*!
* @brief Turn exceptions on or off. Defaults to off. * @brief Turn exceptions on or off. Defaults to off.
* *
* This applies to exceptions from curlpp. curlpp::RuntimeError * Most exceptions will be thrown at you to handle if on.
* and curlpp::LogicError.
* *
* @param value true for on, false for off * @param value true for on, false for off
* *
@ -514,7 +516,7 @@ namespace Mastodon
*/ */
void get_stream(const Mastodon::API::v1 &call, void get_stream(const Mastodon::API::v1 &call,
const parameters &parameters, const parameters &parameters,
std::unique_ptr<Mastodon::API::http> &ptr, unique_ptr<Mastodon::API::http> &ptr,
string &stream); string &stream);
/*! /*!
@ -527,7 +529,7 @@ namespace Mastodon
* @since 0.100.0 * @since 0.100.0
*/ */
void get_stream(const Mastodon::API::v1 &call, void get_stream(const Mastodon::API::v1 &call,
std::unique_ptr<Mastodon::API::http> &ptr, unique_ptr<Mastodon::API::http> &ptr,
string &stream); string &stream);
/*! /*!
@ -540,7 +542,7 @@ namespace Mastodon
* @since 0.100.0 * @since 0.100.0
*/ */
void get_stream(const string &call, void get_stream(const string &call,
std::unique_ptr<Mastodon::API::http> &ptr, unique_ptr<Mastodon::API::http> &ptr,
string &stream); string &stream);
/*! /*!
@ -665,9 +667,9 @@ namespace Mastodon
* *
* @param map Map of parameters * @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<HTMLForm> maptoformdata(const parameters &map);
/*! /*!
* @brief Delete Mastodon::param from Mastodon::parameters. * @brief Delete Mastodon::param from Mastodon::parameters.