304 lines
11 KiB
C++
304 lines
11 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>
|
|
#include <limits>
|
|
|
|
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::setChunkHandler(ChunkHandler &&handler)
|
|
{
|
|
m_chunkProcessing = std::make_unique<ChunkProcessing>();
|
|
m_chunkProcessing->onChunkHeader
|
|
= std::bind(&Session::onChunkHeader, shared_from_this(), std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
|
|
m_chunkProcessing->onChunkBody
|
|
= std::bind(&Session::onChunkBody, shared_from_this(), std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
|
|
m_chunkProcessing->handler = std::move(handler);
|
|
}
|
|
|
|
void Session::run(
|
|
const char *host, const char *port, http::verb verb, const char *target, std::optional<std::uint64_t> bodyLimit, unsigned int version)
|
|
{
|
|
// set SNI Hostname (many hosts need this to handshake successfully)
|
|
auto *const sslStream = std::get_if<SslStream>(&m_stream);
|
|
if (sslStream
|
|
&& !SSL_ctrl(sslStream->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;
|
|
}
|
|
|
|
// 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(bodyLimit.value_or(500 * 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;
|
|
}
|
|
} else if (m_chunkProcessing) {
|
|
auto &emptyResponse = response.emplace<StringResponse>();
|
|
emptyResponse.on_chunk_header(m_chunkProcessing->onChunkHeader);
|
|
emptyResponse.on_chunk_body(m_chunkProcessing->onChunkBody);
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
|
|
inline Session::RawSocket &Session::socket()
|
|
{
|
|
auto *socket = std::get_if<RawSocket>(&m_stream);
|
|
if (!socket) {
|
|
socket = &std::get<SslStream>(m_stream).next_layer();
|
|
}
|
|
return *socket;
|
|
}
|
|
|
|
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(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;
|
|
}
|
|
|
|
if (auto *const sslStream = std::get_if<SslStream>(&m_stream)) {
|
|
// perform the SSL handshake
|
|
sslStream->async_handshake(ssl::stream_base::client, std::bind(&Session::handshakeDone, shared_from_this(), std::placeholders::_1));
|
|
} else {
|
|
sendRequest();
|
|
}
|
|
}
|
|
|
|
void Session::handshakeDone(boost::beast::error_code ec)
|
|
{
|
|
if (ec) {
|
|
m_handler(*this, HttpClientError("SSL handshake", ec));
|
|
return;
|
|
}
|
|
sendRequest();
|
|
}
|
|
|
|
void Session::sendRequest()
|
|
{
|
|
// send the HTTP request to the remote host
|
|
std::visit(
|
|
[this](auto &&stream) {
|
|
boost::beast::http::async_write(
|
|
stream, request, std::bind(&Session::requested, shared_from_this(), std::placeholders::_1, std::placeholders::_2));
|
|
},
|
|
m_stream);
|
|
}
|
|
|
|
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 &stream, auto &&response) {
|
|
if constexpr (std::is_same_v<std::decay_t<decltype(response)>, StringResponse>) {
|
|
http::async_read_header(
|
|
stream, m_buffer, response, std::bind(&Session::chunkReceived, shared_from_this(), std::placeholders::_1, std::placeholders::_2));
|
|
} else {
|
|
http::async_read(
|
|
stream, m_buffer, response, std::bind(&Session::received, shared_from_this(), std::placeholders::_1, std::placeholders::_2));
|
|
}
|
|
},
|
|
m_stream, response);
|
|
}
|
|
|
|
void Session::onChunkHeader(std::uint64_t chunkSize, boost::beast::string_view extensions, boost::beast::error_code &ec)
|
|
{
|
|
// parse the chunk extensions so we can access them easily
|
|
m_chunkProcessing->chunkExtensions.parse(extensions, ec);
|
|
if (ec) {
|
|
return;
|
|
}
|
|
|
|
if (chunkSize > std::numeric_limits<std::size_t>::max()) {
|
|
ec = boost::beast::http::error::body_limit;
|
|
return;
|
|
}
|
|
|
|
// make sure we have enough storage, and reset the container for the upcoming chunk
|
|
m_chunkProcessing->currentChunk.reserve(static_cast<std::size_t>(chunkSize));
|
|
m_chunkProcessing->currentChunk.clear();
|
|
}
|
|
|
|
std::size_t Session::onChunkBody(std::uint64_t bytesLeftInThisChunk, boost::beast::string_view chunkBodyData, boost::beast::error_code &ec)
|
|
{
|
|
// set the error so that the call to `read` returns if this is the last piece of the chunk body and we can process the chunk
|
|
if (bytesLeftInThisChunk == chunkBodyData.size()) {
|
|
ec = boost::beast::http::error::end_of_chunk;
|
|
}
|
|
|
|
// append this piece to our container
|
|
m_chunkProcessing->currentChunk.append(chunkBodyData.data(), chunkBodyData.size());
|
|
|
|
return chunkBodyData.size();
|
|
// note: The return value informs the parser of how much of the body we
|
|
// consumed. We will indicate that we consumed everything passed in.
|
|
}
|
|
|
|
void Session::chunkReceived(boost::beast::error_code ec, std::size_t bytesTransferred)
|
|
{
|
|
if (ec == boost::beast::http::error::end_of_chunk) {
|
|
m_chunkProcessing->handler(m_chunkProcessing->chunkExtensions, m_chunkProcessing->currentChunk);
|
|
} else if (ec) {
|
|
m_handler(*this, HttpClientError("receiving chunk response", ec));
|
|
return;
|
|
}
|
|
if (!continueReadingChunks()) {
|
|
received(ec, bytesTransferred);
|
|
}
|
|
}
|
|
|
|
bool Session::continueReadingChunks()
|
|
{
|
|
auto &parser = std::get<StringResponse>(response);
|
|
if (parser.is_done()) {
|
|
return false;
|
|
}
|
|
std::visit(
|
|
[this, &parser](auto &stream) {
|
|
boost::beast::http::async_read(
|
|
stream, m_buffer, parser, std::bind(&Session::chunkReceived, shared_from_this(), std::placeholders::_1, std::placeholders::_2));
|
|
},
|
|
m_stream);
|
|
return true;
|
|
}
|
|
|
|
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
|
|
if (auto *const sslStream = std::get_if<SslStream>(&m_stream)) {
|
|
// perform the SSL handshake
|
|
sslStream->async_shutdown(std::bind(&Session::closed, shared_from_this(), std::placeholders::_1));
|
|
} else if (auto *const socket = std::get_if<RawSocket>(&m_stream)) {
|
|
socket->shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
|
|
m_handler(*this, ec && ec != boost::beast::errc::not_connected ? HttpClientError("closing connection", ec) : HttpClientError());
|
|
}
|
|
}
|
|
|
|
void Session::closed(boost::beast::error_code ec)
|
|
{
|
|
// rationale regarding boost::asio::error::eof: http://stackoverflow.com/questions/25587403/boost-asio-ssl-async-shutdown-always-finishes-with-an-error
|
|
m_handler(*this, ec && ec != boost::asio::error::eof ? HttpClientError("closing connection", ec) : HttpClientError());
|
|
}
|
|
|
|
std::variant<std::string, std::shared_ptr<Session>> runSessionFromUrl(boost::asio::io_context &ioContext, boost::asio::ssl::context &sslContext,
|
|
std::string_view url, Session::Handler &&handler, std::string &&destinationPath, std::string_view userName, std::string_view password,
|
|
boost::beast::http::verb verb, std::optional<std::uint64_t> bodyLimit, Session::ChunkHandler &&chunkHandler)
|
|
{
|
|
std::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 std::string("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";
|
|
}
|
|
|
|
auto session
|
|
= ssl ? std::make_shared<Session>(ioContext, sslContext, std::move(handler)) : std::make_shared<Session>(ioContext, std::move(handler));
|
|
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 = std::move(destinationPath);
|
|
if (chunkHandler) {
|
|
session->setChunkHandler(std::move(chunkHandler));
|
|
}
|
|
session->run(host.data(), port.data(), verb, target.data(), bodyLimit);
|
|
return std::variant<std::string, std::shared_ptr<Session>>(std::move(session));
|
|
}
|
|
|
|
} // namespace WebClient
|
|
} // namespace LibRepoMgr
|