/* * This file is part of the Flowee project * Copyright (C) 2025 Tom Zander * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "SimpleHttpClient.h" #include #include #include #include // static std::shared_ptr SimpleHttpClient::create(boost::asio::io_context &context, boost::asio::ssl::context &sslContext, const boost::beast::http::request> &request) { std::shared_ptr rc; rc.reset(new SimpleHttpClient(context, sslContext)); if (request.find(boost::beast::http::field::host) == request.end()) throw std::runtime_error("Missing hostname"); rc->m_request = request; auto app = QCoreApplication::instance(); QString useragent = QString("%1%2/%3") .arg(app->organizationName(), app->applicationName(), app->applicationVersion()); rc->m_request.set(boost::beast::http::field::user_agent, useragent.toStdString()); auto size = QString::number(request.body().size()); rc->m_request.set(boost::beast::http::field::content_length, size.toStdString()); // kickstart the process. boost::asio::post(context, std::bind(&SimpleHttpClient::resolve, rc)); return rc; } std::shared_ptr SimpleHttpClient::createIncremental(boost::asio::io_context &context, boost::asio::ssl::context &sslContext, const boost::beast::http::request > &request) { auto p = create(context, sslContext, request); p->m_incremental = true; return p; } int SimpleHttpClient::httpErrorCode() const { if (m_response) return m_response->result_int(); return m_headResponse.result_int(); } std::string_view SimpleHttpClient::headerValue(const char *name) const { if (m_response) return (*m_response)[name]; return m_headResponse[name]; } std::string_view SimpleHttpClient::headerValue(boost::beast::http::field field) const { if (m_response) return (*m_response)[field]; return m_headResponse[field]; } QDateTime SimpleHttpClient::headerDate(boost::beast::http::field field) const { auto strView = headerValue(field); // http date/time format example: "Fri, 14 Mar 2025 21:58:39 GMT" return QDateTime::fromString( QString::fromLatin1(strView.begin(), strView.size()), "ddd, dd MMM yyyy hh:mm:ss t"); } boost::beast::http::request> SimpleHttpClient::request() const { return m_request; } Streaming::ConstBuffer SimpleHttpClient::responseBody() const { return m_responseBody; } SimpleHttpClient::SimpleHttpClient(boost::asio::io_context &context, boost::asio::ssl::context &sslContext) : m_resolver(context), m_stream(context, sslContext) { } void SimpleHttpClient::resolve() { // we always use https, which implies we always need to have a hostname // as opposed to a raw IP. // so we can assume that the user put a DNS-valid name in the request. auto hostIter = m_request.find(boost::beast::http::field::host); assert(hostIter != m_request.end()); std::string hostname = hostIter->value(); // Set SNI (Server Name Indication) if (!SSL_set_tlsext_host_name(m_stream.native_handle(), hostname.c_str())) { boost::beast::error_code ec{static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category()}; logCritical() << "SNI Error: " << ec.message(); setError(DnsIssues); emit finished(); return; } // resolve and then move to the connect method m_resolver.async_resolve(hostname, "443", std::bind(&SimpleHttpClient::connect, shared_from_this(), std::placeholders::_1, std::placeholders::_2)); } void SimpleHttpClient::connect(boost::beast::error_code ec, boost::asio::ip::tcp::resolver::results_type results) { if (ec) { logWarning() << "SimpleHttpClient hit error" << ec.message(); setError(DnsIssues); emit finished(); return; } boost::beast::get_lowest_layer(m_stream).async_connect(results, std::bind(&SimpleHttpClient::sslHandshake, shared_from_this(), std::placeholders::_1)); } void SimpleHttpClient::sslHandshake(boost::beast::error_code ec) { if (ec) { logWarning() << "SimpleHttpClient hit error" << ec.message(); setError(SslIssues); emit finished(); return; } m_stream.async_handshake(boost::asio::ssl::stream_base::client, std::bind(&SimpleHttpClient::sendRequest, shared_from_this(), std::placeholders::_1)); } void SimpleHttpClient::sendRequest(boost::beast::error_code ec) { if (ec) { logWarning() << "SimpleHttpClient hit error" << ec.message(); setError(SslIssues); emit finished(); return; } boost::beast::http::async_write(m_stream, m_request, std::bind(&SimpleHttpClient::requestSent, shared_from_this(), std::placeholders::_1, std::placeholders::_2)); } void SimpleHttpClient::requestSent(boost::beast::error_code ec, std::size_t bytesTransferred) { logInfo() << "Sent" << bytesTransferred << "bytes in request, waiting for reply"; if (ec) { logWarning() << "SimpleHttpClient hit error" << ec.message(); setError(SendIssues); emit finished(); return; } // Receive the HTTP response asynchronously if (m_request.method() == boost::beast::http::verb::head) { // this feels very dirty, but that's boost for you. // there being no body is failing the normal flow, so needs special attention. m_parser.reset(new boost::beast::http::response_parser()); m_parser->skip(true); boost::beast::http::async_read(m_stream, m_readBuffer, *m_parser, std::bind(&SimpleHttpClient::readHeaderReply, shared_from_this(), std::placeholders::_1, std::placeholders::_2)); } else if (m_incremental) { m_partialParser.reset(new boost::beast::http::response_parser()); boost::beast::http::async_read_some(m_stream, m_readBuffer, *m_partialParser, std::bind(&SimpleHttpClient::readPartialReply, shared_from_this(), std::placeholders::_1, std::placeholders::_2)); } else { m_response.reset(new boost::beast::http::response()); boost::beast::http::async_read(m_stream, m_readBuffer, *m_response, std::bind(&SimpleHttpClient::readReply, shared_from_this(), std::placeholders::_1, std::placeholders::_2)); } } void SimpleHttpClient::readHeaderReply(boost::beast::error_code ec, std::size_t bytesTransferred) { assert(m_parser.get()); logInfo() << "Reply HEAD received." << bytesTransferred << "bytes"; if (ec) { logWarning() << "SimpleHttpClient hit error" << ec.message(); setError(ReceiveIssues); } else { m_headResponse = m_parser->release(); if (httpErrorCode() != 200) setError(PeerIssues); } m_stream.async_shutdown( std::bind(&SimpleHttpClient::connectionShutdown, shared_from_this(), std::placeholders::_1)); } void SimpleHttpClient::readReply(boost::beast::error_code ec, std::size_t bytesTransferred) { logInfo() << "Reply received." << bytesTransferred << "bytes"; assert(m_response.get()); if (ec) { logWarning() << "SimpleHttpClient hit error" << ec.message(); setError(ReceiveIssues); } else { assert(m_request.method() != boost::beast::http::verb::head); if (httpErrorCode() != 200) setError(PeerIssues); const auto buf = m_response->body(); auto pool = Streaming::pool(buf.size()); std::string data = boost::beast::buffers_to_string(buf.data()); // misnomer, its just a stupid bytearray pool->write(data.c_str(), data.size()); m_responseBody = pool->commit(); } // Shutdown the SSL stream asynchronously m_stream.async_shutdown( std::bind(&SimpleHttpClient::connectionShutdown, shared_from_this(), std::placeholders::_1)); } void SimpleHttpClient::readPartialReply(boost::beast::error_code ec, std::size_t bytesTransferred) { logInfo() << "Reply received." << bytesTransferred << "bytes"; assert(m_partialParser.get()); if (ec && ec != boost::beast::http::error::end_of_stream) { logWarning() << "SimpleHttpClient hit error" << ec.message(); setError(ReceiveIssues); return; } if (!m_partialParser->is_header_done()) { // need more! boost::beast::http::async_read_some(m_stream, m_readBuffer, *m_partialParser, std::bind(&SimpleHttpClient::readHeaderReply, shared_from_this(), std::placeholders::_1, std::placeholders::_2)); return; } auto &body = m_partialParser->get().body(); auto data = body.data(); std::size_t available = boost::beast::buffer_bytes(data); if (available > 0) { auto pool = Streaming::pool(available); auto *ptr = pool->data(); for (const auto &buf : data) { ptr = std::copy(boost::asio::buffers_begin(buf), boost::asio::buffers_end(buf), ptr); } body.consume(available); emit dataAvailable(pool->commit(available)); } if (m_partialParser->is_done()) { m_stream.async_shutdown( std::bind(&SimpleHttpClient::connectionShutdown, shared_from_this(), std::placeholders::_1)); } else { boost::beast::http::async_read_some(m_stream, m_readBuffer, *m_partialParser, std::bind(&SimpleHttpClient::readPartialReply, shared_from_this(), std::placeholders::_1, std::placeholders::_2)); } } void SimpleHttpClient::connectionShutdown(boost::beast::error_code ec) { if (ec && ec != boost::asio::error::eof && ec != boost::asio::ssl::error::stream_truncated) { logWarning() << "Shutdown error:" << ec.message(); } emit finished(); } SimpleHttpClient::ErrorType SimpleHttpClient::error() const { return m_error; } void SimpleHttpClient::setError(ErrorType newError) { if (m_error == newError) return; m_error = newError; if (m_error != NoError) emit errored(m_error); }