io_request 1.2.0 → 2.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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