io_request 1.2.0 → 2.3.1
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/.gitignore +10 -8
- data/.rubocop.yml +37 -0
- data/.rubocop_todo.yml +7 -0
- data/Gemfile +17 -4
- data/README.md +31 -39
- data/Rakefile +31 -10
- data/bin/console +8 -14
- data/io_request.gemspec +23 -21
- data/lib/io_request.rb +20 -8
- data/lib/io_request/authorizer.rb +42 -0
- data/lib/io_request/client.rb +177 -157
- data/lib/io_request/connection/ssl_sockets.rb +163 -0
- data/lib/io_request/logging.rb +13 -42
- data/lib/io_request/message.rb +68 -85
- data/lib/io_request/utility/multi_thread.rb +64 -0
- data/lib/io_request/utility/with_id.rb +60 -0
- data/lib/io_request/utility/with_prog_name.rb +14 -0
- data/lib/io_request/version.rb +3 -1
- metadata +26 -22
- data/Gemfile.lock +0 -29
- data/examples/simple_example.rb +0 -52
- data/lib/io_request/utility.rb +0 -104
@@ -0,0 +1,163 @@
|
|
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
|
+
attr_reader :clients
|
36
|
+
|
37
|
+
# Start server.
|
38
|
+
def start
|
39
|
+
@clients = []
|
40
|
+
|
41
|
+
@server = TCPServer.new(@port)
|
42
|
+
|
43
|
+
@accept_thread = in_thread(name: 'accept_thr') { accept_loop }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Fully stop server.
|
47
|
+
def stop
|
48
|
+
@clients.each(&:close)
|
49
|
+
@clients = []
|
50
|
+
|
51
|
+
@server.close
|
52
|
+
@server = nil
|
53
|
+
|
54
|
+
@accept_thread&.kill
|
55
|
+
@accept_thread = nil
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def initialize_ssl_context(certificate, key)
|
61
|
+
@ctx = OpenSSL::SSL::SSLContext.new
|
62
|
+
@ctx.cert = OpenSSL::X509::Certificate.new certificate
|
63
|
+
@ctx.key = OpenSSL::PKey::RSA.new key
|
64
|
+
@ctx.ssl_version = :TLSv1_2
|
65
|
+
end
|
66
|
+
|
67
|
+
def accept_loop
|
68
|
+
while (socket = @server.accept)
|
69
|
+
handle_socket(socket)
|
70
|
+
end
|
71
|
+
rescue
|
72
|
+
stop
|
73
|
+
end
|
74
|
+
|
75
|
+
def handle_socket(socket)
|
76
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, @ctx)
|
77
|
+
ssl_socket.accept
|
78
|
+
|
79
|
+
client = IORequest::Client.new authorizer: @authorizer
|
80
|
+
begin
|
81
|
+
client.open read_write: ssl_socket
|
82
|
+
client.on_request { |data| @requests_handler.call(data, client) }
|
83
|
+
@clients << client
|
84
|
+
client.on_close do
|
85
|
+
@clients.select!(&:open?)
|
86
|
+
end
|
87
|
+
rescue StandardError => e
|
88
|
+
IORequest.logger.debug "Failed to open client: #{e}"
|
89
|
+
ssl_socket.close
|
90
|
+
end
|
91
|
+
rescue StandardError => e
|
92
|
+
IORequest.logger.warn "Unknown error while handling sockets: #{e}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# SSL socket client.
|
97
|
+
class Client
|
98
|
+
# Initialize new client.
|
99
|
+
# @param authorizer [Authorizer]
|
100
|
+
# @param certificate [String]
|
101
|
+
# @param key [String]
|
102
|
+
def initialize(
|
103
|
+
authorizer: Authorizer.empty,
|
104
|
+
certificate: nil,
|
105
|
+
key: nil,
|
106
|
+
&requests_handler
|
107
|
+
)
|
108
|
+
@authorizer = authorizer
|
109
|
+
@requests_handler = requests_handler
|
110
|
+
|
111
|
+
@client = nil
|
112
|
+
|
113
|
+
initialize_ssl_context(certificate, key)
|
114
|
+
end
|
115
|
+
|
116
|
+
def connected?
|
117
|
+
!@client.nil?
|
118
|
+
end
|
119
|
+
|
120
|
+
# Connect to server.
|
121
|
+
# @param host [String] host of server.
|
122
|
+
# @param port [Integer] port of server.
|
123
|
+
def connect(host = 'localhost', port = 8000)
|
124
|
+
socket = TCPSocket.new(host, port)
|
125
|
+
|
126
|
+
ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, @ctx)
|
127
|
+
ssl_socket.sync_close = true
|
128
|
+
ssl_socket.connect
|
129
|
+
|
130
|
+
@client = IORequest::Client.new authorizer: @authorizer
|
131
|
+
begin
|
132
|
+
@client.open read_write: ssl_socket
|
133
|
+
@client.on_request(&@requests_handler)
|
134
|
+
rescue StandardError
|
135
|
+
ssl_socket.close
|
136
|
+
@client = nil
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Closes connection to server.
|
141
|
+
def disconnect
|
142
|
+
return unless defined?(@client) && !@client.nil?
|
143
|
+
|
144
|
+
@client.close
|
145
|
+
@client = nil
|
146
|
+
end
|
147
|
+
|
148
|
+
# Wrapper over {IORequest::Client#request}
|
149
|
+
def request(*args, **options, &block)
|
150
|
+
@client.request(*args, **options, &block)
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
def initialize_ssl_context(certificate, key)
|
156
|
+
@ctx = OpenSSL::SSL::SSLContext.new
|
157
|
+
@ctx.cert = OpenSSL::X509::Certificate.new certificate
|
158
|
+
@ctx.key = OpenSSL::PKey::RSA.new key
|
159
|
+
@ctx.ssl_version = :TLSv1_2
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
data/lib/io_request/logging.rb
CHANGED
@@ -1,49 +1,20 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
# @!group Logger
|
5
|
-
|
6
|
-
# Default logger.
|
7
|
-
@@logger = Logger.new($LOG_FILE || STDOUT,
|
8
|
-
formatter: Proc.new do |severity, datetime, progname, msg|
|
9
|
-
"[#{datetime}] #{severity} - #{progname}:\t #{msg}\n"
|
10
|
-
end
|
11
|
-
)
|
12
|
-
@@logger.level = $DEBUG ? Logger::DEBUG : Logger::INFO
|
13
|
-
|
14
|
-
# Setup new logger.
|
15
|
-
#
|
16
|
-
# @param logger [Logger, nil]
|
17
|
-
def self.logger=(logger)
|
18
|
-
@@logger = logger
|
19
|
-
end
|
3
|
+
require 'logger'
|
20
4
|
|
21
|
-
|
22
|
-
#
|
23
|
-
# @return [Logger, nil]
|
5
|
+
module IORequest
|
6
|
+
# @return [Logger]
|
24
7
|
def self.logger
|
25
|
-
@@logger
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
end
|
32
|
-
|
33
|
-
# Log warning message.
|
34
|
-
def self.warn(message = nil, progname = nil)
|
35
|
-
@@logger.log(Logger::WARN, message, progname)
|
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
|
+
)
|
36
14
|
end
|
37
15
|
|
38
|
-
#
|
39
|
-
def self.
|
40
|
-
@@logger
|
16
|
+
# @param new_logger [Logger]
|
17
|
+
def self.logger=(new_logger)
|
18
|
+
@@logger = new_logger # rubocop:disable Style/ClassVars
|
41
19
|
end
|
42
|
-
|
43
|
-
# Log debug message.
|
44
|
-
def self.debug(message = nil, progname = nil)
|
45
|
-
@@logger.log(Logger::DEBUG, message, progname)
|
46
|
-
end
|
47
|
-
|
48
|
-
# @!endgroup
|
49
20
|
end
|
data/lib/io_request/message.rb
CHANGED
@@ -1,112 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module IORequest
|
2
|
-
#
|
4
|
+
# Single message. Either request or response.
|
3
5
|
class Message
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
# @return [Hash] stored data.
|
9
|
-
attr_reader :data
|
6
|
+
include Utility::WithID
|
7
|
+
# Types of messages.
|
8
|
+
TYPES = %i[request response].freeze
|
10
9
|
|
11
|
-
#
|
12
|
-
#
|
10
|
+
# Create new message.
|
13
11
|
# @param data [Hash]
|
14
|
-
# @param
|
15
|
-
#
|
16
|
-
|
17
|
-
|
12
|
+
# @param type [Symbol] one of {TYPES} member.
|
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.
|
17
|
+
def initialize(data, type: :request, id: nil, to: nil)
|
18
18
|
@data = data
|
19
|
-
|
19
|
+
@type = type
|
20
|
+
@id = id.nil? ? extended_id : Utility::ExtendedID.from(id)
|
21
|
+
@to = to.nil? ? nil : Utility::ExtendedID.from(to)
|
20
22
|
|
21
|
-
|
22
|
-
def to_s
|
23
|
-
"#{self.class.name}##{@id}: #{@data.inspect}"
|
23
|
+
check_data
|
24
24
|
end
|
25
25
|
|
26
|
-
#
|
27
|
-
def
|
28
|
-
|
26
|
+
# Check data correctness.
|
27
|
+
def check_data
|
28
|
+
raise '@data is not a hash' unless @data.is_a? Hash
|
29
|
+
raise 'incorrect @type' unless TYPES.include? @type
|
30
|
+
raise 'incorrect @id' unless @id.is_a? Utility::ExtendedID
|
31
|
+
raise '@to not specified for response' if response? && @to.nil?
|
29
32
|
end
|
30
|
-
end
|
31
|
-
|
32
|
-
# Request for server or client.
|
33
|
-
class Request < Message
|
34
|
-
# Amount of time to sleep before checking whether responded.
|
35
|
-
JOIN_SLEEP_TIME = 0.5
|
36
|
-
|
37
|
-
# @return [Integer, Response, nil] ID of response or response itself for this message.
|
38
|
-
attr_reader :response
|
39
33
|
|
40
|
-
#
|
41
|
-
|
34
|
+
# @return [Hash]
|
35
|
+
attr_reader :data
|
42
36
|
|
43
|
-
#
|
44
|
-
|
45
|
-
# @param data [Hash]
|
46
|
-
# @param response [Integer, Response, nil]
|
47
|
-
# @param id [Integer, nil]
|
48
|
-
def initialize(data, response = nil, id = nil)
|
49
|
-
@response = response
|
50
|
-
super(data, id)
|
51
|
-
end
|
37
|
+
# @return [Symbol]
|
38
|
+
attr_reader :type
|
52
39
|
|
53
|
-
# @return [
|
54
|
-
|
55
|
-
"#{super.to_s}; #{@response ? "Response ID: #{@response.to_i}" : "Not responded"}"
|
56
|
-
end
|
40
|
+
# @return [Utility::ExtendedID]
|
41
|
+
attr_reader :id
|
57
42
|
|
58
|
-
#
|
59
|
-
|
60
|
-
# @param timeout [Integer, Float, nil] timeout size or +nil+ if no timeout.
|
61
|
-
#
|
62
|
-
# @return [Integer] amount of time passed
|
63
|
-
def join(timeout = nil)
|
64
|
-
time_passed = 0
|
65
|
-
while @response.nil? && (timeout.nil? || time_passed < timeout)
|
66
|
-
time_passed += (sleep JOIN_SLEEP_TIME)
|
67
|
-
end
|
68
|
-
time_passed
|
69
|
-
end
|
43
|
+
# @return [Utility::ExtendedID]
|
44
|
+
attr_reader :to
|
70
45
|
|
71
|
-
#
|
72
|
-
def
|
73
|
-
|
46
|
+
# @return [Boolean]
|
47
|
+
def request?
|
48
|
+
@type == :request
|
74
49
|
end
|
75
50
|
|
76
|
-
#
|
77
|
-
def
|
78
|
-
|
51
|
+
# @return [Boolean]
|
52
|
+
def response?
|
53
|
+
@type == :response
|
79
54
|
end
|
80
|
-
end
|
81
55
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
# @param data [Hash]
|
90
|
-
# @param request [Integer, Request]
|
91
|
-
# @param id [Integer, nil]
|
92
|
-
def initialize(data, request, id = nil)
|
93
|
-
@request = request
|
94
|
-
super(data, id)
|
56
|
+
# @return [String]
|
57
|
+
def to_s
|
58
|
+
if request?
|
59
|
+
"Request##{@id}: #{data}"
|
60
|
+
else
|
61
|
+
"Response##{@id}: #{data} to ##{@to}"
|
62
|
+
end
|
95
63
|
end
|
96
64
|
|
97
|
-
# @return [String]
|
98
|
-
def
|
99
|
-
|
65
|
+
# @return [String] binary data to be passed over IO.
|
66
|
+
def to_binary
|
67
|
+
json_string = JSON.generate({
|
68
|
+
id: @id.to_s,
|
69
|
+
type: @type.to_s,
|
70
|
+
to: @to.to_s,
|
71
|
+
data: @data
|
72
|
+
})
|
73
|
+
[json_string.size, json_string].pack("Sa#{json_string.size}")
|
100
74
|
end
|
101
75
|
|
102
|
-
#
|
103
|
-
def
|
104
|
-
|
76
|
+
# @param io_w [:write]
|
77
|
+
def write_to(io_w)
|
78
|
+
io_w.write(to_binary)
|
105
79
|
end
|
106
80
|
|
107
|
-
#
|
108
|
-
|
109
|
-
|
81
|
+
# @param io_r [:read]
|
82
|
+
# @return [Message]
|
83
|
+
def self.read_from(io_r)
|
84
|
+
size = io_r.read(2)&.unpack1('S') || 0
|
85
|
+
raise ZeroSizeMessageError if size.zero?
|
86
|
+
|
87
|
+
json_string = io_r.read(size).unpack1("a#{size}")
|
88
|
+
msg = JSON.parse(json_string, symbolize_names: true)
|
89
|
+
Message.new(msg[:data],
|
90
|
+
id: msg[:id],
|
91
|
+
type: msg[:type].to_sym,
|
92
|
+
to: msg[:to])
|
110
93
|
end
|
111
94
|
end
|
112
95
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IORequest
|
4
|
+
# Utility methods.
|
5
|
+
module Utility
|
6
|
+
# Adds some methods to spawn new threads and join them.
|
7
|
+
# @note This module creates instance variables with prefix +@__multi_thread__+.
|
8
|
+
module MultiThread
|
9
|
+
private
|
10
|
+
|
11
|
+
# @return [Array<Thread>] array of running threads.
|
12
|
+
def __multi_thread__threads
|
13
|
+
@__multi_thread__threads ||= []
|
14
|
+
end
|
15
|
+
alias running_threads __multi_thread__threads
|
16
|
+
|
17
|
+
# @return [Mutex] threads manipulations mutex.
|
18
|
+
def __multi_thread__mutex
|
19
|
+
@__multi_thread__mutex ||= Mutex.new
|
20
|
+
end
|
21
|
+
|
22
|
+
# Runs block with provided arguments forwarded as arguments in separate thread.
|
23
|
+
# All the inline args will be passed to block.
|
24
|
+
# @param thread_name [String] thread name.
|
25
|
+
# @return [Thread]
|
26
|
+
def in_thread(*args, name: nil)
|
27
|
+
# Synchronizing addition/deletion of new threads. That's important
|
28
|
+
__multi_thread__mutex.synchronize do
|
29
|
+
new_thread = Thread.new(*args) do |*in_args|
|
30
|
+
yield(*in_args)
|
31
|
+
ensure
|
32
|
+
__multi_thread__remove_current_thread
|
33
|
+
end
|
34
|
+
__multi_thread__threads << new_thread
|
35
|
+
new_thread.name = name if name
|
36
|
+
new_thread
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Removes current thread from thread list.
|
41
|
+
def __multi_thread__remove_current_thread
|
42
|
+
__multi_thread__mutex.synchronize do
|
43
|
+
__multi_thread__threads.delete(Thread.current)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# For each running thread.
|
48
|
+
def each_thread(&block)
|
49
|
+
__multi_thread__threads.each(&block)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Kills each thread.
|
53
|
+
def kill_threads
|
54
|
+
each_thread(&:kill)
|
55
|
+
each_thread(&:join)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Joins each thread.
|
59
|
+
def join_threads
|
60
|
+
each_thread(&:join)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|