diff --git a/include/ilias/detail/expected.hpp b/include/ilias/detail/expected.hpp index 7d39f38..10b322d 100644 --- a/include/ilias/detail/expected.hpp +++ b/include/ilias/detail/expected.hpp @@ -21,7 +21,7 @@ #include #if defined(__cpp_lib_expected) -#if __cpp_lib_expected > 202211L +#if __cpp_lib_expected >= 202211L #include #define ILIAS_STD_EXPECTED_HPP #endif diff --git a/include/ilias/http/cookie.hpp b/include/ilias/http/cookie.hpp index b96a0ea..c6bca7b 100644 --- a/include/ilias/http/cookie.hpp +++ b/include/ilias/http/cookie.hpp @@ -480,7 +480,7 @@ inline auto HttpCookieJar::cookiesForUrl(const Url &url) -> std::vector google.com + while (!cur.empty()) { //for each level of the domain, www.google.com -> .google.com -> google.com auto iter = mCookies.find(cur); if (iter != mCookies.end()) { // Add all items in current domain to it @@ -499,14 +499,18 @@ inline auto HttpCookieJar::cookiesForUrl(const Url &url) -> std::vector #include #include +#include #include #include #include @@ -101,6 +102,20 @@ class HttpSession { * @param proxy */ auto setProxy(const Url &proxy) -> void { mProxy = proxy; } + + /** + * @brief Get the Cookie Jar object + * + * @return HttpCookieJar* + */ + auto cookieJar() const -> HttpCookieJar * { return mCookieJar; } + + /** + * @brief Get the proxy + * + * @return const Url& + */ + auto proxy() const -> const Url & { return mProxy; } private: /** * @brief The sendRequest implementation, only do the connection handling @@ -126,8 +141,9 @@ class HttpSession { * @brief Collect cookies from the reply and add them to the cookie jar * * @param reply + * @param url Current request url */ - auto parseReply(HttpReply &reply) -> void; + auto parseReply(HttpReply &reply, const Url &url) -> void; /** * @brief Connect to the server by url and return the HttpStream for transfer @@ -183,20 +199,37 @@ inline auto HttpSession::sendRequest(std::string_view method, const HttpRequest } int idx = 0; // The number of redirects while (true) { +#if 1 + auto [reply_, timeout] = co_await whenAny( + sendRequestImpl(method, url, headers, payload, request.streamMode()), + sleep(request.transferTimeout()) + ); + if (timeout) { //< Timed out + co_return Unexpected(Error::TimedOut); + } + if (!reply_) { //< No reply, canceled + co_return Unexpected(Error::Canceled); + } + if (!reply_->has_value()) { //< Failed to get + co_return Unexpected(reply_->error()); + } + auto &reply = reply_.value(); +#else auto reply = co_await sendRequestImpl(method, url, headers, payload, request.streamMode()); if (!reply) { co_return Unexpected(reply.error()); } +#endif const std::array redirectCodes = {301, 302, 303, 307, 308}; if (std::find(redirectCodes.begin(), redirectCodes.end(), reply->statusCode()) != redirectCodes.end() && idx < maximumRedirects) { - auto location = reply->headers().value(HttpHeaders::Location); + Url location = reply->headers().value(HttpHeaders::Location); if (location.empty()) { co_return Unexpected(Error::HttpBadReply); } ILIAS_INFO("Http", "Redirecting to {} ({} of maximum {})", location, idx + 1, maximumRedirects); // Do redirect - url = location; + url = url.resolved(location); headers = request.headers(); ++idx; continue; @@ -209,8 +242,8 @@ inline auto HttpSession::sendRequest(std::string_view method, const HttpRequest inline auto HttpSession::sendRequestImpl(std::string_view method, const Url &url, HttpHeaders &headers, std::span payload, bool streamMode) -> Task { + normalizeRequest(url, headers); while (true) { - normalizeRequest(url, headers); bool fromPool = false; auto stream = co_await connect(url, fromPool); if (!stream) { @@ -231,7 +264,7 @@ inline auto HttpSession::sendRequestImpl(std::string_view method, const Url &url } co_return Unexpected(reply.error()); } - parseReply(reply.value()); + parseReply(reply.value(), url); co_return std::move(*reply); } } @@ -270,7 +303,7 @@ inline auto HttpSession::normalizeRequest(const Url &url, HttpHeaders &headers) } } -inline auto HttpSession::parseReply(HttpReply &reply) -> void { +inline auto HttpSession::parseReply(HttpReply &reply, const Url &url) -> void { // Update cookie here if (!mCookieJar) { return; @@ -278,7 +311,7 @@ inline auto HttpSession::parseReply(HttpReply &reply) -> void { const auto cookies = reply.headers().values(HttpHeaders::SetCookie); for (const auto &setCookie : cookies) { for (auto &cookie : HttpCookie::parse(setCookie)) { - cookie.normalize(reply.url()); + cookie.normalize(url); mCookieJar->insertCookie(cookie); } } @@ -296,7 +329,7 @@ inline auto HttpSession::connect(const Url &url, bool &fromPool) -> Tasks_port); } @@ -382,6 +415,10 @@ inline auto HttpSession::connect(const Url &url, bool &fromPool) -> Task > { const auto n = buffer.size(); while (true) { auto buf = readWindow(); - if (!buf.empty()) { + if (!buf.empty() || n == 0) { // Read data from the buffer auto len = std::min(buf.size(), n); ::memcpy(buffer.data(), buf.data(), len); diff --git a/include/ilias/net/sockfd.hpp b/include/ilias/net/sockfd.hpp index 1702aba..6129194 100644 --- a/include/ilias/net/sockfd.hpp +++ b/include/ilias/net/sockfd.hpp @@ -292,6 +292,7 @@ class SocketView { if (ret < 0) { return Unexpected(SystemError::fromErrno()); } + return {}; } #endif diff --git a/include/ilias/net/tcp.hpp b/include/ilias/net/tcp.hpp index 7eddbce..f5c3a09 100644 --- a/include/ilias/net/tcp.hpp +++ b/include/ilias/net/tcp.hpp @@ -179,6 +179,29 @@ class TcpListener { auto close() { return mBase.close(); } + /** + * @brief Set the socket option. + * + * @tparam T + * @param opt + * @return Result + */ + template + auto setOption(const T &opt) -> Result { + return socket().setOption(opt); + } + + /** + * @brief Get the socket option. + * + * @tparam T + * @return Result + */ + template + auto getOption() -> Result { + return socket().getOption(); + } + /** * @brief Bind the listener to a local endpoint. * @@ -217,6 +240,15 @@ class TcpListener { return mBase.localEndpoint(); } + /** + * @brief Get the underlying socket. + * + * @return SocketView + */ + auto socket() const -> SocketView { + return mBase.socket(); + } + /** * @brief Check if the socket is valid. * diff --git a/include/ilias/platform/qt.hpp b/include/ilias/platform/qt.hpp index 9eca933..f464307 100644 --- a/include/ilias/platform/qt.hpp +++ b/include/ilias/platform/qt.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -148,6 +149,7 @@ class QIoContext final : public IoContext, public QObject { auto submitTimer(uint64_t ms, detail::QTimerAwaiter *awaiter) -> int; auto cancelTimer(int timerId) -> void; + SockInitializer mInit; size_t mNumOfDescriptors = 0; //< How many descriptors are added std::map mTimers; //< Timer map friend class detail::QTimerAwaiter; @@ -196,12 +198,18 @@ inline auto QIoContext::addDescriptor(fd_t fd, IoDescriptor::Type type) -> Resul // Prepare env for Socket if (nfd->pollable) { nfd->sockfd = qintptr(fd); - nfd->readNotifier = new QSocketNotifier(QSocketNotifier::Read, nfd.get()); - nfd->writeNotifier = new QSocketNotifier(QSocketNotifier::Write, nfd.get()); - nfd->exceptNotifier = new QSocketNotifier(QSocketNotifier::Exception, nfd.get()); + nfd->readNotifier = new QSocketNotifier(nfd->sockfd, QSocketNotifier::Read, nfd.get()); + nfd->writeNotifier = new QSocketNotifier(nfd->sockfd, QSocketNotifier::Write, nfd.get()); + nfd->exceptNotifier = new QSocketNotifier(nfd->sockfd, QSocketNotifier::Exception, nfd.get()); nfd->readNotifier->setEnabled(false); nfd->writeNotifier->setEnabled(false); nfd->exceptNotifier->setEnabled(false); + + // Set nonblock + SocketView sockfd(nfd->sockfd); + if (auto ret = sockfd.setBlocking(false); !ret) { + return Unexpected(ret.error()); + } } ++mNumOfDescriptors; @@ -332,6 +340,7 @@ inline auto QIoContext::poll(IoDescriptor *fd, uint32_t event) -> Task inline auto QIoContext::timerEvent(QTimerEvent *event) -> void { auto iter = mTimers.find(event->timerId()); if (iter == mTimers.end()) { + ILIAS_WARN("QIo", "Timer {} not found", event->timerId()); return; } auto [id, awaiter] = *iter; @@ -346,6 +355,7 @@ inline auto QIoContext::submitTimer(uint64_t timeout, detail::QTimerAwaiter *awa return 0; } mTimers.emplace(id, awaiter); + return id; } inline auto QIoContext::cancelTimer(int id) -> void { @@ -387,6 +397,7 @@ inline auto detail::QTimerAwaiter::onTimeout() -> void { // Poll inline auto detail::QPollAwaiter::await_suspend(TaskView<> caller) -> void { + ILIAS_TRACE("QIo", "poll fd {}", mFd->sockfd); mCaller = caller; doConnect(); //< Connect the signal mRegistration = caller.cancellationToken().register_(std::bind(&QPollAwaiter::onCancel, this)); @@ -398,17 +409,21 @@ inline auto detail::QPollAwaiter::await_resume() -> Result { } inline auto detail::QPollAwaiter::onCancel() -> void { + ILIAS_TRACE("QIo", "poll fd {} was canceled", mFd->sockfd); doDisconnect(); + mResult = Unexpected(Error::Canceled); mCaller.schedule(); } inline auto detail::QPollAwaiter::onFdDestroyed() -> void { + ILIAS_TRACE("QIo", "fd {} was destroyed", mFd->sockfd); doDisconnect(); mResult = Unexpected(Error::Canceled); mCaller.schedule(); } inline auto detail::QPollAwaiter::onNotifierActivated(QSocketDescriptor, QSocketNotifier::Type type) -> void { + ILIAS_TRACE("QIo", "fd {} was activated", mFd->sockfd); doDisconnect(); if (type == QSocketNotifier::Read) { mResult = PollEvent::In; diff --git a/include/ilias/url.hpp b/include/ilias/url.hpp index 3594c92..2a75b2f 100644 --- a/include/ilias/url.hpp +++ b/include/ilias/url.hpp @@ -55,6 +55,14 @@ class Url { * @return false */ auto isValid() const -> bool; + + /** + * @brief Check the url is relative + * + * @return true + * @return false + */ + auto isRelative() const -> bool; /** * @brief Get the scheme of the url @@ -91,6 +99,14 @@ class Url { */ auto port() const -> std::optional; + /** + * @brief Resolve the relative url to the absolute url (if arg is not relative, just return it) + * + * @param relative + * @return Url + */ + auto resolved(const Url &relative) const -> Url; + /** * @brief Make the url to the string (encoded) * @@ -203,6 +219,10 @@ inline auto Url::isValid() const -> bool { return isSafeString(p); } +inline auto Url::isRelative() const -> bool { + return mScheme.empty(); +} + inline auto Url::scheme() const -> std::string_view { return mScheme; } @@ -226,6 +246,18 @@ inline auto Url::path() const -> std::string_view { return mPath; } +// Resolved +inline auto Url::resolved(const Url &rel) const -> Url { + if (!rel.isRelative()) { + return rel; + } + Url copy(rel); + copy.setScheme(mScheme); + copy.setHost(mHost); + copy.setPort(mPort); + return copy; +} + // Set inline auto Url::setScheme(std::string_view scheme) -> void { mScheme = scheme; diff --git a/tests/unit/net/net.cpp b/tests/unit/net/net.cpp index 91934f3..eaed674 100644 --- a/tests/unit/net/net.cpp +++ b/tests/unit/net/net.cpp @@ -10,7 +10,7 @@ using namespace ILIAS_NAMESPACE; TEST(Net, TcpTransfer) { - ILIAS_LOG_SET_LEVEL(ILIAS_TRACE_LEVEL); + // ILIAS_LOG_SET_LEVEL(ILIAS_TRACE_LEVEL); auto ctxt = IoContext::currentThread(); ILIAS_TRACE("test", "create io context"); TcpListener listener(*ctxt, AF_INET); @@ -21,12 +21,9 @@ TEST(Net, TcpTransfer) { auto endpoint = listener.localEndpoint().value(); std::cout << endpoint.toString() << std::endl; - for (int i = 0; i < 3; i++) { - std::mt19937_64 rng; - rng.seed(std::random_device()()); - std::uniform_int_distribution dist(1024 * 1024, 1024 * 1024 * 1024); - //< Test from 1MB to 1GB. - size_t bytesToTransfer = dist(rng); + const std::array capacities = { 1024, 1024 * 1024, 1024 * 1024 * 1024 }; + for (auto bytesToTransfer : capacities) { + //< Test from 1MB up to 1GB. size_t senderSent = 0; size_t receiverReceived = 0; @@ -82,6 +79,7 @@ TEST(Net, TcpTransfer) { } TEST(Net, CloseCancel) { + ILIAS_LOG_SET_LEVEL(ILIAS_TRACE_LEVEL); auto ctxt = IoContext::currentThread(); UdpClient client(*ctxt, AF_INET); ASSERT_TRUE(client.bind("127.0.0.1:0")); diff --git a/tests/unit/platform/qt.cpp b/tests/unit/platform/qt.cpp new file mode 100644 index 0000000..37e5494 --- /dev/null +++ b/tests/unit/platform/qt.cpp @@ -0,0 +1,155 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include "ui_qt.h" + +#if defined(_WIN32) + #pragma comment(linker, "/subsystem:console") // Windows only +#endif + +using namespace ILIAS_NAMESPACE; + +class App final : public QMainWindow { +public: + App() { + ui.setupUi(this); + mSession.setCookieJar(&mCookieJar); + + // Prep signals + connect(ui.httpSendButton, &QPushButton::clicked, this, [this]() { + spawn(sendHttpRequest()); + }); + + connect(ui.addrinfoButton, &QPushButton::clicked, this, [this]() { + spawn(sendGetAddrInfo()); + }); + + connect(ui.httpSaveButton, &QPushButton::clicked, this, [this]() { + if (mContent.empty()) { + QMessageBox::information(this, "No content", "No content to save"); + return; + } + auto filename = QFileDialog::getSaveFileName(this, "Save file", "", "All Files (*)"); + if (filename.isEmpty()) { + return; + } + QFile file(filename); + if (!file.open(QIODevice::WriteOnly)) { + QMessageBox::critical(this, "Error", "Could not open file for writing"); + return; + } + file.write(reinterpret_cast(mContent.data()), mContent.size()); + file.close(); + }); + + connect(ui.httpProxyButton, &QPushButton::clicked, this, [this]() { + auto prevProxy = mSession.proxy(); + auto proxy = QInputDialog::getText(this, "Proxy", "Proxy URL:", QLineEdit::Normal, QString::fromUtf8(prevProxy.toString())); + mSession.setProxy(proxy.toUtf8().data()); + }); + } + + auto sendHttpRequest() -> Task { + auto url = ui.httpUrlEdit->text(); + if (url.isEmpty()) { + co_return {}; + } + if (!url.startsWith("http://") && !url.startsWith("https://")) { + url = "http://" + url; + } + // Clear status + ui.statusbar->clearMessage(); + ui.httpReplyHeadersWidget->clear(); + + ui.httpContentBroswer->clear(); + ui.httpContentBroswer->hide(); + ui.httpImageLabel->hide(); + + HttpRequest request; + request.setUrl(url.toUtf8().data()); + request.setHeader(HttpHeaders::UserAgent, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36"); + auto reply = co_await mSession.sendRequest(ui.httpMethodBox->currentText().toUtf8().data(), request); + if (!reply) { + ui.statusbar->showMessage(QString::fromUtf8(reply.error().toString())); + co_return {}; + } + auto content = co_await reply->content(); + if (!content) { + ui.statusbar->showMessage(QString::fromUtf8(content.error().toString())); + co_return {}; + } + mContent = std::move(*content); + for (const auto &[key, value] : reply->headers()) { + ui.httpReplyHeadersWidget->addItem(QString("%1: %2").arg( + QString::fromUtf8(key)).arg(QString::fromUtf8(value)) + ); + } + + auto contentType = reply->headers().value("Content-Type"); + if (contentType.contains("image/")) { + ui.httpImageLabel->setPixmap(QPixmap::fromImage(QImage::fromData(mContent))); + ui.httpImageLabel->show(); + } + else if (contentType.contains("text/")) { + ui.httpContentBroswer->setPlainText(QString::fromUtf8(mContent)); + ui.httpContentBroswer->show(); + } + else { + ui.httpContentBroswer->setPlainText(QString::fromUtf8(mContent)); + ui.httpContentBroswer->show(); + } + + ui.statusbar->showMessage( + QString("HTTP %1 %2").arg(reply->statusCode()).arg(QString::fromUtf8(reply->status())) + ); + updateCookieJar(); + co_return {}; + } + + auto sendGetAddrInfo() -> Task { + ui.addrinfoListWidget->clear(); + ui.statusbar->clearMessage(); + auto addrinfo = co_await AddressInfo::fromHostnameAsync(ui.addrinfoEdit->text().toUtf8().data()); + if (!addrinfo) { + ui.statusbar->showMessage(QString::fromUtf8(addrinfo.error().toString())); + co_return {}; + } + for (const auto &addr : addrinfo->addresses()) { + ui.addrinfoListWidget->addItem(QString::fromUtf8(addr.toString())); + } + co_return {}; + } + + auto updateCookieJar() -> void { + auto cookies = mCookieJar.allCookies(); + ui.cookieWidget->clear(); + for (const auto &cookie : cookies) { + auto item = new QTreeWidgetItem(ui.cookieWidget); + item->setText(0, QString::fromUtf8(cookie.domain())); + item->setText(1, QString::fromUtf8(cookie.name())); + item->setText(2, QString::fromUtf8(cookie.value())); + item->setText(3, QString::fromUtf8(cookie.path())); + } + } +private: + QIoContext mCtxt; + HttpCookieJar mCookieJar; + HttpSession mSession {mCtxt}; + Ui::MainWindow ui; + std::vector mContent; +}; + +auto main(int argc, char **argv) -> int { + ILIAS_LOG_SET_LEVEL(ILIAS_TRACE_LEVEL); + QApplication app(argc, argv); + App win; + win.show(); + return app.exec(); +} diff --git a/tests/unit/platform/qt.lua b/tests/unit/platform/qt.lua new file mode 100644 index 0000000..2d1b12e --- /dev/null +++ b/tests/unit/platform/qt.lua @@ -0,0 +1,26 @@ + +option("qt_test") + set_default(false) + set_description("Enable qt test") +option_end() + +if has_config("qt_test") then + add_requires("zlib") + if not is_plat("windows") then + add_requires("openssl") + end + + target("test_qt") + set_default(false) + add_rules("qt.widgetapp") + add_files("qt.cpp") + add_files("qt.ui") + add_frameworks("QtCore", "QtWidgets", "QtGui") + add_defines("ILIAS_ENABLE_LOG") + add_packages("zlib") + + if not is_plat("windows") then + add_packages("openssl") + end + target_end() +end diff --git a/tests/unit/platform/qt.ui b/tests/unit/platform/qt.ui new file mode 100644 index 0000000..a9f02fe --- /dev/null +++ b/tests/unit/platform/qt.ui @@ -0,0 +1,282 @@ + + + MainWindow + + + + 0 + 0 + 944 + 865 + + + + Test Qt Platform + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 1 + + + + Addrinfo + + + + + + + + Hostname: + + + + + + + + + + getaddrinfo + + + + + + + + + + + + + Http + + + + + + + + Method: + + + + + + + true + + + + GET + + + + + POST + + + + + HEAD + + + + + DELETE + + + + + + + + Url: + + + + + + + https://httpbin.org/get + + + + + + + payload... + + + + + + + Send + + + + + + + + + Request header: + + + + + + + + + + Reply headers: + + + + + + + + + + + + Content: + + + + + + + Proxy + + + + + + + Save + + + + + + + + + + + + + + + + + + + + CookieJar + + + + + + -1 + + + 25 + + + + Domain + + + + + Path + + + + + Key + + + + + Value + + + + + + + + + + + + + + 0 + 0 + 944 + 26 + + + + + + + + + addrinfoEdit + returnPressed() + addrinfoButton + click() + + + 306 + 75 + + + 575 + 75 + + + + + httpUrlEdit + returnPressed() + httpSendButton + click() + + + 459 + 75 + + + 881 + 75 + + + + +