diff --git a/Sources/AsyncHTTPClient/ConnectionPool.swift b/Sources/AsyncHTTPClient/ConnectionPool.swift index 3b45eca05..f01b12348 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool.swift @@ -24,6 +24,9 @@ import Musl import Android #elseif os(Linux) || os(FreeBSD) import Glibc +#elseif os(Windows) +import ucrt +import WinSDK #else #error("unsupported target operating system") #endif diff --git a/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+Backoff.swift b/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+Backoff.swift index e2ef564a5..aab6407a5 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+Backoff.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+Backoff.swift @@ -20,6 +20,8 @@ import func Darwin.pow import func Musl.pow #elseif canImport(Android) import func Android.pow +#elseif canImport(ucrt) +import func ucrt.pow #else import func Glibc.pow #endif diff --git a/Sources/AsyncHTTPClient/HTTPClient+HTTPCookie.swift b/Sources/AsyncHTTPClient/HTTPClient+HTTPCookie.swift index 759f6728a..763d1af41 100644 --- a/Sources/AsyncHTTPClient/HTTPClient+HTTPCookie.swift +++ b/Sources/AsyncHTTPClient/HTTPClient+HTTPCookie.swift @@ -28,6 +28,9 @@ import Darwin import Musl #elseif canImport(Android) import Android +#elseif os(Windows) +import ucrt +import WinSDK #elseif canImport(Glibc) import Glibc #endif @@ -216,12 +219,19 @@ extension String.UTF8View.SubSequence { } } +#if !os(Windows) nonisolated(unsafe) private let posixLocale: UnsafeMutableRawPointer = { // All POSIX systems must provide a "POSIX" locale, and its date/time formats are US English. // https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap07.html#tag_07_03_05 let _posixLocale = newlocale(LC_TIME_MASK | LC_NUMERIC_MASK, "POSIX", nil)! return UnsafeMutableRawPointer(_posixLocale) }() +#else +nonisolated(unsafe) private let posixLocale: UnsafeMutableRawPointer = { + // FIXME: This can be cleaner. But the Windows shim doesn't need a locale pointer + return UnsafeMutableRawPointer(bitPattern: 0)! +}() +#endif private func parseTimestamp(_ utf8: String.UTF8View.SubSequence, format: String) -> tm? { var timeComponents = tm() @@ -251,6 +261,16 @@ private func parseCookieTime(_ timestampUTF8: String.UTF8View.SubSequence) -> In else { return nil } + #if os(Windows) + let timegm = _mkgmtime + #endif + let timestamp = Int64(timegm(&timeComponents)) - return timestamp == -1 && errno == EOVERFLOW ? nil : timestamp + + #if os(Windows) + let err = GetLastError() + #else + let err = errno + #endif + return timestamp == -1 && err == EOVERFLOW ? nil : timestamp } diff --git a/Sources/CAsyncHTTPClient/CAsyncHTTPClient.c b/Sources/CAsyncHTTPClient/CAsyncHTTPClient.c index 6342da89f..d782c56cb 100644 --- a/Sources/CAsyncHTTPClient/CAsyncHTTPClient.c +++ b/Sources/CAsyncHTTPClient/CAsyncHTTPClient.c @@ -21,6 +21,208 @@ #include #include +#if defined(_WIN32) +#include +#include +// Windows does not provide strptime/strptime_l. Implement a tiny parser that +// supports the three date formats used for cookie parsing in this package: +// 1) "%a, %d %b %Y %H:%M:%S" +// 2) "%a, %d-%b-%y %H:%M:%S" +// 3) "%a %b %d %H:%M:%S %Y" + +static int month_from_abbrev(const char *p) { + // Return 0-11 for Jan..Dec, or -1 on failure. + if (!p) return -1; + switch (p[0]) { + case 'J': + if (p[1] == 'a' && p[2] == 'n') return 0; // Jan + if (p[1] == 'u' && p[2] == 'n') return 5; // Jun + if (p[1] == 'u' && p[2] == 'l') return 6; // Jul + break; + case 'F': + if (p[1] == 'e' && p[2] == 'b') return 1; // Feb + break; + case 'M': + if (p[1] == 'a' && p[2] == 'r') return 2; // Mar + if (p[1] == 'a' && p[2] == 'y') return 4; // May + break; + case 'A': + if (p[1] == 'p' && p[2] == 'r') return 3; // Apr + if (p[1] == 'u' && p[2] == 'g') return 7; // Aug + break; + case 'S': + if (p[1] == 'e' && p[2] == 'p') return 8; // Sep + break; + case 'O': + if (p[1] == 'c' && p[2] == 't') return 9; // Oct + break; + case 'N': + if (p[1] == 'o' && p[2] == 'v') return 10; // Nov + break; + case 'D': + if (p[1] == 'e' && p[2] == 'c') return 11; // Dec + break; + } + return -1; +} + +static int is_wkday_abbrev(const char *p) { + // Check for valid weekday abbreviation (Mon..Sun) + // Expect exactly 3 ASCII letters. + if (!p) return 0; + char a = p[0], b = p[1], c = p[2]; + if (!isalpha((unsigned char)a) || !isalpha((unsigned char)b) || !isalpha((unsigned char)c)) return 0; + // Accept common English abbreviations, case-sensitive as typically emitted. + return (a=='M'&&b=='o'&&c=='n')||(a=='T'&&b=='u'&&c=='e')||(a=='W'&&b=='e'&&c=='d')|| + (a=='T'&&b=='h'&&c=='u')||(a=='F'&&b=='r'&&c=='i')||(a=='S'&&b=='a'&&c=='t')|| + (a=='S'&&b=='u'&&c=='n'); +} + +static int parse_1to2_digits(const char **pp) { + const char *p = *pp; + if (!isdigit((unsigned char)p[0])) return -1; + int val = p[0]-'0'; + p++; + if (isdigit((unsigned char)p[0])) { + val = val*10 + (p[0]-'0'); + p++; + } + *pp = p; + return val; +} + +static int parse_fixed2(const char **pp) { + const char *p = *pp; + if (!isdigit((unsigned char)p[0]) || !isdigit((unsigned char)p[1])) return -1; + int val = (p[0]-'0')*10 + (p[1]-'0'); + p += 2; + *pp = p; + return val; +} + +static int parse_fixed4(const char **pp) { + const char *p = *pp; + for (int i = 0; i < 4; i++) { + if (!isdigit((unsigned char)p[i])) return -1; + } + int val = (p[0]-'0')*1000 + (p[1]-'0')*100 + (p[2]-'0')*10 + (p[3]-'0'); + p += 4; + *pp = p; + return val; +} + +static int expect_char(const char **pp, char c) { + if (**pp != c) return 0; + (*pp)++; + return 1; +} + +static int expect_space(const char **pp) { + if (**pp != ' ') return 0; + (*pp)++; + return 1; +} + +static int parse_time_hms(const char **pp, int *h, int *m, int *s) { + int hh = parse_fixed2(pp); if (hh < 0) return 0; + if (!expect_char(pp, ':')) return 0; + int mm = parse_fixed2(pp); if (mm < 0) return 0; + if (!expect_char(pp, ':')) return 0; + int ss = parse_fixed2(pp); if (ss < 0) return 0; + if (hh > 23 || mm > 59 || ss > 60) return 0; // allow leap second 60 + *h = hh; *m = mm; *s = ss; + return 1; +} + +static void init_tm_utc(struct tm *out) { + memset(out, 0, sizeof(*out)); + out->tm_isdst = 0; +} + +static bool parse_cookie_format1(const char *p, struct tm *out) { + // "%a, %d %b %Y %H:%M:%S" + if (!is_wkday_abbrev(p)) return false; + p += 3; + if (!expect_char(&p, ',')) return false; + if (!expect_space(&p)) return false; + int mday = parse_1to2_digits(&p); if (mday < 1 || mday > 31) return false; + if (!expect_space(&p)) return false; + int mon = month_from_abbrev(p); if (mon < 0) return false; p += 3; + if (!expect_space(&p)) return false; + int year = parse_fixed4(&p); if (year < 1601) return false; + if (!expect_space(&p)) return false; + int hh, mm, ss; if (!parse_time_hms(&p, &hh, &mm, &ss)) return false; + if (*p != '\0') return false; + init_tm_utc(out); + out->tm_mday = mday; + out->tm_mon = mon; + out->tm_year = year - 1900; + out->tm_hour = hh; out->tm_min = mm; out->tm_sec = ss; + return true; +} + +static bool parse_cookie_format2(const char *p, struct tm *out) { + // "%a, %d-%b-%y %H:%M:%S" + if (!is_wkday_abbrev(p)) return false; + p += 3; + if (!expect_char(&p, ',')) return false; + if (!expect_space(&p)) return false; + int mday = parse_1to2_digits(&p); if (mday < 1 || mday > 31) return false; + if (!expect_char(&p, '-')) return false; + int mon = month_from_abbrev(p); if (mon < 0) return false; p += 3; + if (!expect_char(&p, '-')) return false; + int y2 = parse_fixed2(&p); if (y2 < 0) return false; + int year = (y2 >= 70) ? (1900 + y2) : (2000 + y2); + if (!expect_space(&p)) return false; + int hh, mm, ss; if (!parse_time_hms(&p, &hh, &mm, &ss)) return false; + if (*p != '\0') return false; + init_tm_utc(out); + out->tm_mday = mday; + out->tm_mon = mon; + out->tm_year = year - 1900; + out->tm_hour = hh; out->tm_min = mm; out->tm_sec = ss; + return true; +} + +static bool parse_cookie_format3(const char *p, struct tm *out) { + // "%a %b %d %H:%M:%S %Y" + if (!is_wkday_abbrev(p)) return false; + p += 3; + if (!expect_space(&p)) return false; + int mon = month_from_abbrev(p); if (mon < 0) return false; p += 3; + if (!expect_space(&p)) return false; + int mday = parse_1to2_digits(&p); if (mday < 1 || mday > 31) return false; + if (!expect_space(&p)) return false; + int hh, mm, ss; if (!parse_time_hms(&p, &hh, &mm, &ss)) return false; + if (!expect_space(&p)) return false; + int year = parse_fixed4(&p); if (year < 1601) return false; + if (*p != '\0') return false; + init_tm_utc(out); + out->tm_mday = mday; + out->tm_mon = mon; + out->tm_year = year - 1900; + out->tm_hour = hh; out->tm_min = mm; out->tm_sec = ss; + return true; +} + +static bool parse_cookie_timestamp_windows(const char *string, const char *format, struct tm *result) { + (void)format; // format ignored: we try the three known patterns regardless. + return parse_cookie_format1(string, result) || + parse_cookie_format2(string, result) || + parse_cookie_format3(string, result); +} + +bool swiftahc_cshims_strptime(const char * string, const char * format, struct tm * result) { + return parse_cookie_timestamp_windows(string, format, result); +} + +bool swiftahc_cshims_strptime_l(const char * string, const char * format, struct tm * result, void * locale) { + (void)locale; // locale is ignored on Windows; we always use POSIX month/weekday names. + return parse_cookie_timestamp_windows(string, format, result); +} +#endif // _WIN32 + +#if !defined(_WIN32) bool swiftahc_cshims_strptime(const char * string, const char * format, struct tm * result) { const char * firstNonProcessed = strptime(string, format, result); if (firstNonProcessed) { @@ -41,3 +243,4 @@ bool swiftahc_cshims_strptime_l(const char * string, const char * format, struct } return false; } +#endif // _WIN32 diff --git a/Tests/AsyncHTTPClientTests/HTTP2ClientTests.swift b/Tests/AsyncHTTPClientTests/HTTP2ClientTests.swift index 183a227bd..a4112f612 100644 --- a/Tests/AsyncHTTPClientTests/HTTP2ClientTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTP2ClientTests.swift @@ -426,11 +426,15 @@ class HTTP2ClientTests: XCTestCase { XCTAssertNoThrow( maybeServer = try ServerBootstrap(group: serverGroup) .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + #if !os(Windows) .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEPORT), value: 1) + #endif .childChannelInitializer { channel in channel.close() } + #if !os(Windows) .childChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) + #endif .bind(host: "127.0.0.1", port: serverPort) .wait() ) diff --git a/Tests/AsyncHTTPClientTests/HTTPClientCookieTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientCookieTests.swift index fa9abb9d8..fd9949987 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientCookieTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientCookieTests.swift @@ -474,6 +474,7 @@ class HTTPClientCookieTests: XCTestCase { XCTAssertEqual("abc\"", c?.value) } + #if !os(Windows) func testCookieExpiresDateParsingWithNonEnglishLocale() throws { try withCLocaleSetToGerman { // Check that we are using a German C locale. @@ -500,4 +501,5 @@ class HTTPClientCookieTests: XCTestCase { XCTAssertNil(c?.expires) } } + #endif } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift index 093dc8a6a..2b150f095 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTestUtils.swift @@ -43,6 +43,8 @@ import Musl import Android #elseif canImport(Glibc) import Glibc +#elseif os(Windows) +import WinSDK #endif /// Are we testing NIO Transport services @@ -69,7 +71,9 @@ let canBindIPv6Loopback: Bool = { let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1) defer { try! elg.syncShutdownGracefully() } let serverChannel = try? ServerBootstrap(group: elg) + #if !os(Windows) .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) + #endif .bind(host: "::1", port: 0) .wait() let didBind = (serverChannel != nil) @@ -77,6 +81,7 @@ let canBindIPv6Loopback: Bool = { return didBind }() +#if !os(Windows) /// Runs the given block in the context of a non-English C locale (in this case, German). /// Throws an XCTSkip error if the locale is not supported by the system. func withCLocaleSetToGerman(_ body: () throws -> Void) throws { @@ -94,6 +99,7 @@ func withCLocaleSetToGerman(_ body: () throws -> Void) throws { defer { _ = uselocale(oldLocale) } try body() } +#endif final class TestHTTPDelegate: HTTPClientResponseDelegate { typealias Response = Void @@ -258,7 +264,13 @@ enum TemporaryFileHelpers { let templateBytesCount = templateBytes.count let fd = templateBytes.withUnsafeMutableBufferPointer { ptr in ptr.baseAddress!.withMemoryRebound(to: Int8.self, capacity: templateBytesCount) { ptr in + #if os(Windows) + // _mktemp_s is not great, as it's rumored to have limited randomness, but Windows doesn't have mkstemp + // And this is a test utility only. + _mktemp_s(ptr, templateBytesCount) + #else mkstemp(ptr) + #endif } } templateBytes.removeLast() @@ -511,11 +523,13 @@ where let connectionIDAtomic = ManagedAtomic(0) let serverChannel = try! ServerBootstrap(group: self.group) + #if !os(Windows) .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) .serverChannelOption( ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEPORT), value: reusePort ? 1 : 0 ) + #endif .serverChannelInitializer { [activeConnCounterHandler] channel in channel.pipeline.addHandler(activeConnCounterHandler) }.childChannelInitializer { channel in diff --git a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift index 50c3ecb9d..e06649a6b 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientTests.swift @@ -1373,7 +1373,9 @@ final class HTTPClientTests: XCTestCaseHTTPClientTestsBaseClass { var server: Channel? XCTAssertNoThrow( server = try ServerBootstrap(group: group) + #if !os(Windows) .serverChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1) + #endif .serverChannelOption(ChannelOptions.backlog, value: .init(numberOfParallelWorkers)) .childChannelInitializer { channel in channel.pipeline.configureHTTPServerPipeline( @@ -2309,7 +2311,9 @@ final class HTTPClientTests: XCTestCaseHTTPClientTestsBaseClass { var maybeServer: Channel? XCTAssertNoThrow( maybeServer = try ServerBootstrap(group: self.serverGroup) + #if !os(Windows) .serverChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1) + #endif .childChannelInitializer { channel in channel.pipeline.configureHTTPServerPipeline().flatMap { // We're deliberately adding a handler which is shared between multiple channels. This is normally @@ -2526,7 +2530,9 @@ final class HTTPClientTests: XCTestCaseHTTPClientTestsBaseClass { ) } } + #if !os(Windows) .serverChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1) + #endif .bind(host: "127.0.0.1", port: 0) .wait() } diff --git a/Tests/AsyncHTTPClientTests/HTTPClientUncleanSSLConnectionShutdownTests.swift b/Tests/AsyncHTTPClientTests/HTTPClientUncleanSSLConnectionShutdownTests.swift index b63eb7cba..6558e0b82 100644 --- a/Tests/AsyncHTTPClientTests/HTTPClientUncleanSSLConnectionShutdownTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPClientUncleanSSLConnectionShutdownTests.swift @@ -196,8 +196,10 @@ final class HTTPBinForSSLUncleanShutdown { let context = try! NIOSSLContext(configuration: configuration) self.serverChannel = try! ServerBootstrap(group: self.group) + #if !os(Windows) .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) .childChannelOption(ChannelOptions.socket(IPPROTO_TCP, TCP_NODELAY), value: 1) + #endif .childChannelInitializer { channel in do { let requestDecoder = HTTPRequestDecoder() diff --git a/Tests/AsyncHTTPClientTests/SOCKSTestUtils.swift b/Tests/AsyncHTTPClientTests/SOCKSTestUtils.swift index 50d26b278..b7a743ff3 100644 --- a/Tests/AsyncHTTPClientTests/SOCKSTestUtils.swift +++ b/Tests/AsyncHTTPClientTests/SOCKSTestUtils.swift @@ -51,7 +51,9 @@ class MockSOCKSServer { let bootstrap: ServerBootstrap if misbehave { bootstrap = ServerBootstrap(group: elg) + #if !os(Windows) .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) + #endif .childChannelInitializer { channel in channel.eventLoop.makeCompletedFuture { try channel.pipeline.syncOperations.addHandler(TestSOCKSBadServerHandler()) @@ -59,7 +61,9 @@ class MockSOCKSServer { } } else { bootstrap = ServerBootstrap(group: elg) + #if !os(Windows) .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) + #endif .childChannelInitializer { channel in channel.eventLoop.makeCompletedFuture { let handshakeHandler = SOCKSServerHandshakeHandler()