iodine 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of iodine might be problematic. Click here for more details.

@@ -2,19 +2,25 @@ module Iodine
2
2
 
3
3
  public
4
4
 
5
- # Gets the last time at which the IO Reactor was last active (last "tick").
5
+ # @return [Time] Gets the last time at which the IO Reactor was last active (last "tick").
6
6
  def time
7
7
  @time
8
8
  end
9
-
10
- # replaces an IO's protocol object.
9
+ # Replaces (or creates) an IO's protocol object.
10
+ #
11
+ # Accepts 2 arguments, in the following order:
11
12
  #
12
- # accepts:
13
13
  # io:: the raw IO object.
14
- # protocol:: a protocol instance - should be a Protocol or SSLProtocol (subclass) instance. type will NOT be checked - but Iodine could break if there is a type mismatch.
15
- #
14
+ # protocol:: a protocol instance - should be an instance of a class inheriting from {Iodine::Protocol}. type will NOT be checked - but Iodine could break if there is a type mismatch.
15
+ # @return [Protocol]
16
16
  def switch_protocol *args
17
17
  @io_in << args
18
+ args[1]
19
+ end
20
+
21
+ # @return [Array] Returns an Array with all the currently active connection's Protocol instances.
22
+ def to_a
23
+ @ios.values
18
24
  end
19
25
 
20
26
 
@@ -22,14 +28,16 @@ module Iodine
22
28
 
23
29
  @port = (ARGV.index('-p') && ARGV[ARGV.index('-p') + 1]) || ENV['PORT'] || 3000
24
30
  @bind = (ARGV.index('-ip') && ARGV[ARGV.index('-ip') + 1]) || ENV['IP'] || "0.0.0.0"
31
+ @ssl = (ARGV.index('ssl') && true) || (@port == 443)
25
32
  @protocol = nil
26
33
  @ssl_context = nil
34
+ @ssl_protocols = {}
27
35
  @time = Time.now
28
36
 
29
37
  @timeout_proc = Proc.new {|prot| prot.timeout?(@time) }
30
38
  @status_loop = Proc.new {|io| @io_out << io if io.closed? || !(io.stat.readable? rescue false) }
31
39
  @close_callback = Proc.new {|prot| prot.on_close if prot }
32
- REACTOR = Proc.new do
40
+ REACTOR = [ (Proc.new do
33
41
  if @queue.empty?
34
42
  #clear any closed IO objects.
35
43
  @time = Time.now
@@ -42,43 +50,41 @@ module Iodine
42
50
  until @io_out.empty?
43
51
  o_io = @io_out.pop
44
52
  o_io.close unless o_io.closed?
45
- queue @close_callback, @ios.delete(o_io)
53
+ run @ios.delete(o_io), &@close_callback
46
54
  end
47
55
  # react to IO events
48
56
  begin
49
57
  r = IO.select(@ios.keys, nil, nil, 0.15)
50
- r[0].each {|io| queue @ios[io] } if r
58
+ r[0].each {|io| @queue << [@ios[io]] } if r
51
59
  rescue => e
52
60
 
53
61
  end
54
62
  unless @stop && @queue.empty?
55
63
  # @ios.values.each &@timeout_loop
56
- @check_timers && queue(@check_timers)
57
- queue REACTOR
64
+ @check_timers && (@queue << @check_timers)
65
+ @queue << REACTOR
58
66
  end
59
67
  else
60
- queue REACTOR
68
+ @queue << REACTOR
61
69
  end
62
- end
70
+ end )]
63
71
  # internal helper methods and classes.
64
72
  module Base
65
73
  # the server listener Protocol.
66
74
  class Listener < ::Iodine::Protocol
67
75
  def on_open
68
76
  @protocol = Iodine.protocol
77
+ @ssl = Iodine.ssl
69
78
  end
70
79
  def call
71
80
  begin
72
81
  n_io = nil
73
82
  loop do
74
83
  n_io = @io.accept_nonblock
75
- @protocol.accept(n_io)
84
+ @protocol.accept(n_io, @ssl)
76
85
  end
77
86
  rescue Errno::EWOULDBLOCK => e
78
87
 
79
- rescue OpenSSL::SSL::SSLError => e
80
- warn "SSL Error - Self-signed Certificate?".freeze
81
- n_io.close if n_io && !n_io.closed?
82
88
  rescue => e
