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.
@@ -0,0 +1,1247 @@
1
+ /*
2
+ * winhttp — HTTP for Ruby on the asynchronous WinHTTP client API.
3
+ *
4
+ * Engine/strategy: WINHTTP_FLAG_ASYNC. WinHTTP's status callback runs on a
5
+ * worker thread (or synchronously inside our own call) — a foreign native
6
+ * thread that may NOT call Ruby or take the GVL — so the callback only touches
7
+ * plain C state: a CRITICAL_SECTION-guarded event list + an auto-reset event
8
+ * (Bridge P from the seam research). One Ruby "pump" thread waits on that event
9
+ * (GVL released), drains the list (GVL held), and routes each event to its
10
+ * request's Thread::Queue mailbox. The per-request state machine (Ruby) pops
11
+ * those mailboxes — natively blocking standalone, parking the fiber under a
12
+ * Fiber::Scheduler (winloop). One code path for both modes.
13
+ *
14
+ * PURE C (no C++), so rb_raise/longjmp is the normal, safe error mechanism —
15
+ * the same discipline as winipc/winloop, and the opposite of the lithos /EHsc
16
+ * hazard. Include <ruby.h> before the Windows headers; never name a variable
17
+ * IN/OUT.
18
+ *
19
+ * Control-block lifetime (the load-bearing contract): WinHTTP can hold
20
+ * callbacks/buffers alive after the Ruby wrapper is GC'd, so control blocks are
21
+ * separate heap allocations (NOT embedded in the TypedData wrapper). A request
22
+ * block is freed exactly when BOTH wrapper_gone (Ruby wrapper closed/GC'd) AND
23
+ * os_done (terminal HANDLE_CLOSING reaped, or provably never coming) are set.
24
+ * Every flag transition and every table mutation happens with the GVL held
25
+ * (request creation, pump drain, close, GC free hooks) — so the GVL is the lock
26
+ * for all tables and control blocks; the CRITICAL_SECTION guards ONLY the raw
27
+ * callback event list. Lock ordering (normative): GVL -> g_cs is allowed;
28
+ * g_cs -> GVL is forbidden (the callback takes g_cs without the GVL and acquires
29
+ * nothing else), so there is no deadlock even when WinHTTP invokes the callback
30
+ * synchronously on a Ruby thread holding the GVL.
31
+ *
32
+ * LLP64 reminder: `long` is 32-bit on x64/arm64-mswin — sizes/timeouts from
33
+ * Ruby use NUM2LL / long long, never NUM2LONG. Handle/context values are
34
+ * uint64 ids, never pointers. Arch-neutral: gate on _WIN64, use ULONG_PTR.
35
+ *
36
+ * Links winhttp.lib; kernel32 (events, critical sections, FormatMessage) is
37
+ * linked by default.
38
+ */
39
+
40
+ #include <ruby.h>
41
+ #include <ruby/thread.h>
42
+ #include <ruby/encoding.h>
43
+ #include <limits.h>
44
+ #include <stdint.h>
45
+
46
+ #define WIN32_LEAN_AND_MEAN
47
+ #include <windows.h>
48
+ #include <winhttp.h>
49
+
50
+ /* ------------------------------------------------------------------ globals */
51
+
52
+ static VALUE mWinhttp;
53
+ static VALUE cSession, cRequest;
54
+ static VALUE eError, eOSError, eTimeout, eResolve, eConnect, eTls, eRedirect,
55
+ eProtocol, eCanceled, eClosed;
56
+
57
+ /* Event kinds carried from the callback to the pump to the request mailbox. */
58
+ enum {
59
+ EV_SEND = 1, /* SENDREQUEST_COMPLETE */
60
+ EV_HEADERS = 2, /* HEADERS_AVAILABLE */
61
+ EV_READ = 3, /* READ_COMPLETE (a = byte count) */
62
+ EV_ERROR = 4, /* REQUEST_ERROR (a = dwResult/which API, b = dwError) */
63
+ EV_SECURE = 5, /* SECURE_FAILURE (a = failure flag bits) */
64
+ EV_CLOSING = 6, /* HANDLE_CLOSING (consumed in C; never routed) */
65
+ EV_LOST = 7 /* synthesized by the pump on callback-side OOM */
66
+ };
67
+
68
+ /* Receive buffer: >= 8 KiB per the research (avoids a WinHTTP recursion /
69
+ * stack-exhaustion problem); 64 KiB is the streaming chunk ceiling too. */
70
+ #define RBUF_CAP (64 * 1024)
71
+
72
+ /* Callback event node: plain malloc, allocated/freed with NO Ruby involvement. */
73
+ typedef struct evnode {
74
+ struct evnode *next;
75
+ uint64_t id; /* dwContext (0 = pre-context callback: dropped by the pump) */
76
+ int kind;
77
+ DWORD a;
78
+ DWORD b;
79
+ } evnode;
80
+
81
+ /* Per-session control block (heap; freed when wrapper_gone && live == 0). */
82
+ typedef struct whses {
83
+ HINTERNET hsession;
84
+ long live; /* requests whose ctx is armed and not yet os_done */
85
+ int closed; /* Session#close ran (hsession closed or leaked) */
86
+ int wrapper_gone;
87
+ } whses;
88
+
89
+ /* Per-request control block (heap; freed when wrapper_gone && os_done). */
90
+ typedef struct whreq {
91
+ uint64_t id; /* dwContext value — NEVER a pointer, NEVER reused */
92
+ HINTERNET hconn; /* fresh per request (never cache connects) */
93
+ HINTERNET hreq;
94
+ whses *ses; /* owning session's control block */
95
+ unsigned char *body; /* malloc'd COPY of the request body (or NULL) */
96
+ size_t body_len;
97
+ unsigned char *rbuf; /* malloc'd receive buffer, RBUF_CAP */
98
+ int handles_closed; /* WinHttpCloseHandle(hreq) already issued */
99
+ int ctx_armed; /* WINHTTP_OPTION_CONTEXT_VALUE set OK */
100
+ int wrapper_gone;
101
+ int os_done;
102
+ } whreq;
103
+
104
+ /* Process-lifetime globals, created in Init_winhttp and NEVER destroyed (the
105
+ * phylax immortal-handle precedent — this is what makes late callbacks after
106
+ * any teardown harmless: they write into structures that always exist). */
107
+ static CRITICAL_SECTION g_cs;
108
+ static HANDLE g_event; /* auto-reset; signaled on every queued event */
109
+ static evnode *g_head, *g_tail;
110
+ static volatile LONG g_dropped; /* callback-side malloc failures (see §5.20) */
111
+ static uint64_t g_next_id = 1; /* monotonic; mutated only under the GVL */
112
+ static st_table *g_reqs; /* id -> whreq*; mutated only under the GVL */
113
+
114
+ /* ------------------------------------------------------------ small helpers */
115
+
116
+ /* UTF-8 Ruby String -> freshly xmalloc'd NUL-terminated UTF-16. Caller xfrees.
117
+ * The String is normalized to UTF-8 up front (suite boundary rule). */
118
+ static WCHAR *
119
+ to_wide(VALUE str)
120
+ {
121
+ int len, n;
122
+ WCHAR *w;
123
+ str = rb_str_export_to_enc(StringValue(str), rb_utf8_encoding());
124
+ len = (int)RSTRING_LEN(str);
125
+ if (len == 0) {
126
+ w = (WCHAR *)xmalloc(sizeof(WCHAR));
127
+ w[0] = 0;
128
+ return w;
129
+ }
130
+ n = MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(str), len, NULL, 0);
131
+ if (n <= 0) rb_raise(rb_eArgError, "winhttp: invalid UTF-8 in string");
132
+ w = (WCHAR *)xmalloc(sizeof(WCHAR) * (n + 1));
133
+ MultiByteToWideChar(CP_UTF8, 0, RSTRING_PTR(str), len, w, n);
134
+ w[n] = 0;
135
+ return w;
136
+ }
137
+
138
+ /* UTF-16 (len chars, or -1 NUL-terminated) -> a fresh UTF-8 Ruby String. */
139
+ static VALUE
140
+ wide_to_str(const WCHAR *w, int len)
141
+ {
142
+ int n;
143
+ VALUE out;
144
+ if (!w) return rb_utf8_str_new("", 0);
145
+ n = WideCharToMultiByte(CP_UTF8, 0, w, len, NULL, 0, NULL, NULL);
146
+ if (n <= 0) return rb_utf8_str_new("", 0);
147
+ out = rb_utf8_str_new(NULL, len < 0 ? n - 1 : n);
148
+ WideCharToMultiByte(CP_UTF8, 0, w, len, RSTRING_PTR(out), n, NULL, NULL);
149
+ return out;
150
+ }
151
+
152
+ /* Build + raise a winhttp error carrying @code (the Win32/WinHTTP error). The
153
+ * code must be captured by the caller immediately after the failing call.
154
+ * ERROR_WINHTTP_* (12000-range) message text lives in winhttp.dll, so we ask
155
+ * FormatMessageW to look there as well as in the system table. */
156
+ static void
157
+ raise_code(VALUE klass, const char *api, DWORD code)
158
+ {
159
+ VALUE exc, msg;
160
+ WCHAR *buf = NULL;
161
+ char detail[512];
162
+ DWORD n;
163
+ HMODULE hwh = GetModuleHandleW(L"winhttp.dll");
164
+
165
+ detail[0] = 0;
166
+ n = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
167
+ FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS,
168
+ hwh, code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
169
+ (LPWSTR)&buf, 0, NULL);
170
+ if (n && buf) {
171
+ while (n && (buf[n-1] == L'\r' || buf[n-1] == L'\n' || buf[n-1] == L'.')) buf[--n] = 0;
172
+ WideCharToMultiByte(CP_UTF8, 0, buf, -1, detail, (int)sizeof(detail), NULL, NULL);
173
+ detail[sizeof(detail) - 1] = 0;
174
+ }
175
+ /* Release the OS buffer BEFORE any Ruby allocation, so an OOM longjmp from
176
+ * rb_sprintf / rb_exc_new_str cannot leak it (winipc raise_code discipline). */
177
+ if (buf) LocalFree(buf);
178
+
179
+ if (detail[0])
180
+ msg = rb_sprintf("%s: %s (error %lu)", api, detail, (unsigned long)code);
181
+ else
182
+ msg = rb_sprintf("%s failed (error %lu)", api, (unsigned long)code);
183
+ exc = rb_exc_new_str(klass, msg);
184
+ rb_iv_set(exc, "@code", ULONG2NUM(code));
185
+ rb_exc_raise(exc);
186
+ }
187
+
188
+ /* Map a WinHTTP/Win32 error code to the right exception subclass. */
189
+ static VALUE
190
+ class_for_code(DWORD code)
191
+ {
192
+ switch (code) {
193
+ case ERROR_WINHTTP_TIMEOUT: /* 12002 */
194
+ return eTimeout;
195
+ case ERROR_WINHTTP_NAME_NOT_RESOLVED: /* 12007 */
196
+ return eResolve;
197
+ case ERROR_WINHTTP_CANNOT_CONNECT: /* 12029 */
198
+ case ERROR_WINHTTP_CONNECTION_ERROR: /* 12030 */
199
+ return eConnect;
200
+ case ERROR_WINHTTP_SECURE_FAILURE: /* 12175 */
201
+ case ERROR_WINHTTP_CLIENT_AUTH_CERT_NEEDED: /* 12044 */
202
+ case ERROR_WINHTTP_CLIENT_CERT_NO_PRIVATE_KEY: /* 12185 */
203
+ case ERROR_WINHTTP_CLIENT_CERT_NO_ACCESS_PRIVATE_KEY: /* 12186 */
204
+ case ERROR_WINHTTP_SECURE_CERT_DATE_INVALID: /* 12037 */
205
+ case ERROR_WINHTTP_SECURE_CERT_CN_INVALID: /* 12038 */
206
+ case ERROR_WINHTTP_SECURE_INVALID_CA: /* 12045 */
207
+ case ERROR_WINHTTP_SECURE_CERT_REV_FAILED: /* 12057 */
208
+ case ERROR_WINHTTP_SECURE_CHANNEL_ERROR: /* 12157 */
209
+ case ERROR_WINHTTP_SECURE_INVALID_CERT: /* 12169 */
210
+ case ERROR_WINHTTP_SECURE_CERT_REVOKED: /* 12170 */
211
+ case ERROR_WINHTTP_SECURE_CERT_WRONG_USAGE: /* 12179 */
212
+ return eTls;
213
+ case ERROR_WINHTTP_REDIRECT_FAILED: /* 12156 */
214
+ return eRedirect;
215
+ case ERROR_WINHTTP_INVALID_SERVER_RESPONSE: /* 12152 */
216
+ case ERROR_WINHTTP_HEADER_NOT_FOUND: /* 12150 */
217
+ case ERROR_WINHTTP_INVALID_HEADER: /* 12153 */
218
+ return eProtocol;
219
+ case ERROR_WINHTTP_OPERATION_CANCELLED: /* 12017 */
220
+ case ERROR_OPERATION_ABORTED: /* 995 */
221
+ return eCanceled;
222
+ default:
223
+ return eOSError;
224
+ }
225
+ }
226
+
227
+ /* ============================================================================
228
+ * The status callback (Bridge P) — runs on a WinHTTP worker thread or
229
+ * synchronously inside our own WinHttp* call. Touches ONLY: malloc, an evnode's
230
+ * fields, g_cs, g_dropped, SetEvent. Never Ruby, never the GVL, never blocks,
231
+ * no WinHttp* re-entry. Statuses not in the kind table are filtered out here.
232
+ * ==========================================================================*/
233
+
234
+ static void
235
+ push_event(uint64_t id, int kind, DWORD a, DWORD b)
236
+ {
237
+ evnode *n = (evnode *)malloc(sizeof(evnode));
238
+ if (!n) {
239
+ /* No Ruby available to raise: record the loss + still wake the pump,
240
+ * which synthesizes EV_LOST for every mailbox (§5.20). */
241
+ InterlockedIncrement(&g_dropped);
242
+ SetEvent(g_event);
243
+ return;
244
+ }
245
+ n->next = NULL;
246
+ n->id = id;
247
+ n->kind = kind;
248
+ n->a = a;
249
+ n->b = b;
250
+ EnterCriticalSection(&g_cs);
251
+ if (g_tail) g_tail->next = n; else g_head = n;
252
+ g_tail = n;
253
+ LeaveCriticalSection(&g_cs);
254
+ SetEvent(g_event);
255
+ }
256
+
257
+ static void CALLBACK
258
+ status_callback(HINTERNET h, DWORD_PTR ctx, DWORD status,
259
+ LPVOID info, DWORD len)
260
+ {
261
+ uint64_t id = (uint64_t)ctx;
262
+ (void)h;
263
+ switch (status) {
264
+ case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE:
265
+ push_event(id, EV_SEND, 0, 0);
266
+ break;
267
+ case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE:
268
+ push_event(id, EV_HEADERS, 0, 0);
269
+ break;
270
+ case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
271
+ /* In async mode WinHttpReadData's READ_COMPLETE reports the byte
272
+ * count in `len` (info points at our own buffer). */
273
+ push_event(id, EV_READ, len, 0);
274
+ break;
275
+ case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: {
276
+ WINHTTP_ASYNC_RESULT *r = (WINHTTP_ASYNC_RESULT *)info;
277
+ DWORD which = r ? (DWORD)r->dwResult : 0;
278
+ DWORD err = r ? r->dwError : 0;
279
+ push_event(id, EV_ERROR, which, err);
280
+ break;
281
+ }
282
+ case WINHTTP_CALLBACK_STATUS_SECURE_FAILURE: {
283
+ DWORD flags = (info && len >= sizeof(DWORD)) ? *(DWORD *)info : 0;
284
+ push_event(id, EV_SECURE, flags, 0);
285
+ break;
286
+ }
287
+ case WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING:
288
+ push_event(id, EV_CLOSING, 0, 0);
289
+ break;
290
+ default:
291
+ /* WRITE_COMPLETE, DATA_AVAILABLE, REDIRECT, resolve/connect chatter:
292
+ * no node, no wakeup. */
293
+ break;
294
+ }
295
+ }
296
+
297
+ /* =====================================================================
298
+ * Session — Winhttp::Session
299
+ * ===================================================================== */
300
+
301
+ static void
302
+ whses_free(void *p)
303
+ {
304
+ whses *s = (whses *)p;
305
+ /* GC free hook (GVL held during sweep; table/flag ops are plain C). Safety
306
+ * net, never the API. */
307
+ if (!s) return;
308
+ s->wrapper_gone = 1;
309
+ if (s->live == 0) {
310
+ if (s->hsession && !s->closed) WinHttpCloseHandle(s->hsession);
311
+ free(s);
312
+ }
313
+ /* else: leave it for the pump's EV_CLOSING handler (an orphaned session
314
+ * with in-flight requests) — it frees when live hits 0. */
315
+ }
316
+
317
+ /* The TypedData payload is a POINTER to the heap control block (so WinHTTP may
318
+ * outlive the wrapper). dfree runs the free-hook above on that pointer. */
319
+ static void
320
+ ses_wrapper_free(void *p)
321
+ {
322
+ whses **slot = (whses **)p;
323
+ if (slot && *slot) whses_free(*slot);
324
+ xfree(slot);
325
+ }
326
+
327
+ static size_t
328
+ ses_wrapper_size(const void *p)
329
+ {
330
+ (void)p;
331
+ return sizeof(whses *) + sizeof(whses);
332
+ }
333
+
334
+ static const rb_data_type_t ses_type = {
335
+ "Winhttp::Session",
336
+ { 0, ses_wrapper_free, ses_wrapper_size, },
337
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
338
+ };
339
+
340
+ static VALUE
341
+ ses_alloc(VALUE klass)
342
+ {
343
+ whses **slot;
344
+ VALUE obj = TypedData_Make_Struct(klass, whses *, &ses_type, slot);
345
+ *slot = NULL; /* sentinel: a never-initialized Session is inert */
346
+ return obj;
347
+ }
348
+
349
+ static whses *
350
+ ses_ptr(VALUE self)
351
+ {
352
+ whses **slot;
353
+ TypedData_Get_Struct(self, whses *, &ses_type, slot);
354
+ return *slot;
355
+ }
356
+
357
+ /* Set a DWORD option; return TRUE/FALSE (caller decides whether to raise). */
358
+ static BOOL
359
+ set_dword_opt(HINTERNET h, DWORD opt, DWORD val)
360
+ {
361
+ return WinHttpSetOption(h, opt, &val, sizeof(val));
362
+ }
363
+
364
+ /* Winhttp::Session._open(ua, access_type, proxy, bypass,
365
+ * connect_ms, send_ms, receive_ms, (-1 == unset)
366
+ * http2, decompress, redirect_policy, max_redirects,
367
+ * revocation) revocation: 0 none / 1 best_effort / 2 strict
368
+ * Returns [session, effective_revocation_int].
369
+ */
370
+ static VALUE
371
+ ses_open(int argc, VALUE *argv, VALUE klass)
372
+ {
373
+ VALUE vua, vaccess, vproxy, vbypass, vct, vst, vrt, vh2, vdec, vrp, vmaxr, vrev;
374
+ VALUE obj;
375
+ whses **slot;
376
+ whses *s;
377
+ HINTERNET hs;
378
+ WCHAR *ua = NULL, *proxy = NULL, *bypass = NULL;
379
+ DWORD access, gle;
380
+ long long ct, st, rt;
381
+ int want_h2, want_dec, redirect_policy, rev;
382
+ long long maxr;
383
+ DWORD secure_protocols;
384
+ int effective_rev;
385
+
386
+ /* We pass exactly 12 positional args from Ruby; read them directly. */
387
+ if (argc != 12) rb_raise(rb_eArgError, "winhttp: _open arity");
388
+ vua = argv[0]; vaccess = argv[1]; vproxy = argv[2]; vbypass = argv[3];
389
+ vct = argv[4]; vst = argv[5]; vrt = argv[6];
390
+ vh2 = argv[7]; vdec = argv[8]; vrp = argv[9]; vmaxr = argv[10]; vrev = argv[11];
391
+
392
+ access = (DWORD)NUM2ULONG(vaccess);
393
+ ct = NUM2LL(vct); st = NUM2LL(vst); rt = NUM2LL(vrt);
394
+ want_h2 = RTEST(vh2); want_dec = RTEST(vdec);
395
+ redirect_policy = NUM2INT(vrp);
396
+ maxr = NUM2LL(vmaxr);
397
+ rev = NUM2INT(vrev);
398
+
399
+ /* Convert all strings up front (TypeError here is clean: no handle yet). */
400
+ ua = to_wide(vua);
401
+ if (!NIL_P(vproxy)) proxy = to_wide(vproxy);
402
+ if (!NIL_P(vbypass)) bypass = to_wide(vbypass);
403
+
404
+ obj = ses_alloc(klass);
405
+ TypedData_Get_Struct(obj, whses *, &ses_type, slot);
406
+
407
+ hs = WinHttpOpen(ua, access,
408
+ proxy ? proxy : WINHTTP_NO_PROXY_NAME,
409
+ bypass ? bypass : WINHTTP_NO_PROXY_BYPASS,
410
+ WINHTTP_FLAG_ASYNC);
411
+ gle = GetLastError();
412
+ xfree(ua);
413
+ if (proxy) xfree(proxy);
414
+ if (bypass) xfree(bypass);
415
+ if (!hs) raise_code(eOSError, "WinHttpOpen", gle);
416
+
417
+ /* Register the status callback ONCE on the session before any child handle
418
+ * (inherited by derived handles). */
419
+ if (WinHttpSetStatusCallback(hs, status_callback,
420
+ WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, 0)
421
+ == WINHTTP_INVALID_STATUS_CALLBACK) {
422
+ gle = GetLastError();
423
+ WinHttpCloseHandle(hs);
424
+ raise_code(eOSError, "WinHttpSetStatusCallback", gle);
425
+ }
426
+
427
+ /* Timeouts (resolve fixed at 0 == default-infinite resolve; others only
428
+ * when the caller supplied them; -1 means "leave the WinHTTP default"). */
429
+ if (ct >= 0 || st >= 0 || rt >= 0) {
430
+ int cms = (int)(ct >= 0 ? ct : 0);
431
+ int sms = (int)(st >= 0 ? st : 30000);
432
+ int rms = (int)(rt >= 0 ? rt : 30000);
433
+ if (!WinHttpSetTimeouts(hs, 0, cms, sms, rms)) {
434
+ gle = GetLastError();
435
+ WinHttpCloseHandle(hs);
436
+ raise_code(eOSError, "WinHttpSetTimeouts", gle);
437
+ }
438
+ }
439
+
440
+ /* TLS floor: always TLS1.2|TLS1.3, fall back to TLS1.2 alone, else raise. */
441
+ secure_protocols = WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2 |
442
+ WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_3;
443
+ if (!set_dword_opt(hs, WINHTTP_OPTION_SECURE_PROTOCOLS, secure_protocols)) {
444
+ secure_protocols = WINHTTP_FLAG_SECURE_PROTOCOL_TLS1_2;
445
+ if (!set_dword_opt(hs, WINHTTP_OPTION_SECURE_PROTOCOLS, secure_protocols)) {
446
+ gle = GetLastError();
447
+ WinHttpCloseHandle(hs);
448
+ raise_code(eOSError, "WinHttpSetOption(SECURE_PROTOCOLS)", gle);
449
+ }
450
+ }
451
+
452
+ /* HTTP/2 + decompression: best-effort, a rejecting OS degrades silently. */
453
+ if (want_h2)
454
+ set_dword_opt(hs, WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL,
455
+ WINHTTP_PROTOCOL_FLAG_HTTP2);
456
+ if (want_dec)
457
+ set_dword_opt(hs, WINHTTP_OPTION_DECOMPRESSION,
458
+ WINHTTP_DECOMPRESSION_FLAG_ALL);
459
+
460
+ /* Redirect policy + max count. */
461
+ set_dword_opt(hs, WINHTTP_OPTION_REDIRECT_POLICY, (DWORD)redirect_policy);
462
+ if (maxr >= 0)
463
+ set_dword_opt(hs, WINHTTP_OPTION_MAX_HTTP_AUTOMATIC_REDIRECTS, (DWORD)maxr);
464
+
465
+ /* Revocation policy is settled here by probing throwaway handles (neither
466
+ * call touches the network; both probe handles are context-less, so their
467
+ * HANDLE_CLOSING arrives as id 0 and the pump drops it). */
468
+ effective_rev = 0; /* :none default */
469
+ if (rev != 0) {
470
+ HINTERNET hc = WinHttpConnect(hs, L"localhost", 80, 0);
471
+ if (hc) {
472
+ HINTERNET hrq = WinHttpOpenRequest(hc, L"GET", L"/", NULL,
473
+ WINHTTP_NO_REFERER,
474
+ WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
475
+ if (hrq) {
476
+ BOOL ok_rev = set_dword_opt(hrq, WINHTTP_OPTION_ENABLE_FEATURE,
477
+ WINHTTP_ENABLE_SSL_REVOCATION);
478
+ if (rev == 2) {
479
+ /* :strict — revocation must be available, else raise. */
480
+ if (ok_rev) {
481
+ effective_rev = 2;
482
+ } else {
483
+ gle = GetLastError();
484
+ WinHttpCloseHandle(hrq);
485
+ WinHttpCloseHandle(hc);
486
+ WinHttpCloseHandle(hs);
487
+ raise_code(eOSError,
488
+ "WinHttpSetOption(ENABLE_SSL_REVOCATION)", gle);
489
+ }
490
+ } else {
491
+ /* :best_effort — both options must succeed, else degrade. */
492
+ BOOL ok_off = ok_rev &&
493
+ set_dword_opt(hrq, WINHTTP_OPTION_IGNORE_CERT_REVOCATION_OFFLINE,
494
+ TRUE);
495
+ effective_rev = (ok_rev && ok_off) ? 1 : 0;
496
+ }
497
+ WinHttpCloseHandle(hrq);
498
+ } else if (rev == 2) {
499
+ gle = GetLastError();
500
+ WinHttpCloseHandle(hc);
501
+ WinHttpCloseHandle(hs);
502
+ raise_code(eOSError, "WinHttpOpenRequest(probe)", gle);
503
+ }
504
+ WinHttpCloseHandle(hc);
505
+ } else if (rev == 2) {
506
+ gle = GetLastError();
507
+ WinHttpCloseHandle(hs);
508
+ raise_code(eOSError, "WinHttpConnect(probe)", gle);
509
+ }
510
+ }
511
+
512
+ s = (whses *)calloc(1, sizeof(whses));
513
+ if (!s) { WinHttpCloseHandle(hs); rb_raise(rb_eNoMemError, "winhttp: out of memory"); }
514
+ s->hsession = hs;
515
+ s->live = 0;
516
+ s->closed = 0;
517
+ s->wrapper_gone = 0;
518
+ *slot = s;
519
+
520
+ return rb_ary_new3(2, obj, INT2NUM(effective_rev));
521
+ }
522
+
523
+ static VALUE ses_closed_p(VALUE self) {
524
+ whses *s = ses_ptr(self);
525
+ return (!s || s->closed) ? Qtrue : Qfalse;
526
+ }
527
+
528
+ /* Mark the session closed FIRST (GVL-held, strictly before any handle is
529
+ * closed): new requests / _start's re-check raise Winhttp::Closed. Returns the
530
+ * raw HINTERNET so Ruby can sequence the abort sweep before _close_handle. */
531
+ static VALUE ses_mark_closed(VALUE self) {
532
+ whses *s = ses_ptr(self);
533
+ if (!s) return Qnil;
534
+ s->closed = 1;
535
+ return Qnil;
536
+ }
537
+
538
+ /* Close the session handle iff live == 0. Returns true when closed (or already
539
+ * gone / no live requests), false when deferred because requests are still
540
+ * tearing down. */
541
+ static VALUE ses_try_close_handle(VALUE self) {
542
+ whses *s = ses_ptr(self);
543
+ if (!s) return Qtrue;
544
+ if (s->live > 0) return Qfalse;
545
+ if (s->hsession) { WinHttpCloseHandle(s->hsession); s->hsession = NULL; }
546
+ return Qtrue;
547
+ }
548
+
549
+ static VALUE ses_live_count(VALUE self) {
550
+ whses *s = ses_ptr(self);
551
+ return s ? LONG2NUM(s->live) : INT2NUM(0);
552
+ }
553
+
554
+ /* =====================================================================
555
+ * Request — Winhttp::Request (internal plumbing; no public contract)
556
+ * ===================================================================== */
557
+
558
+ static void
559
+ whreq_maybe_free(whreq *r)
560
+ {
561
+ /* Free iff BOTH flags set. Caller holds the GVL. */
562
+ if (!(r->wrapper_gone && r->os_done)) return;
563
+ st_delete(g_reqs, (st_data_t *)&r->id, NULL);
564
+ if (r->body) free(r->body);
565
+ if (r->rbuf) free(r->rbuf);
566
+ free(r);
567
+ }
568
+
569
+ static void
570
+ whreq_free(void *p)
571
+ {
572
+ /* GC free hook for the Request wrapper (GVL held during sweep). Safety net.
573
+ * Set wrapper_gone; if no callback can ever come (!ctx_armed) or the OS is
574
+ * already done, free now; else leave it for the pump's EV_CLOSING. */
575
+ whreq **slot = (whreq **)p;
576
+ whreq *r = slot ? *slot : NULL;
577
+ if (r) {
578
+ r->wrapper_gone = 1;
579
+ if (!r->handles_closed && r->hreq) {
580
+ WinHttpCloseHandle(r->hreq);
581
+ r->handles_closed = 1;
582
+ }
583
+ if (!r->ctx_armed) {
584
+ /* No context armed => no HANDLE_CLOSING for our id will ever come.
585
+ * The connect handle (if any) is closed here; nothing is registered
586
+ * in g_reqs yet for the pre-arm path, so just free our memory. We
587
+ * must still drop the live count / connect if it was registered. */
588
+ if (r->hconn) WinHttpCloseHandle(r->hconn);
589
+ /* If this block was registered (it is only ever registered after
590
+ * the handles exist, just before arming), st_delete is harmless if
591
+ * absent. ses->live was only incremented at arm time. */
592
+ st_delete(g_reqs, (st_data_t *)&r->id, NULL);
593
+ if (r->body) free(r->body);
594
+ if (r->rbuf) free(r->rbuf);
595
+ free(r);
596
+ } else {
597
+ whreq_maybe_free(r);
598
+ }
599
+ }
600
+ xfree(slot);
601
+ }
602
+
603
+ static size_t
604
+ req_wrapper_size(const void *p)
605
+ {
606
+ whreq * const *slot = (whreq * const *)p;
607
+ whreq *r = slot ? *slot : NULL;
608
+ size_t n = sizeof(whreq *) + sizeof(whreq);
609
+ if (r) n += r->body_len + (r->rbuf ? RBUF_CAP : 0);
610
+ return n;
611
+ }
612
+
613
+ static const rb_data_type_t req_type = {
614
+ "Winhttp::Request",
615
+ { 0, whreq_free, req_wrapper_size, },
616
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
617
+ };
618
+
619
+ static VALUE
620
+ req_alloc(VALUE klass)
621
+ {
622
+ whreq **slot;
623
+ VALUE obj = TypedData_Make_Struct(klass, whreq *, &req_type, slot);
624
+ *slot = NULL;
625
+ return obj;
626
+ }
627
+
628
+ static whreq *
629
+ req_ptr(VALUE self)
630
+ {
631
+ whreq **slot;
632
+ TypedData_Get_Struct(self, whreq *, &req_type, slot);
633
+ return *slot;
634
+ }
635
+
636
+ /* Raises Winhttp::Canceled if the request handle is already closed (the
637
+ * cancelled-op-may-complete-successfully backstop, §3.6.6). */
638
+ static whreq *
639
+ req_live(VALUE self)
640
+ {
641
+ whreq *r = req_ptr(self);
642
+ if (!r) rb_raise(eError, "winhttp: request is not initialized");
643
+ if (r->handles_closed)
644
+ raise_code(eCanceled, "WinHttp(after close)", ERROR_WINHTTP_OPERATION_CANCELLED);
645
+ return r;
646
+ }
647
+
648
+ /* Session#_start(req_obj, verb, host, port, secure, path, header_blob, body,
649
+ * effective_revocation) -> id (Integer)
650
+ *
651
+ * One raise-hygienic C frame. The Request wrapper (req_obj) is allocated in
652
+ * Ruby and passed in so its free hook owns the handles from the instant they
653
+ * exist. Holds the GVL throughout (§3.5), so the ses->closed re-check is
654
+ * race-free against Session#close.
655
+ */
656
+ static VALUE
657
+ req_start(VALUE self, VALUE req_obj, VALUE vverb, VALUE vhost, VALUE vport,
658
+ VALUE vsecure, VALUE vpath, VALUE vblob, VALUE vbody, VALUE vrev)
659
+ {
660
+ whses *ses = ses_ptr(self);
661
+ whreq **slot;
662
+ whreq *r;
663
+ WCHAR *verb = NULL, *host = NULL, *path = NULL, *blob = NULL;
664
+ INTERNET_PORT port = (INTERNET_PORT)NUM2UINT(vport);
665
+ int secure = RTEST(vsecure);
666
+ int rev = NUM2INT(vrev);
667
+ DWORD open_flags = secure ? WINHTTP_FLAG_SECURE : 0;
668
+ DWORD gle;
669
+ uint64_t id;
670
+
671
+ if (!ses || ses->closed)
672
+ rb_raise(eClosed, "winhttp: session is closed");
673
+
674
+ TypedData_Get_Struct(req_obj, whreq *, &req_type, slot);
675
+
676
+ /* Coerce/convert all strings up front (raise-clean: no control block yet). */
677
+ verb = to_wide(vverb);
678
+ host = to_wide(vhost);
679
+ path = to_wide(vpath);
680
+ if (!NIL_P(vblob)) blob = to_wide(vblob);
681
+
682
+ /* Allocate the control block + buffers (plain malloc; free on failure and
683
+ * raise manually so an OOM longjmp can't leak the wide strings). */
684
+ r = (whreq *)calloc(1, sizeof(whreq));
685
+ if (r) r->rbuf = (unsigned char *)malloc(RBUF_CAP);
686
+ if (!r || !r->rbuf) {
687
+ if (r) free(r);
688
+ xfree(verb); xfree(host); xfree(path); if (blob) xfree(blob);
689
+ rb_raise(rb_eNoMemError, "winhttp: out of memory");
690
+ }
691
+ r->ses = ses;
692
+ *slot = r; /* the wrapper free hook owns r from here on */
693
+
694
+ /* Copy the body into a private buffer (WinHTTP does not copy buffers). */
695
+ if (!NIL_P(vbody)) {
696
+ long blen;
697
+ StringValue(vbody);
698
+ blen = RSTRING_LEN(vbody);
699
+ if (blen > 0) {
700
+ r->body = (unsigned char *)malloc((size_t)blen);
701
+ if (!r->body) {
702
+ xfree(verb); xfree(host); xfree(path); if (blob) xfree(blob);
703
+ rb_raise(rb_eNoMemError, "winhttp: out of memory");
704
+ }
705
+ memcpy(r->body, RSTRING_PTR(vbody), (size_t)blen);
706
+ r->body_len = (size_t)blen;
707
+ }
708
+ }
709
+
710
+ /* Re-check ses->closed (TOCTOU close — §3.4/§5.25): GVL-held, race-free. */
711
+ if (ses->closed) {
712
+ xfree(verb); xfree(host); xfree(path); if (blob) xfree(blob);
713
+ rb_raise(eClosed, "winhttp: session is closed");
714
+ }
715
+
716
+ r->hconn = WinHttpConnect(ses->hsession, host, port, 0);
717
+ if (!r->hconn) {
718
+ gle = GetLastError();
719
+ xfree(verb); xfree(host); xfree(path); if (blob) xfree(blob);
720
+ /* pre-arm cleanup: no context, immediate free is safe (free hook). */
721
+ raise_code(class_for_code(gle), "WinHttpConnect", gle);
722
+ }
723
+ r->hreq = WinHttpOpenRequest(r->hconn, verb, path, NULL,
724
+ WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES,
725
+ open_flags);
726
+ if (!r->hreq) {
727
+ gle = GetLastError();
728
+ xfree(verb); xfree(host); xfree(path); if (blob) xfree(blob);
729
+ raise_code(class_for_code(gle), "WinHttpOpenRequest", gle);
730
+ }
731
+ xfree(verb); xfree(host); xfree(path); /* done with these */
732
+
733
+ /* Registration strictly before arming, so an armed context always has a
734
+ * table entry. */
735
+ id = g_next_id++;
736
+ r->id = id;
737
+ st_insert(g_reqs, (st_data_t)id, (st_data_t)r);
738
+ ses->live++;
739
+
740
+ /* Arm the context value before anything can complete (§5.10): this is what
741
+ * makes HANDLE_CLOSING with our id guaranteed. */
742
+ {
743
+ DWORD_PTR ctxv = (DWORD_PTR)id;
744
+ if (!WinHttpSetOption(r->hreq, WINHTTP_OPTION_CONTEXT_VALUE,
745
+ &ctxv, sizeof(ctxv))) {
746
+ gle = GetLastError();
747
+ /* roll back registration; still pre-arm (ctx not set) => free now. */
748
+ st_delete(g_reqs, (st_data_t *)&id, NULL);
749
+ ses->live--;
750
+ if (blob) xfree(blob);
751
+ raise_code(class_for_code(gle), "WinHttpSetOption(CONTEXT_VALUE)", gle);
752
+ }
753
+ r->ctx_armed = 1;
754
+ }
755
+
756
+ /* --- post-arm regime: any failure leaves the block registered + live
757
+ * counted; EV_CLOSING settles it (§3.4). Never free inline here. --- */
758
+
759
+ /* Per-request revocation options matching the construction-settled policy. */
760
+ if (rev != 0) {
761
+ if (!set_dword_opt(r->hreq, WINHTTP_OPTION_ENABLE_FEATURE,
762
+ WINHTTP_ENABLE_SSL_REVOCATION)) {
763
+ gle = GetLastError();
764
+ if (blob) xfree(blob);
765
+ WinHttpCloseHandle(r->hreq);
766
+ r->handles_closed = 1;
767
+ raise_code(class_for_code(gle),
768
+ "WinHttpSetOption(ENABLE_SSL_REVOCATION)", gle);
769
+ }
770
+ if (rev == 1) {
771
+ if (!set_dword_opt(r->hreq, WINHTTP_OPTION_IGNORE_CERT_REVOCATION_OFFLINE,
772
+ TRUE)) {
773
+ gle = GetLastError();
774
+ if (blob) xfree(blob);
775
+ WinHttpCloseHandle(r->hreq);
776
+ r->handles_closed = 1;
777
+ raise_code(class_for_code(gle),
778
+ "WinHttpSetOption(IGNORE_CERT_REVOCATION_OFFLINE)", gle);
779
+ }
780
+ }
781
+ }
782
+
783
+ if (blob) {
784
+ if (!WinHttpAddRequestHeaders(r->hreq, blob, (DWORD)-1,
785
+ WINHTTP_ADDREQ_FLAG_ADD)) {
786
+ gle = GetLastError();
787
+ xfree(blob);
788
+ WinHttpCloseHandle(r->hreq);
789
+ r->handles_closed = 1;
790
+ raise_code(class_for_code(gle), "WinHttpAddRequestHeaders", gle);
791
+ }
792
+ xfree(blob);
793
+ }
794
+
795
+ return ULL2NUM(id);
796
+ }
797
+
798
+ /* Request#_send — WinHttpSendRequest (async). Immediate FALSE => no callback,
799
+ * raise now. */
800
+ static VALUE
801
+ req_send(VALUE self)
802
+ {
803
+ whreq *r = req_live(self);
804
+ DWORD_PTR ctxv = (DWORD_PTR)r->id;
805
+ LPVOID body = r->body ? (LPVOID)r->body : WINHTTP_NO_REQUEST_DATA;
806
+ DWORD blen = (DWORD)r->body_len;
807
+ if (!WinHttpSendRequest(r->hreq, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
808
+ body, blen, blen, ctxv)) {
809
+ DWORD gle = GetLastError();
810
+ raise_code(class_for_code(gle), "WinHttpSendRequest", gle);
811
+ }
812
+ return Qnil;
813
+ }
814
+
815
+ static VALUE
816
+ req_receive(VALUE self)
817
+ {
818
+ whreq *r = req_live(self);
819
+ if (!WinHttpReceiveResponse(r->hreq, NULL)) {
820
+ DWORD gle = GetLastError();
821
+ raise_code(class_for_code(gle), "WinHttpReceiveResponse", gle);
822
+ }
823
+ return Qnil;
824
+ }
825
+
826
+ static VALUE
827
+ req_read_start(VALUE self)
828
+ {
829
+ whreq *r = req_live(self);
830
+ if (!WinHttpReadData(r->hreq, r->rbuf, RBUF_CAP, NULL)) {
831
+ DWORD gle = GetLastError();
832
+ raise_code(class_for_code(gle), "WinHttpReadData", gle);
833
+ }
834
+ return Qnil;
835
+ }
836
+
837
+ /* Request#_read_take(n) -> fresh ASCII-8BIT String of the first n bytes. */
838
+ static VALUE
839
+ req_read_take(VALUE self, VALUE vn)
840
+ {
841
+ whreq *r = req_live(self);
842
+ long n = (long)NUM2LL(vn);
843
+ VALUE out;
844
+ if (n < 0) n = 0;
845
+ if (n > RBUF_CAP) n = RBUF_CAP;
846
+ out = rb_str_new((const char *)r->rbuf, n);
847
+ rb_enc_associate(out, rb_ascii8bit_encoding());
848
+ return out;
849
+ }
850
+
851
+ /* Query a raw-headers / status / url / protocol bundle, GVL held.
852
+ * Returns [status(Integer), raw_headers(String UTF-8), final_url(String),
853
+ * http2(bool)]. */
854
+ static VALUE
855
+ req_headers(VALUE self)
856
+ {
857
+ whreq *r = req_live(self);
858
+ DWORD status = 0, size = 0, gle;
859
+ DWORD idx = 0; /* WINHTTP_NO_HEADER_INDEX is NULL; we pass &idx, reset per call */
860
+ VALUE raw, url, ret;
861
+ WCHAR *buf;
862
+ DWORD proto = 0, psz = sizeof(proto);
863
+
864
+ /* status code (numeric) */
865
+ size = sizeof(status);
866
+ if (!WinHttpQueryHeaders(r->hreq,
867
+ WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
868
+ WINHTTP_HEADER_NAME_BY_INDEX, &status, &size, &idx)) {
869
+ gle = GetLastError();
870
+ raise_code(class_for_code(gle), "WinHttpQueryHeaders(STATUS_CODE)", gle);
871
+ }
872
+
873
+ /* raw headers (CRLF) — two-call size pattern */
874
+ size = 0; idx = 0;
875
+ WinHttpQueryHeaders(r->hreq, WINHTTP_QUERY_RAW_HEADERS_CRLF,
876
+ WINHTTP_HEADER_NAME_BY_INDEX, NULL, &size, &idx);
877
+ gle = GetLastError();
878
+ if (gle != ERROR_INSUFFICIENT_BUFFER && size == 0) {
879
+ raise_code(class_for_code(gle), "WinHttpQueryHeaders(RAW_HEADERS)", gle);
880
+ }
881
+ buf = (WCHAR *)xmalloc(size + sizeof(WCHAR));
882
+ idx = 0;
883
+ if (!WinHttpQueryHeaders(r->hreq, WINHTTP_QUERY_RAW_HEADERS_CRLF,
884
+ WINHTTP_HEADER_NAME_BY_INDEX, buf, &size, &idx)) {
885
+ gle = GetLastError();
886
+ xfree(buf);
887
+ raise_code(class_for_code(gle), "WinHttpQueryHeaders(RAW_HEADERS)", gle);
888
+ }
889
+ raw = wide_to_str(buf, (int)(size / sizeof(WCHAR)));
890
+ xfree(buf);
891
+
892
+ /* final URL (after redirects) — two-call size pattern */
893
+ size = 0;
894
+ WinHttpQueryOption(r->hreq, WINHTTP_OPTION_URL, NULL, &size);
895
+ if (size == 0) {
896
+ url = rb_utf8_str_new("", 0);
897
+ } else {
898
+ buf = (WCHAR *)xmalloc(size + sizeof(WCHAR));
899
+ if (!WinHttpQueryOption(r->hreq, WINHTTP_OPTION_URL, buf, &size)) {
900
+ xfree(buf);
901
+ url = rb_utf8_str_new("", 0);
902
+ } else {
903
+ url = wide_to_str(buf, -1);
904
+ xfree(buf);
905
+ }
906
+ }
907
+
908
+ /* protocol used (best-effort; 0 on failure) */
909
+ if (!WinHttpQueryOption(r->hreq, WINHTTP_OPTION_HTTP_PROTOCOL_USED,
910
+ &proto, &psz)) {
911
+ proto = 0;
912
+ }
913
+
914
+ ret = rb_ary_new3(4, UINT2NUM(status), raw, url,
915
+ (proto & WINHTTP_PROTOCOL_FLAG_HTTP2) ? Qtrue : Qfalse);
916
+ return ret;
917
+ }
918
+
919
+ /* Request#_abort / #_finish — WinHttpCloseHandle(hreq), idempotent. Teardown is
920
+ * async; the pump's EV_CLOSING settles os_done + the connect handle. */
921
+ static VALUE
922
+ req_abort(VALUE self)
923
+ {
924
+ whreq *r = req_ptr(self);
925
+ if (r && !r->handles_closed && r->hreq) {
926
+ WinHttpCloseHandle(r->hreq);
927
+ r->handles_closed = 1;
928
+ }
929
+ return Qnil;
930
+ }
931
+
932
+ /* =====================================================================
933
+ * Pump primitives (Bridge P)
934
+ * ===================================================================== */
935
+
936
+ static void *
937
+ pump_wait_fn(void *p)
938
+ {
939
+ (void)p;
940
+ WaitForSingleObject(g_event, INFINITE);
941
+ return NULL;
942
+ }
943
+
944
+ static void
945
+ pump_wait_ubf(void *p)
946
+ {
947
+ (void)p;
948
+ SetEvent(g_event); /* break the no-GVL wait promptly (Thread#kill / exit) */
949
+ }
950
+
951
+ /* Winhttp._pump_wait — block (GVL released) until the callback signals. */
952
+ static VALUE
953
+ pump_wait(VALUE mod)
954
+ {
955
+ (void)mod;
956
+ rb_thread_call_without_gvl(pump_wait_fn, NULL, pump_wait_ubf, NULL);
957
+ return Qnil;
958
+ }
959
+
960
+ /* Winhttp._pump_drain — detach the event list (g_cs held only for the pointer
961
+ * swap), then per node (GVL held): EV_CLOSING bookkeeping in C (never routed),
962
+ * else build a [id, kind, a, b] tuple. Returns the tuple array. Also emits one
963
+ * EV_LOST tuple with id 0 when g_dropped > 0 (the Ruby pump fans it out to all
964
+ * mailboxes). */
965
+ static VALUE
966
+ pump_drain(VALUE mod)
967
+ {
968
+ evnode *head, *n;
969
+ VALUE out = rb_ary_new();
970
+ LONG dropped;
971
+ (void)mod;
972
+
973
+ EnterCriticalSection(&g_cs);
974
+ head = g_head;
975
+ g_head = g_tail = NULL;
976
+ LeaveCriticalSection(&g_cs);
977
+
978
+ n = head;
979
+ while (n) {
980
+ evnode *next = n->next;
981
+ if (n->kind == EV_CLOSING) {
982
+ /* Drain-side lifetime bookkeeping for id n->id (GVL held). */
983
+ st_data_t v;
984
+ if (st_lookup(g_reqs, (st_data_t)n->id, &v)) {
985
+ whreq *r = (whreq *)v;
986
+ whses *ses = r->ses;
987
+ r->os_done = 1;
988
+ if (r->hconn) { WinHttpCloseHandle(r->hconn); r->hconn = NULL; }
989
+ if (ses && ses->live > 0) ses->live--;
990
+ if (ses && ses->wrapper_gone && ses->live == 0 && !ses->closed) {
991
+ if (ses->hsession) WinHttpCloseHandle(ses->hsession);
992
+ free(ses);
993
+ r->ses = NULL;
994
+ }
995
+ whreq_maybe_free(r);
996
+ }
997
+ /* EV_CLOSING is consumed in C and not routed to any mailbox. */
998
+ } else {
999
+ rb_ary_push(out, rb_ary_new3(4,
1000
+ ULL2NUM(n->id), INT2NUM(n->kind),
1001
+ ULONG2NUM(n->a), ULONG2NUM(n->b)));
1002
+ }
1003
+ free(n);
1004
+ n = next;
1005
+ }
1006
+
1007
+ dropped = InterlockedExchange(&g_dropped, 0);
1008
+ if (dropped > 0) {
1009
+ /* id 0 sentinel — the Ruby pump fans EV_LOST out to every mailbox. */
1010
+ rb_ary_push(out, rb_ary_new3(4, ULL2NUM(0), INT2NUM(EV_LOST),
1011
+ INT2NUM(0), INT2NUM(0)));
1012
+ }
1013
+ return out;
1014
+ }
1015
+
1016
+ /* Winhttp._live_count — test-only introspection (§3.2): number of g_reqs
1017
+ * entries whose os_done is unset. Underscore-named, no public contract; exists
1018
+ * solely so the §5.10 leak probe (test 51) can assert against real counters per
1019
+ * the suite's no-mock rule. */
1020
+ static int
1021
+ live_count_i(st_data_t key, st_data_t val, st_data_t arg)
1022
+ {
1023
+ whreq *r = (whreq *)val;
1024
+ long *acc = (long *)arg;
1025
+ (void)key;
1026
+ if (!r->os_done) (*acc)++;
1027
+ return ST_CONTINUE;
1028
+ }
1029
+
1030
+ static VALUE
1031
+ live_count(VALUE mod)
1032
+ {
1033
+ long acc = 0;
1034
+ (void)mod;
1035
+ st_foreach(g_reqs, live_count_i, (st_data_t)&acc);
1036
+ return LONG2NUM(acc);
1037
+ }
1038
+
1039
+ /* =====================================================================
1040
+ * URL cracking — Winhttp._crack(url) -> [secure, host, port, path]
1041
+ * ===================================================================== */
1042
+
1043
+ static VALUE
1044
+ url_crack(VALUE mod, VALUE vurl)
1045
+ {
1046
+ URL_COMPONENTS uc;
1047
+ WCHAR user[2], pass[2];
1048
+ WCHAR *url, *host = NULL, *path = NULL, *extra = NULL;
1049
+ size_t urllen, cap;
1050
+ int secure, err = 0;
1051
+ VALUE host_s, path_s;
1052
+ (void)mod;
1053
+
1054
+ url = to_wide(vurl);
1055
+
1056
+ /* A cracked component can never be longer than the URL itself, so sizing
1057
+ * host/path/extra at (urllen + 1) WCHARs each is always sufficient — this
1058
+ * is what makes legitimate long URLs (big OAuth/JWT/base64 query strings,
1059
+ * signed URLs) crack instead of failing ERROR_INSUFFICIENT_BUFFER against
1060
+ * the old fixed 256/4096-WCHAR stack buffers. */
1061
+ urllen = wcslen(url);
1062
+ cap = urllen + 1;
1063
+ host = (WCHAR *)xmalloc(sizeof(WCHAR) * cap);
1064
+ path = (WCHAR *)xmalloc(sizeof(WCHAR) * cap);
1065
+ extra = (WCHAR *)xmalloc(sizeof(WCHAR) * cap);
1066
+
1067
+ memset(&uc, 0, sizeof(uc));
1068
+ uc.dwStructSize = sizeof(uc);
1069
+ uc.lpszHostName = host; uc.dwHostNameLength = (DWORD)cap;
1070
+ uc.lpszUrlPath = path; uc.dwUrlPathLength = (DWORD)cap;
1071
+ uc.lpszExtraInfo = extra; uc.dwExtraInfoLength = (DWORD)cap;
1072
+ /* Non-zero length pointers so userinfo presence is detectable. */
1073
+ uc.lpszUserName = user; uc.dwUserNameLength = (DWORD)(sizeof(user) / sizeof(WCHAR));
1074
+ uc.lpszPassword = pass; uc.dwPasswordLength = (DWORD)(sizeof(pass) / sizeof(WCHAR));
1075
+
1076
+ if (!WinHttpCrackUrl(url, 0, 0, &uc)) err = 1;
1077
+ xfree(url);
1078
+
1079
+ /* Free the component buffers before any rb_raise (longjmp): copy out what
1080
+ * we still need into Ruby Strings first, then xfree, then raise/return. */
1081
+ if (err) {
1082
+ xfree(host); xfree(path); xfree(extra);
1083
+ rb_raise(rb_eArgError, "winhttp: could not parse URL");
1084
+ }
1085
+
1086
+ if (uc.nScheme != INTERNET_SCHEME_HTTP && uc.nScheme != INTERNET_SCHEME_HTTPS) {
1087
+ xfree(host); xfree(path); xfree(extra);
1088
+ rb_raise(rb_eArgError, "winhttp: URL scheme must be http or https");
1089
+ }
1090
+
1091
+ /* Reject embedded credentials (user:pass@host — never supported). */
1092
+ if (uc.dwUserNameLength > 0 || uc.dwPasswordLength > 0) {
1093
+ xfree(host); xfree(path); xfree(extra);
1094
+ rb_raise(rb_eArgError, "winhttp: credentials in URL are not supported");
1095
+ }
1096
+
1097
+ secure = (uc.nScheme == INTERNET_SCHEME_HTTPS);
1098
+ host_s = wide_to_str(uc.lpszHostName, (int)uc.dwHostNameLength);
1099
+
1100
+ /* path + query reassembled (OpenRequest takes path+extra together). */
1101
+ {
1102
+ VALUE p = wide_to_str(uc.lpszUrlPath, (int)uc.dwUrlPathLength);
1103
+ VALUE e = (uc.dwExtraInfoLength > 0)
1104
+ ? wide_to_str(uc.lpszExtraInfo, (int)uc.dwExtraInfoLength)
1105
+ : rb_utf8_str_new("", 0);
1106
+ if (RSTRING_LEN(p) == 0) p = rb_utf8_str_new("/", 1);
1107
+ path_s = rb_str_plus(p, e);
1108
+ }
1109
+
1110
+ xfree(host); xfree(path); xfree(extra);
1111
+
1112
+ return rb_ary_new3(4, secure ? Qtrue : Qfalse, host_s,
1113
+ UINT2NUM(uc.nPort), path_s);
1114
+ }
1115
+
1116
+ /* Winhttp._raise_os(api, code, secure_flags) — raise the mapped subclass from
1117
+ * Ruby (the state machine routes EV_ERROR here). For TlsError, decode the
1118
+ * captured SECURE_FAILURE flag bits into the @details symbol array. */
1119
+ static VALUE
1120
+ raise_os(VALUE mod, VALUE vapi, VALUE vcode, VALUE vsecure)
1121
+ {
1122
+ DWORD code = (DWORD)NUM2ULONG(vcode);
1123
+ VALUE klass = class_for_code(code);
1124
+ const char *api = NIL_P(vapi) ? "WinHttp" : StringValueCStr(vapi);
1125
+ (void)mod;
1126
+
1127
+ if (klass == eTls && !NIL_P(vsecure)) {
1128
+ DWORD flags = (DWORD)NUM2ULONG(vsecure);
1129
+ VALUE details = rb_ary_new();
1130
+ VALUE exc, msg;
1131
+ WCHAR *buf = NULL;
1132
+ char detail[512];
1133
+ DWORD n;
1134
+ HMODULE hwh = GetModuleHandleW(L"winhttp.dll");
1135
+
1136
+ if (flags & WINHTTP_CALLBACK_STATUS_FLAG_CERT_REV_FAILED)
1137
+ rb_ary_push(details, ID2SYM(rb_intern("cert_rev_failed")));
1138
+ if (flags & WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CERT)
1139
+ rb_ary_push(details, ID2SYM(rb_intern("invalid_cert")));
1140
+ if (flags & WINHTTP_CALLBACK_STATUS_FLAG_CERT_REVOKED)
1141
+ rb_ary_push(details, ID2SYM(rb_intern("cert_revoked")));
1142
+ if (flags & WINHTTP_CALLBACK_STATUS_FLAG_INVALID_CA)
1143
+ rb_ary_push(details, ID2SYM(rb_intern("invalid_ca")));
1144
+ if (flags & WINHTTP_CALLBACK_STATUS_FLAG_CERT_CN_INVALID)
1145
+ rb_ary_push(details, ID2SYM(rb_intern("cert_cn_invalid")));
1146
+ if (flags & WINHTTP_CALLBACK_STATUS_FLAG_CERT_DATE_INVALID)
1147
+ rb_ary_push(details, ID2SYM(rb_intern("cert_date_invalid")));
1148
+ if (flags & WINHTTP_CALLBACK_STATUS_FLAG_SECURITY_CHANNEL_ERROR)
1149
+ rb_ary_push(details, ID2SYM(rb_intern("security_channel_error")));
1150
+
1151
+ detail[0] = 0;
1152
+ n = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
1153
+ FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS,
1154
+ hwh, code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
1155
+ (LPWSTR)&buf, 0, NULL);
1156
+ if (n && buf) {
1157
+ while (n && (buf[n-1] == L'\r' || buf[n-1] == L'\n' || buf[n-1] == L'.')) buf[--n] = 0;
1158
+ WideCharToMultiByte(CP_UTF8, 0, buf, -1, detail, (int)sizeof(detail), NULL, NULL);
1159
+ detail[sizeof(detail) - 1] = 0;
1160
+ }
1161
+ if (buf) LocalFree(buf);
1162
+ if (detail[0])
1163
+ msg = rb_sprintf("%s: %s (error %lu)", api, detail, (unsigned long)code);
1164
+ else
1165
+ msg = rb_sprintf("%s failed (error %lu)", api, (unsigned long)code);
1166
+ exc = rb_exc_new_str(eTls, msg);
1167
+ rb_iv_set(exc, "@code", ULONG2NUM(code));
1168
+ rb_iv_set(exc, "@details", details);
1169
+ rb_exc_raise(exc);
1170
+ }
1171
+
1172
+ raise_code(klass, api, code);
1173
+ return Qnil; /* unreachable */
1174
+ }
1175
+
1176
+ /* ----------------------------------------------------------------- Init --- */
1177
+
1178
+ void
1179
+ Init_winhttp(void)
1180
+ {
1181
+ mWinhttp = rb_define_module("Winhttp");
1182
+
1183
+ eError = rb_define_class_under(mWinhttp, "Error", rb_eStandardError);
1184
+ eOSError = rb_define_class_under(mWinhttp, "OSError", eError);
1185
+ eTimeout = rb_define_class_under(mWinhttp, "TimeoutError", eOSError);
1186
+ eResolve = rb_define_class_under(mWinhttp, "ResolveError", eOSError);
1187
+ eConnect = rb_define_class_under(mWinhttp, "ConnectError", eOSError);
1188
+ eTls = rb_define_class_under(mWinhttp, "TlsError", eOSError);
1189
+ eRedirect = rb_define_class_under(mWinhttp, "RedirectError", eOSError);
1190
+ eProtocol = rb_define_class_under(mWinhttp, "ProtocolError", eOSError);
1191
+ eCanceled = rb_define_class_under(mWinhttp, "Canceled", eOSError);
1192
+ eClosed = rb_define_class_under(mWinhttp, "Closed", eError);
1193
+
1194
+ /* Event-kind constants (consumed by the Ruby state machine). */
1195
+ rb_define_const(mWinhttp, "EV_SEND", INT2FIX(EV_SEND));
1196
+ rb_define_const(mWinhttp, "EV_HEADERS", INT2FIX(EV_HEADERS));
1197
+ rb_define_const(mWinhttp, "EV_READ", INT2FIX(EV_READ));
1198
+ rb_define_const(mWinhttp, "EV_ERROR", INT2FIX(EV_ERROR));
1199
+ rb_define_const(mWinhttp, "EV_SECURE", INT2FIX(EV_SECURE));
1200
+ rb_define_const(mWinhttp, "EV_LOST", INT2FIX(EV_LOST));
1201
+
1202
+ /* Access-type constants for Session._open. */
1203
+ rb_define_const(mWinhttp, "ACCESS_AUTOMATIC", UINT2NUM(WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY));
1204
+ rb_define_const(mWinhttp, "ACCESS_NO_PROXY", UINT2NUM(WINHTTP_ACCESS_TYPE_NO_PROXY));
1205
+ rb_define_const(mWinhttp, "ACCESS_NAMED", UINT2NUM(WINHTTP_ACCESS_TYPE_NAMED_PROXY));
1206
+
1207
+ /* Redirect-policy constants. */
1208
+ rb_define_const(mWinhttp, "REDIRECT_DISALLOW_HTTPS_TO_HTTP",
1209
+ UINT2NUM(WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP));
1210
+ rb_define_const(mWinhttp, "REDIRECT_NEVER",
1211
+ UINT2NUM(WINHTTP_OPTION_REDIRECT_POLICY_NEVER));
1212
+
1213
+ /* Module pump + introspection primitives. */
1214
+ rb_define_module_function(mWinhttp, "_pump_wait", pump_wait, 0);
1215
+ rb_define_module_function(mWinhttp, "_pump_drain", pump_drain, 0);
1216
+ rb_define_module_function(mWinhttp, "_live_count", live_count, 0);
1217
+ rb_define_module_function(mWinhttp, "_crack", url_crack, 1);
1218
+ rb_define_module_function(mWinhttp, "_raise_os", raise_os, 3);
1219
+
1220
+ /* Session */
1221
+ cSession = rb_define_class_under(mWinhttp, "Session", rb_cObject);
1222
+ rb_define_alloc_func(cSession, ses_alloc);
1223
+ rb_define_singleton_method(cSession, "_open", ses_open, -1);
1224
+ rb_define_method(cSession, "closed?", ses_closed_p, 0);
1225
+ rb_define_method(cSession, "_mark_closed", ses_mark_closed, 0);
1226
+ rb_define_method(cSession, "_try_close_handle", ses_try_close_handle, 0);
1227
+ rb_define_method(cSession, "_live", ses_live_count, 0);
1228
+ rb_define_method(cSession, "_start", req_start, 9);
1229
+
1230
+ /* Request — internal plumbing; all methods underscore-named. */
1231
+ cRequest = rb_define_class_under(mWinhttp, "Request", rb_cObject);
1232
+ rb_define_alloc_func(cRequest, req_alloc);
1233
+ rb_define_method(cRequest, "_send", req_send, 0);
1234
+ rb_define_method(cRequest, "_receive", req_receive, 0);
1235
+ rb_define_method(cRequest, "_read_start", req_read_start, 0);
1236
+ rb_define_method(cRequest, "_read_take", req_read_take, 1);
1237
+ rb_define_method(cRequest, "_headers", req_headers, 0);
1238
+ rb_define_method(cRequest, "_abort", req_abort, 0);
1239
+ rb_define_method(cRequest, "_finish", req_abort, 0); /* same idempotent close */
1240
+
1241
+ /* Process-lifetime globals (NEVER destroyed; immortal-handle precedent). */
1242
+ InitializeCriticalSection(&g_cs);
1243
+ g_event = CreateEventW(NULL, FALSE, FALSE, NULL); /* auto-reset, unnamed */
1244
+ g_head = g_tail = NULL;
1245
+ g_dropped = 0;
1246
+ g_reqs = st_init_numtable();
1247
+ }