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 +4 -4
- data/Gemfile +4 -0
- data/bin/console +2 -9
- data/lib/io_request.rb +10 -20
- data/lib/io_request/client.rb +43 -30
- data/lib/io_request/connection/ssl_sockets.rb +173 -0
- data/lib/io_request/logging.rb +20 -0
- data/lib/io_request/message.rb +6 -5
- data/lib/io_request/version.rb +1 -1
- metadata +4 -3
- data/examples/simple_example.rb +0 -54
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a560819ec6ad8dc50801411fcea4ca4263b29e88067187cc2035e3cbad133394
|
4
|
+
data.tar.gz: 1d82e187a4044f25e3ee061a7f382423e1013644dff7663467e694e4139fc8f2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ff19727550d073a8cccc146a2c1de65a6f67458a276b003e122e471ef73d584a52741bdaf07da51e0ed2513f33d0cf8a139eb4e5f0c342e2f6a77cc929f2982a
|
7
|
+
data.tar.gz: 0eaf1f54fcac605a4dc62a8ff7b7bcc41a7d68ee3e75c2020971e9bfb432cc08f22799506ae6e80409f4892b2e5e12cee2e5da28f4657f13a059dfad900402e2
|
data/Gemfile
CHANGED
data/bin/console
CHANGED
@@ -4,12 +4,5 @@
|
|
4
4
|
require 'bundler/setup'
|
5
5
|
require 'io_request'
|
6
6
|
|
7
|
-
|
8
|
-
|
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__)
|
data/lib/io_request.rb
CHANGED
@@ -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
|
data/lib/io_request/client.rb
CHANGED
@@ -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
|
73
|
-
IORequest.logger.debug(prog_name) { 'Saved
|
74
|
-
@
|
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
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
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 =
|
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 #{
|
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
|
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 =
|
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
|
data/lib/io_request/message.rb
CHANGED
@@ -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
|
14
|
-
#
|
15
|
-
#
|
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)
|
84
|
-
raise
|
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)
|
data/lib/io_request/version.rb
CHANGED
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.
|
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-
|
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
|
data/examples/simple_example.rb
DELETED
@@ -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
|