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
|
@@ -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
|
+
}
|