tcp-client 0.6.0 → 0.9.1

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: 30592a849daf948c39b138e5f2d634761d59328f7a6eab11d45a9b069b8a747d
4
- data.tar.gz: 39d0b9afe676700627a9b671c974ab5465cdb72e5ed6ec5c009add638a2b02b3
3
+ metadata.gz: fe13751730310529097b79992b58a5ed212ecc4119c576c04bcf7b0c4a0373c7
4
+ data.tar.gz: 558f73ee8e06309c8d6ca52cdc4309ceac853a9587b6891959b67f5d49c89413
5
5
  SHA512:
6
- metadata.gz: d6a44381c618b1a89128499f7256bd9559dd4ea1efc5f34b82b681cfcca29f1c51b7ab47b13b513977e3276e298ec5ad02f6619f5ed36320f724e9cd192f925e
7
- data.tar.gz: dfbcb8fb0e4b77770023023afbaee354e71ba4e41fe89daff9e197ce7d7a8a785e7891dbb4801651ae4702b56324d3431695e27acc781fafae9e7336db6c2c52
6
+ metadata.gz: a2a9d54d6de01896b83180e7e94afde9a1e2b8f62ca24c9e91d63a7c809a06286357fdecb5a3ad6fce1dceaaa50067a6f64263c83f4069d07b3ad837db138b9d
7
+ data.tar.gz: ac6168903ebb5c953e850fb97d80281c63b70b208335d34611f57829199e623fac3fbea85959d09d8c9ddec1edbe23ffef23cd9e5e94e788b74b0ded665dd5c2
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/README.md CHANGED
@@ -2,33 +2,43 @@
2
2
 
3
3
  A TCP client implementation with working timeout support.
4
4
 
