io_request 2.0.0 → 2.4.0

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: 9a78b709d9b064cd44737d76a24c27f0df37e9f0d5897829043e9d884287219d
4
- data.tar.gz: a2350c480d0f9cc883526c47021505bfd511ffa12622436ac5bafbe4d050c6df
3
+ metadata.gz: a560819ec6ad8dc50801411fcea4ca4263b29e88067187cc2035e3cbad133394
4
+ data.tar.gz: 1d82e187a4044f25e3ee061a7f382423e1013644dff7663467e694e4139fc8f2
5
5
  SHA512:
6
- metadata.gz: b1cc3c89258eaa0d4511fdcd1dc17cae4b82061ebdb747732a8cc60467a9a39c3414a337f6f85acc4dd6768f16bc45f9fe037405e5e7aeb19f0723deeb01759a
7
- data.tar.gz: eb6918fb66bbaec142dd88abf4c00b69d5204fb88b03f00532e52d12d8a03822275efb3cac4584177a8f9ab40bc549bc5dff09977410f3cf6f99435f594e41ad
6
+ metadata.gz: ff19727550d073a8cccc146a2c1de65a6f67458a276b003e122e471ef73d584a52741bdaf07da51e0ed2513f33d0cf8a139eb4e5f0c342e2f6a77cc929f2982a
7
+ data.tar.gz: 0eaf1f54fcac605a4dc62a8ff7b7bcc41a7d68ee3e75c2020971e9bfb432cc08f22799506ae6e80409f4892b2e5e12cee2e5da28f4657f13a059dfad900402e2
data/Gemfile CHANGED
@@ -11,3 +11,7 @@ gem 'json', '~> 2.3'
11
11
  gem 'timeout', '~> 0.1.0'
12
12
 
13
13
  gem 'logger', '~> 1.4'
14
+
15
+ gem 'openssl', '~> 2.2'
16
+
17
+ gem 'pry', '~> 0.13.1'
@@ -4,12 +4,5 @@
4
4
  require 'bundler/setup'
5
5
  require 'io_request'
6
6
 
7
- # You can add fixtures and/or initialization code here to make experimenting
8
- # with your gem easier. You can also use a different console, if you like.
9
-
10
- # (If you use this, don't forget to add pry to your Gemfile!)
11
- # require "pry"
12
- # Pry.start
13
-
14
- require 'irb'
15
- IRB.start(__FILE__)
7
+ require 'pry'
8
+ Pry.start(__FILE__)
@@ -1,6 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Main module.
4
+ module IORequest
5
+ # Client received message of zero size.
6
+ class ZeroSizeMessageError < RuntimeError; end
7
+
8
+ # Authorization failed.
9
+ class AuthorizationFailureError < RuntimeError; end
10
+ end
11
+
3
12
  require_relative 'io_request/version'
13
+ require_relative 'io_request/logging'
4
14
  require_relative 'io_request/utility/multi_thread'
5
15
  require_relative 'io_request/utility/with_id'
6
16
  require_relative 'io_request/utility/with_prog_name'
@@ -8,23 +18,3 @@ require_relative 'io_request/utility/with_prog_name'
8
18
  require_relative 'io_request/authorizer'
9
19
  require_relative 'io_request/message'
10
20
  require_relative 'io_request/client'
11
-
12
- require 'logger'
13
-
14
- # Main module.
15
- module IORequest
16
- # @return [Logger]
17
- def self.logger
18
- @@logger ||= Logger.new( # rubocop:disable Style/ClassVars
19
- STDOUT,
20
- formatter: proc do |severity, datetime, progname, msg|
21
- "[#{datetime}] #{severity} - #{progname}:\t #{msg}\n"
22
- end
23
- )
24
- end
25
-
26
- # @param new_logger [Logger]
27
- def self.logger=(new_logger)
28
- @@logger = new_logger # rubocop:disable Style/ClassVars
29
- end
30
- end
@@ -45,15 +45,17 @@ module IORequest
45
45
  # @param r [IO] object to read from.
46
46
  # @param w [IO] object to write to.
47
47
  # @param rw [IO] read-write object (replaces `r` and `w` arguments).
48
+ # @return [Object] data from {Authorizer}
48
49
  def open(read: nil, write: nil, read_write: nil)
49
50
  @io_r = read_write || read
50
51
  @io_w = read_write || write
51
52
 
52
53
  IORequest.logger.debug(prog_name) { 'Starting connection' }
53
54
 
54
- authorization
55
+ auth_data = authorization
55
56
  @open = true
56
57
  @data_transition_thread = in_thread(name: 'connection') { data_transition_loop }
58
+ auth_data
57
59
  end
58
60
 
59
61
  def open?
@@ -63,15 +65,21 @@ module IORequest
63
65
  # Close connection.
64
66
  def close
