tcp-client 0.9.3 → 0.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +8 -5
- data/lib/tcp-client/address.rb +16 -7
- data/lib/tcp-client/configuration.rb +37 -45
- data/lib/tcp-client/deadline.rb +1 -3
- data/lib/tcp-client/default_configuration.rb +1 -1
- data/lib/tcp-client/errors.rb +13 -7
- data/lib/tcp-client/mixin/io_with_deadline.rb +126 -88
- data/lib/tcp-client/ssl_socket.rb +1 -1
- data/lib/tcp-client/version.rb +2 -1
- data/lib/tcp-client.rb +87 -51
- data/rakefile.rb +7 -1
- data/sample/google_ssl.rb +8 -5
- data/spec/tcp-client/address_spec.rb +15 -28
- data/spec/tcp-client/configuration_spec.rb +4 -4
- data/spec/tcp_client_spec.rb +227 -23
- metadata +3 -3
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
|
#
|
@@ -30,57 +33,59 @@ class TCPClient
|
|
30
33
|
#
|
31
34
|
# If no `configuration` is given, the {.default_configuration} will be used.
|
32
35
|
#
|
36
|
+
# @overload open(address, configuration = nil)
|
37
|
+
# @yieldparam client [TCPClient] the connected client
|
38
|
+
#
|
39
|
+
# @return [Object] the block result
|
40
|
+
#
|
41
|
+
# @overload open(address, configuration = nil)
|
42
|
+
# @return [TCPClient] the connected client
|
43
|
+
#
|
33
44
|
# If an optional block is given, then the block's result is returned and the
|
34
45
|
# connection will be closed when the block execution ends.
|
35
|
-
# 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
|
36
47
|
# closed.
|
37
48
|
#
|
38
|
-
# If no block is
|
49
|
+
# If no block is given the connected client instance is returned.
|
39
50
|
# This can be used as a shorthand to create & connect a client.
|
40
51
|
#
|
41
|
-
# @param address [Address, String, Addrinfo, Integer] the address
|
42
|
-
#
|
52
|
+
# @param address [Address, String, Addrinfo, Integer] the target address see
|
53
|
+
# {Address#initialize} for valid formats
|
43
54
|
# @param configuration [Configuration] the {Configuration} to be used for
|
44
|
-
#
|
45
|
-
#
|
46
|
-
# @yieldparam client [TCPClient] the connected client
|
47
|
-
# @yieldreturn [Object] any result
|
48
|
-
#
|
49
|
-
# @return [Object, TCPClient] the block result or the connected client
|
55
|
+
# the new instance
|
50
56
|
#
|
51
57
|
# @see #connect
|
52
58
|
#
|
53
59
|
def self.open(address, configuration = nil)
|
54
60
|
client = new
|
55
|
-
client.connect(
|
61
|
+
client.connect(address, configuration)
|
56
62
|
block_given? ? yield(client) : client
|
57
63
|
ensure
|
58
64
|
client.close if block_given?
|
59
65
|
end
|
60
66
|
|
61
67
|
#
|
62
|
-
# Yields
|
63
|
-
# `address`.It limits all {#read} and {#write} actions within the block to
|
68
|
+
# Yields an instance which is connected to the server on the given
|
69
|
+
# `address`. It limits all {#read} and {#write} actions within the block to
|
64
70
|
# the given time.
|
65
71
|
#
|
66
72
|
# It ensures to close the connection when the block execution ends and returns
|
67
|
-
# the block
|
73
|
+
# the block's result.
|
68
74
|
#
|
69
|
-
# 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
|
70
76
|
# closed and which {#read}/{#write} call sequence should not last longer than
|
71
|
-
# the `timeout
|
77
|
+
# the `timeout` seconds.
|
72
78
|
#
|
73
79
|
# If no `configuration` is given, the {.default_configuration} will be used.
|
74
80
|
#
|
75
81
|
# @param timeout [Numeric] maximum time in seconds for all {#read} and
|
76
82
|
# {#write} calls within the block
|
77
|
-
# @param address [Address, String, Addrinfo, Integer] the address
|
78
|
-
#
|
83
|
+
# @param address [Address, String, Addrinfo, Integer] the target address see
|
84
|
+
# {Address#initialize} for valid formats
|
79
85
|
# @param configuration [Configuration] the {Configuration} to be used for
|
80
|
-
#
|
86
|
+
# the instance
|
81
87
|
#
|
82
88
|
# @yieldparam client [TCPClient] the connected client
|
83
|
-
# @yieldreturn [Object] any result
|
84
89
|
#
|
85
90
|
# @return [Object] the block's result
|
86
91
|
#
|
@@ -89,7 +94,6 @@ class TCPClient
|
|
89
94
|
def self.with_deadline(timeout, address, configuration = nil)
|
90
95
|
client = nil
|
91
96
|
raise(NoBlockGivenError) unless block_given?
|
92
|
-
address = Address.new(address)
|
93
97
|
client = new
|
94
98
|
client.with_deadline(timeout) do
|
95
99
|
yield(client.connect(address, configuration))
|
@@ -110,7 +114,7 @@ class TCPClient
|
|
110
114
|
|
111
115
|
#
|
112
116
|
# @!parse attr_reader :closed?
|
113
|
-
# @return [Boolean]
|
117
|
+
# @return [Boolean] whether the connection is closed
|
114
118
|
#
|
115
119
|
def closed?
|
116
120
|
@socket.nil? || @socket.closed?
|
@@ -119,7 +123,7 @@ class TCPClient
|
|
119
123
|
#
|
120
124
|
# Close the current connection if connected.
|
121
125
|
#
|
122
|
-
# @return [
|
126
|
+
# @return [TCPClient] itself
|
123
127
|
#
|
124
128
|
def close
|
125
129
|
@socket&.close
|
@@ -131,25 +135,23 @@ class TCPClient
|
|
131
135
|
end
|
132
136
|
|
133
137
|
#
|
134
|
-
# Establishes a new connection to a given `address`.
|
138
|
+
# Establishes a new connection to a server on given `address`.
|
135
139
|
#
|
136
|
-
# It accepts a connection-specific configuration or uses the
|
137
|
-
# {.default_configuration}.
|
138
|
-
# be a copy of the configuration used for this method call. This allows to
|
139
|
-
# configure the behavior per connection.
|
140
|
+
# It accepts a connection-specific `configuration` or uses the
|
141
|
+
# {.default_configuration}.
|
140
142
|
#
|
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 address
|
145
|
-
#
|
146
|
+
# @param address [Address, String, Addrinfo, Integer] the target address, see
|
147
|
+
# {Address#initialize} for valid formats
|
146
148
|
# @param configuration [Configuration] the {Configuration} to be used for
|
147
149
|
# this instance
|
148
150
|
# @param timeout [Numeric] maximum time in seconds to connect
|
149
|
-
# @param exception [Class] exception class to be used when the
|
150
|
-
# reached
|
151
|
+
# @param exception [Class<Exception>] exception class to be used when the
|
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
|
-
#
|
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 }
|
@@ -182,8 +184,8 @@ class TCPClient
|
|
182
184
|
#
|
183
185
|
# @param nbytes [Integer] the number of bytes to read
|
184
186
|
# @param timeout [Numeric] maximum time in seconds to read
|
185
|
-
# @param exception [Class] exception class to be used when the
|
186
|
-
# reached
|
187
|
+
# @param exception [Class<Exception>] exception class to be used when the
|
188
|
+
# read timeout reached
|
187
189
|
#
|
188
190
|
# @return [String] the read buffer
|
189
191
|
#
|
@@ -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.readto_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
|
#
|
@@ -211,7 +246,7 @@ class TCPClient
|
|
211
246
|
end
|
212
247
|
|
213
248
|
#
|
214
|
-
#
|
249
|
+
# Executes a block with a given overall time limit.
|
215
250
|
#
|
216
251
|
# When you like to ensure that a complete {#read}/{#write} communication
|
217
252
|
# sequence with the server is finished before a given amount of time you use
|
@@ -228,9 +263,8 @@ class TCPClient
|
|
228
263
|
# {#write} calls within the block
|
229
264
|
#
|
230
265
|
# @yieldparam client [TCPClient] self
|
231
|
-
# @yieldreturn [Object] any result
|
232
266
|
#
|
233
|
-
# @return [Object] the block
|
267
|
+
# @return [Object] the block's result
|
234
268
|
#
|
235
269
|
# @raise [NoBlockGivenError] if the block is missing
|
236
270
|
#
|
@@ -245,21 +279,23 @@ class TCPClient
|
|
245
279
|
end
|
246
280
|
|
247
281
|
#
|
248
|
-
#
|
282
|
+
# Writes the given `messages` to the server.
|
249
283
|
#
|
250
284
|
# The optional `timeout` and `exception` parameters allow to override the
|
251
285
|
# `write_timeout` and `write_timeout_error` values of the used
|
252
286
|
# {#configuration}.
|
253
287
|
#
|
254
|
-
# @param messages [String] one or more messages to write
|
288
|
+
# @param messages [Array<String>] one or more messages to write
|
255
289
|
# @param timeout [Numeric] maximum time in seconds to write
|
256
|
-
# @param exception [Class] exception class to be used when the
|
257
|
-
# reached
|
290
|
+
# @param exception [Class<Exception>] exception class to be used when the
|
291
|
+
# write timeout reached
|
258
292
|
#
|
259
293
|
# @return [Integer] bytes written
|
260
294
|
#
|
261
295
|
# @raise [NotConnectedError] if {#connect} was not called before
|
262
296
|
#
|
297
|
+
# @see NetworkError
|
298
|
+
#
|
263
299
|
def write(*messages, timeout: nil, exception: nil)
|
264
300
|
raise(NotConnectedError) if closed?
|
265
301
|
deadline = create_deadline(timeout, configuration.write_timeout)
|
data/rakefile.rb
CHANGED
@@ -6,7 +6,13 @@ require 'rspec/core/rake_task'
|
|
6
6
|
require 'yard'
|
7
7
|
|
8
8
|
$stdout.sync = $stderr.sync = true
|
9
|
-
|
9
|
+
|
10
|
+
CLEAN << 'prj' << 'doc'
|
11
|
+
|
12
|
+
CLOBBER << '.yardoc'
|
13
|
+
|
10
14
|
task(:default) { exec('rake --tasks') }
|
15
|
+
|
11
16
|
RSpec::Core::RakeTask.new { |task| task.ruby_opts = %w[-w] }
|
17
|
+
|
12
18
|
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") #=> see response
|
26
|
+
end
|
27
|
+
|
28
|
+
puts(response)
|
@@ -9,8 +9,8 @@ RSpec.describe TCPClient::Address do
|
|
9
9
|
|
10
10
|
it 'points to the given port on localhost' do
|
11
11
|
expect(address.hostname).to eq 'localhost'
|
12
|
+
expect(address.port).to be 42
|
12
13
|
expect(address.to_s).to eq 'localhost:42'
|
13
|
-
expect(address.addrinfo.ip_port).to be 42
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'uses IPv6' do
|
@@ -29,8 +29,9 @@ RSpec.describe TCPClient::Address do
|
|
29
29
|
end
|
30
30
|
|
31
31
|
it 'points to the given host and port' do
|
32
|
-
expect(address.hostname).to eq
|
33
|
-
expect(address.
|
32
|
+
expect(address.hostname).to eq 'localhost'
|
33
|
+
expect(address.port).to be 42
|
34
|
+
expect(address.to_s).to eq 'localhost:42'
|
34
35
|
end
|
35
36
|
|
36
37
|
it 'uses IPv6' do
|
@@ -46,30 +47,21 @@ RSpec.describe TCPClient::Address do
|
|
46
47
|
|
47
48
|
it 'points to the given host and port' do
|
48
49
|
expect(address.hostname).to eq 'localhost'
|
50
|
+
expect(address.port).to be 42
|
49
51
|
expect(address.to_s).to eq 'localhost:42'
|
50
|
-
expect(address.addrinfo.ip_port).to be 42
|
51
|
-
end
|
52
|
-
|
53
|
-
it 'uses IPv6' do
|
54
52
|
expect(address.addrinfo.ip?).to be true
|
55
|
-
expect(address.addrinfo.ipv6?).to be true
|
56
|
-
expect(address.addrinfo.ipv4?).to be false
|
57
53
|
end
|
54
|
+
|
58
55
|
end
|
59
56
|
|
60
57
|
context 'when only a port is provided' do
|
61
|
-
subject(:address) { TCPClient::Address.new(':
|
58
|
+
subject(:address) { TCPClient::Address.new(':42') }
|
62
59
|
|
63
60
|
it 'points to the given port on localhost' do
|
64
|
-
expect(address.hostname).to eq ''
|
65
|
-
expect(address.
|
66
|
-
expect(address.
|
67
|
-
end
|
68
|
-
|
69
|
-
it 'uses IPv4' do
|
61
|
+
expect(address.hostname).to eq 'localhost'
|
62
|
+
expect(address.port).to be 42
|
63
|
+
expect(address.to_s).to eq 'localhost:42'
|
70
64
|
expect(address.addrinfo.ip?).to be true
|
71
|
-
expect(address.addrinfo.ipv6?).to be false
|
72
|
-
expect(address.addrinfo.ipv4?).to be true
|
73
65
|
end
|
74
66
|
end
|
75
67
|
|
@@ -78,14 +70,9 @@ RSpec.describe TCPClient::Address do
|
|
78
70
|
|
79
71
|
it 'points to the given port on localhost' do
|
80
72
|
expect(address.hostname).to eq '::1'
|
73
|
+
expect(address.port).to be 42
|
81
74
|
expect(address.to_s).to eq '[::1]:42'
|
82
|
-
expect(address.addrinfo.ip_port).to be 42
|
83
|
-
end
|
84
|
-
|
85
|
-
it 'uses IPv6' do
|
86
75
|
expect(address.addrinfo.ip?).to be true
|
87
|
-
expect(address.addrinfo.ipv6?).to be true
|
88
|
-
expect(address.addrinfo.ipv4?).to be false
|
89
76
|
end
|
90
77
|
end
|
91
78
|
end
|
@@ -108,13 +95,13 @@ RSpec.describe TCPClient::Address do
|
|
108
95
|
expect(address_a).to eq address_b
|
109
96
|
end
|
110
97
|
|
111
|
-
context 'using the ==
|
98
|
+
context 'using the == operator' do
|
112
99
|
it 'compares to equal' do
|
113
100
|
expect(address_a == address_b).to be true
|
114
101
|
end
|
115
102
|
end
|
116
103
|
|
117
|
-
context 'using the ===
|
104
|
+
context 'using the === operator' do
|
118
105
|
it 'compares to equal' do
|
119
106
|
expect(address_a === address_b).to be true
|
120
107
|
end
|
@@ -129,13 +116,13 @@ RSpec.describe TCPClient::Address do
|
|
129
116
|
expect(address_a).not_to eq address_b
|
130
117
|
end
|
131
118
|
|
132
|
-
context 'using the ==
|
119
|
+
context 'using the == operator' do
|
133
120
|
it 'compares not to equal' do
|
134
121
|
expect(address_a == address_b).to be false
|
135
122
|
end
|
136
123
|
end
|
137
124
|
|
138
|
-
context 'using the ===
|
125
|
+
context 'using the === operator' do
|
139
126
|
it 'compares not to equal' do
|
140
127
|
expect(address_a === address_b).to be false
|
141
128
|
end
|
@@ -233,13 +233,13 @@ RSpec.describe TCPClient::Configuration do
|
|
233
233
|
expect(config_a).to eq config_b
|
234
234
|
end
|
235
235
|
|
236
|
-
context 'using the ==
|
236
|
+
context 'using the == operator' do
|
237
237
|
it 'compares to equal' do
|
238
238
|
expect(config_a == config_b).to be true
|
239
239
|
end
|
240
240
|
end
|
241
241
|
|
242
|
-
context 'using the ===
|
242
|
+
context 'using the === operator' do
|
243
243
|
it 'compares to equal' do
|
244
244
|
expect(config_a === config_b).to be true
|
245
245
|
end
|
@@ -254,13 +254,13 @@ RSpec.describe TCPClient::Configuration do
|
|
254
254
|
expect(config_a).not_to eq config_b
|
255
255
|
end
|
256
256
|
|
257
|
-
context 'using the ==
|
257
|
+
context 'using the == operator' do
|
258
258
|
it 'compares not to equal' do
|
259
259
|
expect(config_a == config_b).to be false
|
260
260
|
end
|
261
261
|
end
|
262
262
|
|
263
|
-
context 'using the ===
|
263
|
+
context 'using the === operator' do
|
264
264
|
it 'compares not to equal' do
|
265
265
|
expect(config_a === config_b).to be false
|
266
266
|
end
|