tcp-client 0.7.0 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 582575d6b5a66a264900141fd5010a1a80a363e15d0ca369f262889d99d932d0
4
- data.tar.gz: a0c2335a50addbf5f424d869cd2931256a1334a0a075530ef3873d9065246834
3
+ metadata.gz: d29104bd490679a8fcb517f949e021cd1d2da263f3a60d622790bc2349faffab
4
+ data.tar.gz: c7ebaf4f9f83eaf68b51bc47bb91fc5be5d446ef3e80b009bc93bca9d3508ef5
5
5
  SHA512:
6
- metadata.gz: b160bd656c4f6091a669cf65349e54aeaa2e7c057e0a5a3c896edf5e8ab91166a3a5cdafedfe84e4b04d5a79e0602fb2433c6ff9144e6881746f3607b8d8290b
7
- data.tar.gz: 6726ebdfbc777258e62b3d915fa7e4a57f7ae514d6259ce5f0051ee649294142acf09faac490901e32083072d7a68dd4ce71fa697e0bf05c0d9556c0d57bb6a5
6
+ metadata.gz: 5e8c282afbefecb35973f7c563cb07782f8dd0e5df760c89d79aa42424784818c5c0e056c40139554642da8b24823aa9fbb7149e0ed7234f40cc51ea5a7a8165
7
+ data.tar.gz: 2281b62117529f4bbeff55e297b67f8142235772b112d30f9530113e23ab4012242d5eb2fed4e8293a40836455259d37c91403f50714f292ee6e531250e4cc76
data/.gitignore CHANGED
@@ -1,4 +1,6 @@
1
- local/
2
1
  tmp/
3
2
  pkg/
3
+ local/
4
+ doc/
5
+ .yardoc/
4
6
  gems.locked
data/.yardopts ADDED
@@ -0,0 +1,5 @@
1
+ --readme README.md
2
+ --title 'tcp-client Documentation'
3
+ --charset utf-8
4
+ --markup markdown
5
+ 'lib/**/*.rb' - 'LICENSE'
data/README.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  A TCP client implementation with working timeout support.
4
4
 