65
67
  close_internal
68
+
66
69
  join_threads
67
- @open = false
68
70
  end
69
71
 
70
72
  # @yieldparam [Hash]
71
73
  # @yieldreturn [Hash]
72
- def respond(&block)
73
- IORequest.logger.debug(prog_name) { 'Saved responder block' }
74
- @responder = block
74
+ def on_request(&block)
75
+ IORequest.logger.debug(prog_name) { 'Saved on_request block' }
76
+ @on_request = block
77
+ end
78
+ alias respond on_request
79
+
80
+ def on_close(&block)
81
+ IORequest.logger.debug(prog_name) { 'Saved on_close block' }
82
+ @on_close = block
75
83
  end
76
84
 
77
85
  # If callback block is provided, request will be sent asynchroniously.
@@ -96,50 +104,52 @@ module IORequest
96
104
 
97
105
  def close_internal
98
106
  IORequest.logger.debug(prog_name) { 'Closing connection' }
99
- begin
100
- send_zero_size_request
101
- rescue StandardError
102
- IORequest.logger.debug(prog_name) { 'Failed to send zero-sized message. Closing anyway' }
103
- end
104
- stop_data_transition
107
+ send_zero_size_request
105
108
  close_io
106
- end
107
-
108
- def stop_data_transition
109
- return unless defined?(@data_transition_thread) && !@data_transition_thread.nil?
110
-
111
- IORequest.logger.debug(prog_name) { 'Killing data transition thread' }
112
- @data_transition_thread.kill
113
109
  @data_transition_thread = nil
110
+ @open = false
111
+ @on_close&.call if defined?(@on_close)
114
112
  end
115
113
 
116
114
  def close_io
117
- IORequest.logger.debug(prog_name) { 'Closing IO' }
118
- @mutex_r.synchronize { @io_r&.close }
119
- @mutex_w.synchronize { @io_w&.close }
115
+ begin
116
+ @io_r&.close
117
+ rescue StandardError => e
118
+ IORequest.logger.debug "Failed to close read IO: #{e}"
119
+ end
120
+ begin
121
+ @io_w&.close
122
+ rescue StandardError => e
123
+ IORequest.logger.debug "Failed to close write IO: #{e}"
124
+ end
125
+ IORequest.logger.debug(prog_name) { 'Closed IO streams' }
120
126
  end
121
127
 
122
128
  def authorization
123
- auth_successful = @mutex_r.synchronize do
129
+ auth_successful = false
130
+ data = nil
131
+ @mutex_r.synchronize do
124
132
  @mutex_w.synchronize do
125
133
  IORequest.logger.debug(prog_name) { 'Authorizing new client' }
126
- @authorizer.authorize(@io_r, @io_w)
134
+ auth_successful = @authorizer.authorize(@io_r, @io_w)
135
+ data = @authorizer.data
127
136
  end
128
137
  end
129
- unless auth_successful
130
- IORequest.logger.debug(prog_name) { 'Authorization failed' }
131
- raise 'Authorization failed'
132
- end
138
+ raise AuthorizationFailureError unless auth_successful
133
139
 
134
- IORequest.logger.debug(prog_name) { "New client authorized with data #{@authorizer.data}" }
140
+ IORequest.logger.debug(prog_name) { "New client authorized with data #{data}" }
141
+ data
135
142
  end
136
143
 
137
144
  def data_transition_loop
138
145
  IORequest.logger.debug(prog_name) { 'Starting data transition loop' }
139
146
  loop do
140
147
  data_transition_iteration
148
+ rescue ZeroSizeMessageError
149
+ IORequest.logger.debug(prog_name) { 'Connection was closed from the other side' }
150
+ break
141
151
  rescue StandardError => e
142
- IORequest.logger.debug(prog_name) { "Data transition iteration failed: #{e}" }
152
+ IORequest.logger.debug(prog_name) { "Data transition unknown error: #{e}" }
143
153
  break
144
154
  end
145
155
  close_internal
@@ -156,7 +166,8 @@ module IORequest
156
166
  end
157
167
 
158
168
  def handle_request(message)
159
- data = @responder&.call(message.data) || {}
169
+ data = {}
170
+ data = @on_request&.call(message.data) if defined?(@on_request)
160
171
  response = Message.new(data, type: :response, to: message.id)
161
172
  send_response(response)
162
173
  end
@@ -180,6 +191,8 @@ module IORequest
180
191
  IORequest.logger.debug(prog_name) { 'Sending zero size message' }
181
192
  @io_w.write([0].pack('S'))
182
193
  end
194
+ rescue StandardError => e
195
+ IORequest.logger.debug(prog_name) { "Failed to send zero-sized message(#{e})" }
183
196
  end
184
197
 
