winhttp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +41 -0
- data/LICENSE.txt +21 -0
- data/README.md +291 -0
- data/ext/winhttp/extconf.rb +26 -0
- data/ext/winhttp/winhttp.c +1247 -0
- data/lib/winhttp/version.rb +5 -0
- data/lib/winhttp.rb +562 -0
- metadata +121 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2934f4cdb7b7aa87cd433359d2e99b8b69e9235308cfdcf26642de8ffa015e87
|
|
4
|
+
data.tar.gz: dccaaf39953e68065d89ac8d4ca484f4e46d7f8dc2d4cb67678849816c093ca1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 78132ed5d2be6e6fd8cbaeb8a8cb5f6ccf348f13702449f46befdac21f9eb95588b62bcd4232fd9c97bdd1796b075cefdce1da873d65b07549999351bd5f3205
|
|
7
|
+
data.tar.gz: 155e80d64b8b05c412294665b8c649677b75d52e1ca99a81ceaea0c0b028be1aa6a095fc1137fab97611bc5a6c16d044dc5396f277d06c4bb167df03be34bb3d
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-06-27
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- **`Winhttp::Session`** wraps one asynchronous WinHTTP session
|
|
8
|
+
(`WINHTTP_FLAG_ASYNC`) with system TLS via Schannel, the OS certificate store,
|
|
9
|
+
and the user's proxy/PAC settings. Options: `user_agent:`, `proxy:`
|
|
10
|
+
(`:system`/`:none`/a String), `proxy_bypass:`, `http2:`, `decompress:`,
|
|
11
|
+
`revocation:` (`:best_effort`/`:strict`/`:none`), `redirects:`,
|
|
12
|
+
`connect_timeout:`/`send_timeout:`/`receive_timeout:`. A TLS 1.2 floor is
|
|
13
|
+
always enforced (TLS 1.3 when the OS supports it). Thread-safe and fiber-safe.
|
|
14
|
+
- **Module one-liners** `Winhttp.get`, `Winhttp.post`, `Winhttp.request`
|
|
15
|
+
delegate to a lazily-created, process-lifetime `Winhttp.default_session`.
|
|
16
|
+
- **Request verbs** `get`, `head`, `post`, `put`, `patch`, `delete`, and
|
|
17
|
+
arbitrary `request(method, url, ...)`, with `headers:`, `body:`, `timeout:`,
|
|
18
|
+
and a streaming `&chunk` block (each chunk a fresh ASCII-8BIT String ≤ 64 KiB,
|
|
19
|
+
yielded on the caller's thread/fiber).
|
|
20
|
+
- **`Winhttp::Response`**: `status`, `reason`, `headers` (lowercased, duplicates
|
|
21
|
+
joined), `raw_headers` (wire order/case/duplicates), `#[]` (case-insensitive),
|
|
22
|
+
`body` (ASCII-8BIT, or nil when streamed), `text` (charset-aware tagging),
|
|
23
|
+
`final_url`, `http2?`, `success?`. 4xx/5xx are responses, not exceptions.
|
|
24
|
+
- **Error hierarchy** `Winhttp::Error` → `OSError` (with `#code`) →
|
|
25
|
+
`TimeoutError`, `ResolveError`, `ConnectError`, `TlsError` (with `#details`),
|
|
26
|
+
`RedirectError`, `ProtocolError`, `Canceled`; plus `Closed`. A Ruby-level
|
|
27
|
+
`timeout:` deadline raises `TimeoutError` with `code == nil`.
|
|
28
|
+
- **winloop cooperation, one code path.** WinHTTP's status callbacks are bridged
|
|
29
|
+
through a gem-private completion queue and a single pump thread into
|
|
30
|
+
per-request `Thread::Queue` mailboxes; the mailbox pop blocks a plain thread
|
|
31
|
+
natively and parks a fiber under a `Fiber::Scheduler` (e.g. winloop) — with no
|
|
32
|
+
scheduler branches anywhere in winhttp and no winloop dependency.
|
|
33
|
+
- **Safe lifetime by construction**: per-request native buffers are freed only
|
|
34
|
+
on the guaranteed-last `HANDLE_CLOSING` callback; request identity is a
|
|
35
|
+
never-reused uint64 id; `Session#close` aborts in-flight requests and bounds
|
|
36
|
+
its wait for OS teardown before closing the session handle.
|
|
37
|
+
|
|
38
|
+
Windows MSVC (mswin) Ruby only. x64 (`x64-mswin64`); arm64-mswin expected to
|
|
39
|
+
work but untested.
|
|
40
|
+
|
|
41
|
+
[0.1.0]: https://github.com/main-path/winhttp/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ned
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# winhttp
|
|
2
|
+
|
|
3
|
+
**HTTP for Ruby on the stack Windows already ships: WinHTTP async with system TLS, the OS certificate store, and the user's proxy settings — fiber-friendly under winloop, plain blocking without it.**
|
|
4
|
+
|
|
5
|
+
On native Windows Ruby, the usual HTTP stack means `net/http` plus the `openssl`
|
|
6
|
+
gem: you bundle and patch a *second* TLS implementation, you ignore the OS
|
|
7
|
+
certificate store, and you get nothing from the corporate proxy / PAC settings
|
|
8
|
+
the machine is already configured for. The `openssl` gem also has to be built
|
|
9
|
+
and kept current independently of Windows Update.
|
|
10
|
+
|
|
11
|
+
winhttp is a thin binding to **the HTTP client Windows already ships** —
|
|
12
|
+
[WinHTTP](https://learn.microsoft.com/windows/win32/winhttp/about-winhttp) in
|
|
13
|
+
asynchronous mode. Certificate validation, revocation policy, redirects with
|
|
14
|
+
safe defaults, proxy auto-detection, HTTP/2 negotiation, and transparent
|
|
15
|
+
gzip/deflate are **the OS's**, serviced by Windows Update — not ours. Same suite
|
|
16
|
+
logic as phylax ("binds the crypto Windows already ships, implements none of its
|
|
17
|
+
own"): winhttp binds the HTTP stack Windows already ships and implements none of
|
|
18
|
+
it. It is not a full HTTP framework — see [Scope and limitations](#scope-and-limitations).
|
|
19
|
+
|
|
20
|
+
| What | API |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| One-liners | `Winhttp.get(url)` · `Winhttp.post(url, body:)` · `Winhttp.request(verb, url)` |
|
|
23
|
+
| Sessions | `Winhttp::Session.new(...)` · `Session.open(...) { \|s\| ... }` · `s.get/head/post/put/patch/delete/request` |
|
|
24
|
+
| Streaming | `s.get(url) { \|chunk\| ... }` (each chunk ≤ 64 KiB, on your thread/fiber) |
|
|
25
|
+
| Responses | `r.status` · `r.headers` · `r.raw_headers` · `r[name]` · `r.body` · `r.text` · `r.final_url` · `r.http2?` · `r.success?` |
|
|
26
|
+
| Errors | `Winhttp::TlsError#details`, `ResolveError`, `ConnectError`, `TimeoutError`, `RedirectError`, `ProtocolError`, `Canceled`, `Closed` |
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- **Windows 10 1607+ recommended** (HTTP/2 needs 1607; earlier Win10 degrades to
|
|
31
|
+
HTTP/1.1 silently). TLS 1.3 needs Windows 11 / Server 2022; older Windows uses
|
|
32
|
+
TLS 1.2 (winhttp enforces a TLS 1.2 floor and never enables anything older).
|
|
33
|
+
- A native **MSVC (mswin) Ruby**, **3.2+** (`Thread::Queue#pop(timeout:)` is the
|
|
34
|
+
mailbox deadline primitive). **Not supported on MinGW/UCRT** Ruby.
|
|
35
|
+
- Visual Studio 2017+ / Build Tools with the **"Desktop development with C++"**
|
|
36
|
+
workload. No Developer Command Prompt is needed — the build uses the `vcvars`
|
|
37
|
+
gem; if `rake compile` can't find the toolchain, run `vcvars doctor`.
|
|
38
|
+
- Supported platform: x64 (`x64-mswin64`). arm64-mswin is expected to work (the
|
|
39
|
+
code is `_WIN64`/`uintptr_t`-clean, no arch-specific anything) but is untested
|
|
40
|
+
and unsupported until an arm64-mswin Ruby distribution exists.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
gem install winhttp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
require "winhttp"
|
|
52
|
+
|
|
53
|
+
# One-liners on the shared default session
|
|
54
|
+
r = Winhttp.get("https://example.com/")
|
|
55
|
+
r.status # => 200
|
|
56
|
+
r.headers["content-type"] # => "text/html; charset=UTF-8"
|
|
57
|
+
r.text[0, 15] # => "<!doctype html>"
|
|
58
|
+
|
|
59
|
+
# POST JSON
|
|
60
|
+
r = Winhttp.post("https://httpbin.org/post",
|
|
61
|
+
body: '{"hello":"world"}',
|
|
62
|
+
headers: { "Content-Type" => "application/json" })
|
|
63
|
+
r.success? # => true
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Sessions and options
|
|
67
|
+
|
|
68
|
+
WinHTTP pools TCP/TLS connections at the **session**, so reuse one `Session` for
|
|
69
|
+
many requests. A `Session` is thread-safe and fiber-safe — any number of threads
|
|
70
|
+
and/or fibers may issue requests on it concurrently.
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
s = Winhttp::Session.new(
|
|
74
|
+
user_agent: "myapp/1.0",
|
|
75
|
+
proxy: :system, # :system | :none | "proxy.corp:8080"
|
|
76
|
+
proxy_bypass: nil, # only with a String proxy: "<local>;*.internal.corp"
|
|
77
|
+
http2: true, # enable HTTP/2 if the OS supports it (else HTTP/1.1)
|
|
78
|
+
decompress: true, # transparent gzip/deflate
|
|
79
|
+
revocation: :best_effort, # :best_effort | :strict | :none (see below)
|
|
80
|
+
redirects: 10, # 0..65535 max auto-redirects; 0 = return 3xx to you
|
|
81
|
+
connect_timeout: nil, # seconds | nil = WinHTTP default
|
|
82
|
+
send_timeout: nil,
|
|
83
|
+
receive_timeout: nil
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
Winhttp::Session.open(proxy: :none, redirects: 0) do |session| # ensure-closed
|
|
87
|
+
session.get("https://example.com/")
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
TLS / revocation / certificate policy is **fixed at construction** (WinHTTP
|
|
92
|
+
caches certificate validation per session), so there are deliberately no
|
|
93
|
+
per-request TLS knobs.
|
|
94
|
+
|
|
95
|
+
| `revocation:` | Behavior |
|
|
96
|
+
| --- | --- |
|
|
97
|
+
| `:best_effort` (default) | Revocation on, but offline CRLs are forgiven (`IGNORE_CERT_REVOCATION_OFFLINE`). On a pre-2004 build that lacks the ignore-offline option, it **degrades to `:none`** at construction (availability over false-strictness). `Session#revocation` then returns `:none`. |
|
|
98
|
+
| `:strict` | Revocation on with no offline forgiveness — offline CRLs raise `TlsError` (`:cert_rev_failed`). If the OS cannot enable revocation, `Session.new` raises `OSError` (never silently weaker than asked). |
|
|
99
|
+
| `:none` | WinHTTP default (revocation off). |
|
|
100
|
+
|
|
101
|
+
`Session#revocation` reports the **effective** policy settled at construction —
|
|
102
|
+
security-minded users on old builds should check it.
|
|
103
|
+
|
|
104
|
+
## Streaming downloads
|
|
105
|
+
|
|
106
|
+
Pass a block and each received body chunk is yielded as a fresh ASCII-8BIT
|
|
107
|
+
String (≤ 64 KiB each) on your thread/fiber, in order; `Response#body` is then
|
|
108
|
+
`nil`. If the block raises, the request is aborted and the exception propagates.
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
Winhttp::Session.open(receive_timeout: 30) do |s|
|
|
112
|
+
File.open("big.bin", "wb") do |f|
|
|
113
|
+
resp = s.get("https://example.com/big.bin", timeout: 300) { |chunk| f.write(chunk) }
|
|
114
|
+
resp.body # => nil (streamed)
|
|
115
|
+
resp.final_url # => "https://example.com/big.bin"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Concurrency
|
|
121
|
+
|
|
122
|
+
Multiple plain threads sharing one `Session` get true concurrency — WinHTTP's
|
|
123
|
+
thread pool does the I/O; the GVL is only held for the tiny Ruby state-machine
|
|
124
|
+
steps.
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
s = Winhttp::Session.new
|
|
128
|
+
16.times.map { |i| Thread.new { s.get("https://example.com/?#{i}") } }.each(&:join)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Under [winloop](https://github.com/main-path/winloop) (the suite's IOCP
|
|
132
|
+
`Fiber::Scheduler`) the **same code** parks fibers instead of blocking threads:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
require "winloop"
|
|
136
|
+
Winloop.run do
|
|
137
|
+
10.times.map { |i| Fiber.schedule { Winhttp.get("https://example.com/?#{i}") } }
|
|
138
|
+
# ten requests in flight concurrently on one OS thread; the loop keeps running
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
There are no `Fiber.scheduler` branches anywhere in winhttp — one code path
|
|
143
|
+
serves both modes (see [How it works](#how-it-works)).
|
|
144
|
+
|
|
145
|
+
## Timeouts and cancellation
|
|
146
|
+
|
|
147
|
+
Two layers, both available:
|
|
148
|
+
|
|
149
|
+
- **Session socket timeouts** (`connect_timeout` / `send_timeout` /
|
|
150
|
+
`receive_timeout`) are WinHTTP's own and surface as `TimeoutError` with
|
|
151
|
+
`code == 12002`.
|
|
152
|
+
- A per-request **`timeout:`** is a Ruby-level wall-clock deadline for the
|
|
153
|
+
*entire* call (resolve → connect → TLS → send → headers → full body). On
|
|
154
|
+
expiry the request is aborted and `TimeoutError` is raised with `code == nil`.
|
|
155
|
+
|
|
156
|
+
```ruby
|
|
157
|
+
Winhttp.get("https://example.com/", timeout: 0.000001)
|
|
158
|
+
# raises Winhttp::TimeoutError (code nil — Ruby-side deadline)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`Timeout.timeout` and `Thread#kill` also unwind cleanly: the request handle is
|
|
162
|
+
aborted in the `ensure` path and the session stays fully usable.
|
|
163
|
+
|
|
164
|
+
## Library API
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
# --- Module functions (delegate to Winhttp.default_session) ---
|
|
168
|
+
Winhttp.default_session # => Winhttp::Session (lazy, immortal)
|
|
169
|
+
Winhttp.get(url, headers: nil, timeout: nil, &chunk) # => Winhttp::Response
|
|
170
|
+
Winhttp.post(url, body: "", headers: nil, timeout: nil, &chunk) # => Winhttp::Response
|
|
171
|
+
Winhttp.request(method, url, body: nil, headers: nil, timeout: nil, &chunk) # => Winhttp::Response
|
|
172
|
+
|
|
173
|
+
# --- Session ---
|
|
174
|
+
Winhttp::Session.new(user_agent:, proxy:, proxy_bypass:, http2:, decompress:,
|
|
175
|
+
revocation:, redirects:, connect_timeout:, send_timeout:,
|
|
176
|
+
receive_timeout:) # => Session
|
|
177
|
+
Winhttp::Session.open(**opts) { |s| ... } # => block value (ensure-closes)
|
|
178
|
+
session.get(url, headers: nil, timeout: nil, &chunk) # => Response
|
|
179
|
+
session.head(url, headers: nil, timeout: nil) # => Response
|
|
180
|
+
session.post(url, body: "", headers: nil, timeout: nil, &chunk) # => Response
|
|
181
|
+
session.put(url, body:, headers: nil, timeout: nil, &chunk) # => Response
|
|
182
|
+
session.patch(url, body:, headers: nil, timeout: nil, &chunk) # => Response
|
|
183
|
+
session.delete(url, headers: nil, timeout: nil, &chunk) # => Response
|
|
184
|
+
session.request(method, url, body: nil, headers: nil, timeout: nil, &chunk) # => Response
|
|
185
|
+
session.close # => nil (idempotent)
|
|
186
|
+
session.closed? # => true | false
|
|
187
|
+
session.revocation # => :best_effort | :strict | :none (effective)
|
|
188
|
+
|
|
189
|
+
# --- Response (immutable) ---
|
|
190
|
+
response.status # => Integer (200, 404, ...)
|
|
191
|
+
response.reason # => String ("OK"; may be "" under HTTP/2)
|
|
192
|
+
response.headers # => Hash{String=>String} (keys lowercased, duplicates joined ", "; frozen)
|
|
193
|
+
response.raw_headers # => Array[[String, String]] (wire order, original case, duplicates kept; frozen)
|
|
194
|
+
response[name] # => String | nil (case-insensitive single-header lookup)
|
|
195
|
+
response.body # => String (ASCII-8BIT) | nil (nil when a &chunk block was used)
|
|
196
|
+
response.text(enc=nil) # => String (a copy tagged with enc / charset= / UTF-8; never transcodes)
|
|
197
|
+
response.final_url # => String (the URL actually fetched, after redirects)
|
|
198
|
+
response.http2? # => true | false
|
|
199
|
+
response.success? # => true | false ((200..299).cover?(status))
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Errors
|
|
203
|
+
|
|
204
|
+
4xx/5xx are **responses, not exceptions** — `get` returns a `Response` for any
|
|
205
|
+
HTTP status. Exceptions are raised only for transport/TLS/protocol failures and
|
|
206
|
+
misuse. Plain argument misuse raises Ruby's own `ArgumentError`/`TypeError`.
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
Winhttp::Error < StandardError # all winhttp failures
|
|
210
|
+
Winhttp::OSError < Error # carries #code (Integer | nil)
|
|
211
|
+
Winhttp::TimeoutError # 12002, or code nil for Ruby-side deadlines
|
|
212
|
+
Winhttp::ResolveError # 12007 (DNS)
|
|
213
|
+
Winhttp::ConnectError # 12029 / 12030 (TCP)
|
|
214
|
+
Winhttp::TlsError # 12175 + cert failures — see #details
|
|
215
|
+
Winhttp::RedirectError # 12156
|
|
216
|
+
Winhttp::ProtocolError # 12152 (malformed response)
|
|
217
|
+
Winhttp::Canceled # 12017 / 995 (aborted)
|
|
218
|
+
Winhttp::Closed < Error # misuse: session already closed
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`Winhttp::TlsError#details` decodes the `SECURE_FAILURE` flag bits captured just
|
|
222
|
+
before the failure:
|
|
223
|
+
|
|
224
|
+
| Symbol | Meaning |
|
|
225
|
+
| --- | --- |
|
|
226
|
+
| `:cert_rev_failed` | revocation check could not complete |
|
|
227
|
+
| `:invalid_cert` | certificate is invalid |
|
|
228
|
+
| `:cert_revoked` | certificate was revoked |
|
|
229
|
+
| `:invalid_ca` | unknown / untrusted CA |
|
|
230
|
+
| `:cert_cn_invalid` | hostname does not match the certificate |
|
|
231
|
+
| `:cert_date_invalid` | certificate is expired or not yet valid |
|
|
232
|
+
| `:security_channel_error` | other Schannel error |
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
Winhttp.get("https://expired.badssl.com/")
|
|
236
|
+
# raises Winhttp::TlsError, e.code => 12175, e.details => [:cert_date_invalid]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## How it works
|
|
240
|
+
|
|
241
|
+
WinHTTP runs in **async mode** (`WINHTTP_FLAG_ASYNC`): the OS does resolve /
|
|
242
|
+
connect / TLS / send / receive on its own thread pool and delivers completions
|
|
243
|
+
to a status callback. That callback runs on a foreign thread (or synchronously
|
|
244
|
+
inside one of our own calls) — where it may not touch Ruby or the GVL — so it
|
|
245
|
+
only appends a plain-C event node to a critical-section-guarded list and signals
|
|
246
|
+
an event. One lazily-started **pump thread** waits on that event (GVL released),
|
|
247
|
+
drains the list (GVL held), and routes each event to its request's
|
|
248
|
+
`Thread::Queue` mailbox. The per-request state machine pops that mailbox.
|
|
249
|
+
|
|
250
|
+
That mailbox pop is the only place a caller blocks — and it is exactly the
|
|
251
|
+
primitive a `Fiber::Scheduler` hooks: standalone it blocks the thread natively;
|
|
252
|
+
under winloop the pump's cross-thread `Queue#push` wakes the parked fiber. **One
|
|
253
|
+
code path, one extra thread total (the pump), zero winloop coupling** — fibers
|
|
254
|
+
park for free.
|
|
255
|
+
|
|
256
|
+
Per-request native memory (the body copy and the 64 KiB receive buffer) is freed
|
|
257
|
+
only on `HANDLE_CLOSING`, the guaranteed-last callback — never on a Ruby-side
|
|
258
|
+
path the kernel might still write into. Request identity is a never-reused uint64
|
|
259
|
+
id (never a pointer), so late or duplicate callbacks for a dead request are
|
|
260
|
+
dropped harmlessly. `Session#close` marks the session closed, aborts every
|
|
261
|
+
in-flight request, and waits (bounded) for the OS to finish teardown before
|
|
262
|
+
closing the session handle.
|
|
263
|
+
|
|
264
|
+
## Scope and limitations
|
|
265
|
+
|
|
266
|
+
Honest bullets — winhttp is a thin OS binding, not a framework:
|
|
267
|
+
|
|
268
|
+
- **User-set headers cross redirects unchanged** — including `Authorization`,
|
|
269
|
+
which can therefore **leak to a redirect target**. v1 adds no header-stripping
|
|
270
|
+
magic; set `redirects: 0` if you need to inspect 3xx yourself. https→http
|
|
271
|
+
redirects are always refused (OS default).
|
|
272
|
+
- **Cookies are per-session and automatic** (WinHTTP's handling) — there is no
|
|
273
|
+
cookie-jar API or inspection.
|
|
274
|
+
- The **body is buffered** into `Response#body` unless you pass a streaming
|
|
275
|
+
block.
|
|
276
|
+
- `Response#headers` reflects the **wire** headers, except that WinHTTP removes
|
|
277
|
+
`Content-Encoding`/`Content-Length` once it transparently decompresses a
|
|
278
|
+
gzip/deflate body (so the decoded body and the headers stay consistent).
|
|
279
|
+
- **Not in v1:** authentication of any kind (Basic/Digest/NTLM/Negotiate/Kerberos,
|
|
280
|
+
proxy auth, credentials in URLs — actively rejected), WebSockets, HTTP/3,
|
|
281
|
+
client certificates (mutual TLS), upload streaming (whole-string bodies only),
|
|
282
|
+
per-request proxies/PAC, TLS escape hatches (no `insecure_skip_verify`, no
|
|
283
|
+
custom CA stores, no peer-cert accessor), response caps / retry / backoff /
|
|
284
|
+
caching, charset transcoding (`Response#text` tags, never transcodes).
|
|
285
|
+
- **Well-known non-HTTP ports** (21, 25, 110, …) are blocked by WinHTTP — the
|
|
286
|
+
original OS error surfaces as an `OSError`.
|
|
287
|
+
- **One Windows identity per session** — winhttp does no impersonation.
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
[MIT](LICENSE.txt).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
#
|
|
3
|
+
# extconf.rb for the winhttp extension — a thin binding to the asynchronous
|
|
4
|
+
# WinHTTP client API (sessions, requests, system TLS, proxy, status callbacks).
|
|
5
|
+
|
|
6
|
+
require "mkmf"
|
|
7
|
+
|
|
8
|
+
# Arch-neutral guard: match target_os only (passes on x64-mswin64 AND a future
|
|
9
|
+
# arm64-mswin64, whose target_os is still "mswin64"); never pin the x64 arch.
|
|
10
|
+
unless RbConfig::CONFIG["target_os"] =~ /mswin/
|
|
11
|
+
abort <<~MSG
|
|
12
|
+
winhttp requires a native Windows MSVC (mswin) Ruby — it binds the
|
|
13
|
+
asynchronous WinHTTP client API (system TLS via Schannel, the OS certificate
|
|
14
|
+
store, the user's proxy/PAC settings, HTTP/2 and gzip), and is built with
|
|
15
|
+
cl.exe. Your Ruby is "#{RbConfig::CONFIG['arch']}".
|
|
16
|
+
MSG
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# System import libs (bare "NAME.lib" tokens on mswin):
|
|
20
|
+
# winhttp - the OS HTTP client (sessions, requests, TLS, proxy, callbacks)
|
|
21
|
+
# kernel32 (events, critical sections, FormatMessage) is linked by default.
|
|
22
|
+
# No /MACHINE, no -arch: let mkmf inherit Ruby's RbConfig flags (arm64-clean).
|
|
23
|
+
# Pure C — no -EHsc, so rb_raise/longjmp is the normal, safe error mechanism.
|
|
24
|
+
$libs = [$libs, "winhttp.lib"].join(" ")
|
|
25
|
+
|
|
26
|
+
create_makefile("winhttp/winhttp")
|