diff --git a/source/mijin/net/http.cpp b/source/mijin/net/http.cpp index 66bd938..60ac471 100644 --- a/source/mijin/net/http.cpp +++ b/source/mijin/net/http.cpp @@ -1,6 +1,8 @@ #include "./http.hpp" +#include + #include "../util/iterators.hpp" #include "../util/string.hpp" @@ -69,23 +71,14 @@ Task HTTPStream::c_writeRequest(const mijin::HTTPRequest& request) } } - MIJIN_HTTP_WRITE(request.method); - MIJIN_HTTP_WRITE(" "); - MIJIN_HTTP_WRITE(request.address); - MIJIN_HTTP_WRITE(" HTTP/1.0\n"); + MIJIN_HTTP_WRITE(std::format("{} {} HTTP/{}.{}\n", request.method, request.address, request.version.major, request.version.minor)); for (const auto& [key, value] : moreHeaders) { - MIJIN_HTTP_WRITE(key); - MIJIN_HTTP_WRITE(": "); - MIJIN_HTTP_WRITE(value); - MIJIN_HTTP_WRITE("\n"); + MIJIN_HTTP_WRITE(std::format("{}: {}\n", key, value)); } for (const auto& [key, value] : request.headers) { - MIJIN_HTTP_WRITE(key); - MIJIN_HTTP_WRITE(": "); - MIJIN_HTTP_WRITE(value); - MIJIN_HTTP_WRITE("\n"); + MIJIN_HTTP_WRITE(std::format("{}: {}\n", key, value)); } MIJIN_HTTP_WRITE("\n"); @@ -163,7 +156,106 @@ Task> HTTPStream::c_readResponse() noexcept co_return response; } + +Task> HTTPClient::c_request(ip_address_t address, std::uint16_t port, bool https, + HTTPRequest request) noexcept +{ + if (const StreamError error = createSocket(address, port, https); error != StreamError::SUCCESS) + { + co_return error; + } + if (!request.headers.contains("connection")) + { + request.headers.emplace("connection", "keep-alive"); + } + StreamResult response = co_await stream_->c_request(request); + if (response.isError()) + { + disconnect(); + if (const StreamError error = createSocket(address, port, https); error != StreamError::SUCCESS) + { + co_return error; + } + response = co_await stream_->c_request(request); + } + co_return response; +} + +Task> HTTPClient::c_request(const URL& url, HTTPRequest request) noexcept +{ + if (url.getHost().empty()) + { + co_return StreamError::UNKNOWN_ERROR; + } + + std::uint16_t port = url.getPort(); + bool https = false; + if (equalsIgnoreCase(url.getScheme(), "http")) + { + port = (port != 0) ? port : 80; + } + else if (equalsIgnoreCase(url.getScheme(), "https")) + { + port = (port != 0) ? port : 443; + https = true; + } + else + { + co_return StreamError::UNKNOWN_ERROR; + } + Optional ipAddress = ipAddressFromString(url.getHost()); + // TODO: lookup host + if (ipAddress.empty()) + { + co_return StreamError::UNKNOWN_ERROR; + } + + if (!request.headers.contains("host")) + { + request.headers.emplace("host", url.getHost()); + } + request.address = url.getPathQueryFragment(); + if (request.address.empty()) + { + request.address = "/"; + } + + co_return co_await c_request(*ipAddress, port, https, std::move(request)); +} + +void HTTPClient::disconnect() noexcept +{ + if (socket_ == nullptr) + { + return; + } + stream_.destroy(); + socket_ = nullptr; +} + +StreamError HTTPClient::createSocket(ip_address_t address, std::uint16_t port, bool https) noexcept +{ + if (socket_ != nullptr && address == lastIP_ && port == lastPort_ && https == lastWasHttps_) + { + return StreamError::SUCCESS; + } + + disconnect(); + MIJIN_ASSERT(!https, "HTTPS not supported yet."); + std::unique_ptr newSocket = std::make_unique(); + if (const StreamError error = newSocket->open(address, port); error != StreamError::SUCCESS) + { + return error; + } + lastIP_ = address; + lastPort_ = port; + lastWasHttps_ = https; + socket_ = std::move(newSocket); + stream_.construct(socket_->getStream()); + return StreamError::SUCCESS; +} } #undef MIJIN_HTTP_WRITE -#undef MIJIN_HTTP_READLINE +#undef MIJIN_HTTP_CHECKREAD +#undef MIJIN_HTTP_READLINE \ No newline at end of file diff --git a/source/mijin/net/http.hpp b/source/mijin/net/http.hpp index 48be325..dacda00 100644 --- a/source/mijin/net/http.hpp +++ b/source/mijin/net/http.hpp @@ -7,6 +7,9 @@ #include #include #include +#include "./socket.hpp" +#include "./url.hpp" +#include "../container/boxed_object.hpp" #include "../io/stream.hpp" namespace mijin @@ -19,12 +22,11 @@ struct HTTPVersion struct HTTPRequest { + HTTPVersion version = {1, 1}; std::string address; std::string method = "GET"; std::multimap headers; std::string body; - - explicit HTTPRequest(std::string address_) noexcept : address(std::move(address_)) {} }; struct HTTPResponse @@ -50,6 +52,23 @@ private: Task c_writeRequest(const HTTPRequest& request) noexcept; Task> c_readResponse() noexcept; }; + +class HTTPClient +{ +private: + std::unique_ptr socket_; + mijin::BoxedObject stream_; + ip_address_t lastIP_; + std::uint16_t lastPort_ = 0; + bool lastWasHttps_ = false; +public: + ~HTTPClient() noexcept { disconnect(); } + Task> c_request(ip_address_t address, std::uint16_t port, bool https, HTTPRequest request) noexcept; + Task> c_request(const URL& url, HTTPRequest request = {}) noexcept; + void disconnect() noexcept; +private: + StreamError createSocket(ip_address_t address, std::uint16_t port, bool https) noexcept; +}; } #endif // !defined(MIJIN_NET_HTTP_HPP_INCLUDED) diff --git a/source/mijin/net/socket.cpp b/source/mijin/net/socket.cpp index 96a3576..e71a2a1 100644 --- a/source/mijin/net/socket.cpp +++ b/source/mijin/net/socket.cpp @@ -265,16 +265,25 @@ mijin::Task TCPStream::c_readRaw(std::span buffer, co MIJIN_ASSERT(isOpen(), "Socket is not open."); setAsync(true); + if (buffer.empty()) + { + co_return StreamError::SUCCESS; + } + while(true) { const long bytesRead = osRecv(handle_, buffer, readFlags(options)); - if (bytesRead >= 0) + if (bytesRead > 0) { if (outBytesRead != nullptr) { *outBytesRead = static_cast(bytesRead); } co_return StreamError::SUCCESS; } + else if (bytesRead == 0) + { + co_return StreamError::CONNECTION_CLOSED; + } else if (errno != EAGAIN) { co_return translateErrno(); @@ -286,14 +295,25 @@ mijin::Task TCPStream::c_readRaw(std::span buffer, co mijin::Task TCPStream::c_writeRaw(std::span buffer) { MIJIN_ASSERT(isOpen(), "Socket is not open."); + + if (buffer.empty()) + { + co_return StreamError::SUCCESS; + } + setAsync(true); while (true) { - if (osSend(handle_, buffer, 0) >= 0) + const long bytesSent = osSend(handle_, buffer, 0); + if (bytesSent == static_cast(buffer.size())) { co_return StreamError::SUCCESS; } + else if (bytesSent == 0) + { + co_return StreamError::CONNECTION_CLOSED; + } else if (errno != EAGAIN) { co_return translateErrno();