tcp-client 0.7.0 → 0.9.2

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 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