Skip to content

Commit 8b14eaf

Browse files
authored
KTOR-735 Support WinHTTP engine (ktorio#3243)
* KTOR-735 Support WinHTTP engine It allows using WinHTTP on Windows platform which supports HTTP 1/2 and WebSocket protocols. * KTOR-735 use dedicated package name for cinterop With upcoming Kotlin 1.8 release it will be possible to use WinHTTP as platform library. * KTOR-735 fix klint errors * KTOR-735 Use IllegalStateException instead of WinHttpIllegalStateException * KTOR-735 Resolve review comments
1 parent f101c78 commit 8b14eaf

30 files changed

+1815
-7
lines changed

buildSrc/src/main/kotlin/KotlinExtensions.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,13 @@ fun NamedDomainObjectContainer<KotlinSourceSet>.desktopTest(block: KotlinSourceS
7272
block(sourceSet)
7373
}
7474

75+
fun NamedDomainObjectContainer<KotlinSourceSet>.windowsMain(block: KotlinSourceSet.() -> Unit) {
76+
val sourceSet = findByName("windowsMain") ?: return
77+
block(sourceSet)
78+
}
79+
80+
fun NamedDomainObjectContainer<KotlinSourceSet>.windowsTest(block: KotlinSourceSet.() -> Unit) {
81+
val sourceSet = findByName("windowsTest") ?: return
82+
block(sourceSet)
83+
}
84+

buildSrc/src/main/kotlin/NativeUtils.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ fun Project.fastOr(block: () -> List<String>): List<String> {
1111
return block()
1212
}
1313

14-
fun Project.posixTargets(): List<String> = fastOr { nixTargets() + kotlin.mingwX64().name }
14+
fun Project.posixTargets(): List<String> = fastOr {
15+
nixTargets() + windowsTargets()
16+
}
1517

1618
fun Project.nixTargets(): List<String> = fastOr {
1719
darwinTargets() + kotlin.linuxX64().name
@@ -73,3 +75,11 @@ fun Project.desktopTargets(): List<String> = fastOr {
7375
).map { it.name }
7476
}
7577
}
78+
79+
fun Project.windowsTargets(): List<String> = fastOr {
80+
with(kotlin) {
81+
listOf(
82+
mingwX64()
83+
).map { it.name }
84+
}
85+
}