5
+ [![Gem Version](https://badge.fury.io/rb/tcp-client.svg)](https://badge.fury.io/rb/tcp-client)
6
+
5
7
  ## Description
6
8
 
7
9
  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.
8
10
 
11
+ ## Help
12
+
13
+ The latest help can be found at [rubydoc.info](https://rubydoc.info/github/mblumtritt/tcp-client/main/index)
14
+
9
15
  ## Sample
10
16
 
11
17
  ```ruby
12
18
  require 'tcp-client'
13
19
 
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
20
+ # create a configuration:
21
+ # - don't use internal buffering
22
+ # - use TLS 1.2 or TLS 1.3
23
+ cfg = TCPClient::Configuration.create(
24
+ buffered: false,
25
+ ssl_params: {min_version: :TLS1_2, max_version: :TLS1_3}
26
+ )
27
+
28
+ # request to Google.com:
29
+ # - limit all network interactions to 1.5 seconds
30
+ # - use the Configuration cfg
31
+ # - send a simple HTTP get request
32
+ # - read 12 byte: "HTTP/1.1 " + 3 byte HTTP status code
33
+ TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
34
+ client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") # >= 40
35
+ client.read(12) # => "HTTP/1.1 200"
28
36
  end
29
37
  ```
30
38
 
31
- ### Installation
39
+ For more samples see [the samples dir](https://github.com/mblumtritt/tcp-client/tree/main/sample)
40
+
41
+ ## Installation
32
42
 
33
43
  Use [Bundler](http://gembundler.com/) to use TCPClient in your own project:
34
44
 
@@ -41,13 +51,13 @@ gem 'tcp-client'
41
51
  and install it by running Bundler:
42
52
 
43
53
  ```bash
44
- $ bundle
54
+ bundle
45
55
  ```
46
56
 
47
57
  To install the gem globally use:
48
58
 
49
59
  ```bash
50
- $ gem install tcp-client
60
+ gem install tcp-client
51
61
  ```
52
62
 
53
63
  After that you need only a single line of code in your project to have all tools on board:
@@ -3,9 +3,44 @@
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
+ # - a valid named address containing the port like "my.host.test:80"
25
+ # - a valid TCPv4 address like "142.250.181.206:80"
26
+ # - a valid TCPv6 address like "[2001:16b8:5093:3500:ad77:abe6:eb88:47b6]:80"
27
+ #
28
+ # @param addr [String] address string
29
+ #
30
+ # @overload initialize(address)
31
+ # Used to create a copy
32
+ #
33
+ # @param address [Address]
34
+ #
35
+ # @overload initialize(addrinfo)
36
+ #
37
+ # @param addrinfo [Addrinfo] containing the addressed host and port
38
+ #
39
+ # @overload initialize(port)
40
+ # Adresses the port on the local machine.
41
+ #
42
+ # @param port [Integer] the addressed port
43
+ #
9
44
  def initialize(addr)
10
45
  case addr
11
46
  when self.class
@@ -20,20 +55,28 @@ class TCPClient
20
55
  @addrinfo.freeze
21
56
  end
22
57
 
58
+ #
59
+ # @return [String] text representation of self as "<host>:<port>"
60
+ #
23
61
  def to_s
24
62
  return "[#{@hostname}]:#{@addrinfo.ip_port}" if @hostname.index(':') # IP6
25
63
  "#{@hostname}:#{@addrinfo.ip_port}"
26
64
  end
27
65
 
66
+ #
67
+ # @return [Hash] containing the host and port
68
+ #
28
69
  def to_h
29
70
  { host: @hostname, port: @addrinfo.ip_port }
30
71
  end
31
72
 
73
+ # @!visibility private
32
74
  def ==(other)
33
75
  to_h == other.to_h
34
76
  end
35
77
  alias eql? ==
36
78
 
79
+ # @!visibility private
37
80
  def equal?(other)
38
81
  self.class == other.class && self == other
39
82
  end
@@ -3,126 +3,285 @@
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 initialize a new configuration.
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 cfg {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
+ cfg = new(options)
39
+ yield(cfg) if block_given?
40
+ cfg
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 [Boolean] :normalize_network_errors, see {#normalize_network_errors}
51
+ # @option options [Numeric] :connect_timeout, see {#connect_timeout}
52
+ # @option options [Exception] :connect_timeout_error, see {#connect_timeout_error}
53
+ # @option options [Numeric] :read_timeout, see {#read_timeout}
54
+ # @option options [Exception] :read_timeout_error, see {#read_timeout_error}
55
+ # @option options [Numeric] :write_timeout, see {#write_timeout}
56
+ # @option options [Exception] :write_timeout_error, see {#write_timeout_error}
57
+ # @option options [Hash<Symbol, Object>] :ssl_params, see {#ssl_params}
58
+ #
24
59
  def initialize(options = {})
25
60
  @buffered = @keep_alive = @reverse_lookup = true
26
61
  self.timeout = @ssl_params = nil
27
62
  @connect_timeout_error = ConnectTimeoutError
28
63
  @read_timeout_error = ReadTimeoutError
29
64
  @write_timeout_error = WriteTimeoutError
65
+ @normalize_network_errors = false
30
66
  options.each_pair { |attribute, value| set(attribute, value) }
31
67
  end
32
68
 
33
- def freeze
34
- @ssl_params.freeze
35
- super
36
- end
37
-
38
- def initialize_copy(_org)
39
- super
40
- @ssl_params = Hash[@ssl_params] if @ssl_params
41
- self
42
- end
43
-
44
- def ssl?
45
- @ssl_params ? true : false
46
- end
47
-
48
- def ssl=(value)
49
- @ssl_params =
50
- if Hash === value
51
- Hash[value]
52
- else
53
- value ? {} : nil
54
- end
55
- end
69
+ #
70
+ # Enables/disables use of Socket-level buffers.
71
+ #
72
+ # @return [true] if the connection is allowed to use internal buffers (default)
73
+ # @return [false] if buffering is not allowed
74
+ #
75
+ attr_reader :buffered
56
76
 
57
77
  def buffered=(value)
58
78
  @buffered = value ? true : false
59
79
  end
60
80
 
81
+ #
82
+ # Enables/disables use of Socket-level keep alive handling.
83
+ #
84
+ # @return [true] if the connection is allowed to use keep alive signals (default)
85
+ # @return [false] if the connection should not check keep alive
86
+ #
87
+ attr_reader :keep_alive
88
+
61
89
  def keep_alive=(value)
62
90
  @keep_alive = value ? true : false
63
91
  end
64
92
 
93
+ #
94
+ # Enables/disables address lookup.
95
+ #
96
+ # @return [true] if the connection is allowed to lookup the address (default)
97
+ # @return [false] if the address lookup is not required
98
+ #
99
+ attr_reader :reverse_lookup
100
+
65
101
  def reverse_lookup=(value)
66
102
  @reverse_lookup = value ? true : false
67
103
  end
68
104
 
69
- def timeout=(seconds)
70
- @connect_timeout = @write_timeout = @read_timeout = seconds(seconds)
105
+ #
106
+ # Enables/disables if network exceptions should be raised as {NetworkError}.
107
+ #
108
+ # @return [true] if all network exceptions should be raised as {NetworkError}
109
+ # @return [false] if socket/system errors should not be normalzed (default)
110
+ #
111
+ attr_reader :normalize_network_errors
112
+
113
+ def normalize_network_errors=(value)
114
+ @normalize_network_errors = value ? true : false
71
115
  end
72
116
 
73
- def connect_timeout=(seconds)
74
- @connect_timeout = seconds(seconds)
117
+ #
118
+ # @attribute [w] timeout
119
+ # Shorthand to set timeout value for connect, read and write at once or to disable any timeout monitoring
120
+ #
121
+ # @return [Numeric] maximum time in seconds for any action
122
+ # @return [nil] if all timeout monitoring should be disabled (default)
123
+ #
124
+ # @see #connect_timeout
125
+ # @see #read_timeout
126
+ # @see #write_timeout
127
+ #
128
+ def timeout=(value)
129
+ @connect_timeout = @write_timeout = @read_timeout = seconds(value)
75
130
  end
76
131
 
77
- def read_timeout=(seconds)
78
- @read_timeout = seconds(seconds)
132
+ #
133
+ # @attribute [w] timeout_error
134
+ # Shorthand to configure exception class raised when connect, read or write exceeded the configured timeout
135
+ #
136
+ # @return [Class] exception class raised
137
+ #
138
+ # @raise [NotAnExceptionError] if given argument is not an Exception class
139
+ #
140
+ # @see #connect_timeout_error
141
+ # @see #read_timeout_error
142
+ # @see #write_timeout_error
143
+ #
144
+ def timeout_error=(value)
145
+ raise(NotAnExceptionError, value) unless exception_class?(value)
146
+ @connect_timeout_error =
147
+ @read_timeout_error = @write_timeout_error = value
79
148
  end
80
149
 
81
- def write_timeout=(seconds)
82
- @write_timeout = seconds(seconds)
150
+ #
151
+ # Configures maximum time in seconds to establish a connection.
152
+ #
153
+ # @return [Numeric] maximum time in seconds to establish a connection
154
+ # @return [nil] if the connect time should not be checked (default)
155
+ #
156
+ attr_reader :connect_timeout
157
+
158
+ def connect_timeout=(value)
159
+ @connect_timeout = seconds(value)
83
160
  end
84
161
 
85
- def timeout_error=(exception)
86
- raise(NotAnException, exception) unless exception_class?(exception)
87
- @connect_timeout_error =
88
- @read_timeout_error = @write_timeout_error = exception
162
+ #
163
+ # @return [Class] exception class raised if a {TCPClient#connect} timed out
164
+ # @raise [NotAnExceptionError] if given argument is not an Exception class
165
+ #
166
+ attr_reader :connect_timeout_error
167
+
168
+ def connect_timeout_error=(value)
169
+ raise(NotAnExceptionError, value) unless exception_class?(value)
170
+ @connect_timeout_error = value
171
+ end
172
+
173
+ #
174
+ # Configures maximum time in seconds to finish a {TCPClient#read}.
175
+ #
176
+ # @return [Numeric] maximum time in seconds to finish a {TCPClient#read} request
177
+ # @return [nil] if the read time should not be checked (default)
178
+ #
179
+ attr_reader :read_timeout
180
+
181
+ def read_timeout=(value)
182
+ @read_timeout = seconds(value)
183
+ end
184
+
185
+ #
186
+ # @return [Class] exception class raised if a {TCPClient#read} timed out
187
+ # @raise [NotAnExceptionError] if given argument is not an Exception class
188
+ #
189
+ attr_reader :read_timeout_error
190
+
191
+ def read_timeout_error=(value)
192
+ raise(NotAnExceptionError, value) unless exception_class?(value)
193
+ @read_timeout_error = value
89
194
  end
90
195
 
91
- def connect_timeout_error=(exception)
92
- raise(NotAnException, exception) unless exception_class?(exception)
93
- @connect_timeout_error = exception
196
+ #
197
+ # Configures maximum time in seconds to finish a {TCPClient#write}.
198
+ #
199
+ # @return [Numeric] maximum time in seconds to finish a {TCPClient#write} request
200
+ # @return [nil] if the write time should not be checked (default)
201
+ #
202
+ attr_reader :write_timeout
203
+
204
+ def write_timeout=(value)
205
+ @write_timeout = seconds(value)
94
206
  end
95
207
 
96
- def read_timeout_error=(exception)
97
- raise(NotAnException, exception) unless exception_class?(exception)
98
- @read_timeout_error = exception
208
+ #
209
+ # @return [Class] exception class raised if a {TCPClient#write} timed out
210
+ # @raise [NotAnExceptionError] if given argument is not an Exception class
211
+ #
212
+ attr_reader :write_timeout_error
213
+
214
+ def write_timeout_error=(value)
215
+ raise(NotAnExceptionError, value) unless exception_class?(value)
216
+ @write_timeout_error = value
99
217
  end
100
218
 
101
- def write_timeout_error=(exception)
102
- raise(NotAnException, exception) unless exception_class?(exception)
103
- @write_timeout_error = exception
219
+ #
220
+ # @attribute ssl?
221
+ # @return [Boolean] wheter SSL is configured, see {#ssl_params}
222
+ #
223
+ def ssl?
224
+ @ssl_params ? true : false
225
+ end
226
+
227
+ #
228
+ # Configures the SSL parameters used to initialize a SSL context.
229
+ #
230
+ # @return [Hash<Symbol, Object>] SSL parameters for the SSL context
231
+ # @return [nil] if no SSL should be used (default)
232
+ #
233
+ attr_reader :ssl_params
234
+
235
+ def ssl_params=(value)
236
+ @ssl_params =
237
+ if value.respond_to?(:to_hash)
238
+ Hash[value.to_hash]
239
+ elsif value.respond_to?(:to_h)
240
+ Hash[value.to_h]
241
+ else
242
+ value ? {} : nil
243
+ end
104
244
  end
245
+ alias ssl= ssl_params=
105
246
 
247
+ #
248
+ # @return [Hash] configuration as a Hash
249
+ #
106
250
  def to_h
107
251
  {
108
252
  buffered: @buffered,
109
253
  keep_alive: @keep_alive,
110
254
  reverse_lookup: @reverse_lookup,
111
255
  connect_timeout: @connect_timeout,
112
- read_timeout: @read_timeout,
113
- write_timeout: @write_timeout,
114
256
  connect_timeout_error: @connect_timeout_error,
257
+ read_timeout: @read_timeout,
115
258
  read_timeout_error: @read_timeout_error,
259
+ write_timeout: @write_timeout,
116
260
  write_timeout_error: @write_timeout_error,
117
261
  ssl_params: @ssl_params
118
262
  }
119
263
  end
120
264
 
265
+ # @!visibility private
266
+ def freeze
267
+ @ssl_params.freeze
268
+ super
269
+ end
270
+
271
+ # @!visibility private
272
+ def initialize_copy(_org)
273
+ super
274
+ @ssl_params = Hash[@ssl_params] if @ssl_params
275
+ self
276
+ end
277
+
278
+ # @!visibility private
121
279
  def ==(other)
122
280
  to_h == other.to_h
123
281
  end
124
282
  alias eql? ==
125
283
 
284
+ # @!visibility private
126
285
  def equal?(other)
127
286
  self.class == other.class && self == other
128
287
  end
@@ -136,7 +295,7 @@ class TCPClient
136
295
  def set(attribute, value)
137
296
  public_send("#{attribute}=", value)
138
297
  rescue NoMethodError
139
- raise(UnknownAttribute, attribute)
298
+ raise(UnknownAttributeError, attribute)
140
299
  end
141
300
 
142
301
  def seconds(value)
@@ -6,14 +6,36 @@ class TCPClient
6
6
  @default_configuration = Configuration.new
7
7
 
8
8
  class << self
9
+ #
10
+ # @return [Configuration] used by default if no dedicated configuration was specified
11
+ #
9
12
  attr_reader :default_configuration
10
13
 
14
+ #
15
+ # Configure the default configuration which is used if no dedicated
16
+ # configuration was specified.
17
+ #
18
+ # @example
19
+ # TCPClient.configure do |cfg|
20
+ # cfg.buffered = false
21
+ # cfg.ssl_params = { min_version: :TLS1_2, max_version: :TLS1_3 }
22
+ # end
23
+ #
24
+ # @param options [Hash] see {Configuration#initialize} for details
25
+ #
26
+ # @yieldparam cfg {Configuration} the new configuration
27
+ #
28
+ # @return [Configuration] the new default configuration
29
+ #
11
30
  def configure(options = {}, &block)
12
31
  @default_configuration = Configuration.create(options, &block)
13
32
  end
14
33
  end
15
34
 
16
35
  class Configuration
36
+ #
37
+ # @return [Configuration] used by default if no dedicated configuration was specified
38
+ #
17
39
  def self.default
18
40
  TCPClient.default_configuration
19
41
  end