5
+ - Gem: [rubygems.org](https://rubygems.org/gems/tcp-client)
6
+ - Source: [github.com](https://github.com/mblumtritt/tcp-client)
7
+ - Help: [rubydoc.info](https://rubydoc.info/github/mblumtritt/tcp-client/main/index)
8
+
9
+
5
10
  ## Description
6
11
 
7
12
  This Gem implements a TCP client with (optional) SSL support. It is an easy to use, versatile configurable client that can correctly handle time limits. Unlike other implementations, this client respects predefined/configurable time limits for each method (`connect`, `read`, `write`). Deadlines for a sequence of read/write actions can also be monitored.
@@ -11,24 +16,28 @@ This Gem implements a TCP client with (optional) SSL support. It is an easy to u
11
16
  ```ruby
12
17
  require 'tcp-client'
13
18
 
14
- TCPClient.configure do |cfg|
15
- cfg.connect_timeout = 1 # limit connect time the server to 1 second
16
- cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
17
- end
18
-
19
- TCPClient.open('www.google.com:443') do |client|
20
- # next sequence should not last longer than 0.5 seconds
21
- client.with_deadline(0.5) do
22
- # simple HTTP get request
23
- pp client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
24
-
25
- # read "HTTP/1.1 " + 3 byte HTTP status code
26
- pp client.read(12)
27
- end
19
+ # create a configuration:
20
+ # - don't use internal buffering
21
+ # - use TLS 1.2 or TLS 1.3
22
+ cfg = TCPClient::Configuration.create(
23
+ buffered: false,
24
+ ssl_params: {min_version: :TLS1_2, max_version: :TLS1_3}
25
+ )
26
+
27
+ # request to Google.com:
28
+ # - limit all network interactions to 1.5 seconds
29
+ # - use the Configuration cfg
30
+ # - send a simple HTTP get request
31
+ # - read 12 byte: "HTTP/1.1 " + 3 byte HTTP status code
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.read(12) # => "HTTP/1.1 200"
28
35
  end
29
36
  ```
30
37
 
31
- ### Installation
38
+ For more samples see [the samples dir](https://github.com/mblumtritt/tcp-client/tree/main/sample)
39
+
40
+ ## Installation
32
41
 
33
42
  Use [Bundler](http://gembundler.com/) to use TCPClient in your own project:
34
43
 
@@ -41,13 +50,13 @@ gem 'tcp-client'
41
50
  and install it by running Bundler:
42
51
 
43
52
  ```bash
44
- $ bundle
53
+ bundle
45
54
  ```
46
55
 
47
56
  To install the gem globally use:
48
57
 
49
58
  ```bash
50
- $ gem install tcp-client
59
+ gem install tcp-client
51
60
  ```
52
61
 
53
62
  After that you need only a single line of code in your project to have all tools on board:
data/gems.rb CHANGED
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source('https://rubygems.org') { gemspec }
3
+ source 'https://rubygems.org'
4
+ gemspec
@@ -3,9 +3,51 @@
3
3
  require 'socket'
4
4
 
5
5
  class TCPClient
6
+ #
7
+ # The address used by a TCPClient
8
+ #
6
9
  class Address
7
- attr_reader :hostname, :addrinfo
10
+ #
11
+ # @return [String] the host name
12
+ #
13
+ attr_reader :hostname
8
14
 
15
+ #
16
+ # @return [Addrinfo] the address info
17
+ #
18
+ attr_reader :addrinfo
19
+
20
+ #
21
+ # Initializes an address
22
+ # @overload initialize(addr)
23
+ # The addr can be specified as
24
+ #
25
+ # - a valid named address containing the port like "my.host.test:80"
26
+ # - a valid TCPv4 address like "142.250.181.206:80"
27
+ # - a valid TCPv6 address like
28
+ # "[2001:16b8:5093:3500:ad77:abe6:eb88:47b6]:80"
29
+ #
30
+ # @example create an Address instance with a host name and port
31
+ # Address.new('www.google.com:80')
32
+ #
33
+ # @param addr [String] address containing host and port name
34
+ #
35
+ #
36
+ # @overload initialize(addrinfo)
37
+ #
38
+ # @example create an Address with an Addrinfo
39
+ # Address.new(Addrinfo.tcp('www.google.com', 'http'))
40
+ #
41
+ # @param addrinfo [Addrinfo] containing the addressed host and port
42
+ #
43
+ # @overload initialize(port)
44
+ # Adresses the port on the local machine.
45
+ #
46
+ # @example create an Address for localhost on port 80
47
+ # Address.new(80)
48
+ #
49
+ # @param port [Integer] the addressed port
50
+ #
9
51
  def initialize(addr)
10
52
  case addr
11
53
  when self.class
@@ -20,20 +62,28 @@ class TCPClient
20
62
  @addrinfo.freeze
21
63
  end
22
64
 
65
+ #
66
+ # @return [String] text representation of self as "<host>:<port>"
67
+ #
23
68
  def to_s
24
69
  return "[#{@hostname}]:#{@addrinfo.ip_port}" if @hostname.index(':') # IP6
25
70
  "#{@hostname}:#{@addrinfo.ip_port}"
26
71
  end
27
72
 
73
+ #
74
+ # @return [Hash] containing the host and port
75
+ #
28
76
  def to_h
29
77
  { host: @hostname, port: @addrinfo.ip_port }
30
78
  end
31
79
 
80
+ # @!visibility private
32
81
  def ==(other)
33
82
  to_h == other.to_h
34
83
  end
35
84
  alias eql? ==
36
85
 
86
+ # @!visibility private
37
87
  def equal?(other)
38
88
  self.class == other.class && self == other
39
89
  end
@@ -3,126 +3,326 @@
3
3
  require_relative 'errors'
4
4
 
5
5
  class TCPClient
6
+ #
7
+ # A Configuration is used to configure the behavior of a {TCPClient} instance.
8
+ #
9
+ # It allows to specify to monitor timeout, how to handle exceptions, if SSL
10
+ # should be used and to setup the underlying Socket.
11
+ #
6
12
  class Configuration
13
+ #
14
+ # @overload create(options)
15
+ # Shorthand to create a new configuration with given options.
16
+ #
17
+ # @example
18
+ # config = TCPClient::Configuration.create(buffered: false)
19
+ #
20
+ # @param options [Hash] see {#initialize} for details
21
+ #
22
+ # @return [Configuration] the initialized configuration
23
+ #
24
+ # @overload create(&block)
25
+ # Shorthand to create a new configuration within a code block.
26
+ #
27
+ # @example
28
+ # config = TCPClient::Configuration.create do |cfg|
29
+ # cfg.buffered = false
30
+ # cfg.ssl_params = { min_version: :TLS1_2, max_version: :TLS1_3 }
31
+ # end
32
+ #
33
+ # @yieldparam configuration {Configuration}
34
+ #
35
+ # @return [Configuration] the initialized configuration
36
+ #
7
37
  def self.create(options = {})
8
- ret = new(options)
9
- yield(ret) if block_given?
10
- ret
11
- end
12
-
13
- attr_reader :buffered,
14
- :keep_alive,
15
- :reverse_lookup,
16
- :connect_timeout,
17
- :read_timeout,
18
- :write_timeout,
19
- :connect_timeout_error,
20
- :read_timeout_error,
21
- :write_timeout_error
22
- attr_accessor :ssl_params
38
+ configuration = new(options)
39
+ yield(configuration) if block_given?
40
+ configuration
41
+ end
23
42
 
43
+ #
44
+ # Intializes the instance with given options.
45
+ #
46
+ # @param options [Hash]
47
+ # @option options [Boolean] :buffered, see {#buffered}
48
+ # @option options [Boolean] :keep_alive, see {#keep_alive}
49
+ # @option options [Boolean] :reverse_lookup, see {#reverse_lookup}
50
+ # @option options [Hash<Symbol, Object>] :ssl_params, see {#ssl_params}
51
+ # @option options [Numeric] :connect_timeout, see {#connect_timeout}
52
+ # @option options [Exception] :connect_timeout_error, see
53
+ # {#connect_timeout_error}
54
+ # @option options [Numeric] :read_timeout, see {#read_timeout}
55
+ # @option options [Exception] :read_timeout_error, see {#read_timeout_error}
56
+ # @option options [Numeric] :write_timeout, see {#write_timeout}
57
+ # @option options [Exception] :write_timeout_error, see
58
+ # {#write_timeout_error}
59
+ # @option options [Boolean] :normalize_network_errors, see
60
+ # {#normalize_network_errors}
61
+ #
62
+ #
24
63
  def initialize(options = {})
25
64
  @buffered = @keep_alive = @reverse_lookup = true
26
65
  self.timeout = @ssl_params = nil
27
66
  @connect_timeout_error = ConnectTimeoutError
28
67
  @read_timeout_error = ReadTimeoutError
29
68
  @write_timeout_error = WriteTimeoutError
69
+ @normalize_network_errors = false
30
70
  options.each_pair { |attribute, value| set(attribute, value) }
31
71
  end
32
72
 
33
- def freeze
34
- @ssl_params.freeze
35
- super
73
+ # @!group Instance Attributes Socket Level
74
+
75
+ #
76
+ # Enables/disables use of Socket-level buffering
77
+ #
78
+ # @return [true] if the connection is allowed to use internal buffers
79
+ # (default)
80
+ # @return [false] if buffering is not allowed
81
+ #
82
+ attr_reader :buffered
83
+
84
+ def buffered=(value)
85
+ @buffered = value ? true : false
36
86
  end
37
87
 
38
- def initialize_copy(_org)
39
- super
40
- @ssl_params = Hash[@ssl_params] if @ssl_params
41
- self
88
+ #
89
+ # Enables/disables use of Socket-level keep alive handling.
90
+ #
91
+ # @return [true] if the connection is allowed to use keep alive signals
92
+ # (default)
93
+ # @return [false] if the connection should not check keep alive
94
+ #
95
+ attr_reader :keep_alive
96
+
97
+ def keep_alive=(value)
98
+ @keep_alive = value ? true : false
99
+ end
100
+
101
+ #
102
+ # Enables/disables address lookup.
103
+ #
104
+ # @return [true] if the connection is allowed to lookup the address
105
+ # (default)
106
+ # @return [false] if the address lookup is not required
107
+ #
108
+ attr_reader :reverse_lookup
109
+
110
+ def reverse_lookup=(value)
111
+ @reverse_lookup = value ? true : false
42
112
  end
43
113
 
114
+ #
115
+ # @!parse attr_reader :ssl?
116
+ # @return [Boolean] wheter SSL is configured, see {#ssl_params}
117
+ #
44
118
  def ssl?
45
119
  @ssl_params ? true : false
46
120
  end
47
121
 
48
- def ssl=(value)
122
+ #
123
+ # Parameters used to initialize a SSL context.
124
+ #
125
+ # @return [Hash<Symbol, Object>] SSL parameters for the SSL context
126
+ # @return [nil] if no SSL should be used (default)
127
+ #
128
+ attr_reader :ssl_params
129
+
130
+ def ssl_params=(value)
49
131
  @ssl_params =
50
- if Hash === value
51
- Hash[value]
132
+ if value.respond_to?(:to_hash)
133
+ Hash[value.to_hash]
134
+ elsif value.respond_to?(:to_h)
135
+ Hash[value.to_h]
52
136
  else
53
137
  value ? {} : nil
54
138
  end
55
139
  end
140
+ alias ssl= ssl_params=
56
141
 
57
- def buffered=(value)
58
- @buffered = value ? true : false
142
+ # @!endgroup
143
+
144
+ # @!group Instance Attributes Timeout Monitoring
145
+
146
+ #
147
+ # The maximum time in seconds to establish a connection.
148
+ #
149
+ # @return [Numeric] maximum time in seconds
150
+ # @return [nil] if the connect time should not be monitored (default)
151
+ #
152
+ # @see TCPClient#connect
153
+ #
154
+ attr_reader :connect_timeout
155
+
156
+ def connect_timeout=(value)
157
+ @connect_timeout = seconds(value)
59
158
  end
60
159
 
61
- def keep_alive=(value)
62
- @keep_alive = value ? true : false
160
+ #
161
+ # The exception class which will be raised if {TCPClient#connect} can not
162
+ # be finished in time.
163
+ #
164
+ # @return [Class] exception class raised
165
+ # @raise [NotAnExceptionError] if given argument is not an Exception class
166
+ #
167
+ attr_reader :connect_timeout_error
168
+
169
+ def connect_timeout_error=(value)
170
+ raise(NotAnExceptionError, value) unless exception_class?(value)
171
+ @connect_timeout_error = value
63
172
  end
64
173
 
65
- def reverse_lookup=(value)
66
- @reverse_lookup = value ? true : false
174
+ #
175
+ # The maximum time in seconds to read from a connection.
176
+ #
177
+ # @return [Numeric] maximum time in seconds
178
+ # @return [nil] if the read time should not be monitored (default)
179
+ #
180
+ # @see TCPClient#read
181
+ #
182
+ attr_reader :read_timeout
183
+
184
+ def read_timeout=(value)
185
+ @read_timeout = seconds(value)
67
186
  end
68
187
 
69
- def timeout=(seconds)
70
- @connect_timeout = @write_timeout = @read_timeout = seconds(seconds)
188
+ #
189
+ # The exception class which will be raised if {TCPClient#read} can not be
190
+ # finished in time.
191
+ #
192
+ # @return [Class] exception class raised
193
+ # @raise [NotAnExceptionError] if given argument is not an Exception class
194
+ #
195
+ attr_reader :read_timeout_error
196
+
197
+ def read_timeout_error=(value)
198
+ raise(NotAnExceptionError, value) unless exception_class?(value)
199
+ @read_timeout_error = value
71
200
  end
72
201
 
73
- def connect_timeout=(seconds)
74
- @connect_timeout = seconds(seconds)
202
+ #
203
+ # The maximum time in seconds to write to a connection.
204
+ #
205
+ # @return [Numeric] maximum time in seconds
206
+ # @return [nil] if the write time should not be monitored (default)
207
+ #
208
+ # @see TCPClient#write
209
+ #
210
+ attr_reader :write_timeout
211
+
212
+ def write_timeout=(value)
213
+ @write_timeout = seconds(value)
75
214
  end
76
215
 
77
- def read_timeout=(seconds)
78
- @read_timeout = seconds(seconds)
216
+ #
217
+ # The exception class which will be raised if {TCPClient#write} can not be
218
+ # finished in time.
219
+ #
220
+ # @return [Class] exception class raised
221
+ # @raise [NotAnExceptionError] if given argument is not an Exception class
222
+ #
223
+ attr_reader :write_timeout_error
224
+
225
+ def write_timeout_error=(value)
226
+ raise(NotAnExceptionError, value) unless exception_class?(value)
227
+ @write_timeout_error = value
79
228
  end
80
229
 
81
- def write_timeout=(seconds)
82
- @write_timeout = seconds(seconds)
230
+ #
231
+ # @attribute [w] timeout
232
+ # Shorthand to set maximum time in seconds for all timeut monitoring.
233
+ #
234
+ # @return [Numeric] maximum time in seconds for any actwion
235
+ # @return [nil] if all timeout monitoring should be disabled (default)
236
+ #
237
+ # @see #connect_timeout
238
+ # @see #read_timeout
239
+ # @see #write_timeout
240
+ #
241
+ def timeout=(value)
242
+ @connect_timeout = @write_timeout = @read_timeout = seconds(value)
83
243
  end
84
244
 
85
- def timeout_error=(exception)
86
- raise(NotAnExceptionError, exception) unless exception_class?(exception)
245
+ #
246
+ # @attribute [w] timeout_error
247
+ # Shorthand to set the exception class wich will by raised by any timeut.
248
+ #
249
+ # @return [Class] exception class raised
250
+ #
251
+ # @raise [NotAnExceptionError] if given argument is not an Exception class
252
+ #
253
+ # @see #connect_timeout_error
254
+ # @see #read_timeout_error
255
+ # @see #write_timeout_error
256
+ #
257
+ def timeout_error=(value)
258
+ raise(NotAnExceptionError, value) unless exception_class?(value)
87
259
  @connect_timeout_error =
88
- @read_timeout_error = @write_timeout_error = exception
260
+ @read_timeout_error = @write_timeout_error = value
89
261
  end
90
262
 
91
- def connect_timeout_error=(exception)
92
- raise(NotAnExceptionError, exception) unless exception_class?(exception)
93
- @connect_timeout_error = exception
94
- end
263
+ # @!endgroup
95
264
 
96
- def read_timeout_error=(exception)
97
- raise(NotAnExceptionError, exception) unless exception_class?(exception)
98
- @read_timeout_error = exception
99
- end
265
+ #
266
+ # Enables/disables if network exceptions should be raised as {NetworkError}.
267
+ #
268
+ # This allows to handle all network/socket related exceptions like
269
+ # `SocketError`, `OpenSSL::SSL::SSLError`, `IOError`, etc. in a uniform
270
+ # manner. If this option is set to true all these error cases are raised as
271
+ # {NetworkError} and can be easily captured.
272
+ #
273
+ # @return [true] if all network exceptions should be raised as
274
+ # {NetworkError}
275
+ # @return [false] if socket/system errors should not be normalzed (default)
276
+ #
277
+ attr_reader :normalize_network_errors
100
278
 
101
- def write_timeout_error=(exception)
102
- raise(NotAnExceptionError, exception) unless exception_class?(exception)
103
- @write_timeout_error = exception
279
+ def normalize_network_errors=(value)
280
+ @normalize_network_errors = value ? true : false
104
281
  end
105
282
 
283
+ #
284
+ # Convert `self` to a Hash containing all attributes.
285
+ #
286
+ # @return [Hash<Symbol, Object>]
287
+ #
288
+ # @see #initialize
289
+ #
106
290
  def to_h
107
291
  {
108
292
  buffered: @buffered,
109
293
  keep_alive: @keep_alive,
110
294
  reverse_lookup: @reverse_lookup,
295
+ ssl_params: @ssl_params,
111
296
  connect_timeout: @connect_timeout,
112
- read_timeout: @read_timeout,
113
- write_timeout: @write_timeout,
114
297
  connect_timeout_error: @connect_timeout_error,
298
+ read_timeout: @read_timeout,
115
299
  read_timeout_error: @read_timeout_error,
300
+ write_timeout: @write_timeout,
116
301
  write_timeout_error: @write_timeout_error,
117
- ssl_params: @ssl_params
302
+ normalize_network_errors: @normalize_network_errors
118
303
  }
119
304
  end
120
305
 
306
+ # @!visibility private
307
+ def freeze
308
+ @ssl_params.freeze
309
+ super
310
+ end
311
+
312
+ # @!visibility private
313
+ def initialize_copy(_org)
314
+ super
315
+ @ssl_params = Hash[@ssl_params] if @ssl_params
316
+ self
317
+ end
318
+
319
+ # @!visibility private
121
320
  def ==(other)
122
321
  to_h == other.to_h
123
322
  end
124
323
  alias eql? ==
125
324
 
325
+ # @!visibility private
126
326
  def equal?(other)
127
327
  self.class == other.class && self == other
128
328
  end
@@ -6,16 +6,50 @@ class TCPClient
6
6
  @default_configuration = Configuration.new
7
7
 
8
8
  class << self
9
+ #
10
+ # The default configuration.
11
+ # This is used by default if no dedicated configuration was specified to
12
+ # {.open} or {#connect}.
13
+ #
14
+ # @return [Configuration]
15
+ #
9
16
  attr_reader :default_configuration
10
17
 
18
+ #
19
+ # Configure the {.default_configuration} which is used if no dedicated
20
+ # configuration was specified to {.open} or {#connect}.
21
+ #
22
+ # @example
23
+ # TCPClient.configure do |cfg|
24
+ # cfg.buffered = false
25
+ # cfg.ssl_params = { min_version: :TLS1_2, max_version: :TLS1_3 }
26
+ # end
27
+ #
28
+ # @param options [Hash] see {Configuration#initialize} for details
29
+ #
30
+ # @yieldparam cfg {Configuration} the new configuration
31
+ #
32
+ # @return [Configuration] the new default configuration
33
+ #
11
34
  def configure(options = {}, &block)
12
35
  @default_configuration = Configuration.create(options, &block)
13
36
  end
14
37
  end
15
38
 
16
39
  class Configuration
17
- def self.default
18
- TCPClient.default_configuration
40
+ class << self
41
+ #
42
+ # @!parse attr_reader :default
43
+ # @return [Configuration] used by default if no dedicated configuration
44
+ # was specified
45
+ #
46
+ # @see TCPClient.open
47
+ # @see TCPClient.with_deadline
48
+ # @see TCPClient#connect
49
+ #
50
+ def default
51
+ TCPClient.default_configuration
52
+ end
19
53
  end
20
54
  end
21
55
  end