83
89
  n_io.close if n_io && !n_io.closed?
84
90
  @stop = true
@@ -92,19 +98,56 @@ module Iodine
92
98
  ## remember to set traps (once) when 'listen' is called.
93
99
  run do
94
100
  next unless @protocol
95
-
96
- shut_down_proc = Proc.new {|protocol| protocol.on_shutdown ; protocol.close }
97
- on_shutdown do
98
- @logger << "Stopping to listen on port #{@port} and shutting down.\n"
99
- @server.close unless @server.closed?
100
- @ios.values.each {|p| queue shut_down_proc, p }
101
+ if @protocol.is_a?( ::Class ) && @protocol.ancestors.include?( ::Iodine::Protocol )
102
+ begin
103
+ @server = ::TCPServer.new(@bind, @port)
104
+ rescue => e
105
+ Iodine.fatal e.message
106
+ Iodine.fatal "Running existing tasks and exiting."
107
+ @stop = true
108
+ next
109
+ end
110
+ shut_down_proc = Proc.new {|protocol| protocol.on_shutdown ; protocol.close }
111
+ on_shutdown do
112
+ @logger << "Stopping to listen on port #{@port} and shutting down.\n"
113
+ @server.close unless @server.closed?
114
+ @ios.values.each {|p| run p, &shut_down_proc }
115
+ end
116
+ ::Iodine::Base::Listener.accept(@server, false)
117
+ @logger << "Iodine #{VERSION} is listening on port #{@port}#{ ' to SSL/TLS connections.' if @ssl}\n"
118
+ if @spawn_count && @spawn_count.to_i > 1 && Process.respond_to?(:fork)
119
+ @logger << "Server will run using #{@spawn_count.to_i} processes - Spawning #{@spawn_count.to_i - 1 } more processes.\n"
120
+ (@spawn_count.to_i - 1).times do
121
+ Process.fork do
122
+ @logger << "Spawned process: #{Process.pid}.\n"
123
+ on_shutdown { @logger << "Shutting down process #{Process.pid}.\n" }
124
+ threads = []
125
+ @queue.clear
126
+ @queue << REACTOR
127
+ @thread_count.times { threads << Thread.new { cycle } }
128
+ unless @stop
129
+ old_int_trap = trap('INT') { throw :stop; trap('INT', old_int_trap) if old_int_trap }
130
+ old_term_trap = trap('TERM') { throw :stop; trap('TERM', old_term_trap) if old_term_trap }
131
+ catch(:stop) { sleep }
132
+ @stop = true
133
+ # setup exit timeout.
134
+ threads.each {|t| Thread.new {sleep 25; t.kill; t.kill } }
135
+ end
136
+ threads.each {|t| t.join rescue true }
137
+ end
138
+ end
139
+
140
+ end
141
+ @logger << "Press ^C to stop the server.\n"
142
+ else
143
+ @logger << "Iodine #{VERSION} is running.\n"
144
+ on_shutdown do
145
+ @logger << "Iodine says goodbye.\n"
146
+ end
147
+ @logger << "Press ^C to stop the cycling.\n"
101
148
  end
102
- @server = ::TCPServer.new(@bind, @port)
103
- ::Iodine::Base::Listener.accept(@server)
104
- @logger << "Iodine #{VERSION} is listening on port #{@port}\n"
105
149
  old_int_trap = trap('INT') { throw :stop; trap('INT', old_int_trap) if old_int_trap }
106
150
  old_term_trap = trap('TERM') { throw :stop; trap('TERM', old_term_trap) if old_term_trap }
107
- @logger << "Press ^C to stop the server.\n"
108
- queue REACTOR
151
+ @queue << REACTOR
109
152
  end
110
153
  end
@@ -4,12 +4,12 @@ module Iodine
4
4
  #
5
5
  # A new protocol instance will be created for every network connection.
6
6
  #
7
- # The recommended use is to inherit this class (or {SSLProtocol}) and override any of the following:
7
+ # The recommended use is to inherit this class and override any of the following:
8
8
  # on_open:: called whenever the Protocol is initialized. Override this to initialize the Protocol object.
9
9
  # on_message(data):: called whenever data is received from the IO. Override this to implement the actual network protocol.
10
10
  # on_close:: called AFTER the Protocol's IO is closed.
