tcp-client 0.7.0 → 0.8.0

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: 14f787ad4e8e06910bfebf67755ae7142183df4c4fb090edce84aae7d497a2b6
4
+ data.tar.gz: 6349bea2eac6bac053c724e0c6a162b1fe99502e94cf1c6734854d97f194f37a
5
5
  SHA512:
6
- metadata.gz: b160bd656c4f6091a669cf65349e54aeaa2e7c057e0a5a3c896edf5e8ab91166a3a5cdafedfe84e4b04d5a79e0602fb2433c6ff9144e6881746f3607b8d8290b
7
- data.tar.gz: 6726ebdfbc777258e62b3d915fa7e4a57f7ae514d6259ce5f0051ee649294142acf09faac490901e32083072d7a68dd4ce71fa697e0bf05c0d9556c0d57bb6a5
6
+ metadata.gz: b53fafbf091a67a329832663c058b4e8243b36a779f8bc52cc7f37de556ad0aa016245e0a059489b972e6afaccfea7d26a63a735686d5f50d88036a2f4ecaca1
7
+ data.tar.gz: 4a6ff368e221073c5addfab51e31df1b094ee3f69e09ebe3f38d6d552ebc761b14c552a1a2cd90dcfd148b1ba34ebb1b2e3abfcea9622ded8413870f21a60285
data/README.md CHANGED
@@ -11,20 +11,22 @@ This Gem implements a TCP client with (optional) SSL support. It is an easy to u
11
11
  ```ruby
12
12
  require 'tcp-client'
13
13
 
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
14
+ # create a configuration:
15
+ # - don't use internal buffering
16
+ # - use TLS 1.2 or TLS 1.3
17
+ cfg = TCPClient::Configuration.create(
18
+ buffered: false,
19
+ ssl_params: {min_version: :TLS1_2, max_version: :TLS1_3}
20
+ )
21
+
22
+ # request to Google.com:
23
+ # - limit all network interactions to 1.5 seconds
24
+ # - use the Configuration cfg
25
+ # - send a simple HTTP get request
26
+ # - read 12 byte: "HTTP/1.1 " + 3 byte HTTP status code
27
+ TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
28
+ client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n") # >= 40
29
+ client.read(12) # => "HTTP/1.1 200"
28
30
  end
29
31
  ```
30
32
 
@@ -41,13 +43,13 @@ gem 'tcp-client'
41
43
  and install it by running Bundler:
42
44
 
43
45
  ```bash
44
- $ bundle
46
+ bundle
45
47
  ```
46
48
 
47
49
  To install the gem globally use:
48
50
 
49
51
  ```bash
50
- $ gem install tcp-client
52
+ gem install tcp-client
51
53
  ```
52
54
 
53
55
  After that you need only a single line of code in your project to have all tools on board:
@@ -25,12 +25,16 @@ class TCPClient
25
25
  "#{@hostname}:#{@addrinfo.ip_port}"
26
26
  end
27
27
 
28
- def to_h
28
+ def to_hash
29
29
  { host: @hostname, port: @addrinfo.ip_port }
30
30
  end
31
31
 
32
+ def to_h(*args)
33
+ args.empty? ? to_hash : to_hash.slice(*args)
34
+ end
35
+
32
36
  def ==(other)
33
- to_h == other.to_h
37
+ to_hash == other.to_hash
34
38
  end
35
39
  alias eql? ==
36
40
 
@@ -13,6 +13,7 @@ class TCPClient
13
13
  attr_reader :buffered,
14
14
  :keep_alive,
15
15
  :reverse_lookup,
16
+ :normalize_network_errors,
16
17
  :connect_timeout,
17
18
  :read_timeout,
18
19
  :write_timeout,
@@ -27,6 +28,7 @@ class TCPClient
27
28
  @connect_timeout_error = ConnectTimeoutError
28
29
  @read_timeout_error = ReadTimeoutError
29
30
  @write_timeout_error = WriteTimeoutError
31
+ @normalize_network_errors = false
30
32
  options.each_pair { |attribute, value| set(attribute, value) }
31
33
  end
32
34
 
@@ -47,8 +49,8 @@ class TCPClient
47
49
 
48
50
  def ssl=(value)
49
51
  @ssl_params =