185
198
  def send_request_and_wait_for_response(request)
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../io_request'
4
+
5
+ require 'socket'
6
+ require 'openssl'
7
+
8
+ module IORequest
9
+ # Connection via SSL sockets
10
+ module SSLSockets
11
+ # SSL socket server.
12
+ class Server
13
+ include Utility::MultiThread
14
+
15
+ # Initalize new server.
16
+ # @param port [Integer] port of server.
17
+ # @param authorizer [Authorizer]
18
+ # @param certificate [String]
19
+ # @param key [String]
20
+ def initialize(
21
+ port: 8000,
22
+ authorizer: Authorizer.empty,
23
+ certificate: nil,
24
+ key: nil,
25
+ &requests_handler
26
+ )
27
+ @port = port
28
+ @authorizer = authorizer
29
+ @requests_handler = requests_handler
30
+
31
+ initialize_ssl_context(certificate, key)
32
+ end
33
+
34
+ # @return [Array<IORequest::Client>]
35
+ def clients
36
+ @clients_data.keys
37
+ end
38
+
39
+ # @param client [IORequest::Client]
40
+ # @return [Hash, nil] you are free to store anything you want in hash.
41
+ # Only field you will find in it is `auth` with authenticator data.
42
+ def data(client)
43
+ @clients_data[client]
44
+ end
45
+
46
+ # Start server.
47
+ def start
48
+ @clients_data = {}
49
+
50
+ @server = TCPServer.new(@port)
51
+
52
+ @accept_thread = in_thread(name: 'accept_thr') { accept_loop }
53
+ end
54
+
55
+ # Fully stop server.
56
+ def stop
57
+ clients.each(&:close)
58
+ @clients_data.clear
59
+
60
+ @server.close
61
+ @server = nil
62
+
63
+ @accept_thread&.kill
64
+ @accept_thread = nil
65
+ end
66
+
67
+ private
68
+
69
+ def initialize_ssl_context(certificate, key)
70
+ @ctx = OpenSSL::SSL::SSLContext.new
71
+ @ctx.cert = OpenSSL::X509::Certificate.new certificate
72
+ @ctx.key = OpenSSL::PKey::RSA.new key
73
+ @ctx.ssl_version = :TLSv1_2
74
+ end
75
+
76
+ def accept_loop
77
+ while (socket = @server.accept)
78
+ handle_socket(socket)
79
+ end
80
+ rescue StandardError
81
+ stop
82
+ end
83
+
84
+ def handle_socket(socket)
85
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, @ctx)
86
+ ssl_socket.accept
87
+
88
+ handle_client(ssl_socket, IORequest::Client.new(authorizer: @authorizer))
89
+ rescue StandardError => e
90
+ IORequest.logger.warn "Unknown error while handling sockets: #{e}"
91
+ end
92
+
93
+ def handle_client(ssl_socket, client)
94
+ auth_data = client.open read_write: ssl_socket
95
+ client.on_request { |data| @requests_handler.call(data, client) }
96
+ @clients_data[client] = { auth: auth_data }
97
+ client.on_close do
98
+ @clients_data.select! { |c, _d| c.open? }
99
+ end
100
+ rescue StandardError => e
101
+ IORequest.logger.debug "Failed to open client: #{e}"
102
+ ssl_socket.close
103
+ end
104
+ end
105
+
106
+ # SSL socket client.
107
+ class Client
108
+ # Initialize new client.
109
+ # @param authorizer [Authorizer]
110
+ # @param certificate [String]
111
+ # @param key [String]
112
+ def initialize(
113
+ authorizer: Authorizer.empty,
114
+ certificate: nil,
115
+ key: nil,
116
+ &requests_handler
117
+ )
118
+ @authorizer = authorizer
119
+ @requests_handler = requests_handler
120
+
121
+ @client = nil
122
+
123
+ initialize_ssl_context(certificate, key)
124
+ end
125
+
126
+ def connected?
127
+ !@client.nil?
128
+ end
129
+
130
+ # Connect to server.
131
+ # @param host [String] host of server.
132
+ # @param port [Integer] port of server.
133
+ def connect(host = 'localhost', port = 8000)
134
+ socket = TCPSocket.new(host, port)
135
+
136
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, @ctx)
137
+ ssl_socket.sync_close = true
138
+ ssl_socket.connect
139
+
140
+ @client = IORequest::Client.new authorizer: @authorizer
141
+ begin
142
+ @client.open read_write: ssl_socket
143
+ @client.on_request(&@requests_handler)
144
+ rescue StandardError
145
+ ssl_socket.close
146
+ @client = nil
147
+ end
148
+ end
149
+
150
+ # Closes connection to server.
151
+ def disconnect
152
+ return unless defined?(@client) && !@client.nil?
153
+
154
+ @client.close
155
+ @client = nil
156
+ end
157
+
158
+ # Wrapper over {IORequest::Client#request}
159
+ def request(*args, **options, &block)
160
+ @client.request(*args, **options, &block)
161
+ end
162
+
163
+ private
164
+
165
+ def initialize_ssl_context(certificate, key)
166
+ @ctx = OpenSSL::SSL::SSLContext.new
167
+ @ctx.cert = OpenSSL::X509::Certificate.new certificate
168
+ @ctx.key = OpenSSL::PKey::RSA.new key
169
+ @ctx.ssl_version = :TLSv1_2
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module IORequest
6
+ # @return [Logger]
7
+ def self.logger
8
+ @@logger ||= Logger.new( # rubocop:disable Style/ClassVars
9
+ STDOUT,
10
+ formatter: proc do |severity, datetime, progname, msg|
11
+ "[#{datetime}] #{severity} - #{progname}:\t #{msg}\n"
12
+ end
13
+ )
14
+ end
15
+
16
+ # @param new_logger [Logger]
17
+ def self.logger=(new_logger)
18
+ @@logger = new_logger # rubocop:disable Style/ClassVars
19
+ end
20
+ end
@@ -10,9 +10,10 @@ module IORequest
10
10
  # Create new message.
