io_request 2.0.0 → 2.4.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: 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