50
- if Hash === value
51
- Hash[value]
52
+ if value.respond_to?(:to_hash)
53
+ Hash[value.to_hash]
52
54
  else
53
55
  value ? {} : nil
54
56
  end
@@ -66,6 +68,10 @@ class TCPClient
66
68
  @reverse_lookup = value ? true : false
67
69
  end
68
70
 
71
+ def normalize_network_errors=(value)
72
+ @normalize_network_errors = value ? true : false
73
+ end
74
+
69
75
  def timeout=(seconds)
70
76
  @connect_timeout = @write_timeout = @read_timeout = seconds(seconds)
71
77
  end
@@ -103,23 +109,27 @@ class TCPClient
103
109
  @write_timeout_error = exception
104
110
  end
105
111
 
106
- def to_h
112
+ def to_hash
107
113
  {
108
114
  buffered: @buffered,
109
115
  keep_alive: @keep_alive,
110
116
  reverse_lookup: @reverse_lookup,
111
117
  connect_timeout: @connect_timeout,
112
- read_timeout: @read_timeout,
113
- write_timeout: @write_timeout,
114
118
  connect_timeout_error: @connect_timeout_error,
119
+ read_timeout: @read_timeout,
115
120
  read_timeout_error: @read_timeout_error,
121
+ write_timeout: @write_timeout,
116
122
  write_timeout_error: @write_timeout_error,
117
123
  ssl_params: @ssl_params
118
124
  }
119
125
  end
120
126
 
127
+ def to_h(*args)
128
+ args.empty? ? to_hash : to_hash.slice(*args)
129
+ end
130
+
121
131
  def ==(other)
122
- to_h == other.to_h
132
+ to_hash == other.to_hash
123
133
  end
124
134
  alias eql? ==
125
135
 
@@ -31,13 +31,15 @@ class TCPClient
31
31
  end
32
32
  end
33
33
 
34
- class NotConnectedError < IOError
34
+ NetworkError = Class.new(StandardError)
35
+
36
+ class NotConnectedError < NetworkError
35
37
  def initialize
36
38
  super('client not connected')
37
39
  end
38
40
  end
39
41
 
40
- class TimeoutError < IOError
42
+ class TimeoutError < NetworkError
41
43
  def initialize(message = nil)
42
44
  super(message || "unable to #{action} in time")
43
45
  end
@@ -18,6 +18,7 @@ class TCPClient
18
18
  super(socket, create_context(ssl_params))
19
19
  self.sync_close = true
20
20
  self.hostname = address.hostname
21
+ check_new_session if @new_session
21
22
  deadline.valid? ? connect_with_deadline(deadline, exception) : connect
22
23
  post_connection_check(address.hostname) if should_verify?(ssl_params)
23
24
  end
@@ -25,7 +26,19 @@ class TCPClient
25
26
  private
26
27
 
27
28
  def create_context(ssl_params)
28
- OpenSSL::SSL::SSLContext.new.tap { |ctx| ctx.set_params(ssl_params) }
29
+ @new_session = nil
30
+ ::OpenSSL::SSL::SSLContext.new.tap do |ctx|
31
+ ctx.set_params(ssl_params)
32
+ ctx.session_cache_mode = CONTEXT_CACHE_MODE
33
+ ctx.session_new_cb = proc { |_, sess| @new_session = sess }
34
+ end
35
+ end
36
+
37
+ def check_new_session
38
+ time = @new_session.time.to_f + @new_session.timeout
39
+ if Process.clock_gettime(Process::CLOCK_REALTIME) < time
40
+ self.session = @new_session
41
+ end
29
42
  end
30
43
 
31
44
  def connect_with_deadline(deadline, exception)
@@ -33,9 +46,13 @@ class TCPClient
33
46
  end
34
47
 
35
48
  def should_verify?(ssl_params)
36
- ssl_params[:verify_mode] != OpenSSL::SSL::VERIFY_NONE &&
49
+ ssl_params[:verify_mode] != ::OpenSSL::SSL::VERIFY_NONE &&
37
50
  context.verify_hostname
38
51
  end
52
+
53
+ CONTEXT_CACHE_MODE =
54
+ ::OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
55
+ ::OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
39
56
  end
40
57
 
41
58
  private_constant(:SSLSocket)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class TCPClient
4
- VERSION = '0.7.0'
4
+ VERSION = '0.8.0'
5
5
  end
