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.
@@ -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
@@ -1,49 +1,20 @@
1
- require "logger"
1
+ # frozen_string_literal: true
2
2
 
3
- module IORequest
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
- # Access current logger.
22
- #
23
- # @return [Logger, nil]
5
+ module IORequest
6
+ # @return [Logger]
24
7
  def self.logger
25
- @@logger
26
- end
27
-
28
- # Log message.
29
- def self.log(severity, message = nil, progname = nil)
30
- @@logger.log(severity, message, progname) if @@logger
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
- # Log info message.
39
- def self.info(message = nil, progname = nil)
40
- @@logger.log(Logger::INFO, message, progname)
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
@@ -1,112 +1,95 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module IORequest
2
- # Message to other side of IO.
4
+ # Single message. Either request or response.
3
5
  class Message
4
- # @return [Integer] ID of message.
5
- attr_reader :id
6
- alias_method :to_i, :id
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
- # Initialize new message.
12
- #
10
+ # Create new message.
13
11
  # @param data [Hash]
14
- # @param id [Integer, nil] if +nil+ provided {Message.generate_id} will be
15
- # used to generate random id.
16
- def initialize(data, id = nil)
17
- @id = id || Message.generate_id
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
- end
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
- # @return [String] human-readable form.
22
- def to_s
23
- "#{self.class.name}##{@id}: #{@data.inspect}"
23
+ check_data
24
24
  end
25
25
 
26
- # @return [Integer] random numerical ID based on current time and random salt.
27
- def self.generate_id
28
- ((rand(999) + 1) * Time.now.to_f * 1000).to_i % 2**32
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
- # @!visibility private
41
- attr_writer :response
34
+ # @return [Hash]
35
+ attr_reader :data
42
36
 
43
- # Initialize new request.
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 [String] human readable form.
54
- def to_s
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
- # Freezes thread until request is responded or until timeout expends.
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
- # Save into hash.
72
- def to_hash
73
- { type: "request", data: @data, id: @id, response: @response.to_i }
46
+ # @return [Boolean]
47
+ def request?
48
+ @type == :request
74
49
  end
75
50
 
76
- # Initialize new request from hash obtained with {Request#to_hash}.
77
- def self.from_hash(hash)
78
- Request.new(hash[:data], hash[:response], hash[:id])
51
+ # @return [Boolean]
52
+ def response?
53
+ @type == :response
79
54
  end
80
- end
81
55
 
82
- # Response to some request.
83
- class Response < Message
84
- # @return [Integer, Request] ID of initial request or request itself.
85
- attr_reader :request
86
-
87
- # Initialize new response.
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] human readable form.
98
- def to_s
99
- "#{super.to_s}; Initial request ID: #{@request.to_i}"
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
- # Save into hash.
103
- def to_hash
104
- { type: "response", data: @data, id: @id, request: @request.to_i }
76
+ # @param io_w [:write]
77
+ def write_to(io_w)
78
+ io_w.write(to_binary)
105
79
  end
106
80
 
107
- # Initialize new request from hash obtained with {Response#to_hash}.
108
- def self.from_hash(hash)
109
- Response.new(hash[:data], hash[:request], hash[:id])
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