11
11
  # @param data [Hash]
12
12
  # @param type [Symbol] one of {TYPES} member.
13
- # @param id [Utility::ExtendedID, String, nil] only should be filled if message is received from outside.
14
- # @param to [Utility::ExtendedID, String, nil] if message is response, it should include integer
15
- # of original request.
13
+ # @param id [Utility::ExtendedID, String, nil] only should be filled if
14
+ # message is received from outside.
15
+ # @param to [Utility::ExtendedID, String, nil] if message is response, it
16
+ # should include integer of original request.
16
17
  def initialize(data, type: :request, id: nil, to: nil)
17
18
  @data = data
18
19
  @type = type
@@ -80,8 +81,8 @@ module IORequest
80
81
  # @param io_r [:read]
81
82
  # @return [Message]
82
83
  def self.read_from(io_r)
83
- size = io_r.read(2).unpack1('S')
84
- raise '0 size received' if size.zero?
84
+ size = io_r.read(2)&.unpack1('S') || 0
85
+ raise ZeroSizeMessageError if size.zero?
85
86
 
86
87
  json_string = io_r.read(size).unpack1("a#{size}")
87
88
  msg = JSON.parse(json_string, symbolize_names: true)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module IORequest
4
4
  # Gem version.
5
- VERSION = '2.0.0'
5
+ VERSION = '2.4.0'
6
6
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: io_request
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fizvlad
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-07-18 00:00:00.000000000 Z
11
+ date: 2020-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -110,11 +110,12 @@ files:
110
110
  - README.md
111
111
  - Rakefile
112
112
  - bin/console
113
- - examples/simple_example.rb
114
113
  - io_request.gemspec
115
114
  - lib/io_request.rb
116
115
  - lib/io_request/authorizer.rb
117
116
  - lib/io_request/client.rb
117
+ - lib/io_request/connection/ssl_sockets.rb
118
+ - lib/io_request/logging.rb
118
119
  - lib/io_request/message.rb
119
120
  - lib/io_request/utility/multi_thread.rb
120
121
  - lib/io_request/utility/with_id.rb
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'io_request'
4
-
5
- r1, w1 = IO.pipe
6
- r2, w2 = IO.pipe
7
-
8
- client_1 = IORequest::Client.new read: r1, write: w2
9
- client_2 = IORequest::Client.new read: r2, write: w1
10
-
11
- # Use
12
- # Set up responders
13
- # Authorization
14
- client_2.respond type: 'auth' do |request|
15
- puts "Client 2: Authorization attempt as #{request.data[:username].inspect}"
16
- sleep 2 # Some processing
17
- { type: 'auth_success' }
18
- end
19
-
20
- # Default
21
- client_2.respond do |request|
22
- puts "Client 2: #{request.data.inspect}"
23
- { type: 'success' }
24
- end
25
-
26
- # Send requests
27
- auth = false
28
- auth_request = client_1.request(
29
- data: { type: 'auth', username: 'mymail@example.com', password: "let's pretend password hash is here" },
30
- sync: true
31
- ) do |response|
32
- unless response.data[:type] == 'auth_success'
33
- puts "Client 1: Authorization failed. Response: #{response.data.inspect}"
34
- next
35
- end
36
-
37
- auth = true
38
- # Do something
39
- end
40
- exit unless auth
41
- puts 'Client 1: Authorized!'
42
-
43
- message = client_1.request(
44
- data: { type: 'message', message: 'Hello!' },
45
- sync: true
46
- ) do |_response|
47
- puts 'Client 1: Message responded'
48
- end
49
-
50
- # Close
51
- r1.close
52
- w1.close
53
- r2.close
54
- w2.close