ld-eventsource 1.0.2 → 2.0.1
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 +5 -5
- data/.circleci/config.yml +28 -81
- data/.gitignore +2 -0
- data/.ldrelease/circleci/linux/execute.sh +18 -0
- data/.ldrelease/circleci/mac/execute.sh +18 -0
- data/.ldrelease/circleci/template/build.sh +19 -0
- data/.ldrelease/circleci/template/gems-setup.sh +16 -0
- data/.ldrelease/circleci/template/prepare.sh +17 -0
- data/.ldrelease/circleci/template/publish.sh +19 -0
- data/.ldrelease/circleci/template/test.sh +10 -0
- data/.ldrelease/circleci/template/update-version.sh +8 -0
- data/.ldrelease/circleci/windows/execute.ps1 +19 -0
- data/.ldrelease/config.yml +13 -0
- data/CHANGELOG.md +24 -0
- data/README.md +2 -2
- data/ld-eventsource.gemspec +3 -3
- data/lib/ld-eventsource/client.rb +86 -21
- data/lib/ld-eventsource/impl/backoff.rb +0 -4
- data/lib/ld-eventsource/version.rb +1 -1
- data/spec/backoff_spec.rb +52 -0
- data/spec/client_spec.rb +68 -8
- data/spec/http_stub.rb +2 -0
- metadata +36 -24
- data/Gemfile.lock +0 -46
- data/lib/ld-eventsource/impl/streaming_http.rb +0 -222
- data/spec/streaming_http_spec.rb +0 -263
@@ -38,10 +38,6 @@ module SSE
|
|
38
38
|
good_duration = Time.now.to_f - @last_good_time
|
39
39
|
@attempts = 0 if good_duration >= @reconnect_reset_interval
|
40
40
|
end
|
41
|
-
if @attempts == 0
|
42
|
-
@attempts += 1
|
43
|
-
return 0
|
44
|
-
end
|
45
41
|
@last_good_time = nil
|
46
42
|
target = ([@base_interval * (2 ** @attempts), @max_interval].min).to_f
|
47
43
|
@attempts += 1
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "ld-eventsource"
|
2
|
+
|
3
|
+
require "http_stub"
|
4
|
+
|
5
|
+
module SSE
|
6
|
+
module Impl
|
7
|
+
describe Backoff do
|
8
|
+
it "increases exponentially with jitter" do
|
9
|
+
initial = 1.5
|
10
|
+
max = 60
|
11
|
+
b = Backoff.new(initial, max)
|
12
|
+
previous = 0
|
13
|
+
|
14
|
+
for i in 1..6 do
|
15
|
+
interval = b.next_interval
|
16
|
+
expect(interval).to be > previous
|
17
|
+
target = initial * (2 ** (i - 1))
|
18
|
+
expect(interval).to be <= target
|
19
|
+
expect(interval).to be >= target / 2
|
20
|
+
previous = i
|
21
|
+
end
|
22
|
+
|
23
|
+
interval = b.next_interval
|
24
|
+
expect(interval).to be >= previous
|
25
|
+
expect(interval).to be <= max
|
26
|
+
end
|
27
|
+
|
28
|
+
it "resets to initial delay if reset threshold has elapsed" do
|
29
|
+
initial = 1.5
|
30
|
+
max = 60
|
31
|
+
threshold = 2
|
32
|
+
b = Backoff.new(initial, max, reconnect_reset_interval: threshold)
|
33
|
+
|
34
|
+
for i in 1..6 do
|
35
|
+
# just cause the backoff to increase quickly, don't actually do these delays
|
36
|
+
b.next_interval
|
37
|
+
end
|
38
|
+
|
39
|
+
b.mark_success
|
40
|
+
sleep(threshold + 0.001)
|
41
|
+
|
42
|
+
interval = b.next_interval
|
43
|
+
expect(interval).to be <= initial
|
44
|
+
expect(interval).to be >= initial / 2
|
45
|
+
|
46
|
+
interval = b.next_interval # make sure it continues increasing after that
|
47
|
+
expect(interval).to be <= (initial * 2)
|
48
|
+
expect(interval).to be >= initial
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/spec/client_spec.rb
CHANGED
@@ -1,6 +1,4 @@
|
|
1
1
|
require "ld-eventsource"
|
2
|
-
require "socketry"
|
3
|
-
require "http_stub"
|
4
2
|
|
5
3
|
#
|
6
4
|
# End-to-end tests of the SSE client against a real server
|
@@ -62,8 +60,11 @@ EOT
|
|
62
60
|
expect(received_req.header).to eq({
|
63
61
|
"accept" => ["text/event-stream"],
|
64
62
|
"cache-control" => ["no-cache"],
|
65
|
-
"host" => ["127.0.0.1"],
|
66
|
-
"authorization" => ["secret"]
|
63
|
+
"host" => ["127.0.0.1:" + server.port.to_s],
|
64
|
+
"authorization" => ["secret"],
|
65
|
+
"user-agent" => ["ruby-eventsource"],
|
66
|
+
"content-length" => ["0"],
|
67
|
+
"connection" => ["close"]
|
67
68
|
})
|
68
69
|
end
|
69
70
|
end
|
@@ -85,9 +86,12 @@ EOT
|
|
85
86
|
expect(received_req.header).to eq({
|
86
87
|
"accept" => ["text/event-stream"],
|
87
88
|
"cache-control" => ["no-cache"],
|
88
|
-
"host" => ["127.0.0.1"],
|
89
|
+
"host" => ["127.0.0.1:" + server.port.to_s],
|
89
90
|
"authorization" => ["secret"],
|
90
|
-
"last-event-id" => [id]
|
91
|
+
"last-event-id" => [id],
|
92
|
+
"user-agent" => ["ruby-eventsource"],
|
93
|
+
"content-length" => ["0"],
|
94
|
+
"connection" => ["close"]
|
91
95
|
})
|
92
96
|
end
|
93
97
|
end
|
@@ -330,7 +334,7 @@ EOT
|
|
330
334
|
expect(event_sink.pop).to eq(simple_event_1)
|
331
335
|
if i > 0
|
332
336
|
interval = request_times[i] - request_end_times[i - 1]
|
333
|
-
expect(interval).to be <= initial_interval
|
337
|
+
expect(interval).to be <= (initial_interval + 0.1)
|
334
338
|
end
|
335
339
|
end
|
336
340
|
end
|
@@ -362,7 +366,63 @@ EOT
|
|
362
366
|
with_client(client) do |client|
|
363
367
|
expect(event_sink.pop).to eq(simple_event_1)
|
364
368
|
interval = request_times[1] - request_times[0]
|
365
|
-
expect(interval).to be <
|
369
|
+
expect(interval).to be < 0.5
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
it "connects to HTTP server through proxy" do
|
375
|
+
events_body = simple_event_1_text
|
376
|
+
with_server do |server|
|
377
|
+
server.setup_response("/") do |req,res|
|
378
|
+
send_stream_content(res, events_body, keep_open: false)
|
379
|
+
end
|
380
|
+
with_server(StubProxyServer.new) do |proxy|
|
381
|
+
event_sink = Queue.new
|
382
|
+
client = subject.new(server.base_uri, proxy: proxy.base_uri) do |c|
|
383
|
+
c.on_event { |event| event_sink << event }
|
384
|
+
end
|
385
|
+
|
386
|
+
with_client(client) do |client|
|
387
|
+
expect(event_sink.pop).to eq(simple_event_1)
|
388
|
+
expect(proxy.request_count).to eq(1)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
it "resets read timeout between events" do
|
395
|
+
event_body = simple_event_1_text
|
396
|
+
with_server do |server|
|
397
|
+
attempt = 0
|
398
|
+
server.setup_response("/") do |req,res|
|
399
|
+
attempt += 1
|
400
|
+
if attempt == 1
|
401
|
+
stream = send_stream_content(res, event_body, keep_open: true)
|
402
|
+
Thread.new do
|
403
|
+
2.times {
|
404
|
+
# write within timeout interval
|
405
|
+
sleep(0.75)
|
406
|
+
stream.write(event_body)
|
407
|
+
}
|
408
|
+
# cause timeout
|
409
|
+
sleep(1.25)
|
410
|
+
end
|
411
|
+
elsif attempt == 2
|
412
|
+
send_stream_content(res, event_body, keep_open: false)
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
event_sink = Queue.new
|
417
|
+
client = subject.new(server.base_uri, reconnect_time: reconnect_asap, read_timeout: 1) do |c|
|
418
|
+
c.on_event { |event| event_sink << event }
|
419
|
+
end
|
420
|
+
|
421
|
+
with_client(client) do |client|
|
422
|
+
4.times {
|
423
|
+
expect(event_sink.pop).to eq(simple_event_1)
|
424
|
+
}
|
425
|
+
expect(attempt).to eq 2
|
366
426
|
end
|
367
427
|
end
|
368
428
|
end
|
data/spec/http_stub.rb
CHANGED
metadata
CHANGED
@@ -1,29 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ld-eventsource
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- LaunchDarkly
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-08-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 2.2.10
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 2.2.10
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rspec
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -53,47 +53,53 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: 0.3.0
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: webrick
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '1.
|
62
|
-
type: :
|
61
|
+
version: '1.7'
|
62
|
+
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '1.
|
68
|
+
version: '1.7'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: concurrent-ruby
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 0
|
75
|
+
version: '1.0'
|
76
76
|
type: :runtime
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 0
|
82
|
+
version: '1.0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: http
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 4.4.1
|
90
|
+
- - "<"
|
88
91
|
- !ruby/object:Gem::Version
|
89
|
-
version: 0.
|
92
|
+
version: 6.0.0
|
90
93
|
type: :runtime
|
91
94
|
prerelease: false
|
92
95
|
version_requirements: !ruby/object:Gem::Requirement
|
93
96
|
requirements:
|
94
|
-
- - "
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: 4.4.1
|
100
|
+
- - "<"
|
95
101
|
- !ruby/object:Gem::Version
|
96
|
-
version: 0.
|
102
|
+
version: 6.0.0
|
97
103
|
description: LaunchDarkly SSE client for Ruby
|
98
104
|
email:
|
99
105
|
- team@launchdarkly.com
|
@@ -103,10 +109,18 @@ extra_rdoc_files: []
|
|
103
109
|
files:
|
104
110
|
- ".circleci/config.yml"
|
105
111
|
- ".gitignore"
|
112
|
+
- ".ldrelease/circleci/linux/execute.sh"
|
113
|
+
- ".ldrelease/circleci/mac/execute.sh"
|
114
|
+
- ".ldrelease/circleci/template/build.sh"
|
115
|
+
- ".ldrelease/circleci/template/gems-setup.sh"
|
116
|
+
- ".ldrelease/circleci/template/prepare.sh"
|
117
|
+
- ".ldrelease/circleci/template/publish.sh"
|
118
|
+
- ".ldrelease/circleci/template/test.sh"
|
119
|
+
- ".ldrelease/circleci/template/update-version.sh"
|
120
|
+
- ".ldrelease/circleci/windows/execute.ps1"
|
106
121
|
- ".ldrelease/config.yml"
|
107
122
|
- CHANGELOG.md
|
108
123
|
- Gemfile
|
109
|
-
- Gemfile.lock
|
110
124
|
- LICENSE
|
111
125
|
- README.md
|
112
126
|
- ld-eventsource.gemspec
|
@@ -116,14 +130,13 @@ files:
|
|
116
130
|
- lib/ld-eventsource/events.rb
|
117
131
|
- lib/ld-eventsource/impl/backoff.rb
|
118
132
|
- lib/ld-eventsource/impl/event_parser.rb
|
119
|
-
- lib/ld-eventsource/impl/streaming_http.rb
|
120
133
|
- lib/ld-eventsource/version.rb
|
121
134
|
- scripts/gendocs.sh
|
122
135
|
- scripts/release.sh
|
136
|
+
- spec/backoff_spec.rb
|
123
137
|
- spec/client_spec.rb
|
124
138
|
- spec/event_parser_spec.rb
|
125
139
|
- spec/http_stub.rb
|
126
|
-
- spec/streaming_http_spec.rb
|
127
140
|
homepage: https://github.com/launchdarkly/ruby-eventsource
|
128
141
|
licenses:
|
129
142
|
- Apache-2.0
|
@@ -143,13 +156,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
143
156
|
- !ruby/object:Gem::Version
|
144
157
|
version: '0'
|
145
158
|
requirements: []
|
146
|
-
|
147
|
-
rubygems_version: 2.5.2.3
|
159
|
+
rubygems_version: 3.2.15
|
148
160
|
signing_key:
|
149
161
|
specification_version: 4
|
150
162
|
summary: LaunchDarkly SSE client
|
151
163
|
test_files:
|
164
|
+
- spec/backoff_spec.rb
|
152
165
|
- spec/client_spec.rb
|
153
166
|
- spec/event_parser_spec.rb
|
154
167
|
- spec/http_stub.rb
|
155
|
-
- spec/streaming_http_spec.rb
|
data/Gemfile.lock
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
ld-eventsource (1.0.2)
|
5
|
-
concurrent-ruby (~> 1.0)
|
6
|
-
http_tools (~> 0.4.5)
|
7
|
-
socketry (~> 0.5.1)
|
8
|
-
|
9
|
-
GEM
|
10
|
-
remote: https://rubygems.org/
|
11
|
-
specs:
|
12
|
-
concurrent-ruby (1.1.6)
|
13
|
-
diff-lcs (1.3)
|
14
|
-
hitimes (1.3.1)
|
15
|
-
hitimes (1.3.1-java)
|
16
|
-
http_tools (0.4.5)
|
17
|
-
rspec (3.7.0)
|
18
|
-
rspec-core (~> 3.7.0)
|
19
|
-
rspec-expectations (~> 3.7.0)
|
20
|
-
rspec-mocks (~> 3.7.0)
|
21
|
-
rspec-core (3.7.1)
|
22
|
-
rspec-support (~> 3.7.0)
|
23
|
-
rspec-expectations (3.7.0)
|
24
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
25
|
-
rspec-support (~> 3.7.0)
|
26
|
-
rspec-mocks (3.7.0)
|
27
|
-
diff-lcs (>= 1.2.0, < 2.0)
|
28
|
-
rspec-support (~> 3.7.0)
|
29
|
-
rspec-support (3.7.0)
|
30
|
-
rspec_junit_formatter (0.3.0)
|
31
|
-
rspec-core (>= 2, < 4, != 2.12.0)
|
32
|
-
socketry (0.5.1)
|
33
|
-
hitimes (~> 1.2)
|
34
|
-
|
35
|
-
PLATFORMS
|
36
|
-
java
|
37
|
-
ruby
|
38
|
-
|
39
|
-
DEPENDENCIES
|
40
|
-
bundler (~> 1.7)
|
41
|
-
ld-eventsource!
|
42
|
-
rspec (~> 3.2)
|
43
|
-
rspec_junit_formatter (~> 0.3.0)
|
44
|
-
|
45
|
-
BUNDLED WITH
|
46
|
-
1.17.3
|
@@ -1,222 +0,0 @@
|
|
1
|
-
require "ld-eventsource/errors"
|
2
|
-
|
3
|
-
require "concurrent/atomics"
|
4
|
-
require "http_tools"
|
5
|
-
require "socketry"
|
6
|
-
|
7
|
-
module SSE
|
8
|
-
module Impl
|
9
|
-
#
|
10
|
-
# Wrapper around a socket providing a simplified HTTP request-response cycle including streaming.
|
11
|
-
# The socket is created and managed by Socketry, which we use so that we can have a read timeout.
|
12
|
-
#
|
13
|
-
class StreamingHTTPConnection
|
14
|
-
attr_reader :status, :headers
|
15
|
-
|
16
|
-
#
|
17
|
-
# Opens a new connection.
|
18
|
-
#
|
19
|
-
# @param [String] uri the URI to connect o
|
20
|
-
# @param [String] proxy the proxy server URI, if any
|
21
|
-
# @param [Hash] headers request headers
|
22
|
-
# @param [Float] connect_timeout connection timeout
|
23
|
-
# @param [Float] read_timeout read timeout
|
24
|
-
#
|
25
|
-
def initialize(uri, proxy: nil, headers: {}, connect_timeout: nil, read_timeout: nil)
|
26
|
-
@socket = HTTPConnectionFactory.connect(uri, proxy, connect_timeout, read_timeout)
|
27
|
-
@socket.write(build_request(uri, headers))
|
28
|
-
@reader = HTTPResponseReader.new(@socket, read_timeout)
|
29
|
-
@status = @reader.status
|
30
|
-
@headers = @reader.headers
|
31
|
-
@closed = Concurrent::AtomicBoolean.new(false)
|
32
|
-
end
|
33
|
-
|
34
|
-
#
|
35
|
-
# Closes the connection.
|
36
|
-
#
|
37
|
-
def close
|
38
|
-
if @closed.make_true
|
39
|
-
@socket.close if @socket
|
40
|
-
@socket = nil
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
#
|
45
|
-
# Generator that returns one line of the response body at a time (delimited by \r, \n,
|
46
|
-
# or \r\n) until the response is fully consumed or the socket is closed.
|
47
|
-
#
|
48
|
-
def read_lines
|
49
|
-
@reader.read_lines
|
50
|
-
end
|
51
|
-
|
52
|
-
#
|
53
|
-
# Consumes the entire response body and returns it.
|
54
|
-
#
|
55
|
-
# @return [String] the response body
|
56
|
-
#
|
57
|
-
def read_all
|
58
|
-
@reader.read_all
|
59
|
-
end
|
60
|
-
|
61
|
-
private
|
62
|
-
|
63
|
-
# Build an HTTP request line and headers.
|
64
|
-
def build_request(uri, headers)
|
65
|
-
ret = "GET #{uri.request_uri} HTTP/1.1\r\n"
|
66
|
-
ret << "Host: #{uri.host}\r\n"
|
67
|
-
headers.each { |k, v|
|
68
|
-
ret << "#{k}: #{v}\r\n"
|
69
|
-
}
|
70
|
-
ret + "\r\n"
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
#
|
75
|
-
# Used internally to send the HTTP request, including the proxy dialogue if necessary.
|
76
|
-
# @private
|
77
|
-
#
|
78
|
-
class HTTPConnectionFactory
|
79
|
-
def self.connect(uri, proxy, connect_timeout, read_timeout)
|
80
|
-
if !proxy
|
81
|
-
return open_socket(uri, connect_timeout)
|
82
|
-
end
|
83
|
-
|
84
|
-
socket = open_socket(proxy, connect_timeout)
|
85
|
-
socket.write(build_proxy_request(uri, proxy))
|
86
|
-
|
87
|
-
# temporarily create a reader just for the proxy connect response
|
88
|
-
proxy_reader = HTTPResponseReader.new(socket, read_timeout)
|
89
|
-
if proxy_reader.status != 200
|
90
|
-
raise Errors::HTTPProxyError.new(proxy_reader.status)
|
91
|
-
end
|
92
|
-
|
93
|
-
# start using TLS at this point if appropriate
|
94
|
-
if uri.scheme.downcase == 'https'
|
95
|
-
wrap_socket_in_ssl_socket(socket)
|
96
|
-
else
|
97
|
-
socket
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
private
|
102
|
-
|
103
|
-
def self.open_socket(uri, connect_timeout)
|
104
|
-
if uri.scheme.downcase == 'https'
|
105
|
-
Socketry::SSL::Socket.connect(uri.host, uri.port, timeout: connect_timeout)
|
106
|
-
else
|
107
|
-
Socketry::TCP::Socket.connect(uri.host, uri.port, timeout: connect_timeout)
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
# Build a proxy connection header.
|
112
|
-
def self.build_proxy_request(uri, proxy)
|
113
|
-
ret = "CONNECT #{uri.host}:#{uri.port} HTTP/1.1\r\n"
|
114
|
-
ret << "Host: #{uri.host}:#{uri.port}\r\n"
|
115
|
-
if proxy.user || proxy.password
|
116
|
-
encoded_credentials = Base64.strict_encode64([proxy.user || '', proxy.password || ''].join(":"))
|
117
|
-
ret << "Proxy-Authorization: Basic #{encoded_credentials}\r\n"
|
118
|
-
end
|
119
|
-
ret << "\r\n"
|
120
|
-
ret
|
121
|
-
end
|
122
|
-
|
123
|
-
def self.wrap_socket_in_ssl_socket(socket)
|
124
|
-
io = IO.try_convert(socket)
|
125
|
-
ssl_sock = OpenSSL::SSL::SSLSocket.new(io, OpenSSL::SSL::SSLContext.new)
|
126
|
-
ssl_sock.connect
|
127
|
-
Socketry::SSL::Socket.new.from_socket(ssl_sock)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
#
|
132
|
-
# Used internally to read the HTTP response, either all at once or as a stream of text lines.
|
133
|
-
# Incoming data is fed into an instance of HTTPTools::Parser, which gives us the header and
|
134
|
-
# chunks of the body via callbacks.
|
135
|
-
# @private
|
136
|
-
#
|
137
|
-
class HTTPResponseReader
|
138
|
-
DEFAULT_CHUNK_SIZE = 10000
|
139
|
-
|
140
|
-
attr_reader :status, :headers
|
141
|
-
|
142
|
-
def initialize(socket, read_timeout)
|
143
|
-
@socket = socket
|
144
|
-
@read_timeout = read_timeout
|
145
|
-
@parser = HTTPTools::Parser.new
|
146
|
-
@buffer = ""
|
147
|
-
@done = false
|
148
|
-
@lock = Mutex.new
|
149
|
-
|
150
|
-
# Provide callbacks for the Parser to give us the headers and body. This has to be done
|
151
|
-
# before we start piping any data into the parser.
|
152
|
-
have_headers = false
|
153
|
-
@parser.on(:header) do
|
154
|
-
have_headers = true
|
155
|
-
end
|
156
|
-
@parser.on(:stream) do |data|
|
157
|
-
@lock.synchronize { @buffer << data } # synchronize because we're called from another thread in Socketry
|
158
|
-
end
|
159
|
-
@parser.on(:finish) do
|
160
|
-
@lock.synchronize { @done = true }
|
161
|
-
end
|
162
|
-
|
163
|
-
# Block until the status code and headers have been successfully read.
|
164
|
-
while !have_headers
|
165
|
-
raise EOFError if !read_chunk_into_buffer
|
166
|
-
end
|
167
|
-
@headers = Hash[@parser.header.map { |k,v| [k.downcase, v] }]
|
168
|
-
@status = @parser.status_code
|
169
|
-
end
|
170
|
-
|
171
|
-
def read_lines
|
172
|
-
Enumerator.new do |gen|
|
173
|
-
loop do
|
174
|
-
line = read_line
|
175
|
-
break if line.nil?
|
176
|
-
gen.yield line
|
177
|
-
end
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
def read_all
|
182
|
-
while read_chunk_into_buffer
|
183
|
-
end
|
184
|
-
@buffer
|
185
|
-
end
|
186
|
-
|
187
|
-
private
|
188
|
-
|
189
|
-
# Attempt to read some more data from the socket. Return true if successful, false if EOF.
|
190
|
-
# A read timeout will result in an exception from Socketry's readpartial method.
|
191
|
-
def read_chunk_into_buffer
|
192
|
-
# If @done is set, it means the Parser has signaled end of response body
|
193
|
-
@lock.synchronize { return false if @done }
|
194
|
-
begin
|
195
|
-
data = @socket.readpartial(DEFAULT_CHUNK_SIZE, timeout: @read_timeout)
|
196
|
-
rescue Socketry::TimeoutError
|
197
|
-
# We rethrow this as our own type so the caller doesn't have to know the Socketry API
|
198
|
-
raise Errors::ReadTimeoutError.new(@read_timeout)
|
199
|
-
end
|
200
|
-
return false if data == :eof
|
201
|
-
@parser << data
|
202
|
-
# We are piping the content through the parser so that it can handle things like chunked
|
203
|
-
# encoding for us. The content ends up being appended to @buffer via our callback.
|
204
|
-
true
|
205
|
-
end
|
206
|
-
|
207
|
-
# Extract the next line of text from the read buffer, refilling the buffer as needed.
|
208
|
-
def read_line
|
209
|
-
loop do
|
210
|
-
@lock.synchronize do
|
211
|
-
i = @buffer.index(/[\r\n]/)
|
212
|
-
if !i.nil?
|
213
|
-
i += 1 if (@buffer[i] == "\r" && i < @buffer.length - 1 && @buffer[i + 1] == "\n")
|
214
|
-
return @buffer.slice!(0, i + 1).force_encoding(Encoding::UTF_8)
|
215
|
-
end
|
216
|
-
end
|
217
|
-
return nil if !read_chunk_into_buffer
|
218
|
-
end
|
219
|
-
end
|
220
|
-
end
|
221
|
-
end
|
222
|
-
end
|