ld-eventsource 1.0.3 → 2.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 +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 +19 -0
- data/README.md +2 -2
- data/ld-eventsource.gemspec +3 -3
- data/lib/ld-eventsource/client.rb +95 -21
- data/lib/ld-eventsource/version.rb +1 -1
- data/spec/backoff_spec.rb +2 -0
- data/spec/client_spec.rb +81 -6
- data/spec/http_stub.rb +2 -0
- metadata +34 -24
- data/Gemfile.lock +0 -46
- data/lib/ld-eventsource/impl/streaming_http.rb +0 -222
- data/spec/streaming_http_spec.rb +0 -263
data/spec/backoff_spec.rb
CHANGED
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
|
|
@@ -366,4 +370,75 @@ EOT
|
|
|
366
370
|
end
|
|
367
371
|
end
|
|
368
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
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
it "returns true from closed? when closed" do
|
|
431
|
+
with_server do |server|
|
|
432
|
+
server.setup_response("/") do |req,res|
|
|
433
|
+
send_stream_content(res, "", keep_open: true)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
with_client(subject.new(server.base_uri)) do |client|
|
|
437
|
+
expect(client.closed?).to be(false)
|
|
438
|
+
|
|
439
|
+
client.close
|
|
440
|
+
expect(client.closed?).to be(true)
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
end
|
|
369
444
|
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: 1.0
|
|
4
|
+
version: 2.1.0
|
|
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-11 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,7 +130,6 @@ 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
|
|
@@ -124,7 +137,6 @@ files:
|
|
|
124
137
|
- spec/client_spec.rb
|
|
125
138
|
- spec/event_parser_spec.rb
|
|
126
139
|
- spec/http_stub.rb
|
|
127
|
-
- spec/streaming_http_spec.rb
|
|
128
140
|
homepage: https://github.com/launchdarkly/ruby-eventsource
|
|
129
141
|
licenses:
|
|
130
142
|
- Apache-2.0
|
|
@@ -144,8 +156,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
144
156
|
- !ruby/object:Gem::Version
|
|
145
157
|
version: '0'
|
|
146
158
|
requirements: []
|
|
147
|
-
|
|
148
|
-
rubygems_version: 2.5.2.3
|
|
159
|
+
rubygems_version: 3.2.15
|
|
149
160
|
signing_key:
|
|
150
161
|
specification_version: 4
|
|
151
162
|
summary: LaunchDarkly SSE client
|
|
@@ -154,4 +165,3 @@ test_files:
|
|
|
154
165
|
- spec/client_spec.rb
|
|
155
166
|
- spec/event_parser_spec.rb
|
|
156
167
|
- spec/http_stub.rb
|
|
157
|
-
- spec/streaming_http_spec.rb
|
data/Gemfile.lock
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
PATH
|
|
2
|
-
remote: .
|
|
3
|
-
specs:
|
|
4
|
-
ld-eventsource (1.0.3)
|
|
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
|