[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
| 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
|===================================================

View File

@ -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
"$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}>"
"$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>"
PUBLIC
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src>"
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>")
@ -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}

View File

@ -16,19 +16,36 @@
#include <iostream>
#include <functional> // std::bind
#include <list>
#include <cstring> // std::strncmp
#include <exception>
#include <thread>
#include <curlpp/Options.hpp>
#include <curlpp/Exception.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 "mastodon-cpp.hpp"
#include <string>
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<HTMLForm>());
}
return_call API::http::request(const http_method &meth, const string &path,
const curlpp::Forms &formdata)
unique_ptr<HTMLForm> 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<HTMLForm>(), 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<HTMLForm> formdata,
string &answer)
{
using namespace std::placeholders; // _1, _2, _3
ttdebug << "Path is: " << path << '\n';
try
{
curlpp::Easy request;
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);
}
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<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");
{
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<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();
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, "" };
}
}

View File

@ -14,20 +14,20 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <sstream>
#include <regex>
#include <locale>
#include <codecvt>
#include <utility>
#include <iostream>
#include <algorithm>
#include <fstream>
#include <exception>
#include <Poco/Net/FilePartSource.h>
#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<HTMLForm> API::maptoformdata(const parameters &map)
{
curlpp::Forms formdata;
unique_ptr<HTMLForm> formdata =
make_unique<HTMLForm>(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);
}
}
}

View File

@ -27,12 +27,15 @@
#include <cstdint>
#include <curlpp/cURLpp.hpp>
#include <curlpp/Easy.hpp>
#include <Poco/Net/HTMLForm.h>
#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<HTMLForm> 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<HTMLForm> 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 &parameters,
std::unique_ptr<Mastodon::API::http> &ptr,
unique_ptr<Mastodon::API::http> &ptr,
string &stream);
/*!
@ -527,7 +529,7 @@ namespace Mastodon
* @since 0.100.0
*/
void get_stream(const Mastodon::API::v1 &call,
std::unique_ptr<Mastodon::API::http> &ptr,
unique_ptr<Mastodon::API::http> &ptr,
string &stream);
/*!
@ -540,7 +542,7 @@ namespace Mastodon
* @since 0.100.0
*/
void get_stream(const string &call,
std::unique_ptr<Mastodon::API::http> &ptr,
unique_ptr<Mastodon::API::http> &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<HTMLForm> maptoformdata(const parameters &map);
/*!
* @brief Delete Mastodon::param from Mastodon::parameters.