data/lib/tcp-client.rb CHANGED
@@ -10,7 +10,7 @@ require_relative 'tcp-client/default_configuration'
10
10
  require_relative 'tcp-client/version'
11
11
 
12
12
  class TCPClient
13
- def self.open(address, configuration = Configuration.default)
13
+ def self.open(address, configuration = nil)
14
14
  client = new
15
15
  client.connect(Address.new(address), configuration)
16
16
  block_given? ? yield(client) : client
@@ -18,11 +18,7 @@ class TCPClient
18
18
  client.close if block_given?
19
19
  end
20
20
 
21
- def self.with_deadline(
22
- timeout,
23
- address,
24
- configuration = Configuration.default
25
- )
21
+ def self.with_deadline(timeout, address, configuration = nil)
26
22
  client = nil
27
23
  raise(NoBlockGivenError) unless block_given?
28
24
  address = Address.new(address)
@@ -36,19 +32,15 @@ class TCPClient
36
32
 
37
33
  attr_reader :address, :configuration
38
34
 
39
- def initialize
40
- @socket = @address = @deadline = @configuration = nil
41
- end
42
-
43
35
  def to_s
44
36
  @address&.to_s || ''
45
37
  end
46
38
 
47
- def connect(address, configuration, timeout: nil, exception: nil)
39
+ def connect(address, configuration = nil, timeout: nil, exception: nil)
48
40
  close if @socket
49
41
  raise(NoOpenSSLError) if configuration.ssl? && !defined?(SSLSocket)
50
42
  @address = Address.new(address)
51
- @configuration = configuration.dup.freeze
43
+ @configuration = (configuration || Configuration.default).dup
52
44
  @socket = create_socket(timeout, exception)
53
45
  self
54
46
  end
@@ -56,7 +48,7 @@ class TCPClient
56
48
  def close
57
49
  @socket&.close
58
50
  self
59
- rescue IOError
51
+ rescue *NETWORK_ERRORS
60
52
  self
61
53
  ensure
62
54
  @socket = @deadline = nil
@@ -79,23 +71,27 @@ class TCPClient
79
71
  def read(nbytes = nil, timeout: nil, exception: nil)
80
72
  raise(NotConnectedError) if closed?
81
73
  deadline = create_deadline(timeout, configuration.read_timeout)
82
- return @socket.read(nbytes) unless deadline.valid?
74
+ return stem_errors { @socket.read(nbytes) } unless deadline.valid?
83
75
  exception ||= configuration.read_timeout_error
84
- @socket.read_with_deadline(nbytes, deadline, exception)
76
+ stem_errors(exception) do
77
+ @socket.read_with_deadline(nbytes, deadline, exception)
78
+ end
85
79
  end
86
80
 
87
81
  def write(*msg, timeout: nil, exception: nil)
88
82
  raise(NotConnectedError) if closed?
89
83
  deadline = create_deadline(timeout, configuration.write_timeout)
90
- return @socket.write(*msg) unless deadline.valid?
84
+ return stem_errors { @socket.write(*msg) } unless deadline.valid?
91
85
  exception ||= configuration.write_timeout_error
92
- msg.sum do |chunk|
93
- @socket.write_with_deadline(chunk.b, deadline, exception)
86
+ stem_errors(exception) do
87
+ msg.sum do |chunk|
88
+ @socket.write_with_deadline(chunk.b, deadline, exception)
89
+ end
94
90
  end
95
91
  end
96
92
 
97
93
  def flush
98
- @socket&.flush
94
+ stem_errors { @socket&.flush }
99
95
  self
100
96
  end
101
97
 
@@ -108,8 +104,33 @@ class TCPClient
108
104
  def create_socket(timeout, exception)
109
105
  deadline = create_deadline(timeout, configuration.connect_timeout)
110
106
  exception ||= configuration.connect_timeout_error
111
- @socket = TCPSocket.new(address, configuration, deadline, exception)
112
- return @socket unless configuration.ssl?
113
- SSLSocket.new(@socket, address, configuration, deadline, exception)
107
+ stem_errors(exception) do
108
+ @socket = TCPSocket.new(address, configuration, deadline, exception)
109
+ return @socket unless configuration.ssl?
110
+ SSLSocket.new(@socket, address, configuration, deadline, exception)
111
+ end
114
112
  end