11
11
  # on_shutdown:: called when the server's shutdown process had started and BEFORE the Protocol's IO is closed. It allows graceful shutdown for network protocols.
12
- # ping::
12
+ # ping:: called when timeout was reached. see {#set_timeout}
13
13
  #
14
14
  # Once the network protocol class was created, remember to tell Iodine about it:
15
15
  # class MyProtocol << Iodine::Protocol
@@ -20,12 +20,14 @@ module Iodine
20
20
  #
21
21
  class Protocol
22
22
 
23
- # returns the raw IO object. Using one of the Protocol methods {#write}, {#read}, {#close} is prefered over direct access.
23
+ # returns the IO object. If the connection uses SSL/TLS, this will return the SSLSocket (not a native IO object).
24
+ #
25
+ # Using one of the Protocol methods {#write}, {#read}, {#close} is prefered over direct access.
24
26
  attr_reader :io
25
27
 
26
28
  # Sets the timeout in seconds for IO activity (set timeout within {#on_open}).
27
29
  #
28
- # After timeout is reached, {#ping} will be closed. The connection will be closed if {#ping} returns `false` or `nil`.
30
+ # After timeout is reached, {#ping} will be called. The connection will be closed if {#ping} returns `false` or `nil`.
29
31
  def set_timeout seconds
30
32
  @timeout = seconds
31
33
  end
@@ -46,11 +48,19 @@ module Iodine
46
48
  def on_shutdown
47
49
  end
48
50
 
49
- # This method is called whenever a timeout has occurred. Either implement a ping or return `false` to disconnect.
50
- #
51
- # A `false` or `nil` return value will cause disconnection
51
+ # This method is called whenever a timeout has occurred. Either implement a ping or close the connection.
52
+ # The default implementation closes the connection.
52
53
  def ping
53
- false
54
+ close
55
+ end
56
+
57
+ #############
58
+ ## functionality and helpers
59
+
60
+
61
+ # returns true id the protocol is using an encrypted connection (the IO is an OpenSSL::SSL::SSLSocket).
62
+ def ssl?
63
+ @io.is_a?(OpenSSL::SSL::SSLSocket) # io.npn_protocol
54
64
  end
55
65
 
56
66
 
@@ -65,9 +75,15 @@ module Iodine
65
75
  # reads from the IO up to the specified number of bytes (defaults to ~2Mb).
66
76
  def read size = 2_097_152
67
77
  touch
68
- @io.recv_nonblock( size )
69
- rescue => e
78
+ ssl? ? read_ssl(size) : @io.recv_nonblock( size )
79
+ # @io.read_nonblock( size ) # this one is a bit slower...
80
+ rescue OpenSSL::SSL::SSLErrorWaitReadable, IO::WaitReadable, IO::WaitWritable
70
81
  nil
82
+ rescue IOError, Errno::ECONNRESET
83
+ close
84
+ rescue => e
85
+ Iodine.warn "Protocol read error: #{e.class.name} #{e.message} (closing connection)"
86
+ close
71
87
  end
72
88
 
73
89
  # this method, writes data to the socket / io object.
@@ -79,36 +95,48 @@ module Iodine
79
95
  r
80
96
  end
81
97
  rescue => e
82
- # GReactor.warn e
83
98
  close
84
99
  end
85
100
  end
86
101
 
102
+ # returns the connection's unique local ID as a Hex string.
103
+ #
104
+ # This can be used locally but not across processes.
105
+ def id
106
+ @id ||= @io.to_io.to_s(16)
107
+ end
87
108
 
88
- # This method allows switiching the IO's protocol that will be used the NEXT time
89
- # Iodine receives data using this protocol's IO.
109
+ # returns an [Enumerable](http://ruby-doc.org/core-2.2.3/Enumerable.html) with all the active connections.
90
110
  #
91
- # Switing protocols bypasses the {#on_close} method. Override this method for any cleanup needed (if at any),
92
- # but remember to call `super` for the actual protocol switching implementation.
93
- def switch_protocol new_protocol
94
- Iodine.switch_protocol @io, new_protocol
111
+ # if a block is passed, than this method exceutes the block.
112
+ def self.each
113
+ if block_given?
114
+ Iodine.to_a.each {|p| yield(p) if p.is_a?(self) }
115
+ else
116
+ ( Iodine.to_a.select {|p| p.is_a?(self) } ).each
117
+ end
95
118
  end
96
119
 