buildSrc/src/main/kotlin/TargetsConfig.kt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ val Project.hasPosix: Boolean get() = hasCommon || files.any { it.name == "posix
1616
val Project.hasDesktop: Boolean get() = hasPosix || files.any { it.name == "desktop" }
1717
val Project.hasNix: Boolean get() = hasPosix || hasJvmAndNix || files.any { it.name == "nix" }
1818
val Project.hasDarwin: Boolean get() = hasNix || files.any { it.name == "darwin" }
19+
val Project.hasWindows: Boolean get() = hasPosix || files.any { it.name == "windows" }
1920
val Project.hasJs: Boolean get() = hasCommon || files.any { it.name == "js" }
2021
val Project.hasJvm: Boolean get() = hasCommon || hasJvmAndNix || files.any { it.name == "jvm" }
21-
val Project.hasNative: Boolean get() = hasCommon || hasNix || hasPosix || hasDarwin || hasDesktop
22+
val Project.hasNative: Boolean get() = hasCommon || hasNix || hasPosix || hasDarwin || hasDesktop || hasWindows
2223

2324
fun Project.configureTargets() {
2425
configureCommon()
@@ -36,7 +37,7 @@ fun Project.configureTargets() {
3637
configureJs()
3738
}
3839

39-
if (hasPosix || hasDarwin) extra.set("hasNative", true)
40+
if (hasPosix || hasDarwin || hasWindows) extra.set("hasNative", true)
4041

4142
sourceSets {
4243
if (hasPosix) {
@@ -71,6 +72,11 @@ fun Project.configureTargets() {
7172
val desktopTest by creating
7273
}
7374

75+
if (hasWindows) {
76+
val windowsMain by creating
77+
val windowsTest by creating
78+
}
79+
7480
if (hasJvmAndNix) {
7581
val jvmAndNixMain by creating {
7682
findByName("commonMain")?.let { dependsOn(it) }
@@ -181,6 +187,19 @@ fun Project.configureTargets() {
181187
}
182188
}
183189

190+
if (hasWindows) {
191+
val windowsMain by getting {
192+
findByName("posixMain")?.let { dependsOn(it) }
193+
}
194+
195+
val windowsTest by getting
196+
197+
windowsTargets().forEach {
198+
getByName("${it}Main").dependsOn(windowsMain)
199+
getByName("${it}Test").dependsOn(windowsTest)
200+
}
201+
}
202+
184203
if (hasNative) {
185204
tasks.findByName("linkDebugTestLinuxX64")?.onlyIf { HOST_NAME == "linux" }
186205
tasks.findByName("linkDebugTestMingwX64")?.onlyIf { HOST_NAME == "windows" }

gradle/compatibility.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ apiValidation {
1919
'ktor-client-ios',
2020
'ktor-client-darwin',
2121
'ktor-client-darwin-legacy',
22+
'ktor-client-winhttp',
2223
]
2324
}
2425

ktor-client/ktor-client-tests/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ kotlin.sourceSets {
9090
api(project(":ktor-client:ktor-client-darwin-legacy"))
9191
}
9292
}
93+
94+
windowsTest {
95+
dependencies {
96+
api(project(":ktor-client:ktor-client-winhttp"))
97+
}
98+
}
9399
}
94100

95101
useJdkVersionForJvmTests(11)

ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/ClientHeadersTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import kotlin.test.*
1414
class ClientHeadersTest : ClientLoader() {
1515

1616
@Test
17-
fun testHeadersReturnNullWhenMissing() = clientTests(listOf("Java", "Curl", "Js", "Darwin", "DarwinLegacy")) {
17+
fun testHeadersReturnNullWhenMissing() = clientTests(listOf("Java", "Curl", "Js", "Darwin", "DarwinLegacy", "WinHttp")) {
1818
test { client ->
1919
client.get("$TEST_SERVER/headers").let {
2020
assertEquals(HttpStatusCode.OK, it.status)
@@ -106,7 +106,7 @@ class ClientHeadersTest : ClientLoader() {
106106
}
107107

108108
@Test
109-
fun testRequestHasContentLength() = clientTests(listOf("Java", "Curl", "Js", "Darwin", "DarwinLegacy")) {
109+
fun testRequestHasContentLength() = clientTests(listOf("Java", "Curl", "Js", "Darwin", "DarwinLegacy", "WinHttp")) {
110110
test { client ->
111111
val get = client.get("$TEST_SERVER/headers").bodyAsText()
112112
assertEquals("", get)

ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/PostTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class PostTest : ClientLoader() {
2424
}
2525

2626
@Test
27-
fun testHugePost() = clientTests(listOf("Js", "Darwin", "CIO", "Curl", "DarwinLegacy")) {
27+
fun testHugePost() = clientTests(listOf("Js", "Darwin", "CIO", "Curl", "DarwinLegacy", "WinHttp")) {
2828
test { client ->
2929
client.postHelper(makeString(32 * 1024 * 1024))
3030
}

ktor-client/ktor-client-tests/common/test/io/ktor/client/tests/plugins/CookiesTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ class CookiesTest : ClientLoader() {
203203
}
204204

205205
@Test
206-
fun testCookiesWithWrongValue() = clientTests(listOf("js", "Darwin", "DarwinLegacy")) {
206+
fun testCookiesWithWrongValue() = clientTests(listOf("js", "Darwin", "DarwinLegacy", "WinHttp")) {
207207
config {
208208
install(HttpCookies)
209209
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import org.jetbrains.kotlin.gradle.targets.native.tasks.*
2+
3+
apply<test.server.TestServerPlugin>()
4+
5+
val WIN_LIBRARY_PATH = "c:\\msys64\\mingw64\\bin;c:\\tools\\msys64\\mingw64\\bin;C:\\Tools\\msys2\\mingw64\\bin"
6+
7+
plugins {
8+
id("kotlinx-serialization")
9+
}
10+
11+
kotlin {
12+
fastTarget()
13+
14+
createCInterop("winhttp", windowsTargets()) {
15+
defFile = File(projectDir, "windows/interop/winhttp.def")
16+
}
17+
18+
sourceSets {
19+
windowsMain {
20+
dependencies {
21+
api(project(":ktor-client:ktor-client-core"))
22+
api(project(":ktor-http:ktor-http-cio"))
23+
}
24+
}
25+
windowsTest {
26+
dependencies {
27+
api(project(":ktor-client:ktor-client-plugins:ktor-client-logging"))
28+
api(project(":ktor-client:ktor-client-plugins:ktor-client-json"))
29+
}
30+
}
31+
}
32+
33+
afterEvaluate {
34+
if (HOST_NAME != "windows") return@afterEvaluate
35+
val winTests = tasks.findByName("mingwX64Test") as? KotlinNativeTest? ?: return@afterEvaluate
36+
winTests.environment("PATH", WIN_LIBRARY_PATH)
37+
}
38+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package = ktor.cinterop.winhttp
2+
headers = wtypes.h winhttp.h
3+
headerFilter = winhttp.h
4+
5+
compilerOpts = -DUNICODE
6+
linkerOpts = -lwinhttp
7+
---
8+
// These WebSocket declarations are not present in our sysroots,
9+
// so we add them here.
10+
#define WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET 114
11+
12+
typedef enum _WINHTTP_WEB_SOCKET_OPERATION
13+
{
14+
WINHTTP_WEB_SOCKET_SEND_OPERATION = 0,
15+
WINHTTP_WEB_SOCKET_RECEIVE_OPERATION = 1,
16+
WINHTTP_WEB_SOCKET_CLOSE_OPERATION = 2,
17+
WINHTTP_WEB_SOCKET_SHUTDOWN_OPERATION = 3
18+
} WINHTTP_WEB_SOCKET_OPERATION;
19+
20+
typedef enum _WINHTTP_WEB_SOCKET_BUFFER_TYPE
21+
{
22+
WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE = 0,
23+
WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE = 1,
24+
WINHTTP_WEB_SOCKET_UTF8_MESSAGE_BUFFER_TYPE = 2,
25+
WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE = 3,
26+
WINHTTP_WEB_SOCKET_CLOSE_BUFFER_TYPE = 4
27+
} WINHTTP_WEB_SOCKET_BUFFER_TYPE;
28+
29+
typedef enum _WINHTTP_WEB_SOCKET_CLOSE_STATUS
30+
{
31+
WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS = 1000,
32+
WINHTTP_WEB_SOCKET_ENDPOINT_TERMINATED_CLOSE_STATUS = 1001,
33+
WINHTTP_WEB_SOCKET_PROTOCOL_ERROR_CLOSE_STATUS = 1002,
34+
WINHTTP_WEB_SOCKET_INVALID_DATA_TYPE_CLOSE_STATUS = 1003,
35+
WINHTTP_WEB_SOCKET_EMPTY_CLOSE_STATUS = 1005,
36+
WINHTTP_WEB_SOCKET_ABORTED_CLOSE_STATUS = 1006,
37+
WINHTTP_WEB_SOCKET_INVALID_PAYLOAD_CLOSE_STATUS = 1007,
38+
WINHTTP_WEB_SOCKET_POLICY_VIOLATION_CLOSE_STATUS = 1008,
39+
WINHTTP_WEB_SOCKET_MESSAGE_TOO_BIG_CLOSE_STATUS = 1009,
40+
WINHTTP_WEB_SOCKET_UNSUPPORTED_EXTENSIONS_CLOSE_STATUS = 1010,
41+
WINHTTP_WEB_SOCKET_SERVER_ERROR_CLOSE_STATUS = 1011,
42+
WINHTTP_WEB_SOCKET_SECURE_HANDSHAKE_ERROR_CLOSE_STATUS = 1015
43+
} WINHTTP_WEB_SOCKET_CLOSE_STATUS;
44+
45+
typedef struct _WINHTTP_WEB_SOCKET_ASYNC_RESULT
46+
{
47+
WINHTTP_ASYNC_RESULT AsyncResult;
48+
WINHTTP_WEB_SOCKET_OPERATION Operation;
49+
} WINHTTP_WEB_SOCKET_ASYNC_RESULT;
50+
51+
typedef struct _WINHTTP_WEB_SOCKET_STATUS
52+
{
53+
DWORD dwBytesTransferred;
54+
WINHTTP_WEB_SOCKET_BUFFER_TYPE eBufferType;
55+
} WINHTTP_WEB_SOCKET_STATUS;
56+
57+
#define WINHTTP_WEB_SOCKET_MAX_CLOSE_REASON_LENGTH 123
58+
#define WINHTTP_WEB_SOCKET_MIN_KEEPALIVE_VALUE 15000
59+
60+
#define WINHTTP_CALLBACK_STATUS_CLOSE_COMPLETE 0x02000000
61+
#define WINHTTP_CALLBACK_STATUS_SHUTDOWN_COMPLETE 0x04000000
62+
63+
WINHTTPAPI
64+
HINTERNET
65+
WINAPI
66+
WinHttpWebSocketCompleteUpgrade
67+
(
68+
_In_ HINTERNET hRequest,
69+
_In_opt_ DWORD_PTR pContext
70+
);
71+
72+
WINHTTPAPI
73+
DWORD
74+
WINAPI
75+
WinHttpWebSocketSend
76+
(
77+
_In_ HINTERNET hWebSocket,
78+
_In_ WINHTTP_WEB_SOCKET_BUFFER_TYPE eBufferType,
79+
_In_reads_opt_(dwBufferLength) PVOID pvBuffer,
80+
_In_ DWORD dwBufferLength
81+
);
82+
83+
WINHTTPAPI
84+
DWORD
85+
WINAPI
86+
WinHttpWebSocketReceive
87+
(
88+
_In_ HINTERNET hWebSocket,
89+
_Out_writes_bytes_to_(dwBufferLength, *pdwBytesRead) PVOID pvBuffer,
90+
_In_ DWORD dwBufferLength,
91+
_Out_range_(0, dwBufferLength) DWORD *pdwBytesRead,
92+
_Out_ WINHTTP_WEB_SOCKET_BUFFER_TYPE *peBufferType
93+
);
94+
95+
WINHTTPAPI
96+
DWORD
97+
WINAPI
98+
WinHttpWebSocketShutdown
99+
(
100+
_In_ HINTERNET hWebSocket,
101+
_In_ USHORT usStatus,
102+
_In_reads_bytes_opt_(dwReasonLength) PVOID pvReason,
103+
_In_range_(0, WINHTTP_WEB_SOCKET_MAX_CLOSE_REASON_LENGTH) DWORD dwReasonLength
104+
);
105+
106+
WINHTTPAPI
107+
DWORD
108+
WINAPI
109+
WinHttpWebSocketClose
110+
(
111+
_In_ HINTERNET hWebSocket,
112+
_In_ USHORT usStatus,
113+
_In_reads_bytes_opt_(dwReasonLength) PVOID pvReason,
114+
_In_range_(0, WINHTTP_WEB_SOCKET_MAX_CLOSE_REASON_LENGTH) DWORD dwReasonLength
115+
);
116+
117+
WINHTTPAPI
118+
DWORD
119+
WINAPI
120+
WinHttpWebSocketQueryCloseStatus
121+
(
122+
_In_ HINTERNET hWebSocket,
123+
_Out_ USHORT *pusStatus,
124+
_Out_writes_bytes_to_opt_(dwReasonLength, *pdwReasonLengthConsumed) PVOID pvReason,
125+
_In_range_(0, WINHTTP_WEB_SOCKET_MAX_CLOSE_REASON_LENGTH) DWORD dwReasonLength,
126+
_Out_range_(0, WINHTTP_WEB_SOCKET_MAX_CLOSE_REASON_LENGTH) DWORD *pdwReasonLengthConsumed
127+
);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2014-2022 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package io.ktor.client.engine.winhttp
6+
7+
import io.ktor.client.engine.*
8+
import io.ktor.util.*
9+
10+
@Suppress("DEPRECATION")
11+
@OptIn(ExperimentalStdlibApi::class)
12+
@EagerInitialization
13+
private val initHook = WinHttp
14+
15+
/**
16+
* A Kotlin/Native client engine that targets Windows-based operating systems.
17+
*
18+
* To create the client with this engine, pass it to the `HttpClient` constructor:
19+
* ```kotlin
20+
* val client = HttpClient(WinHttp)
21+
* ```
22+
* To configure the engine, pass settings exposed by [WinHttpClientEngineConfig] to the `engine` method:
23+
* ```kotlin
24+
* val client = HttpClient(WinHttp) {
25+
* engine {
26+
* // this: WinHttpClientEngineConfig
27+
* }
28+
* }
29+
* ```
30+
*
31+
* You can learn more about client engines from [Engines](https://ktor.io/docs/http-client-engines.html).
32+
*/
33+
@OptIn(InternalAPI::class)
34+
public object WinHttp : HttpClientEngineFactory<WinHttpClientEngineConfig> {
35+
init {
36+
engines.append(this)
37+
}
38+
39+
override fun create(block: WinHttpClientEngineConfig.() -> Unit): HttpClientEngine {
40+
return WinHttpClientEngine(WinHttpClientEngineConfig().apply(block))
41+
}
42+
43+
override fun toString(): String {
44+
return "WinHttp"
45+
}
46+
}

0 commit comments

Comments
 (0)