113
+
114
+ def stem_errors(except = nil)
115
+ yield
116
+ rescue *NETWORK_ERRORS => e
117
+ raise unless configuration.normalize_network_errors
118
+ (except && e.is_a?(except)) ? raise : raise(NetworkError, e)
119
+ end
120
+
121
+ NETWORK_ERRORS =
122
+ [
123
+ Errno::EADDRNOTAVAIL,
124
+ Errno::ECONNABORTED,
125
+ Errno::ECONNREFUSED,
126
+ Errno::ECONNRESET,
127
+ Errno::EHOSTUNREACH,
128
+ Errno::EINVAL,
129
+ Errno::ENETUNREACH,
130
+ Errno::EPIPE,
131
+ IOError,
132
+ SocketError
133
+ ].tap do |errors|
134
+ errors << ::OpenSSL::SSL::SSLError if defined?(::OpenSSL::SSL::SSLError)
135
+ end.freeze
115
136
  end
data/rakefile.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rake/clean'
4
- require 'rake/testtask'
5
4
  require 'bundler/gem_tasks'
5
+ require 'rspec/core/rake_task'
6
6
 
7
7
  $stdout.sync = $stderr.sync = true
8
8
 
@@ -10,7 +10,4 @@ CLOBBER << 'prj'
10
10
 
11
11
  task(:default) { exec('rake --tasks') }
12
12
 
13
- Rake::TestTask.new(:test) do |task|
14
- task.pattern = 'test/**/*_test.rb'
15
- task.warning = task.verbose = true
16
- end
13
+ RSpec::Core::RakeTask.new { |task| task.ruby_opts = %w[-w] }
data/sample/google_ssl.rb CHANGED
@@ -2,23 +2,24 @@
2
2
 
3
3
  require_relative '../lib/tcp-client'
4
4
 
5
- # create a configuration.
6
- # - use TLS 1.2
5
+ # create a configuration:
7
6
  # - don't use internal buffering
7
+ # - use TLS 1.2 or TLS 1.3
8
8
  cfg =
9
9
  TCPClient::Configuration.create(
10
10
  buffered: false,
11
11
  ssl_params: {
12
- ssl_version: :TLSv1_2
12
+ min_version: :TLS1_2,
13
+ max_version: :TLS1_3
13
14
  }
14
15
  )
15
16
 
16
- # request to Google:
17
- # - limit all interactions to 0.5 seconds
17
+ # request to Google.com:
18
+ # - limit all network interactions to 1.5 seconds
18
19
  # - use the Configuration cfg
19
20
  # - send a simple HTTP get request
20
21
  # - read 12 byte: "HTTP/1.1 " + 3 byte HTTP status code
21
- TCPClient.with_deadline(0.5, 'www.google.com:443', cfg) do |client|
22
+ TCPClient.with_deadline(1.5, 'www.google.com:443', cfg) do |client|
22
23
  p client.write("GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n")
23
24
  p client.read(12)
24
25
  end