120
+
97
121
  #################
98
122
  ## the following are Iodine's "system" methods, used internally. Don't override.
99
123
 
100
124
 
101
125
  # This method is used by Iodine to initialized the Protocol.
102
126
  #
127
+ # A new Protocol instance set itself up as the IO's protocol (replacing any previous protocol).
128
+ #
103
129
  # Normally you won't need to override this method. Override {#on_open} instead.
104
130
  def initialize io
105
131
  @timeout ||= nil
106
132
  @send_locker = Mutex.new
107
133
  @locker = Mutex.new
108
134
  @io = io
109
- switch_protocol self
110
135
  touch
111
- on_open
136
+ @locker.synchronize do
137
+ Iodine.switch_protocol @io.to_io, self
138
+ on_open
139
+ end
112
140
  end
113
141
 
114
142
  # Called by Iodine whenever there is data in the IO's read buffer.
@@ -132,24 +160,45 @@ module Iodine
132
160
  #
133
161
  # Normally you won't need to override this method. See {#ping}
134
162
  def timeout? time
135
- (ping || close) if @timeout && !@send_locker.locked? && ( (time - @last_active) > @timeout )
163
+ ping if @timeout && !@send_locker.locked? && ( (time - @last_active) > @timeout )
136
164
  end
137
165
 
138
166
 
139
167
 
168
+
169
+ protected
140
170
  # This method is used by Iodine to create the IO handler whenever a new connection is established.
141
171
  #
142
172
  # Normally you won't need to override this method.
143
- def self.accept io
144
- self.new(io)
173
+ def self.accept io, ssl
174
+ ssl ? SSLConnector.new(io, self) : self.new(io)
145
175
  end
146
-
147
- protected
148
-
149
176
  # This methos updates the timeout "watch", signifying the IO was active.
150
177
  def touch
151
178
  @last_active = Iodine.time
152
179
  end
180
+
181
+ # reads from the IO up to the specified number of bytes (defaults to ~1Mb).
182
+ def read_ssl size
183
+ @send_locker.synchronize do
184
+ data = ''
185
+ begin
186
+ (data << @io.read_nonblock(size).to_s) until data.bytesize >= size
187
+ rescue OpenSSL::SSL::SSLErrorWaitReadable, IO::WaitReadable, IO::WaitWritable
188
+
189
+ rescue IOError
190
+ close
191
+ rescue => e
192
+ Iodine.warn "SSL Protocol read error: #{e.class.name} #{e.message} (closing connection)"
193
+ close
194
+ end
195
+ return false if data.to_s.empty?
196
+ touch
197
+ data
198
+ end
199
+ end
200
+
201
+
153
202
  end
154
203
 
155
204
  end
@@ -14,6 +14,15 @@ module Iodine
14
14
  @thread_count = count
15
15
  end
16
16
 
17
+ # Sets the number of processes that should be spawned in Server mode. Defaults to 1 (no processes spawned).
18
+ #
19
+ # * Forking (spwaning processes) might NOT work on all systems (forking is supported by Ruby on Unix systems).
20
+ # * Please make sure your code is safe to fork into different processes. For instance, Websocket broadcasting and unicasting won't work across different processes unless synced using an external Pub/Sub service/database such as Redis.
21
+ # * Forking might cause some tasks (such as time based tasks) to be performed twice (once for each process). This is a feature. To avoid duplicated task performance, use a task (delayed execution) to initialize any tasks you want to perform only once. While the initial time based tasks and the server are shared across processes, the initial task stack will only run on the main process.
22
+ def processes= count
23
+ @spawn_count = count
24
+ end
25
+
17
26
  # Sets the server port. Defaults to the runtime `-p` argument, or the ENV['PORT'] or 3000 (in this order).
18
27
  def port= port
19
28
  @port = port
@@ -23,7 +32,9 @@ module Iodine
23
32
  @bind = address
24
33
  end
25
34
 
26
- # Sets the Protocol the Iodine Server will use. Should be a child of Protocol or SSLProtocol. Defaults to nil (no server).
35
+ # Sets the Protocol the Iodine Server will use. Should be a child of {Iodine::Protocol}. Defaults to nil (no server).
36
+ #
37
+ # If the protocol passed does NOT inherit from {Iodine::Protocol}, Iodine will cycle without initiating a server until stopped (TimedEvent mode).
27
38
  def protocol= protocol
