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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Winhttp
4
+ VERSION = "0.1.0"
5
+ end
data/lib/winhttp.rb ADDED
@@ -0,0 +1,562 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "winhttp/version"
4
+ require "winhttp/winhttp" # native ext: Session/Request classes, errors, EV_*/ACCESS_*/REDIRECT_* constants
5
+ require "monitor"
6
+
7
+ # winhttp — HTTP for Ruby on the stack Windows already ships: WinHTTP in async
8
+ # mode (system TLS via Schannel, the OS certificate store, the user's
9
+ # proxy/PAC settings, HTTP/2, and transparent gzip), behind a thin client that
10
+ # parks fibers under winloop and blocks plainly without it.
11
+ #
12
+ # require "winhttp"
13
+ #
14
+ # r = Winhttp.get("https://example.com/")
15
+ # r.status # => 200
16
+ # r.headers["content-type"] # => "text/html; charset=UTF-8"
17
+ # r.text[0, 15] # => "<!doctype html>"
18
+ #
19
+ # A Session pools connections; reuse one for many requests. The same code path
20
+ # blocks a plain thread and parks a fiber under a Fiber::Scheduler (winloop) —
21
+ # there are no scheduler branches anywhere in winhttp.
22
+ module Winhttp
23
+ # ---- HTTP token grammar (RFC 7230) for method + header names --------------
24
+ TOKEN_RE = /\A[!#$%&'*+\-.^_`|~0-9A-Za-z]+\z/.freeze
25
+
26
+ # Header names WinHTTP manages itself; supplying them is an error.
27
+ MANAGED_HEADERS = %w[host content-length connection].freeze
28
+
29
+ # Revocation policy symbol <-> the C integer the session probe understands.
30
+ REVOCATION_INT = { none: 0, best_effort: 1, strict: 2 }.freeze
31
+ REVOCATION_SYM = { 0 => :none, 1 => :best_effort, 2 => :strict }.freeze
32
+
33
+ # A WinHTTP/Win32 failure carries the originating error code (Integer, or nil
34
+ # for a Ruby-side `timeout:` deadline), set on the exception in C / by the
35
+ # state machine.
36
+ class OSError
37
+ def code
38
+ @code
39
+ end
40
+ end
41
+
42
+ # Certificate/TLS failures decode the SECURE_FAILURE flag bits captured in the
43
+ # status callback immediately before the failing REQUEST_ERROR.
44
+ class TlsError
45
+ # Array[Symbol] (possibly empty): :cert_rev_failed, :invalid_cert,
46
+ # :cert_revoked, :invalid_ca, :cert_cn_invalid, :cert_date_invalid,
47
+ # :security_channel_error.
48
+ def details
49
+ @details || []
50
+ end
51
+ end
52
+
53
+ # A plain, immutable HTTP response. Built in Ruby from the values the state
54
+ # machine queried out of WinHTTP.
55
+ class Response
56
+ attr_reader :status, :reason, :headers, :raw_headers, :body, :final_url
57
+
58
+ # status: Integer; raw: the WINHTTP_QUERY_RAW_HEADERS_CRLF blob (status line
59
+ # + header lines, CRLF-separated); final_url: String; http2: bool;
60
+ # body: String (ASCII-8BIT) or nil when streamed.
61
+ def initialize(status, raw, final_url, http2, body)
62
+ @status = status
63
+ @http2 = http2
64
+ @final_url = final_url
65
+ @body = body
66
+ reason, pairs = Winhttp.send(:parse_raw_headers, raw)
67
+ @reason = reason
68
+ @raw_headers = pairs.map { |k, v| [k.dup.freeze, v.dup.freeze].freeze }.freeze
69
+ merged = {}
70
+ pairs.each do |k, v|
71
+ lk = k.downcase
72
+ merged[lk] = merged.key?(lk) ? "#{merged[lk]}, #{v}" : v
73
+ end
74
+ merged.each_value(&:freeze)
75
+ @headers = merged.freeze
76
+ freeze
77
+ end
78
+
79
+ # Case-insensitive single-header lookup (merged value), or nil.
80
+ def [](name)
81
+ @headers[name.to_s.downcase]
82
+ end
83
+
84
+ def http2?
85
+ @http2
86
+ end
87
+
88
+ def success?
89
+ (200..299).cover?(@status)
90
+ end
91
+
92
+ # A copy of the body tagged with `encoding` (else charset= from
93
+ # Content-Type, else UTF-8). Tags only (force_encoding) — never transcodes;
94
+ # unknown charset names fall back to UTF-8. Raises if the body was streamed.
95
+ def text(encoding = nil)
96
+ raise Error, "winhttp: body was streamed (use the chunk block)" if @body.nil?
97
+
98
+ enc = encoding || charset_encoding || Encoding::UTF_8
99
+ @body.dup.force_encoding(enc)
100
+ rescue ArgumentError
101
+ @body.dup.force_encoding(Encoding::UTF_8)
102
+ end
103
+
104
+ private
105
+
106
+ def charset_encoding
107
+ ct = @headers["content-type"]
108
+ return nil unless ct && ct =~ /charset=\s*"?([\w\-]+)"?/i
109
+
110
+ Encoding.find(Regexp.last_match(1))
111
+ rescue ArgumentError
112
+ nil
113
+ end
114
+ end
115
+
116
+ class << self
117
+ # The lazily-created, process-lifetime default Session (all-default options).
118
+ # Creation is mutex-guarded. It is never closed; the OS reclaims it at exit.
119
+ def default_session
120
+ @default_session || @default_mutex.synchronize { @default_session ||= Session.new }
121
+ end
122
+
123
+ # Thin delegations to the default session (signatures/defaults identical to
124
+ # the Session instance methods).
125
+ def get(url, headers: nil, timeout: nil, &chunk)
126
+ default_session.get(url, headers: headers, timeout: timeout, &chunk)
127
+ end
128
+
129
+ def post(url, body: "", headers: nil, timeout: nil, &chunk)
130
+ default_session.post(url, body: body, headers: headers, timeout: timeout, &chunk)
131
+ end
132
+
133
+ def request(method, url, body: nil, headers: nil, timeout: nil, &chunk)
134
+ default_session.request(method, url, body: body, headers: headers, timeout: timeout, &chunk)
135
+ end
136
+
137
+ private
138
+
139
+ # ---- pump thread (one per process) -------------------------------------
140
+
141
+ def ensure_pump!
142
+ @pump_mutex.synchronize do
143
+ return if @pump&.alive?
144
+
145
+ @pump = Thread.new { pump_loop }
146
+ @pump.report_on_exception = false
147
+ end
148
+ end
149
+
150
+ def pump_loop
151
+ loop do
152
+ Winhttp._pump_wait
153
+ Winhttp._pump_drain.each do |id, kind, a, b|
154
+ if kind == EV_LOST
155
+ # callback-side OOM: fan out to every registered mailbox
156
+ @mb_lock.synchronize { @mailboxes.values }.each { |q| q.push([EV_LOST, 0, 0]) }
157
+ else
158
+ q = @mb_lock.synchronize { @mailboxes[id] }
159
+ q&.push([kind, a, b])
160
+ end
161
+ end
162
+ rescue StandardError => e
163
+ warn "winhttp: pump error: #{e.class}: #{e.message}" if $VERBOSE
164
+ end
165
+ end
166
+
167
+ def register_mailbox(id, queue)
168
+ ensure_pump!
169
+ @mb_lock.synchronize { @mailboxes[id] = queue }
170
+ end
171
+
172
+ def unregister_mailbox(id)
173
+ @mb_lock.synchronize { @mailboxes.delete(id) }
174
+ end
175
+
176
+ # ---- shared validation / assembly --------------------------------------
177
+
178
+ MONO = Process::CLOCK_MONOTONIC
179
+
180
+ def monotonic
181
+ Process.clock_gettime(MONO)
182
+ end
183
+
184
+ # Validate a positive Numeric timeout (seconds) -> Float, or raise.
185
+ def check_timeout(timeout)
186
+ return nil if timeout.nil?
187
+
188
+ t = Float(timeout)
189
+ raise ArgumentError, "winhttp: timeout must be positive, got #{timeout.inspect}" unless t.positive?
190
+
191
+ t
192
+ end
193
+
194
+ # Upcase + token-validate an HTTP method.
195
+ def normalize_method(method)
196
+ m = method.to_s.upcase
197
+ raise ArgumentError, "winhttp: invalid HTTP method #{method.inspect}" unless m.match?(TOKEN_RE)
198
+
199
+ m
200
+ end
201
+
202
+ # Assemble a CRLF-joined header blob (or nil) with injection + token guards.
203
+ def build_header_blob(headers)
204
+ return nil if headers.nil?
205
+ raise TypeError, "winhttp: headers must be a Hash or nil" unless headers.is_a?(Hash)
206
+
207
+ lines = []
208
+ headers.each do |name, value|
209
+ n = name.to_s
210
+ raise ArgumentError, "winhttp: invalid header name #{name.inspect}" unless n.match?(TOKEN_RE)
211
+ if MANAGED_HEADERS.include?(n.downcase)
212
+ raise ArgumentError, "winhttp: #{n} is managed by WinHTTP and cannot be set"
213
+ end
214
+
215
+ Array(value).each do |v|
216
+ sv = v.is_a?(Integer) ? v.to_s : v.to_s
217
+ check_header_value!(n, sv)
218
+ lines << "#{n}: #{sv}"
219
+ end
220
+ end
221
+ return nil if lines.empty?
222
+
223
+ lines.join("\r\n")
224
+ end
225
+
226
+ # Header values must be printable ASCII + TAB (header-injection guard: any
227
+ # CR/LF, or a byte outside 0x20..0x7E plus TAB, raises).
228
+ def check_header_value!(name, value)
229
+ value.each_byte do |b|
230
+ next if b == 0x09 || (b >= 0x20 && b <= 0x7E)
231
+
232
+ raise ArgumentError, "winhttp: illegal byte in header #{name.inspect} value"
233
+ end
234
+ end
235
+
236
+ # Validate a request body -> String (raw bytes) <= 4 GiB - 1, or nil.
237
+ def check_body(body)
238
+ return nil if body.nil?
239
+
240
+ s = String.try_convert(body)
241
+ raise TypeError, "winhttp: body must be a String" unless s
242
+ if s.bytesize > 0xFFFF_FFFE
243
+ raise ArgumentError, "winhttp: body too large (> 4 GiB - 1)"
244
+ end
245
+
246
+ s
247
+ end
248
+
249
+ # Split the RAW_HEADERS_CRLF blob into [reason, [[name, value], ...]].
250
+ # First line is the status line ("HTTP/1.1 200 OK"); the rest are headers.
251
+ def parse_raw_headers(raw)
252
+ lines = raw.to_s.split("\r\n")
253
+ status_line = lines.shift || ""
254
+ reason = ""
255
+ if status_line =~ %r{\AHTTP/\S+\s+\d+\s+(.*)\z}
256
+ reason = Regexp.last_match(1)
257
+ elsif status_line =~ %r{\AHTTP/\S+\s+\d+\s*\z}
258
+ reason = ""
259
+ end
260
+ pairs = []
261
+ lines.each do |line|
262
+ next if line.empty?
263
+
264
+ k, v = line.split(":", 2)
265
+ next if k.nil?
266
+
267
+ pairs << [k.strip, (v || "").strip]
268
+ end
269
+ [reason, pairs]
270
+ end
271
+ end
272
+
273
+ # One async WinHTTP session handle plus its configuration. Thread-safe and
274
+ # fiber-safe: any number of threads/fibers may issue requests concurrently.
275
+ class Session
276
+ # The effective revocation policy, settled at construction.
277
+ attr_reader :revocation
278
+
279
+ # Open a Session. See the gem README for the full option contract.
280
+ def self.new(user_agent: "winhttp-ruby/#{Winhttp::VERSION}",
281
+ proxy: :system, proxy_bypass: nil, http2: true, decompress: true,
282
+ revocation: :best_effort, redirects: 10,
283
+ connect_timeout: nil, send_timeout: nil, receive_timeout: nil)
284
+ access, proxy_str = resolve_proxy(proxy, proxy_bypass)
285
+ rev_int = REVOCATION_INT[revocation]
286
+ raise ArgumentError, "winhttp: revocation must be :best_effort/:strict/:none" unless rev_int
287
+
288
+ unless redirects.is_a?(Integer) && (0..65_535).cover?(redirects)
289
+ raise ArgumentError, "winhttp: redirects must be an Integer in 0..65535"
290
+ end
291
+
292
+ redirect_policy = redirects.zero? ? REDIRECT_NEVER : REDIRECT_DISALLOW_HTTPS_TO_HTTP
293
+
294
+ ct = ms_or_unset(connect_timeout, "connect_timeout")
295
+ st = ms_or_unset(send_timeout, "send_timeout")
296
+ rt = ms_or_unset(receive_timeout, "receive_timeout")
297
+
298
+ session, eff = _open(user_agent.to_s, access, proxy_str, proxy_bypass,
299
+ ct, st, rt, http2 ? true : false, decompress ? true : false,
300
+ redirect_policy, redirects, rev_int)
301
+ session.send(:finish_init, REVOCATION_SYM[eff], rev_int)
302
+ session
303
+ end
304
+
305
+ # Resolve a proxy: kwarg -> [access_type, named_proxy_string_or_nil].
306
+ def self.resolve_proxy(proxy, proxy_bypass)
307
+ case proxy
308
+ when :system
309
+ raise ArgumentError, "winhttp: proxy_bypass requires a String proxy" if proxy_bypass
310
+ [ACCESS_AUTOMATIC, nil]
311
+ when :none
312
+ raise ArgumentError, "winhttp: proxy_bypass requires a String proxy" if proxy_bypass
313
+ [ACCESS_NO_PROXY, nil]
314
+ when String
315
+ validate_proxy_string!(proxy)
316
+ validate_proxy_string!(proxy_bypass) if proxy_bypass
317
+ [ACCESS_NAMED, proxy]
318
+ else
319
+ raise ArgumentError, "winhttp: proxy must be :system, :none, or a String"
320
+ end
321
+ end
322
+ private_class_method :resolve_proxy
323
+
324
+ # Proxy strings must not carry whitespace/CR/LF or userinfo (credentials).
325
+ def self.validate_proxy_string!(str)
326
+ if str =~ /[\s]/ || str.include?("\r") || str.include?("\n")
327
+ raise ArgumentError, "winhttp: proxy string must not contain whitespace or CR/LF"
328
+ end
329
+ if str.include?("@")
330
+ raise ArgumentError, "winhttp: proxy string must not contain credentials (user:pass@)"
331
+ end
332
+ end
333
+ private_class_method :validate_proxy_string!
334
+
335
+ # nil -> -1 (leave WinHTTP default); positive Numeric seconds -> ms Integer.
336
+ def self.ms_or_unset(timeout, name)
337
+ return -1 if timeout.nil?
338
+
339
+ t = Float(timeout)
340
+ raise ArgumentError, "winhttp: #{name} must be non-negative, got #{timeout.inspect}" if t.negative?
341
+
342
+ ms = (t * 1000).round
343
+ ms.zero? && t.positive? ? 1 : ms
344
+ end
345
+ private_class_method :ms_or_unset
346
+
347
+ # Open a session and ensure-close it around the block (block form).
348
+ def self.open(**opts)
349
+ s = new(**opts)
350
+ return s unless block_given?
351
+
352
+ begin
353
+ yield s
354
+ ensure
355
+ s.close
356
+ end
357
+ end
358
+
359
+ def get(url, headers: nil, timeout: nil, &chunk)
360
+ perform("GET", url, nil, headers, timeout, &chunk)
361
+ end
362
+
363
+ def head(url, headers: nil, timeout: nil)
364
+ perform("HEAD", url, nil, headers, timeout)
365
+ end
366
+
367
+ def post(url, body: "", headers: nil, timeout: nil, &chunk)
368
+ perform("POST", url, body, headers, timeout, &chunk)
369
+ end
370
+
371
+ def put(url, body:, headers: nil, timeout: nil, &chunk)
372
+ perform("PUT", url, body, headers, timeout, &chunk)
373
+ end
374
+
375
+ def patch(url, body:, headers: nil, timeout: nil, &chunk)
376
+ perform("PATCH", url, body, headers, timeout, &chunk)
377
+ end
378
+
379
+ def delete(url, headers: nil, timeout: nil, &chunk)
380
+ perform("DELETE", url, nil, headers, timeout, &chunk)
381
+ end
382
+
383
+ def request(method, url, body: nil, headers: nil, timeout: nil, &chunk)
384
+ perform(Winhttp.send(:normalize_method, method), url, body, headers, timeout, &chunk)
385
+ end
386
+
387
+ # Close the session: mark closed FIRST (new requests raise Closed), abort
388
+ # every in-flight request (those raise Canceled), wait — bounded, <= 5 s —
389
+ # for the OS to finish teardown, then close the session handle. Idempotent.
390
+ def close
391
+ @lock.synchronize do
392
+ return nil if @closed_flag
393
+
394
+ @closed_flag = true
395
+ _mark_closed
396
+ inflight = @inflight.values
397
+ end
398
+ abort_inflight
399
+ bounded_close_handle
400
+ nil
401
+ end
402
+
403
+ private
404
+
405
+ # Called by Session.new after the C handle is created.
406
+ def finish_init(effective_revocation, _rev_int)
407
+ @revocation = effective_revocation
408
+ @lock = Monitor.new
409
+ @inflight = {}
410
+ @closed_flag = false
411
+ self
412
+ end
413
+
414
+ def abort_inflight
415
+ reqs = @lock.synchronize { @inflight.values.dup }
416
+ reqs.each do |entry|
417
+ mutex, req = entry
418
+ mutex.synchronize { req._abort }
419
+ end
420
+ end
421
+
422
+ def bounded_close_handle
423
+ deadline = Winhttp.send(:monotonic) + 5.0
424
+ loop do
425
+ return if _try_close_handle
426
+
427
+ if Winhttp.send(:monotonic) >= deadline
428
+ warn "winhttp: session handle leaked (teardown did not settle in 5s)" if $VERBOSE
429
+ return
430
+ end
431
+ sleep 0.01
432
+ end
433
+ end
434
+
435
+ # The per-request state machine. Strictly sequential, so exactly one
436
+ # outstanding WinHTTP operation per request handle by construction.
437
+ def perform(verb, url, body, headers, timeout, &chunk)
438
+ raise Closed, "winhttp: session is closed" if @closed_flag
439
+
440
+ secure, host, port, path = Winhttp._crack(url)
441
+ blob = Winhttp.send(:build_header_blob, headers)
442
+ bytes = Winhttp.send(:check_body, body)
443
+ seconds = Winhttp.send(:check_timeout, timeout)
444
+ deadline = seconds && (Winhttp.send(:monotonic) + seconds)
445
+
446
+ req = Winhttp::Request.allocate
447
+ mutex = Thread::Mutex.new
448
+ mb = Thread::Queue.new
449
+ id = nil
450
+ secure_flags = []
451
+
452
+ begin
453
+ id_num = _start(req, verb, host, port, secure, path, blob, bytes,
454
+ Winhttp::REVOCATION_INT.fetch(@revocation, 0))
455
+ id = id_num
456
+ Winhttp.send(:register_mailbox, id, mb)
457
+ @lock.synchronize do
458
+ raise Closed, "winhttp: session is closed" if @closed_flag
459
+
460
+ @inflight[id] = [mutex, req]
461
+ end
462
+
463
+ mutex.synchronize { req._send }
464
+ wait!(mb, deadline, mutex, req, EV_SEND, secure_flags)
465
+ mutex.synchronize { req._receive }
466
+ wait!(mb, deadline, mutex, req, EV_HEADERS, secure_flags)
467
+ status, raw, final_url, proto = mutex.synchronize { req._headers }
468
+
469
+ chunks = chunk ? nil : []
470
+ loop do
471
+ mutex.synchronize { req._read_start }
472
+ n = wait!(mb, deadline, mutex, req, EV_READ, secure_flags)
473
+ break if n.zero?
474
+
475
+ data = mutex.synchronize { req._read_take(n) }
476
+ chunk ? chunk.call(data) : (chunks << data)
477
+ end
478
+ mutex.synchronize { req._finish }
479
+
480
+ Response.new(status, raw, final_url, proto,
481
+ chunks && chunks.join.force_encoding(Encoding::BINARY))
482
+ ensure
483
+ mutex.synchronize { req._abort } rescue nil
484
+ Winhttp.send(:unregister_mailbox, id) if id
485
+ @lock.synchronize { @inflight.delete(id) } if id
486
+ end
487
+ end
488
+
489
+ # Pop the mailbox until the expected kind arrives. Honors the wall-clock
490
+ # deadline, routes EV_ERROR/EV_SECURE/EV_LOST, returns the payload.
491
+ def wait!(mailbox, deadline, mutex, req, expected, secure_flags)
492
+ loop do
493
+ msg =
494
+ if deadline
495
+ remaining = deadline - Winhttp.send(:monotonic)
496
+ if remaining <= 0
497
+ mutex.synchronize { req._abort }
498
+ raise_timeout
499
+ end
500
+ mailbox.pop(timeout: remaining)
501
+ else
502
+ mailbox.pop
503
+ end
504
+
505
+ if msg.nil?
506
+ mutex.synchronize { req._abort }
507
+ raise_timeout
508
+ end
509
+
510
+ kind, a, b = msg
511
+ case kind
512
+ when expected
513
+ return a
514
+ when EV_SECURE
515
+ secure_flags << a
516
+ next
517
+ when EV_ERROR
518
+ mutex.synchronize { req._abort }
519
+ Winhttp._raise_os(api_name_for(a), b, secure_flags.last)
520
+ when EV_LOST
521
+ mutex.synchronize { req._abort }
522
+ raise Error, "winhttp: completion notification lost (out of memory)"
523
+ else
524
+ warn "winhttp: unexpected event #{kind} (expected #{expected})" if $VERBOSE
525
+ next
526
+ end
527
+ end
528
+ end
529
+
530
+ def raise_timeout
531
+ exc = TimeoutError.new("winhttp: request exceeded the timeout deadline")
532
+ exc.instance_variable_set(:@code, nil)
533
+ raise exc
534
+ end
535
+
536
+ # Map WINHTTP_ASYNC_RESULT.dwResult (which API failed) to a label.
537
+ def api_name_for(which)
538
+ {
539
+ 1 => "WinHttpReceiveResponse",
540
+ 2 => "WinHttpQueryDataAvailable",
541
+ 3 => "WinHttpReadData",
542
+ 4 => "WinHttpWriteData",
543
+ 5 => "WinHttpSendRequest"
544
+ }.fetch(which, "WinHttp")
545
+ end
546
+ end
547
+
548
+ # Eagerly initialize the cross-thread routing state at module load — under the
549
+ # GVL, single-threaded, before any request thread can race. Lazy `x ||= alloc`
550
+ # is NOT atomic in MRI (the allocation between ivar-read and ivar-write is a
551
+ # thread-switch/GC point), so two threads' first concurrent use of a shared or
552
+ # the default Session could each install a DIFFERENT @mailboxes Hash / @mb_lock
553
+ # Monitor: register_mailbox writes one, the pump reads the other, the lookup
554
+ # misses, the completion is dropped, and the request hangs to its deadline.
555
+ # Initializing here removes the check-then-act window. (@pump itself stays lazy,
556
+ # guarded by the now-always-present @pump_mutex.) These ivars live on the
557
+ # Winhttp module object, which is `self` for the `class << self` methods above.
558
+ @pump_mutex = Mutex.new
559
+ @mb_lock = Monitor.new
560
+ @mailboxes = {}
561
+ @default_mutex = Mutex.new
562
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: winhttp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - ned
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake-compiler
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.2'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.2'
40
+ - !ruby/object:Gem::Dependency
41
+ name: minitest
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '5.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '5.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: vcvars
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.1'
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: 0.1.1
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: '0.1'
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: 0.1.1
74
+ description: |
75
+ winhttp is a native extension that binds the asynchronous WinHTTP API into a
76
+ thin, hard-to-misuse Ruby HTTP client: system TLS via Schannel with the OS
77
+ certificate store and revocation policy, the user's proxy and PAC settings,
78
+ HTTP/2 negotiation, transparent gzip/deflate, safe redirect defaults, and
79
+ streaming downloads. Requests park the calling fiber under a Fiber scheduler
80
+ (e.g. winloop) and block plainly without one — one code path. Windows MSVC
81
+ (mswin) Ruby only.
82
+ executables: []
83
+ extensions:
84
+ - ext/winhttp/extconf.rb
85
+ extra_rdoc_files: []
86
+ files:
87
+ - CHANGELOG.md
88
+ - LICENSE.txt
89
+ - README.md
90
+ - ext/winhttp/extconf.rb
91
+ - ext/winhttp/winhttp.c
92
+ - lib/winhttp.rb
93
+ - lib/winhttp/version.rb
94
+ homepage: https://github.com/main-path/winhttp
95
+ licenses:
96
+ - MIT
97
+ metadata:
98
+ homepage_uri: https://github.com/main-path/winhttp
99
+ source_code_uri: https://github.com/main-path/winhttp
100
+ changelog_uri: https://github.com/main-path/winhttp/blob/main/CHANGELOG.md
101
+ bug_tracker_uri: https://github.com/main-path/winhttp/issues
102
+ rubygems_mfa_required: 'true'
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '3.2'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubygems_version: 3.6.9
118
+ specification_version: 4
119
+ summary: 'HTTP client for Ruby on the Windows OS stack: WinHTTP with system TLS, cert
120
+ store, and proxy.'
121
+ test_files: []