arch-repo-manager/librepomgr/webclient/session.cpp

297 lines
10 KiB
C++

#include "./session.h"
#include "../serversetup.h"
#include "resources/config.h"
#include <c++utilities/conversion/stringbuilder.h>
#include <c++utilities/conversion/stringconversion.h>
#include <c++utilities/io/ansiescapecodes.h>
#include <boost/asio/connect.hpp>
#include <boost/asio/error.hpp>
#include <boost/system/error_code.hpp>
#include <iostream>
using namespace std;
using namespace boost::asio;
using namespace boost::beast;
using namespace CppUtilities;
using namespace CppUtilities::EscapeCodes;
namespace LibRepoMgr {
namespace WebClient {
HttpClientError::HttpClientError(const char *context, boost::beast::error_code errorCode)
: std::runtime_error(argsToString(context, ':', ' ', errorCode.message()))
, context(context)
, errorCode(errorCode)
{
}
void Session::run(const char *host, const char *port, http::verb verb, const char *target, unsigned int version)
{
// set up an HTTP request message
request.version(version);
request.method(verb);
request.target(target);
request.set(http::field::host, host);
request.set(http::field::user_agent, APP_NAME " " APP_VERSION);
// setup a file response
if (!destinationFilePath.empty()) {
auto &fileResponse = response.emplace<FileResponse>();
boost::beast::error_code errorCode;
fileResponse.body_limit(100 * 1024 * 1024);
fileResponse.get().body().open(destinationFilePath.data(), file_mode::write, errorCode);
if (errorCode != boost::beast::errc::success) {
m_handler(*this, HttpClientError("opening output file", errorCode));
return;
}
}
// look up the domain name
m_resolver.async_resolve(host, port,
boost::asio::ip::tcp::resolver::canonical_name | boost::asio::ip::tcp::resolver::passive | boost::asio::ip::tcp::resolver::all_matching,
std::bind(&Session::resolved, shared_from_this(), std::placeholders::_1, std::placeholders::_2));
}
void Session::resolved(boost::beast::error_code ec, ip::tcp::resolver::results_type results)
{
if (ec) {
m_handler(*this, HttpClientError("resolving", ec));
return;
}
// make the connection on the IP address we get from a lookup
boost::asio::async_connect(m_socket, results.begin(), results.end(), std::bind(&Session::connected, shared_from_this(), std::placeholders::_1));
}
void Session::connected(boost::beast::error_code ec)
{
if (ec) {
m_handler(*this, HttpClientError("connecting", ec));
return;
}
// perform the SSL handshake
http::async_write(m_socket, request, std::bind(&Session::requested, shared_from_this(), std::placeholders::_1, std::placeholders::_2));
}
void Session::requested(boost::beast::error_code ec, std::size_t bytesTransferred)
{
boost::ignore_unused(bytesTransferred);
if (ec) {
m_handler(*this, HttpClientError("sending request", ec));
return;
}
// receive the HTTP response
std::visit(
[this](auto &&response) {
http::async_read(
m_socket, m_buffer, response, std::bind(&Session::received, shared_from_this(), std::placeholders::_1, std::placeholders::_2));
},
response);
}
void Session::received(boost::beast::error_code ec, std::size_t bytesTransferred)
{
boost::ignore_unused(bytesTransferred);
if (ec) {
m_handler(*this, HttpClientError("receiving response", ec));
return;
}
// close the stream gracefully
m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
if (ec && ec != boost::beast::errc::not_connected) {
m_handler(*this, HttpClientError("closing connection", ec));
return;
}
// if we get here then the connection is closed gracefully
m_handler(*this, HttpClientError());
}
void SslSession::run(const char *host, const char *port, http::verb verb, const char *target, unsigned int version)
{
// set SNI Hostname (many hosts need this to handshake successfully)
if (!SSL_ctrl(
m_stream.native_handle(), SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, reinterpret_cast<void *>(const_cast<char *>(host)))) {
m_handler(*this,
HttpClientError(
"setting SNI hostname", boost::beast::error_code{ static_cast<int>(::ERR_get_error()), boost::asio::error::get_ssl_category() }));
return;
}
// setup an HTTP request message
request.version(version);
request.method(verb);
request.target(target);
request.set(http::field::host, host);
request.set(http::field::user_agent, APP_NAME " " APP_VERSION);
// setup a file response
if (!destinationFilePath.empty()) {
auto &fileResponse = response.emplace<FileResponse>();
boost::beast::error_code errorCode;
fileResponse.body_limit(100 * 1024 * 1024);
fileResponse.get().body().open(destinationFilePath.data(), file_mode::write, errorCode);
if (errorCode != boost::beast::errc::success) {
m_handler(*this, HttpClientError("opening output file", errorCode));
return;
}
}
// look up the domain name
m_resolver.async_resolve(host, port,
boost::asio::ip::tcp::resolver::canonical_name | boost::asio::ip::tcp::resolver::passive | boost::asio::ip::tcp::resolver::all_matching,
std::bind(&SslSession::resolved, shared_from_this(), std::placeholders::_1, std::placeholders::_2));
}
void SslSession::resolved(boost::beast::error_code ec, ip::tcp::resolver::results_type results)
{
if (ec) {
m_handler(*this, HttpClientError("resolving", ec));
return;
}
// make the connection on the IP address we get from a lookup
boost::asio::async_connect(
m_stream.next_layer(), results.begin(), results.end(), std::bind(&SslSession::connected, shared_from_this(), std::placeholders::_1));
}
void SslSession::connected(boost::beast::error_code ec)
{
if (ec) {
m_handler(*this, HttpClientError("connecting", ec));
return;
}
// perform the SSL handshake
m_stream.async_handshake(ssl::stream_base::client, std::bind(&SslSession::handshakeDone, shared_from_this(), std::placeholders::_1));
}
void SslSession::handshakeDone(boost::beast::error_code ec)
{
if (ec) {
m_handler(*this, HttpClientError("SSL handshake", ec));
return;
}
// send the HTTP request to the remote host
boost::beast::http::async_write(
m_stream, request, std::bind(&SslSession::requested, shared_from_this(), std::placeholders::_1, std::placeholders::_2));
}
void SslSession::requested(boost::beast::error_code ec, std::size_t bytesTransferred)
{
boost::ignore_unused(bytesTransferred);
if (ec) {
m_handler(*this, HttpClientError("sending request", ec));
return;
}
// receive the HTTP response
std::visit(
[this](auto &&response) {
http::async_read(
m_stream, m_buffer, response, std::bind(&SslSession::received, shared_from_this(), std::placeholders::_1, std::placeholders::_2));
},
response);
}
void SslSession::received(boost::beast::error_code ec, std::size_t bytesTransferred)
{
boost::ignore_unused(bytesTransferred);
if (ec) {
m_handler(*this, HttpClientError("receiving response", ec));
return;
}
// close the stream gracefully
m_stream.async_shutdown(std::bind(&SslSession::closed, shared_from_this(), std::placeholders::_1));
}
void SslSession::closed(boost::beast::error_code ec)
{
if (ec == boost::asio::error::eof) {
// rationale: http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error
ec = {};
}
if (ec) {
m_handler(*this, HttpClientError("closing connection", ec));
return;
}
// if we get here then the connection is closed gracefully
m_handler(*this, HttpClientError());
}
template <typename SessionType, typename... ArgType>
std::variant<string, std::shared_ptr<Session>, std::shared_ptr<SslSession>> runSession(const std::string &host, const std::string &port,
const std::string &target, std::function<void(SessionData, const HttpClientError &)> &&handler, std::string &&destinationPath,
std::string_view userName, std::string_view password, ArgType &&...args)
{
auto session = make_shared<SessionType>(args..., [handler{ move(handler) }](auto &session2, const HttpClientError &error) mutable {
handler(SessionData{ session2.shared_from_this(), session2.request, session2.response, session2.destinationFilePath }, error);
});
if (!userName.empty()) {
const auto authInfo = userName % ":" + password;
session->request.set(boost::beast::http::field::authorization,
"Basic " + encodeBase64(reinterpret_cast<const std::uint8_t *>(authInfo.data()), static_cast<std::uint32_t>(authInfo.size())));
}
session->destinationFilePath = move(destinationPath);
session->run(host.data(), port.data(), http::verb::get, target.data());
return std::variant<string, std::shared_ptr<Session>, std::shared_ptr<SslSession>>(std::move(session));
}
std::variant<string, std::shared_ptr<Session>, std::shared_ptr<SslSession>> runSessionFromUrl(boost::asio::io_context &ioContext,
boost::asio::ssl::context &sslContext, std::string_view url, std::function<void(SessionData, const HttpClientError &)> &&handler,
std::string &&destinationPath, std::string_view userName, std::string_view password)
{
string host, port, target;
auto ssl = false;
if (startsWith(url, "http:")) {
url = url.substr(5);
} else if (startsWith(url, "https:")) {
url = url.substr(6);
ssl = true;
} else {
return "db mirror for database has unsupported protocol";
}
auto urlParts = splitStringSimple<vector<std::string_view>>(url, "/");
target.reserve(url.size());
for (const auto &part : urlParts) {
if (part.empty()) {
continue;
}
if (host.empty()) {
host = part;
continue;
}
target += '/';
target += part;
}
if (const auto lastColon = host.find_last_of(':'); lastColon != std::string_view::npos) {
port = host.substr(lastColon + 1);
host = host.substr(0, lastColon);
}
if (port.empty()) {
port = ssl ? "443" : "80";
}
if (ssl) {
return runSession<SslSession>(host, port, target, move(handler), move(destinationPath), userName, password, ioContext, sslContext);
} else {
return runSession<Session>(host, port, target, move(handler), move(destinationPath), userName, password, ioContext);
}
}
} // namespace WebClient
} // namespace LibRepoMgr