tcp-client 0.9.4 → 0.11.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 +4 -4
- data/README.md +9 -6
- data/lib/tcp-client/address.rb +1 -1
- data/lib/tcp-client/configuration.rb +48 -35
- data/lib/tcp-client/default_configuration.rb +3 -3
- data/lib/tcp-client/errors.rb +2 -2
- data/lib/tcp-client/mixin/io_with_deadline.rb +85 -78
- data/lib/tcp-client/ssl_socket.rb +1 -1
- data/lib/tcp-client/version.rb +2 -1
- data/lib/tcp-client.rb +61 -23
- data/rakefile.rb +5 -1
- data/sample/google_ssl.rb +8 -5
- data/spec/tcp-client/configuration_spec.rb +62 -44
- data/spec/tcp-client/default_configuration_spec.rb +3 -2
- data/spec/tcp_client_spec.rb +230 -32
- data/tcp-client.gemspec +2 -2
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 91bdfe4046f3e4baeefe4a140d7a2bdb0cac02d3c069102e9c574fe02e5ca1c8
|
4
|
+
data.tar.gz: e26cc5ce187d50aba43d06403730445f3605667a4cffc5d4c7105dd549ca4c9a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dfc647093949536fa3c040f9140ec5039a37934e1b77ccc3150f9c16a8de824123c7e8b374faa9479c5572a84f7cb9e08a051e356fe76e79d9245e10efd61bf4
|
7
|
+
data.tar.gz: 6046435c921660e284838c0e33559540f81de5f8b4c150ab63bf37ee72fc7605bc128d5c2b79d8ac7cb6cf657267bc445a4bef36103508ffaed4d6b0961d023c
|
data/README.md
CHANGED
@@ -27,11 +27,14 @@ cfg = TCPClient::Configuration.create(
|
|
27
27
|
# - limit all network interactions to 1.5 seconds
|
28
28
|
# - use the Configuration cfg
|
29
29
|
# - send a simple HTTP get request
|
30
|
-
# - read
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
30
|
+
# - read the returned message and headers
|
31
|
+
response =
|
32
|
+
TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
|
33
|
+
client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") #=> 40
|
34
|
+
client.readline("\r\n\r\n") #=> see response
|
35
|
+
end
|
36
|
+
|
37
|
+
puts(response)
|
35
38
|
```
|
36
39
|
|
37
40
|
For more samples see [the samples dir](https://github.com/mblumtritt/tcp-client/tree/main/sample)
|
@@ -58,7 +61,7 @@ To install the gem globally use:
|
|
58
61
|
gem install tcp-client
|
59
62
|
```
|
60
63
|
|
61
|
-
After that you need only a single line of code in your project to have
|
64
|
+
After that you need only a single line of code in your project to have on board:
|
62
65
|
|
63
66
|
```ruby
|
64
67
|
require 'tcp-client'
|
data/lib/tcp-client/address.rb
CHANGED
@@ -41,7 +41,7 @@ class TCPClient
|
|
41
41
|
# @param addrinfo [Addrinfo] containing the addressed host and port
|
42
42
|
#
|
43
43
|
# @overload initialize(port)
|
44
|
-
#
|
44
|
+
# Addresses the port on the local machine.
|
45
45
|
#
|
46
46
|
# @example create an Address for localhost on port 80
|
47
47
|
# Address.new(80)
|
@@ -26,45 +26,29 @@ class TCPClient
|
|
26
26
|
# @example
|
27
27
|
# config = TCPClient::Configuration.create(buffered: false)
|
28
28
|
#
|
29
|
-
# @param options [
|
29
|
+
# @param options [{Symbol => Object}] see {#initialize} for details
|
30
30
|
#
|
31
31
|
# @return [Configuration] the initialized configuration
|
32
32
|
#
|
33
|
-
def self.create(options =
|
33
|
+
def self.create(options = nil)
|
34
34
|
configuration = new(options)
|
35
35
|
yield(configuration) if block_given?
|
36
36
|
configuration
|
37
37
|
end
|
38
38
|
|
39
39
|
#
|
40
|
-
#
|
40
|
+
# Initializes and optionally configures the instance with given options.
|
41
41
|
#
|
42
|
-
# @
|
43
|
-
# @option options [Boolean] :buffered, see {#buffered}
|
44
|
-
# @option options [Boolean] :keep_alive, see {#keep_alive}
|
45
|
-
# @option options [Boolean] :reverse_lookup, see {#reverse_lookup}
|
46
|
-
# @option options [Hash<Symbol, Object>] :ssl_params, see {#ssl_params}
|
47
|
-
# @option options [Numeric] :connect_timeout, see {#connect_timeout}
|
48
|
-
# @option options [Class<Exception>] :connect_timeout_error, see
|
49
|
-
# {#connect_timeout_error}
|
50
|
-
# @option options [Numeric] :read_timeout, see {#read_timeout}
|
51
|
-
# @option options [Class<Exception>] :read_timeout_error, see
|
52
|
-
# {#read_timeout_error}
|
53
|
-
# @option options [Numeric] :write_timeout, see {#write_timeout}
|
54
|
-
# @option options [Class<Exception>] :write_timeout_error, see
|
55
|
-
# {#write_timeout_error}
|
56
|
-
# @option options [Boolean] :normalize_network_errors, see
|
57
|
-
# {#normalize_network_errors}
|
42
|
+
# @see #configure
|
58
43
|
#
|
59
|
-
|
60
|
-
def initialize(options = {})
|
44
|
+
def initialize(options = nil)
|
61
45
|
@buffered = @keep_alive = @reverse_lookup = true
|
62
46
|
self.timeout = @ssl_params = nil
|
63
47
|
@connect_timeout_error = ConnectTimeoutError
|
64
48
|
@read_timeout_error = ReadTimeoutError
|
65
49
|
@write_timeout_error = WriteTimeoutError
|
66
50
|
@normalize_network_errors = false
|
67
|
-
options
|
51
|
+
configure(options) if options
|
68
52
|
end
|
69
53
|
|
70
54
|
# @!group Instance Attributes Socket Level
|
@@ -72,8 +56,8 @@ class TCPClient
|
|
72
56
|
#
|
73
57
|
# Enables/disables use of Socket-level buffering
|
74
58
|
#
|
75
|
-
# @return [Boolean]
|
76
|
-
# (default) or not
|
59
|
+
# @return [Boolean] whether the connection is allowed to use internal
|
60
|
+
# buffers (default) or not
|
77
61
|
#
|
78
62
|
attr_reader :buffered
|
79
63
|
|
@@ -84,7 +68,7 @@ class TCPClient
|
|
84
68
|
#
|
85
69
|
# Enables/disables use of Socket-level keep alive handling.
|
86
70
|
#
|
87
|
-
# @return [Boolean]
|
71
|
+
# @return [Boolean] whether the connection is allowed to use keep alive
|
88
72
|
# signals (default) or not
|
89
73
|
#
|
90
74
|
attr_reader :keep_alive
|
@@ -96,7 +80,7 @@ class TCPClient
|
|
96
80
|
#
|
97
81
|
# Enables/disables address lookup.
|
98
82
|
#
|
99
|
-
# @return [Boolean]
|
83
|
+
# @return [Boolean] whether the connection is allowed to lookup the address
|
100
84
|
# (default) or not
|
101
85
|
#
|
102
86
|
attr_reader :reverse_lookup
|
@@ -107,7 +91,7 @@ class TCPClient
|
|
107
91
|
|
108
92
|
#
|
109
93
|
# @!parse attr_reader :ssl?
|
110
|
-
# @return [Boolean]
|
94
|
+
# @return [Boolean] whether SSL is configured, see {#ssl_params}
|
111
95
|
#
|
112
96
|
def ssl?
|
113
97
|
@ssl_params ? true : false
|
@@ -117,7 +101,7 @@ class TCPClient
|
|
117
101
|
# Parameters used to initialize a SSL context. SSL/TLS will only be used if
|
118
102
|
# this attribute is not `nil`.
|
119
103
|
#
|
120
|
-
# @return [
|
104
|
+
# @return [{Symbol => Object}] SSL parameters for the SSL context
|
121
105
|
# @return [nil] if no SSL should be used (default)
|
122
106
|
#
|
123
107
|
attr_reader :ssl_params
|
@@ -226,7 +210,7 @@ class TCPClient
|
|
226
210
|
# @attribute [w] timeout
|
227
211
|
# Shorthand to set maximum time in seconds for all timeout monitoring.
|
228
212
|
#
|
229
|
-
# @return [Numeric] maximum time in seconds for any
|
213
|
+
# @return [Numeric] maximum time in seconds for any action
|
230
214
|
# @return [nil] if all timeout monitoring should be disabled (default)
|
231
215
|
#
|
232
216
|
# @see #connect_timeout
|
@@ -239,7 +223,7 @@ class TCPClient
|
|
239
223
|
|
240
224
|
#
|
241
225
|
# @attribute [w] timeout_error
|
242
|
-
# Shorthand to set the exception class
|
226
|
+
# Shorthand to set the exception class which will by raised by any reached
|
243
227
|
# timeout.
|
244
228
|
#
|
245
229
|
# @return [Class<Exception>] exception class raised
|
@@ -258,6 +242,8 @@ class TCPClient
|
|
258
242
|
|
259
243
|
# @!endgroup
|
260
244
|
|
245
|
+
# @!group Other Instance Attributes
|
246
|
+
|
261
247
|
#
|
262
248
|
# Enables/disables if network exceptions should be raised as {NetworkError}.
|
263
249
|
#
|
@@ -266,7 +252,7 @@ class TCPClient
|
|
266
252
|
# manner. If this option is set to true all these error cases are raised as
|
267
253
|
# {NetworkError} and can be easily captured.
|
268
254
|
#
|
269
|
-
# @return [Boolean]
|
255
|
+
# @return [Boolean] whether all network exceptions should be raised as
|
270
256
|
# {NetworkError}, or not (default)
|
271
257
|
#
|
272
258
|
attr_reader :normalize_network_errors
|
@@ -275,12 +261,12 @@ class TCPClient
|
|
275
261
|
@normalize_network_errors = value ? true : false
|
276
262
|
end
|
277
263
|
|
264
|
+
# @!endgroup
|
265
|
+
|
278
266
|
#
|
279
|
-
#
|
280
|
-
#
|
281
|
-
# @return [Hash<Symbol, Object>]
|
267
|
+
# @return [{Symbol => Object}] Hash containing all attributes
|
282
268
|
#
|
283
|
-
# @see #
|
269
|
+
# @see #configure
|
284
270
|
#
|
285
271
|
def to_h
|
286
272
|
{
|
@@ -298,6 +284,33 @@ class TCPClient
|
|
298
284
|
}
|
299
285
|
end
|
300
286
|
|
287
|
+
#
|
288
|
+
# Configures the instance with given options Hash.
|
289
|
+
#
|
290
|
+
# @param options [{Symbol => Object}]
|
291
|
+
# @option options [Boolean] :buffered, see {#buffered}
|
292
|
+
# @option options [Boolean] :keep_alive, see {#keep_alive}
|
293
|
+
# @option options [Boolean] :reverse_lookup, see {#reverse_lookup}
|
294
|
+
# @option options [{Symbol => Object}] :ssl_params, see {#ssl_params}
|
295
|
+
# @option options [Numeric] :connect_timeout, see {#connect_timeout}
|
296
|
+
# @option options [Class<Exception>] :connect_timeout_error, see
|
297
|
+
# {#connect_timeout_error}
|
298
|
+
# @option options [Numeric] :read_timeout, see {#read_timeout}
|
299
|
+
# @option options [Class<Exception>] :read_timeout_error, see
|
300
|
+
# {#read_timeout_error}
|
301
|
+
# @option options [Numeric] :write_timeout, see {#write_timeout}
|
302
|
+
# @option options [Class<Exception>] :write_timeout_error, see
|
303
|
+
# {#write_timeout_error}
|
304
|
+
# @option options [Boolean] :normalize_network_errors, see
|
305
|
+
# {#normalize_network_errors}
|
306
|
+
#
|
307
|
+
# @return [Configuration] self
|
308
|
+
#
|
309
|
+
def configure(options)
|
310
|
+
options.each_pair { |attribute, value| set(attribute, value) }
|
311
|
+
self
|
312
|
+
end
|
313
|
+
|
301
314
|
# @!visibility private
|
302
315
|
def freeze
|
303
316
|
@ssl_params.freeze
|
@@ -25,13 +25,13 @@ class TCPClient
|
|
25
25
|
# cfg.ssl_params = { min_version: :TLS1_2, max_version: :TLS1_3 }
|
26
26
|
# end
|
27
27
|
#
|
28
|
-
# @param options [Hash] see {Configuration#
|
28
|
+
# @param options [Hash] see {Configuration#configure} for details
|
29
29
|
#
|
30
30
|
# @yieldparam cfg {Configuration} the new configuration
|
31
31
|
#
|
32
32
|
# @return [Configuration] the new default configuration
|
33
33
|
#
|
34
|
-
def configure(options =
|
34
|
+
def configure(options = nil, &block)
|
35
35
|
@default_configuration = Configuration.create(options, &block)
|
36
36
|
end
|
37
37
|
end
|
@@ -41,7 +41,7 @@ class TCPClient
|
|
41
41
|
#
|
42
42
|
# @!parse attr_reader :default
|
43
43
|
# @return [Configuration] used by default if no dedicated configuration
|
44
|
-
#
|
44
|
+
# was specified
|
45
45
|
#
|
46
46
|
# @see TCPClient.open
|
47
47
|
# @see TCPClient.with_deadline
|
data/lib/tcp-client/errors.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
class TCPClient
|
4
4
|
#
|
5
|
-
# Raised when a SSL connection should be
|
5
|
+
# Raised when a SSL connection should be established but the OpenSSL gem is
|
6
6
|
# not available.
|
7
7
|
#
|
8
8
|
class NoOpenSSLError < RuntimeError
|
@@ -38,7 +38,7 @@ class TCPClient
|
|
38
38
|
#
|
39
39
|
class UnknownAttributeError < ArgumentError
|
40
40
|
#
|
41
|
-
# @param attribute [Object] the undefined
|
41
|
+
# @param attribute [Object] the undefined attribute
|
42
42
|
#
|
43
43
|
def initialize(attribute)
|
44
44
|
super("unknown attribute - #{attribute}")
|
@@ -1,57 +1,80 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
module IOWithDeadlineMixin
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
mod.include(
|
9
|
-
elsif methods.index(:to_io)
|
10
|
-
mod.include(ViaIOWaitMethod)
|
11
|
-
else
|
12
|
-
mod.include(ViaSelect)
|
3
|
+
class TCPClient
|
4
|
+
module IOWithDeadlineMixin
|
5
|
+
def self.included(mod)
|
6
|
+
methods = mod.instance_methods
|
7
|
+
return if methods.index(:wait_writable) && methods.index(:wait_readable)
|
8
|
+
mod.include(methods.index(:to_io) ? WaitWithIO : WaitWithSelect)
|
13
9
|
end
|
14
|
-
end
|
15
10
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
return
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
11
|
+
def read_with_deadline(nbytes, deadline, exception)
|
12
|
+
raise(exception) unless deadline.remaining_time
|
13
|
+
return fetch_avail(deadline, exception) if nbytes.nil?
|
14
|
+
return ''.b if nbytes.zero?
|
15
|
+
@read_buffer ||= ''.b
|
16
|
+
while @read_buffer.bytesize < nbytes
|
17
|
+
read = fetch_next(deadline, exception) and next @read_buffer << read
|
18
|
+
close
|
19
|
+
break
|
20
|
+
end
|
21
|
+
fetch_slice(nbytes)
|
24
22
|
end
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
23
|
+
|
24
|
+
def read_to_with_deadline(sep, deadline, exception)
|
25
|
+
raise(exception) unless deadline.remaining_time
|
26
|
+
@read_buffer ||= ''.b
|
27
|
+
while @read_buffer.index(sep).nil?
|
28
|
+
read = fetch_next(deadline, exception) and next @read_buffer << read
|
29
|
+
close
|
30
|
+
break
|
31
|
+
end
|
32
|
+
index = @read_buffer.index(sep)
|
33
|
+
return fetch_slice(index + sep.bytesize) if index
|
34
|
+
result = @read_buffer
|
35
|
+
@read_buffer = nil
|
36
|
+
result
|
34
37
|
end
|
35
|
-
result
|
36
|
-
end
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
39
|
+
def write_with_deadline(data, deadline, exception)
|
40
|
+
raise(exception) unless deadline.remaining_time
|
41
|
+
return 0 if (size = data.bytesize).zero?
|
42
|
+
result = 0
|
43
|
+
loop do
|
44
|
+
written =
|
45
|
+
with_deadline(deadline, exception) do
|
46
|
+
write_nonblock(data, exception: false)
|
47
|
+
end
|
48
|
+
(result += written) >= size and return result
|
49
|
+
data = data.byteslice(written, data.bytesize - written)
|
50
|
+
end
|
50
51
|
end
|
51
|
-
end
|
52
52
|
|
53
|
-
|
54
|
-
|
53
|
+
private
|
54
|
+
|
55
|
+
def fetch_avail(deadline, exception)
|
56
|
+
if (result = @read_buffer || fetch_next(deadline, exception)).nil?
|
57
|
+
close
|
58
|
+
return ''.b
|
59
|
+
end
|
60
|
+
@read_buffer = nil
|
61
|
+
result
|
62
|
+
end
|
63
|
+
|
64
|
+
def fetch_slice(size)
|
65
|
+
result = @read_buffer.byteslice(0, size)
|
66
|
+
rest = @read_buffer.bytesize - result.bytesize
|
67
|
+
@read_buffer = rest.zero? ? nil : @read_buffer.byteslice(size, rest)
|
68
|
+
result
|
69
|
+
end
|
70
|
+
|
71
|
+
def fetch_next(deadline, exception)
|
72
|
+
with_deadline(deadline, exception) do
|
73
|
+
read_nonblock(65_536, exception: false)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def with_deadline(deadline, exception)
|
55
78
|
loop do
|
56
79
|
case ret = yield
|
57
80
|
when :wait_writable
|
@@ -67,45 +90,29 @@ module IOWithDeadlineMixin # :nodoc:
|
|
67
90
|
rescue Errno::ETIMEDOUT
|
68
91
|
raise(exception)
|
69
92
|
end
|
70
|
-
end
|
71
93
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
when :wait_readable
|
80
|
-
remaining_time = deadline.remaining_time or raise(exception)
|
81
|
-
raise(exception) if to_io.wait_readable(remaining_time).nil?
|
82
|
-
else
|
83
|
-
return ret
|
84
|
-
end
|
94
|
+
module WaitWithIO
|
95
|
+
def wait_writable(remaining_time)
|
96
|
+
to_io.wait_writable(remaining_time)
|
97
|
+
end
|
98
|
+
|
99
|
+
def wait_readable(remaining_time)
|
100
|
+
to_io.wait_readable(remaining_time)
|
85
101
|
end
|
86
|
-
rescue Errno::ETIMEDOUT
|
87
|
-
raise(exception)
|
88
102
|
end
|
89
|
-
end
|
90
103
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
when :wait_readable
|
99
|
-
remaining_time = deadline.remaining_time or raise(exception)
|
100
|
-
raise(exception) if ::IO.select([self], nil, nil, remaining_time).nil?
|
101
|
-
else
|
102
|
-
return ret
|
103
|
-
end
|
104
|
+
module WaitWithSelect
|
105
|
+
def wait_writable(remaining_time)
|
106
|
+
::IO.select(nil, [self], nil, remaining_time)
|
107
|
+
end
|
108
|
+
|
109
|
+
def wait_readable(remaining_time)
|
110
|
+
::IO.select([self], nil, nil, remaining_time)
|
104
111
|
end
|
105
|
-
rescue Errno::ETIMEDOUT
|
106
|
-
raise(exception)
|
107
112
|
end
|
113
|
+
|
114
|
+
private_constant(:WaitWithIO, :WaitWithSelect)
|
108
115
|
end
|
109
116
|
|
110
|
-
private_constant(:
|
117
|
+
private_constant(:IOWithDeadlineMixin)
|
111
118
|
end
|
@@ -30,7 +30,7 @@ class TCPClient
|
|
30
30
|
::OpenSSL::SSL::SSLContext.new.tap do |ctx|
|
31
31
|
ctx.set_params(ssl_params)
|
32
32
|
ctx.session_cache_mode = CONTEXT_CACHE_MODE
|
33
|
-
ctx.session_new_cb = proc { |_,
|
33
|
+
ctx.session_new_cb = proc { |_, session| @new_session = session }
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
data/lib/tcp-client/version.rb
CHANGED
data/lib/tcp-client.rb
CHANGED
@@ -13,15 +13,18 @@ require_relative 'tcp-client/version'
|
|
13
13
|
# Client class to communicate with a server via TCP w/o SSL.
|
14
14
|
#
|
15
15
|
# All connect/read/write actions can be monitored to ensure that all actions
|
16
|
-
# terminate before given time
|
16
|
+
# terminate before given time limit - or raise an exception.
|
17
17
|
#
|
18
18
|
# @example request to Google.com and limit network interactions to 1.5 seconds
|
19
|
-
#
|
20
|
-
#
|
21
|
-
# client.read(12)
|
22
|
-
# end
|
23
|
-
# # => "HTTP/1.1 200"
|
19
|
+
# # create a configuration to use at least TLS 1.2
|
20
|
+
# cfg = TCPClient::Configuration.create(ssl_params: {min_version: :TLS1_2})
|
24
21
|
#
|
22
|
+
# response =
|
23
|
+
# TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
|
24
|
+
# client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") #=> 40
|
25
|
+
# client.readline("\r\n\r\n") #=> see response
|
26
|
+
# end
|
27
|
+
# # response contains the returned message and header
|
25
28
|
#
|
26
29
|
class TCPClient
|
27
30
|
#
|
@@ -40,10 +43,10 @@ class TCPClient
|
|
40
43
|
#
|
41
44
|
# If an optional block is given, then the block's result is returned and the
|
42
45
|
# connection will be closed when the block execution ends.
|
43
|
-
# This can be used to create an ad-hoc connection which is
|
46
|
+
# This can be used to create an ad-hoc connection which is guaranteed to be
|
44
47
|
# closed.
|
45
48
|
#
|
46
|
-
# If no block is
|
49
|
+
# If no block is given the connected client instance is returned.
|
47
50
|
# This can be used as a shorthand to create & connect a client.
|
48
51
|
#
|
49
52
|
# @param address [Address, String, Addrinfo, Integer] the target address see
|
@@ -55,7 +58,7 @@ class TCPClient
|
|
55
58
|
#
|
56
59
|
def self.open(address, configuration = nil)
|
57
60
|
client = new
|
58
|
-
client.connect(
|
61
|
+
client.connect(address, configuration)
|
59
62
|
block_given? ? yield(client) : client
|
60
63
|
ensure
|
61
64
|
client.close if block_given?
|
@@ -67,9 +70,9 @@ class TCPClient
|
|
67
70
|
# the given time.
|
68
71
|
#
|
69
72
|
# It ensures to close the connection when the block execution ends and returns
|
70
|
-
# the block
|
73
|
+
# the block's result.
|
71
74
|
#
|
72
|
-
# This can be used to create an ad-hoc connection which is
|
75
|
+
# This can be used to create an ad-hoc connection which is guaranteed to be
|
73
76
|
# closed and which {#read}/{#write} call sequence should not last longer than
|
74
77
|
# the `timeout` seconds.
|
75
78
|
#
|
@@ -91,7 +94,6 @@ class TCPClient
|
|
91
94
|
def self.with_deadline(timeout, address, configuration = nil)
|
92
95
|
client = nil
|
93
96
|
raise(NoBlockGivenError) unless block_given?
|
94
|
-
address = Address.new(address)
|
95
97
|
client = new
|
96
98
|
client.with_deadline(timeout) do
|
97
99
|
yield(client.connect(address, configuration))
|
@@ -112,7 +114,7 @@ class TCPClient
|
|
112
114
|
|
113
115
|
#
|
114
116
|
# @!parse attr_reader :closed?
|
115
|
-
# @return [Boolean]
|
117
|
+
# @return [Boolean] whether the connection is closed
|
116
118
|
#
|
117
119
|
def closed?
|
118
120
|
@socket.nil? || @socket.closed?
|
@@ -121,7 +123,7 @@ class TCPClient
|
|
121
123
|
#
|
122
124
|
# Close the current connection if connected.
|
123
125
|
#
|
124
|
-
# @return [
|
126
|
+
# @return [TCPClient] itself
|
125
127
|
#
|
126
128
|
def close
|
127
129
|
@socket&.close
|
@@ -133,7 +135,7 @@ class TCPClient
|
|
133
135
|
end
|
134
136
|
|
135
137
|
#
|
136
|
-
# Establishes a new connection to a given `address`.
|
138
|
+
# Establishes a new connection to a server on given `address`.
|
137
139
|
#
|
138
140
|
# It accepts a connection-specific `configuration` or uses the
|
139
141
|
# {.default_configuration}.
|
@@ -141,7 +143,7 @@ class TCPClient
|
|
141
143
|
# The optional `timeout` and `exception` parameters allow to override the
|
142
144
|
# `connect_timeout` and `connect_timeout_error` values.
|
143
145
|
#
|
144
|
-
# @param address [Address, String, Addrinfo, Integer] the target address see
|
146
|
+
# @param address [Address, String, Addrinfo, Integer] the target address, see
|
145
147
|
# {Address#initialize} for valid formats
|
146
148
|
# @param configuration [Configuration] the {Configuration} to be used for
|
147
149
|
# this instance
|
@@ -149,7 +151,7 @@ class TCPClient
|
|
149
151
|
# @param exception [Class<Exception>] exception class to be used when the
|
150
152
|
# connect timeout reached
|
151
153
|
#
|
152
|
-
# @return [
|
154
|
+
# @return [TCPClient] itself
|
153
155
|
#
|
154
156
|
# @raise {NoOpenSSLError} if SSL should be used but OpenSSL is not avail
|
155
157
|
#
|
@@ -157,17 +159,17 @@ class TCPClient
|
|
157
159
|
#
|
158
160
|
def connect(address, configuration = nil, timeout: nil, exception: nil)
|
159
161
|
close if @socket
|
160
|
-
@address = Address.new(address)
|
161
162
|
@configuration = (configuration || Configuration.default).dup
|
162
163
|
raise(NoOpenSSLError) if @configuration.ssl? && !defined?(SSLSocket)
|
164
|
+
@address = stem_errors { Address.new(address) }
|
163
165
|
@socket = create_socket(timeout, exception)
|
164
166
|
self
|
165
167
|
end
|
166
168
|
|
167
169
|
#
|
168
|
-
# Flushes all internal buffers (write all
|
170
|
+
# Flushes all internal buffers (write all buffered data).
|
169
171
|
#
|
170
|
-
# @return [
|
172
|
+
# @return [TCPClient] itself
|
171
173
|
#
|
172
174
|
def flush
|
173
175
|
stem_errors { @socket&.flush }
|
@@ -201,6 +203,39 @@ class TCPClient
|
|
201
203
|
end
|
202
204
|
end
|
203
205
|
|
206
|
+
#
|
207
|
+
# Reads the next line from server.
|
208
|
+
#
|
209
|
+
# The standard record separator is used as `separator`.
|
210
|
+
#
|
211
|
+
# The optional `timeout` and `exception` parameters allow to override the
|
212
|
+
# `read_timeout` and `read_timeout_error` values of the used {#configuration}.
|
213
|
+
#
|
214
|
+
# @param separator [String] the line separator to be used
|
215
|
+
# @param timeout [Numeric] maximum time in seconds to read
|
216
|
+
# @param exception [Class<Exception>] exception class to be used when the
|
217
|
+
# read timeout reached
|
218
|
+
#
|
219
|
+
# @return [String] the read line
|
220
|
+
#
|
221
|
+
# @raise [NotConnectedError] if {#connect} was not called before
|
222
|
+
#
|
223
|
+
# @see NetworkError
|
224
|
+
#
|
225
|
+
def readline(separator = $/, chomp: false, timeout: nil, exception: nil)
|
226
|
+
raise(NotConnectedError) if closed?
|
227
|
+
deadline = create_deadline(timeout, configuration.read_timeout)
|
228
|
+
unless deadline.valid?
|
229
|
+
return stem_errors { @socket.readline(separator, chomp: chomp) }
|
230
|
+
end
|
231
|
+
exception ||= configuration.read_timeout_error
|
232
|
+
line =
|
233
|
+
stem_errors(exception) do
|
234
|
+
@socket.read_to_with_deadline(separator, deadline, exception)
|
235
|
+
end
|
236
|
+
chomp ? line.chomp : line
|
237
|
+
end
|
238
|
+
|
204
239
|
#
|
205
240
|
# @return [String] the currently used address as text.
|
206
241
|
#
|
@@ -229,7 +264,7 @@ class TCPClient
|
|
229
264
|
#
|
230
265
|
# @yieldparam client [TCPClient] self
|
231
266
|
#
|
232
|
-
# @return [Object] the block
|
267
|
+
# @return [Object] the block's result
|
233
268
|
#
|
234
269
|
# @raise [NoBlockGivenError] if the block is missing
|
235
270
|
#
|
@@ -259,6 +294,8 @@ class TCPClient
|
|
259
294
|
#
|
260
295
|
# @raise [NotConnectedError] if {#connect} was not called before
|
261
296
|
#
|
297
|
+
# @see NetworkError
|
298
|
+
#
|
262
299
|
def write(*messages, timeout: nil, exception: nil)
|
263
300
|
raise(NotConnectedError) if closed?
|
264
301
|
deadline = create_deadline(timeout, configuration.write_timeout)
|
@@ -307,7 +344,8 @@ class TCPClient
|
|
307
344
|
IOError,
|
308
345
|
SocketError
|
309
346
|
].tap do |errors|
|
310
|
-
|
311
|
-
|
347
|
+
errors << ::OpenSSL::SSL::SSLError if defined?(::OpenSSL::SSL::SSLError)
|
348
|
+
end
|
349
|
+
.freeze
|
312
350
|
private_constant(:NETWORK_ERRORS)
|
313
351
|
end
|
data/rakefile.rb
CHANGED
@@ -6,7 +6,11 @@ require 'rspec/core/rake_task'
|
|
6
6
|
require 'yard'
|
7
7
|
|
8
8
|
$stdout.sync = $stderr.sync = true
|
9
|
-
|
9
|
+
|
10
|
+
CLEAN << '.yardoc'
|
11
|
+
CLOBBER << 'prj' << 'doc'
|
12
|
+
|
10
13
|
task(:default) { exec('rake --tasks') }
|
14
|
+
task(test: :spec)
|
11
15
|
RSpec::Core::RakeTask.new { |task| task.ruby_opts = %w[-w] }
|
12
16
|
YARD::Rake::YardocTask.new { |task| task.stats_options = %w[--list-undoc] }
|
data/sample/google_ssl.rb
CHANGED
@@ -18,8 +18,11 @@ cfg =
|
|
18
18
|
# - limit all network interactions to 1.5 seconds
|
19
19
|
# - use the Configuration cfg
|
20
20
|
# - send a simple HTTP get request
|
21
|
-
# - read
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
# - read the returned message and headers
|
22
|
+
response =
|
23
|
+
TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
|
24
|
+
client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") #=> 40
|
25
|
+
client.readline("\r\n\r\n") #=> header, see response
|
26
|
+
end
|
27
|
+
|
28
|
+
puts(response)
|
@@ -60,20 +60,34 @@ RSpec.describe TCPClient::Configuration do
|
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
|
-
context '
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
timeout_error: custom_error
|
73
|
-
)
|
63
|
+
context 'when options are given' do
|
64
|
+
let(:options) { double(:options) }
|
65
|
+
|
66
|
+
it 'calls #configure with given options' do
|
67
|
+
expect_any_instance_of(TCPClient::Configuration).to receive(
|
68
|
+
:configure
|
69
|
+
).once.with(options)
|
70
|
+
|
71
|
+
TCPClient::Configuration.new(options)
|
74
72
|
end
|
75
|
-
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '#configure' do
|
77
|
+
subject(:configuration) do
|
78
|
+
TCPClient::Configuration.new.configure(
|
79
|
+
buffered: false,
|
80
|
+
keep_alive: false,
|
81
|
+
reverse_lookup: false,
|
82
|
+
normalize_network_errors: true,
|
83
|
+
ssl: true,
|
84
|
+
timeout: 60,
|
85
|
+
timeout_error: custom_error
|
86
|
+
)
|
87
|
+
end
|
88
|
+
let(:custom_error) { Class.new(StandardError) }
|
76
89
|
|
90
|
+
context 'with valid options' do
|
77
91
|
it 'allows to configure buffering' do
|
78
92
|
expect(configuration.buffered).to be false
|
79
93
|
end
|
@@ -107,52 +121,56 @@ RSpec.describe TCPClient::Configuration do
|
|
107
121
|
end
|
108
122
|
|
109
123
|
it 'allows to configure dedicated timeout values' do
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
expect(
|
117
|
-
expect(
|
118
|
-
expect(config.write_timeout).to be 84
|
124
|
+
configuration.configure(
|
125
|
+
connect_timeout: 21,
|
126
|
+
read_timeout: 42,
|
127
|
+
write_timeout: 84
|
128
|
+
)
|
129
|
+
expect(configuration.connect_timeout).to be 21
|
130
|
+
expect(configuration.read_timeout).to be 42
|
131
|
+
expect(configuration.write_timeout).to be 84
|
119
132
|
end
|
120
133
|
|
121
134
|
it 'allows to configure dedicated timeout errors' do
|
122
135
|
custom_connect = Class.new(StandardError)
|
123
136
|
custom_read = Class.new(StandardError)
|
124
137
|
custom_write = Class.new(StandardError)
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
expect(
|
132
|
-
expect(
|
133
|
-
expect(config.write_timeout_error).to be custom_write
|
138
|
+
configuration.configure(
|
139
|
+
connect_timeout_error: custom_connect,
|
140
|
+
read_timeout_error: custom_read,
|
141
|
+
write_timeout_error: custom_write
|
142
|
+
)
|
143
|
+
expect(configuration.connect_timeout_error).to be custom_connect
|
144
|
+
expect(configuration.read_timeout_error).to be custom_read
|
145
|
+
expect(configuration.write_timeout_error).to be custom_write
|
134
146
|
end
|
147
|
+
end
|
135
148
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
149
|
+
context 'when an invalid attribute is given' do
|
150
|
+
it 'raises an error' do
|
151
|
+
expect { configuration.configure(invalid: :value) }.to raise_error(
|
152
|
+
TCPClient::UnknownAttributeError
|
153
|
+
)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
context 'when no exception class is used to configure a timeout error' do
|
158
|
+
it 'raises with invalid connect_timeout_error' do
|
142
159
|
expect do
|
143
|
-
|
160
|
+
configuration.configure(connect_timeout_error: double(:something))
|
144
161
|
end.to raise_error(TCPClient::NotAnExceptionError)
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'raises with invalid read_timeout_error' do
|
145
165
|
expect do
|
146
|
-
|
166
|
+
configuration.configure(read_timeout_error: double(:something))
|
147
167
|
end.to raise_error(TCPClient::NotAnExceptionError)
|
148
168
|
end
|
149
|
-
end
|
150
169
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
)
|
170
|
+
it 'raises with invalid write_timeout_error' do
|
171
|
+
expect do
|
172
|
+
configuration.configure(write_timeout_error: double(:something))
|
173
|
+
end.to raise_error(TCPClient::NotAnExceptionError)
|
156
174
|
end
|
157
175
|
end
|
158
176
|
end
|
@@ -9,8 +9,9 @@ RSpec.describe 'TCPClient.configure' do
|
|
9
9
|
|
10
10
|
context 'called with parameters' do
|
11
11
|
it 'creates a new configuratiion' do
|
12
|
-
|
13
|
-
TCPClient.
|
12
|
+
options = double(:options)
|
13
|
+
expect(TCPClient::Configuration).to receive(:create).once.with(options)
|
14
|
+
TCPClient.configure(options)
|
14
15
|
end
|
15
16
|
|
16
17
|
it 'returns the new configuratiion' do
|
data/spec/tcp_client_spec.rb
CHANGED
@@ -26,7 +26,7 @@ RSpec.describe TCPClient do
|
|
26
26
|
subject(:client) { TCPClient.new }
|
27
27
|
|
28
28
|
it 'is closed' do
|
29
|
-
expect(client
|
29
|
+
expect(client).to be_closed
|
30
30
|
end
|
31
31
|
|
32
32
|
it 'has no address' do
|
@@ -64,7 +64,7 @@ RSpec.describe TCPClient do
|
|
64
64
|
before { allow_any_instance_of(::Socket).to receive(:connect) }
|
65
65
|
|
66
66
|
it 'is not closed' do
|
67
|
-
expect(client
|
67
|
+
expect(client).not_to be_closed
|
68
68
|
end
|
69
69
|
|
70
70
|
it 'has an address' do
|
@@ -115,7 +115,7 @@ RSpec.describe TCPClient do
|
|
115
115
|
end
|
116
116
|
|
117
117
|
it 'is closed' do
|
118
|
-
expect(client
|
118
|
+
expect(client).to be_closed
|
119
119
|
end
|
120
120
|
|
121
121
|
it 'has an address' do
|
@@ -130,11 +130,11 @@ RSpec.describe TCPClient do
|
|
130
130
|
expect(client.configuration).to be configuration
|
131
131
|
end
|
132
132
|
|
133
|
-
it '
|
133
|
+
it 'fails when read is called' do
|
134
134
|
expect { client.read(42) }.to raise_error(TCPClient::NotConnectedError)
|
135
135
|
end
|
136
136
|
|
137
|
-
it '
|
137
|
+
it 'fails when write is called' do
|
138
138
|
expect { client.write('?!') }.to raise_error(TCPClient::NotConnectedError)
|
139
139
|
end
|
140
140
|
|
@@ -149,14 +149,10 @@ RSpec.describe TCPClient do
|
|
149
149
|
end
|
150
150
|
end
|
151
151
|
|
152
|
-
xdescribe '.open' do
|
153
|
-
end
|
154
|
-
|
155
|
-
xdescribe '.with_deadline' do
|
156
|
-
end
|
157
|
-
|
158
152
|
context 'when not using SSL' do
|
159
153
|
describe '#connect' do
|
154
|
+
subject(:client) { TCPClient.new }
|
155
|
+
|
160
156
|
it 'configures the socket' do
|
161
157
|
expect_any_instance_of(::Socket).to receive(:sync=).once.with(true)
|
162
158
|
expect_any_instance_of(::Socket).to receive(:setsockopt)
|
@@ -169,7 +165,7 @@ RSpec.describe TCPClient do
|
|
169
165
|
.once
|
170
166
|
.with(false)
|
171
167
|
expect_any_instance_of(::Socket).to receive(:connect)
|
172
|
-
|
168
|
+
client.connect('localhost:1234', configuration)
|
173
169
|
end
|
174
170
|
|
175
171
|
context 'when a timeout is specified' do
|
@@ -177,7 +173,26 @@ RSpec.describe TCPClient do
|
|
177
173
|
expect_any_instance_of(::Socket).to receive(:connect_nonblock)
|
178
174
|
.once
|
179
175
|
.with(kind_of(String), exception: false)
|
180
|
-
|
176
|
+
client.connect('localhost:1234', configuration, timeout: 10)
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'is returns itself' do
|
180
|
+
allow_any_instance_of(::Socket).to receive(:connect_nonblock).with(
|
181
|
+
kind_of(String),
|
182
|
+
exception: false
|
183
|
+
)
|
184
|
+
result = client.connect('localhost:1234', configuration, timeout: 10)
|
185
|
+
|
186
|
+
expect(result).to be client
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'is not closed' do
|
190
|
+
allow_any_instance_of(::Socket).to receive(:connect_nonblock).with(
|
191
|
+
kind_of(String),
|
192
|
+
exception: false
|
193
|
+
)
|
194
|
+
client.connect('localhost:1234', configuration, timeout: 10)
|
195
|
+
expect(client).not_to be_closed
|
181
196
|
end
|
182
197
|
|
183
198
|
context 'when the connection can not be established in time' do
|
@@ -188,25 +203,29 @@ RSpec.describe TCPClient do
|
|
188
203
|
|
189
204
|
it 'raises an exception' do
|
190
205
|
expect do
|
191
|
-
|
192
|
-
'localhost:1234',
|
193
|
-
configuration,
|
194
|
-
timeout: 0.25
|
195
|
-
)
|
206
|
+
client.connect('localhost:1234', configuration, timeout: 0.1)
|
196
207
|
end.to raise_error(TCPClient::ConnectTimeoutError)
|
197
208
|
end
|
198
209
|
|
199
210
|
it 'allows to raise a custom exception' do
|
200
211
|
exception = Class.new(StandardError)
|
201
212
|
expect do
|
202
|
-
|
213
|
+
client.connect(
|
203
214
|
'localhost:1234',
|
204
215
|
configuration,
|
205
|
-
timeout: 0.
|
216
|
+
timeout: 0.1,
|
206
217
|
exception: exception
|
207
218
|
)
|
208
219
|
end.to raise_error(exception)
|
209
220
|
end
|
221
|
+
|
222
|
+
it 'is still closed' do
|
223
|
+
begin
|
224
|
+
client.connect('localhost:1234', configuration, timeout: 0.1)
|
225
|
+
rescue TCPClient::ConnectTimeoutError
|
226
|
+
end
|
227
|
+
expect(client).to be_closed
|
228
|
+
end
|
210
229
|
end
|
211
230
|
end
|
212
231
|
|
@@ -226,7 +245,7 @@ RSpec.describe TCPClient do
|
|
226
245
|
end
|
227
246
|
|
228
247
|
SOCKET_ERRORS.each do |error_class|
|
229
|
-
it "raises
|
248
|
+
it "raises TCPClient::NetworkError when a #{error_class} appeared" do
|
230
249
|
allow_any_instance_of(::Socket).to receive(:connect) {
|
231
250
|
raise error_class
|
232
251
|
}
|
@@ -270,18 +289,61 @@ RSpec.describe TCPClient do
|
|
270
289
|
expect(client.read(timeout: 10)).to be data
|
271
290
|
end
|
272
291
|
|
292
|
+
context 'when socket closed before any data can be read' do
|
293
|
+
it 'returns empty buffer' do
|
294
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
295
|
+
.and_return(nil)
|
296
|
+
expect(client.read(timeout: 10)).to be_empty
|
297
|
+
end
|
298
|
+
|
299
|
+
it 'is closed' do
|
300
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
301
|
+
.and_return(nil)
|
302
|
+
|
303
|
+
client.read(timeout: 10)
|
304
|
+
expect(client).to be_closed
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
273
308
|
context 'when data can not be fetched in a single chunk' do
|
274
309
|
it 'reads chunk by chunk' do
|
275
310
|
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
276
311
|
.once
|
277
|
-
.with(
|
312
|
+
.with(instance_of(Integer), exception: false)
|
278
313
|
.and_return(data)
|
279
314
|
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
280
315
|
.once
|
281
|
-
.with(
|
316
|
+
.with(instance_of(Integer), exception: false)
|
282
317
|
.and_return(data)
|
283
318
|
expect(client.read(data_size * 2, timeout: 10)).to eq data * 2
|
284
319
|
end
|
320
|
+
|
321
|
+
context 'when socket closed before enough data is avail' do
|
322
|
+
it 'returns available data only' do
|
323
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
324
|
+
.once
|
325
|
+
.with(instance_of(Integer), exception: false)
|
326
|
+
.and_return(data)
|
327
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
328
|
+
.once
|
329
|
+
.with(instance_of(Integer), exception: false)
|
330
|
+
.and_return(nil)
|
331
|
+
expect(client.read(data_size * 2, timeout: 10)).to eq data
|
332
|
+
end
|
333
|
+
|
334
|
+
it 'is closed' do
|
335
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
336
|
+
.once
|
337
|
+
.with(instance_of(Integer), exception: false)
|
338
|
+
.and_return(data)
|
339
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
340
|
+
.once
|
341
|
+
.with(instance_of(Integer), exception: false)
|
342
|
+
.and_return(nil)
|
343
|
+
client.read(data_size * 2, timeout: 10)
|
344
|
+
expect(client).to be_closed
|
345
|
+
end
|
346
|
+
end
|
285
347
|
end
|
286
348
|
|
287
349
|
context 'when the data can not be read in time' do
|
@@ -329,6 +391,146 @@ RSpec.describe TCPClient do
|
|
329
391
|
end
|
330
392
|
end
|
331
393
|
|
394
|
+
describe '#readline' do
|
395
|
+
before { allow_any_instance_of(::Socket).to receive(:connect) }
|
396
|
+
|
397
|
+
it 'reads from socket' do
|
398
|
+
expect_any_instance_of(::Socket).to receive(:readline)
|
399
|
+
.once
|
400
|
+
.with($/, chomp: false)
|
401
|
+
.and_return("Hello World\n")
|
402
|
+
expect(client.readline).to eq "Hello World\n"
|
403
|
+
end
|
404
|
+
|
405
|
+
context 'when a separator is specified' do
|
406
|
+
it 'forwards the separator' do
|
407
|
+
expect_any_instance_of(::Socket).to receive(:readline)
|
408
|
+
.once
|
409
|
+
.with('/', chomp: false)
|
410
|
+
.and_return('Hello/')
|
411
|
+
expect(client.readline('/')).to eq 'Hello/'
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
context 'when chomp is true' do
|
416
|
+
it 'forwards the flag' do
|
417
|
+
expect_any_instance_of(::Socket).to receive(:readline)
|
418
|
+
.once
|
419
|
+
.with($/, chomp: true)
|
420
|
+
.and_return('Hello World')
|
421
|
+
expect(client.readline(chomp: true)).to eq 'Hello World'
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
context 'when a timeout is specified' do
|
426
|
+
it 'checks the time' do
|
427
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
428
|
+
.and_return("Hello World\nHello World\n")
|
429
|
+
expect(client.readline(timeout: 10)).to eq "Hello World\n"
|
430
|
+
end
|
431
|
+
|
432
|
+
it 'optional chomps the line' do
|
433
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
434
|
+
.and_return("Hello World\nHello World\n")
|
435
|
+
expect(client.readline(chomp: true, timeout: 10)).to eq 'Hello World'
|
436
|
+
end
|
437
|
+
|
438
|
+
it 'uses the given separator' do
|
439
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
440
|
+
.and_return("Hello/World\n")
|
441
|
+
expect(client.readline('/', timeout: 10)).to eq 'Hello/'
|
442
|
+
end
|
443
|
+
|
444
|
+
context 'when data can not be fetched in a single chunk' do
|
445
|
+
it 'reads chunk by chunk' do
|
446
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
447
|
+
.once
|
448
|
+
.with(instance_of(Integer), exception: false)
|
449
|
+
.and_return('Hello ')
|
450
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
451
|
+
.once
|
452
|
+
.with(instance_of(Integer), exception: false)
|
453
|
+
.and_return('World')
|
454
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
455
|
+
.once
|
456
|
+
.with(instance_of(Integer), exception: false)
|
457
|
+
.and_return("\nAnd so...")
|
458
|
+
expect(client.readline(timeout: 10)).to eq "Hello World\n"
|
459
|
+
end
|
460
|
+
|
461
|
+
context 'when socket closed before enough data is avail' do
|
462
|
+
it 'returns available data only' do
|
463
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
464
|
+
.once
|
465
|
+
.with(instance_of(Integer), exception: false)
|
466
|
+
.and_return('Hello ')
|
467
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
468
|
+
.once
|
469
|
+
.with(instance_of(Integer), exception: false)
|
470
|
+
.and_return(nil)
|
471
|
+
expect(client.readline(timeout: 10)).to eq 'Hello '
|
472
|
+
end
|
473
|
+
|
474
|
+
it 'is closed' do
|
475
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
476
|
+
.once
|
477
|
+
.with(instance_of(Integer), exception: false)
|
478
|
+
.and_return('Hello ')
|
479
|
+
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
480
|
+
.once
|
481
|
+
.with(instance_of(Integer), exception: false)
|
482
|
+
.and_return(nil)
|
483
|
+
client.readline(timeout: 10)
|
484
|
+
expect(client).to be_closed
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
context 'when the data can not be read in time' do
|
490
|
+
before do
|
491
|
+
allow_any_instance_of(::Socket).to receive(:read_nonblock)
|
492
|
+
.and_return(:wait_readable)
|
493
|
+
end
|
494
|
+
it 'raises an exception' do
|
495
|
+
expect { client.readline(timeout: 0.25) }.to raise_error(
|
496
|
+
TCPClient::ReadTimeoutError
|
497
|
+
)
|
498
|
+
end
|
499
|
+
|
500
|
+
it 'allows to raise a custom exception' do
|
501
|
+
exception = Class.new(StandardError)
|
502
|
+
expect do
|
503
|
+
client.read(timeout: 0.25, exception: exception)
|
504
|
+
end.to raise_error(exception)
|
505
|
+
end
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
context 'when a SocketError appears' do
|
510
|
+
it 'does not handle it' do
|
511
|
+
allow_any_instance_of(::Socket).to receive(:read) {
|
512
|
+
raise SocketError
|
513
|
+
}
|
514
|
+
expect { client.read(10) }.to raise_error(SocketError)
|
515
|
+
end
|
516
|
+
|
517
|
+
context 'when normalize_network_errors is configured' do
|
518
|
+
let(:configuration) do
|
519
|
+
TCPClient::Configuration.create(normalize_network_errors: true)
|
520
|
+
end
|
521
|
+
|
522
|
+
SOCKET_ERRORS.each do |error_class|
|
523
|
+
it "raises a TCPClient::NetworkError when a #{error_class} appeared" do
|
524
|
+
allow_any_instance_of(::Socket).to receive(:read) {
|
525
|
+
raise error_class
|
526
|
+
}
|
527
|
+
expect { client.read(12) }.to raise_error(TCPClient::NetworkError)
|
528
|
+
end
|
529
|
+
end
|
530
|
+
end
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
332
534
|
describe '#write' do
|
333
535
|
let(:data) { 'some bytes' }
|
334
536
|
let(:data_size) { data.bytesize }
|
@@ -463,20 +665,16 @@ RSpec.describe TCPClient do
|
|
463
665
|
.with(kind_of(String), exception: false)
|
464
666
|
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
465
667
|
.once
|
466
|
-
.with(
|
467
|
-
.and_return('
|
668
|
+
.with(instance_of(Integer), exception: false)
|
669
|
+
.and_return('123456789012abcdefgAB')
|
468
670
|
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
469
671
|
.once
|
470
672
|
.with('123456', exception: false)
|
471
673
|
.and_return(6)
|
472
674
|
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
473
675
|
.once
|
474
|
-
.with(
|
475
|
-
.and_return('
|
476
|
-
expect_any_instance_of(::Socket).to receive(:read_nonblock)
|
477
|
-
.once
|
478
|
-
.with(7, exception: false)
|
479
|
-
.and_return('ABCDEFG')
|
676
|
+
.with(instance_of(Integer), exception: false)
|
677
|
+
.and_return('CDEFG')
|
480
678
|
expect_any_instance_of(::Socket).to receive(:write_nonblock)
|
481
679
|
.once
|
482
680
|
.with('abc', exception: false)
|
@@ -575,7 +773,7 @@ RSpec.describe TCPClient do
|
|
575
773
|
# :set_params
|
576
774
|
# )
|
577
775
|
# .once
|
578
|
-
# .with(
|
776
|
+
# .with(max_version: :TLS1_3, min_version: :TLS1_2)
|
579
777
|
# .and_call_original
|
580
778
|
expect_any_instance_of(::OpenSSL::SSL::SSLSocket).to receive(
|
581
779
|
:sync_close=
|
data/tcp-client.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
|
10
10
|
spec.author = 'Mike Blumtritt'
|
11
11
|
spec.summary = 'A TCP client implementation with working timeout support.'
|
12
|
-
spec.description = <<~
|
12
|
+
spec.description = <<~DESCRIPTION
|
13
13
|
This Gem implements a TCP client with (optional) SSL support.
|
14
14
|
It is an easy to use, versatile configurable client that can correctly
|
15
15
|
handle time limits.
|
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
|
|
17
17
|
predefined/configurable time limits for each method
|
18
18
|
(`connect`, `read`, `write`). Deadlines for a sequence of read/write
|
19
19
|
actions can also be monitored.
|
20
|
-
|
20
|
+
DESCRIPTION
|
21
21
|
|
22
22
|
spec.homepage = 'https://github.com/mblumtritt/tcp-client'
|
23
23
|
spec.license = 'BSD-3-Clause'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tcp-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Blumtritt
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-07-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -129,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
129
129
|
- !ruby/object:Gem::Version
|
130
130
|
version: '0'
|
131
131
|
requirements: []
|
132
|
-
rubygems_version: 3.
|
132
|
+
rubygems_version: 3.3.7
|
133
133
|
signing_key:
|
134
134
|
specification_version: 4
|
135
135
|
summary: A TCP client implementation with working timeout support.
|