diff --git a/LibConf b/LibConf index d727b08..5d49cca 100644 --- a/LibConf +++ b/LibConf @@ -7,6 +7,8 @@ mijin_sources = Split(""" source/mijin/debug/symbol_info.cpp source/mijin/io/process.cpp source/mijin/io/stream.cpp + source/mijin/net/http.cpp + source/mijin/net/socket.cpp source/mijin/util/os.cpp source/mijin/types/name.cpp source/mijin/virtual_filesystem/filesystem.cpp diff --git a/SModule b/SModule index 1a823ac..48b8777 100644 --- a/SModule +++ b/SModule @@ -10,6 +10,7 @@ mijin_sources = Split(""" source/mijin/debug/symbol_info.cpp source/mijin/io/process.cpp source/mijin/io/stream.cpp + source/mijin/net/http.cpp source/mijin/net/socket.cpp source/mijin/util/os.cpp source/mijin/types/name.cpp diff --git a/source/mijin/io/stream.cpp b/source/mijin/io/stream.cpp index acb1e8b..46b6784 100644 --- a/source/mijin/io/stream.cpp +++ b/source/mijin/io/stream.cpp @@ -36,20 +36,12 @@ void Stream::flush() {} mijin::Task Stream::c_readRaw(std::span buffer, const ReadOptions& options, std::size_t* outBytesRead) { - (void) buffer; - (void) options; - (void) outBytesRead; - - MIJIN_ASSERT(!getFeatures().async || !getFeatures().read, "Stream advertises async read, but doesn't implement it."); - co_return StreamError::NOT_SUPPORTED; + co_return readRaw(buffer, options, outBytesRead); } mijin::Task Stream::c_writeRaw(std::span buffer) { - (void) buffer; - - MIJIN_ASSERT(!getFeatures().async || !getFeatures().write, "Stream advertises async write, but doesn't implement it."); - co_return StreamError::NOT_SUPPORTED; + co_return writeRaw(buffer); } StreamError Stream::readBinaryString(std::string& outString) diff --git a/source/mijin/io/stream.hpp b/source/mijin/io/stream.hpp index 4defaa2..22d7094 100644 --- a/source/mijin/io/stream.hpp +++ b/source/mijin/io/stream.hpp @@ -68,6 +68,7 @@ enum class [[nodiscard]] StreamError IO_ERROR = 1, NOT_SUPPORTED = 2, CONNECTION_CLOSED = 3, + PROTOCOL_ERROR = 4, UNKNOWN_ERROR = -1 }; @@ -430,6 +431,8 @@ inline const char* errorName(StreamError error) noexcept return "not supported"; case StreamError::CONNECTION_CLOSED: return "connection closed"; + case StreamError::PROTOCOL_ERROR: + return "protocol error"; case StreamError::UNKNOWN_ERROR: return "unknown error"; } diff --git a/source/mijin/net/http.cpp b/source/mijin/net/http.cpp new file mode 100644 index 0000000..66bd938 --- /dev/null +++ b/source/mijin/net/http.cpp @@ -0,0 +1,169 @@ + +#include "./http.hpp" + +#include "../util/iterators.hpp" +#include "../util/string.hpp" + +#define MIJIN_HTTP_WRITE(text) \ +do \ +{ \ + if (const StreamError error = co_await base_->c_writeText(text); error != StreamError::SUCCESS) \ + { \ + co_return error; \ + } \ +} while(false) + +#define MIJIN_HTTP_CHECKREAD(read) \ +do \ +{ \ + if (const StreamError error = co_await read; error != StreamError::SUCCESS) \ + { \ + co_return error; \ + } \ +} while(false) + +#define MIJIN_HTTP_READLINE(text) MIJIN_HTTP_CHECKREAD(base_->c_readLine(text)); text = trim(text) + +namespace mijin +{ +namespace +{ +inline constexpr std::size_t CONTENT_LENGTH_LIMIT = 100 << 20; // 100MiB +bool parseHTTPVersion(std::string_view version, HTTPVersion& outVersion) noexcept +{ + std::vector parts = split(version, "."); + if (parts.size() != 2) + { + return false; + } + return toNumber(parts[0], outVersion.major) && toNumber(parts[1], outVersion.minor); +} +} + +Task> HTTPStream::c_request(HTTPRequest request) noexcept +{ + if (const StreamError error = co_await c_writeRequest(request); error != StreamError::SUCCESS) + { + co_return error; + } + co_return co_await c_readResponse(); +} + +Task HTTPStream::c_writeRequest(const mijin::HTTPRequest& request) noexcept +{ + std::map moreHeaders; + if (!request.body.empty()) + { + auto itLength = request.headers.find("content-length"); + if (itLength == request.headers.end()) + { + moreHeaders.emplace("content-length", std::to_string(request.body.size())); + } + else + { + std::size_t headerValue = 0; + if (!toNumber(itLength->second, headerValue) || headerValue != request.body.size()) + { + co_return StreamError::PROTOCOL_ERROR; + } + } + } + + MIJIN_HTTP_WRITE(request.method); + MIJIN_HTTP_WRITE(" "); + MIJIN_HTTP_WRITE(request.address); + MIJIN_HTTP_WRITE(" HTTP/1.0\n"); + for (const auto& [key, value] : moreHeaders) + { + MIJIN_HTTP_WRITE(key); + MIJIN_HTTP_WRITE(": "); + MIJIN_HTTP_WRITE(value); + MIJIN_HTTP_WRITE("\n"); + } + 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("\n"); + if (!request.body.empty()) + { + MIJIN_HTTP_WRITE(request.body); + } + + co_return StreamError::SUCCESS; +} + +Task> HTTPStream::c_readResponse() noexcept +{ + std::string line; + MIJIN_HTTP_READLINE(line); + + std::vector parts = split(line, " ", {.limitParts = 3}); + if (parts.size() != 3) + { + co_return StreamError::PROTOCOL_ERROR; + } + if (!parts[0].starts_with("HTTP/")) + { + co_return StreamError::PROTOCOL_ERROR; + } + + HTTPResponse response; + if (!parseHTTPVersion(parts[0].substr(5), response.version) + || !toNumber(parts[1], response.status)) + { + co_return StreamError::PROTOCOL_ERROR; + } + response.statusMessage = parts[2]; + + decltype(response.headers)::iterator lastHeader; + while (true) + { + MIJIN_HTTP_READLINE(line); + if (line.empty()) { + break; + } + if (line[0] == ' ' || line[0] == '\t') + { + // continuation + if (lastHeader == response.headers.end()) + { + co_return StreamError::PROTOCOL_ERROR; + } + lastHeader->second.push_back(' '); + lastHeader->second.append(trim(line)); + } + parts = split(line, ":", {.limitParts = 2}); + if (parts.size() != 2) + { + co_return StreamError::PROTOCOL_ERROR; + } + lastHeader = response.headers.emplace(toLower(trim(parts[0])), trim(parts[1])); + } + + auto itContentLength = response.headers.find("content-length"); + if (itContentLength != response.headers.end()) + { + std::size_t contentLength = 0; + if (!toNumber(itContentLength->second, contentLength)) + { + co_return StreamError::PROTOCOL_ERROR; + } + if (contentLength > CONTENT_LENGTH_LIMIT) + { + co_return StreamError::PROTOCOL_ERROR; + } + response.content.resize(contentLength); + MIJIN_HTTP_CHECKREAD(base_->c_readRaw(response.content)); + } + + co_return response; +} +} + +#undef MIJIN_HTTP_WRITE +#undef MIJIN_HTTP_READLINE diff --git a/source/mijin/net/http.hpp b/source/mijin/net/http.hpp new file mode 100644 index 0000000..48be325 --- /dev/null +++ b/source/mijin/net/http.hpp @@ -0,0 +1,55 @@ + +#pragma once + +#if !defined(MIJIN_NET_HTTP_HPP_INCLUDED) +#define MIJIN_NET_HTTP_HPP_INCLUDED 1 + +#include +#include +#include +#include "../io/stream.hpp" + +namespace mijin +{ +struct HTTPVersion +{ + unsigned major; + unsigned minor; +}; + +struct HTTPRequest +{ + 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 +{ + HTTPVersion version; + unsigned status; + std::string statusMessage; + std::multimap headers; + std::string content; +}; + +class HTTPStream +{ +private: + Stream* base_; +public: + HTTPStream(Stream& base) noexcept : base_(&base) + { + MIJIN_ASSERT(base_ != nullptr, "Invalid parameter for base."); + } + Task> c_request(HTTPRequest request) noexcept; +private: + Task c_writeRequest(const HTTPRequest& request) noexcept; + Task> c_readResponse() noexcept; +}; +} + +#endif // !defined(MIJIN_NET_HTTP_HPP_INCLUDED) diff --git a/source/mijin/net/socket.cpp b/source/mijin/net/socket.cpp index 8bd0a9b..4236556 100644 --- a/source/mijin/net/socket.cpp +++ b/source/mijin/net/socket.cpp @@ -1,13 +1,17 @@ #include "./socket.hpp" +#include + #include "../detect.hpp" +#include "../util/string.hpp" #if MIJIN_TARGET_OS == MIJIN_OS_LINUX #include #include #include #include +#include "../util/variant.hpp" #endif namespace mijin @@ -51,6 +55,77 @@ int readFlags(const ReadOptions& options) } } +Optional IPv4Address::fromString(std::string_view stringView) noexcept +{ + std::vector parts = split(stringView, ".", {.limitParts = 4}); + if (parts.size() != 4) { + return NULL_OPTIONAL; + } + IPv4Address address; + for (int idx = 0; idx < 4; ++idx) + { + if (!toNumber(parts[idx], address.octets[idx])) + { + return NULL_OPTIONAL; + } + } + return address; +} + +Optional IPv6Address::fromString(std::string_view stringView) noexcept +{ + // very specific edge case + if (stringView.contains(":::")) + { + return NULL_OPTIONAL; + } + + std::vector parts = split(stringView, "::", {.ignoreEmpty = false}); + if (parts.size() > 2) + { + return NULL_OPTIONAL; + } + if (parts.size() == 1) + { + parts.emplace_back(""); + } + + std::vector partsLeft = split(parts[0], ":"); + std::vector partsRight = split(parts[1], ":"); + + std::erase_if(partsLeft, std::mem_fn(&std::string_view::empty)); + std::erase_if(partsRight, std::mem_fn(&std::string_view::empty)); + + if (partsLeft.size() + partsRight.size() > 8) + { + return NULL_OPTIONAL; + } + + IPv6Address address; + unsigned hextet = 0; + for (std::string_view part : partsLeft) + { + if (!toNumber(part, address.hextets[hextet], /* base = */ 16)) + { + return NULL_OPTIONAL; + } + ++hextet; + } + for (; hextet < (8 - partsRight.size()); ++hextet) + { + address.hextets[hextet] = 0; + } + for (std::string_view part : partsRight) + { + if (!toNumber(part, address.hextets[hextet], /* base = */ 16)) + { + return NULL_OPTIONAL; + } + ++hextet; + } + return address; +} + StreamError TCPStream::readRaw(std::span buffer, const ReadOptions& options, std::size_t* outBytesRead) { MIJIN_ASSERT(isOpen(), "Socket is not open."); @@ -174,7 +249,7 @@ StreamFeatures TCPStream::getFeatures() }; } -StreamError TCPStream::open(const char* address, std::uint16_t port) noexcept +StreamError TCPStream::open(ip_address_t address, std::uint16_t port) noexcept { MIJIN_ASSERT(!isOpen(), "Socket is already open."); @@ -183,13 +258,31 @@ StreamError TCPStream::open(const char* address, std::uint16_t port) noexcept { return translateErrno(); } - sockaddr_in connectAddress = - { - .sin_family = AF_INET, - .sin_port = htons(port), - .sin_addr = {inet_addr(address)} - }; - if (connect(handle_, reinterpret_cast(&connectAddress), sizeof(sockaddr_in)) < 0) + + const bool connected = std::visit(Visitor{ + [&](const IPv4Address& address4) + { +#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__ +#error "TODO: swap byte orderof thre address" +#endif + sockaddr_in connectAddress = + { + .sin_family = AF_INET, + .sin_port = htons(port), + .sin_addr = {.s_addr = std::bit_cast(address4)} + }; + return connect(handle_, reinterpret_cast(&connectAddress), sizeof(sockaddr_in)) == 0; + }, + [&](const IPv6Address& address6) { + sockaddr_in6 connectAddress = + { + .sin6_family = AF_INET, + .sin6_port = htons(port), + .sin6_addr = std::bit_cast(address6) + }; + return connect(handle_, reinterpret_cast(&connectAddress), sizeof(sockaddr_in6)) == 0;} + }, address); + if (!connected) { ::close(handle_); handle_ = -1; diff --git a/source/mijin/net/socket.hpp b/source/mijin/net/socket.hpp index 4ea09eb..eb73990 100644 --- a/source/mijin/net/socket.hpp +++ b/source/mijin/net/socket.hpp @@ -4,7 +4,10 @@ #if !defined(MIJIN_NET_SOCKET_HPP_INCLUDED) #define MIJIN_NET_SOCKET_HPP_INCLUDED 1 +#include +#include #include "../async/coroutine.hpp" +#include "../container/optional.hpp" #include "../io/stream.hpp" namespace mijin @@ -14,6 +17,41 @@ namespace mijin // public types // +struct IPv4Address +{ + std::array octets; + + auto operator<=>(const IPv4Address&) const noexcept = default; + + [[nodiscard]] + static Optional fromString(std::string_view stringView) noexcept; +}; + +struct IPv6Address +{ + std::array hextets; + + auto operator<=>(const IPv6Address&) const noexcept = default; + + [[nodiscard]] + static Optional fromString(std::string_view stringView) noexcept; +}; +using ip_address_t = std::variant; + +[[nodiscard]] +inline Optional ipAddressFromString(std::string_view stringView) noexcept +{ + if (Optional ipv4Address = IPv4Address::fromString(stringView); !ipv4Address.empty()) + { + return ip_address_t(*ipv4Address); + } + if (Optional ipv6Address = IPv6Address::fromString(stringView); !ipv6Address.empty()) + { + return ip_address_t(*ipv6Address); + } + return NULL_OPTIONAL; +} + class Socket { protected: @@ -61,7 +99,7 @@ public: bool isAtEnd() override; StreamFeatures getFeatures() override; - StreamError open(const char* address, std::uint16_t port) noexcept; + StreamError open(ip_address_t address, std::uint16_t port) noexcept; void close() noexcept; [[nodiscard]] bool isOpen() const noexcept { return handle_ >= 0; } private: @@ -77,7 +115,15 @@ private: public: TCPStream& getStream() noexcept override; - StreamError open(const char* address, std::uint16_t port) noexcept { return stream_.open(address, port); } + StreamError open(ip_address_t address, std::uint16_t port) noexcept { return stream_.open(address, port); } + StreamError open(std::string_view addressText, std::uint16_t port) noexcept + { + if (Optional address = ipAddressFromString(addressText); !address.empty()) + { + return open(*address, port); + } + return StreamError::UNKNOWN_ERROR; + } void close() noexcept { stream_.close(); } [[nodiscard]] bool isOpen() const noexcept { return stream_.isOpen(); } diff --git a/source/mijin/util/string.hpp b/source/mijin/util/string.hpp index 76752a7..8ba8237 100644 --- a/source/mijin/util/string.hpp +++ b/source/mijin/util/string.hpp @@ -4,8 +4,11 @@ #if !defined(MIJIN_UTIL_STRING_HPP_INCLUDED) #define MIJIN_UTIL_STRING_HPP_INCLUDED 1 +#include +#include #include #include +#include #include #include #include @@ -24,6 +27,13 @@ namespace mijin // public constants // +// +// public traits +// + +template +using char_type_t = decltype(std::string_view(std::declval()))::value_type; + // // public types // @@ -151,8 +161,35 @@ bool equalsIgnoreCaseImpl(std::basic_string_view stringA, std:: return true; } +template +std::basic_string_view trimPrefixImpl(std::basic_string_view stringView, + std::basic_string_view charsToTrim) +{ + stringView.remove_prefix(std::min(stringView.find_first_not_of(charsToTrim), stringView.size())); + return stringView; +} + +template +std::basic_string_view trimSuffixImpl(std::basic_string_view stringView, + std::basic_string_view charsToTrim) +{ + stringView.remove_suffix(stringView.size() - std::min(stringView.find_last_not_of(charsToTrim) + 1, stringView.size())); + return stringView; +} + +template +std::basic_string_view trimImpl(std::basic_string_view stringView, + std::basic_string_view charsToTrim) +{ + return trimPrefixImpl(trimSuffixImpl(stringView, charsToTrim), charsToTrim); +} + template -static const TChar SPACE = TChar(' '); +static const std::array DEFAULT_TRIM_CHARS_DATA = {TChar(' '), TChar('\t'), TChar('\r'), TChar('\n')}; + +template +static const std::basic_string_view> DEFAULT_TRIM_CHARS + = {DEFAULT_TRIM_CHARS_DATA.begin(), DEFAULT_TRIM_CHARS_DATA.end()}; } template @@ -162,27 +199,46 @@ template std::basic_string_view(std::forward(separator)), options); } -template -std::basic_string_view trimPrefix(std::basic_string_view stringView, - std::basic_string_view charsToTrim = {&detail::SPACE, &detail::SPACE + 1}) +template +[[nodiscard]] +auto trimPrefix(TString&& string, TChars&& chars) { - stringView.remove_prefix(std::min(stringView.find_first_not_of(charsToTrim), stringView.size())); - return stringView; + return detail::trimPrefixImpl(std::string_view(std::forward(string)), std::string_view(std::forward(chars))); } -template -std::basic_string_view trimSuffix(std::basic_string_view stringView, - std::basic_string_view charsToTrim = {&detail::SPACE, &detail::SPACE + 1}) +template +[[nodiscard]] +auto trimPrefix(TString&& string) { - stringView.remove_suffix(stringView.size() - std::min(stringView.find_last_not_of(charsToTrim) + 1, stringView.size())); - return stringView; + return trimPrefix(string, detail::DEFAULT_TRIM_CHARS>); } -template -std::basic_string_view trim(std::basic_string_view stringView, - std::basic_string_view charsToTrim = {&detail::SPACE, &detail::SPACE + 1}) +template +[[nodiscard]] +auto trimSuffix(TString&& string, TChars&& chars) { - return trimPrefix(trimSuffix(stringView, charsToTrim), charsToTrim); + return detail::trimSuffixImpl(std::string_view(std::forward(string)), std::string_view(std::forward(chars))); +} + +template +[[nodiscard]] +auto trimSuffix(TString&& string) +{ + return trimSuffix(string, detail::DEFAULT_TRIM_CHARS>); +} + +template +[[nodiscard]] +auto trim(TString&& string, TChars&& chars) +{ + return detail::trimImpl(std::string_view(std::forward(string)), std::string_view(std::forward(chars))); +} + +template +[[nodiscard]] +auto trim(TString&& string) +{ + return trim(string, detail::DEFAULT_TRIM_CHARS>); } template @@ -191,6 +247,51 @@ template return detail::equalsIgnoreCaseImpl(std::string_view(left), std::string_view(right)); } +template +void makeLower(std::basic_string& string) +{ + std::transform(string.begin(), string.end(), string.begin(), [locale = std::locale()](TChar chr) + { + return std::tolower(chr, locale); + }); +} + +template +void makeUpper(std::basic_string& string) +{ + std::transform(string.begin(), string.end(), string.begin(), [locale = std::locale()](TChar chr) + { + return std::toupper(chr, locale); + }); +} + +template +[[nodiscard]] +auto toLower(TArgs&&... args) +{ + std::basic_string string(std::forward(args)...); + makeLower(string); + return string; +} + +template +[[nodiscard]] +auto toUpper(TArgs&&... args) +{ + std::basic_string string(std::forward(args)...); + makeUpper(string); + return string; +} + + +template +[[nodiscard]] +bool toNumber(std::string_view stringView, TNumber& outNumber, int base = 10) noexcept +{ + const std::from_chars_result res = std::from_chars(&*stringView.begin(), &*stringView.end(), outNumber, base); + return res.ec == std::errc{} && res.ptr == &*stringView.end(); +} + namespace pipe { struct Join