28
39
  @stop = protocol ? false : true
29
40
  @protocol = protocol
@@ -33,15 +44,45 @@ module Iodine
33
44
  @protocol
34
45
  end
35
46
 
36
- # Sets the SSL Context to be used when using an SSLProtocol. Defaults to a self signed certificate.
47
+ # Sets the SSL flag, so that Iodine will require that new connection be encrypted.
48
+ def ssl= required
49
+ @ssl = required && true
50
+ end
51
+ # Returns true if Iodine will require that new connection be encrypted.
52
+ def ssl
53
+ @ssl
54
+ end
55
+
56
+ # Sets the SSL Context to be used when using an encrypted connection. Defaults to a self signed certificate and no verification.
57
+ #
58
+ # Manually setting the context will automatically set the SSL flag,
59
+ # so that Iodine will require encryption for new incoming connections.
37
60
  def ssl_context= context
61
+ @ssl = true
38
62
  @ssl_context = context
39
63
  end
40
- # Gets the SSL Context to be used when using an SSLProtocol.
64
+
65
+ # Gets the SSL Context to be used when using an encrypted connection.
41
66
  def ssl_context
42
67
  @ssl_context ||= init_ssl_context
43
68
  end
44
69
 
70
+ # Sets the an SSL Protocol Hash (`'name' => Protocol`), allowing dynamic Protocol Negotiation.
71
+ # At the moment only NPN is supported. ALPN support should be established in a future release.
72
+ #
73
+ # * please notice that using allowing dynamic Protocol Negotiation could cause unexpected protocol choices when attempting to implement Opportunistic Encryption with {Iodine::SSLConnector}.
74
+ def ssl_protocols= protocol_hash
75
+ raise TypeError, "Iodine.ssl_protocols should be a Hash with Strings for keys (protocol identifiers) and Classes as values (Protocol classes)." unless protocol_hash.is_a?(Hash)
76
+ @ssl = true
77
+ ssl_context.npn_protocols = protocol_hash.keys
78
+ @ssl_protocols = protocol_hash
79
+ end
80
+
81
+ # Gets the SSL Protocol Hash used for
82
+ def ssl_protocols
83
+ @ssl_protocols
84
+ end
85
+
45
86
 
46
87
  protected
47
88
 
@@ -0,0 +1,47 @@
1
+ module Iodine
2
+
3
+ # This is a mini-protocol used only to implement the SSL Handshake in a non-blocking manner,
4
+ # allowing for a hardcoded timeout (which you can monkey patch) of 3 seconds.
5
+ class SSLConnector < Protocol
6
+ def initialize io, protocol
7
+ @protocol = protocol
8
+ super(io)
9
+ end
10
+ TIMEOUT = 3 # hardcoded SSL/TLS handshake timeout
11
+ def on_open
12
+ timeout = TIMEOUT
13
+ @ssl_socket = ::OpenSSL::SSL::SSLSocket.new(@io, ::Iodine.ssl_context)
14
+ @ssl_socket.sync_close = true
15
+ end
16
+
17
+ # atempt an SSL Handshale
18
+ def call
19
+ return if @locker.locked?
20
+ return unless @locker.try_lock
21
+ begin
22
+ @ssl_socket.accept_nonblock
23
+ rescue ::IO::WaitReadable, ::IO::WaitWritable
24
+ return
25
+ rescue ::OpenSSL::SSL::SSLError
26
+ @e = ::OpenSSL::SSL::SSLError.new "Self-signed Certificate?".freeze
27
+ close
28
+ return
29
+ rescue => e
30
+ ::Iodine.warn "SSL Handshake failed with: #{e.message}".freeze
31
+ @e = e
32
+ close
33
+ return
34
+ ensure
35
+ @locker.unlock
36
+ end
37
+ ( (@ssl_socket.npn_protocol && ::Iodine.ssl_protocols[@ssl_socket.npn_protocol]) || @protocol).new @ssl_socket
38
+ end
39
+ def on_close
40
+ # inform
41
+ ::Iodine.warn "SSL Handshake #{@e ? "failed with: #{@e.message} (#{@e.class.name})" : 'timed-out.'}".freeze
42
+ # the core @io is already closed, but let's make sure the SSL object is closed as well.
43
+ @ssl_socket.close unless @ssl_socket.closed?
44
+ end
45
+ end
46
+
47
+ end