data/spec/helper.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/core'
4
+ require_relative '../lib/tcp-client'
5
+
6
+ $stdout.sync = $stderr.sync = true
7
+
8
+ RSpec.configure do |config|
9
+ config.disable_monkey_patching!
10
+ config.warnings = true
11
+ config.order = :random
12
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helper'
4
+
5
+ RSpec.describe TCPClient::Address do
6
+ describe '.new' do
7
+ context 'when called with an Integer parameter' do
8
+ subject(:address) { TCPClient::Address.new(42) }
9
+
10
+ it 'points to the given port on localhost' do
11
+ expect(address.hostname).to eq 'localhost'
12
+ expect(address.to_s).to eq 'localhost:42'
13
+ expect(address.addrinfo.ip_port).to be 42
14
+ end
15
+
16
+ it 'uses IPv6' do
17
+ expect(address.addrinfo.ip?).to be true
18
+ expect(address.addrinfo.ipv6?).to be true
19
+ expect(address.addrinfo.ipv4?).to be false
20
+ end
21
+ end
22
+
23
+ context 'when called with an Addrinfo' do
24
+ subject(:address) { TCPClient::Address.new(addrinfo) }
25
+ let(:addrinfo) { Addrinfo.tcp('::1', 42) }
26
+
27
+ it 'uses the given Addrinfo' do
28
+ expect(address.addrinfo).to eq addrinfo
29
+ end
30
+
31
+ it 'points to the given host and port' do
32
+ expect(address.hostname).to eq addrinfo.getnameinfo[0]
33
+ expect(address.addrinfo.ip_port).to be 42
34
+ end
35
+
36
+ it 'uses IPv6' do
37
+ expect(address.addrinfo.ip?).to be true
38
+ expect(address.addrinfo.ipv6?).to be true
39
+ expect(address.addrinfo.ipv4?).to be false
40
+ end
41
+ end
42
+
43
+ context 'when called with a String' do
44
+ context 'when a host name and port is provided' do
45
+ subject(:address) { TCPClient::Address.new('localhost:42') }
46
+
47
+ it 'points to the given host and port' do
48
+ expect(address.hostname).to eq 'localhost'
49
+ 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
+ expect(address.addrinfo.ip?).to be true
55
+ expect(address.addrinfo.ipv6?).to be true
56
+ expect(address.addrinfo.ipv4?).to be false
57
+ end
58
+ end
59
+
60
+ context 'when only a port is provided' do
61
+ subject(:address) { TCPClient::Address.new(':21') }
62
+
63
+ it 'points to the given port on localhost' do
64
+ expect(address.hostname).to eq ''
65
+ expect(address.to_s).to eq ':21'
66
+ expect(address.addrinfo.ip_port).to be 21
67
+ end
68
+
69
+ it 'uses IPv4' do
70
+ expect(address.addrinfo.ip?).to be true
71
+ expect(address.addrinfo.ipv6?).to be false
72
+ expect(address.addrinfo.ipv4?).to be true
73
+ end
74
+ end
75
+
76
+ context 'when an IPv6 address is provided' do
77
+ subject(:address) { TCPClient::Address.new('[::1]:42') }
78
+
79
+ it 'points to the given port on localhost' do
80
+ expect(address.hostname).to eq '::1'
81
+ 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
+ expect(address.addrinfo.ip?).to be true
87
+ expect(address.addrinfo.ipv6?).to be true
88
+ expect(address.addrinfo.ipv4?).to be false
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ describe '#to_hash' do
95
+ subject(:address) { TCPClient::Address.new('localhost:42') }
96
+
97
+ it 'returns itself as an Hash' do
98
+ expect(address.to_hash).to eq(host: 'localhost', port: 42)
99
+ end
100
+ end
101
+
102
+ describe '#to_h' do
103
+ subject(:address) { TCPClient::Address.new('localhost:42') }
104
+
105
+ it 'returns itself as an Hash' do
106
+ expect(address.to_h).to eq(host: 'localhost', port: 42)
107
+ end
108
+
109
+ it 'allows to specify the keys the result should contain' do
110
+ expect(address.to_h(:port)).to eq(port: 42)
111
+ expect(address.to_h(:host)).to eq(host: 'localhost')
112
+ end
113
+ end
114
+
115
+ describe 'comparison' do
116
+ context 'comparing two equal instances' do
117
+ let(:address_a) { TCPClient::Address.new('localhost:42') }
118
+ let(:address_b) { TCPClient::Address.new('localhost:42') }
119
+
120
+ it 'compares to equal' do
121
+ expect(address_a).to eq address_b
122
+ end
123
+
124
+ context 'using the == opperator' do
125
+ it 'compares to equal' do
126
+ expect(address_a == address_b).to be true
127
+ end
128
+ end
129
+
130
+ context 'using the === opperator' do
131
+ it 'compares to equal' do
132
+ expect(address_a === address_b).to be true
133
+ end
134
+ end
135
+ end
136
+
137
+ context 'comparing two non-equal instances' do
138
+ let(:address_a) { TCPClient::Address.new('localhost:42') }
139
+ let(:address_b) { TCPClient::Address.new('localhost:21') }
140
+
141
+ it 'compares not to equal' do
142
+ expect(address_a).not_to eq address_b
143
+ end
144
+
145
+ context 'using the == opperator' do
146
+ it 'compares not to equal' do
147
+ expect(address_a == address_b).to be false
148
+ end
149
+ end
150
+
151
+ context 'using the === opperator' do
152
+ it 'compares not to equal' do
153
+ expect(address_a === address_b).to be false
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end