iodine 0.1.4 → 0.1.5

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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1b7b0693d39f0937c97786d6070a972dfdadbefb
4
- data.tar.gz: 9392682f01bd3f81f77ce6f4397aeef61df33ad5
3
+ metadata.gz: 3a9f87061376cb31f4e3d6b1cc7fd9dcca401151
4
+ data.tar.gz: 2cde195704434f6ff3af7b4a98a35c3b06f637af
5
5
  SHA512:
6
- metadata.gz: c3cf5a23ae4547f126d11523a8926f974f602bf033a0e34012bd342559ada37701e5b6beb91e26c5a044083621d53e0187c87bd49ba66d2edb180642b8cc3a76
7
- data.tar.gz: e9e14352d3460fdae6d1823b8e5f8a45a61dbb2b28cb0cf92e8f4eadb95000f3d2bb9a5d742b0312371003cde1fa698b202b1abcc2c6ede3b68e511e677c0a9e
6
+ metadata.gz: 0bb1203ab2d5d5ab58ec4b6dd97146b0328802db7b426031319a5e8be6936826811e43a86ae6326bbe86f0811a3013656d15a56fdfb0f29a1d0046d6bf9b485a
7
+ data.tar.gz: 773e868701784aca9b5a5899f0d027cafab8e586f50acf43a40f2efb51252350d689f0af6bc90caeff6f79a6a1e4a0d0bc5a683ca4b0003dde1be3182326d866
@@ -8,6 +8,14 @@ Please notice that this change log contains changes for upcoming releases as wel
8
8
 
9
9
  ***
10
10
 
11
+ Change log v.0.1.5
12
+
13
+ **Feature**: The Response#body can now be set to a File object, allowing Iodine to preserve memory when serving large static files from disc. Limited Range requests are also supported - together, these changes allow Iodine to serve media files (such as movies) while suffering a smaller memory penalty and supporting a wider variaty of players (Safari requires Range request support for it's media player).
14
+
15
+ **Fix**: Fixed an issue where Iodine might take a long time to shut down after a Fatal Error during the server initialization.
16
+
17
+ ***
18
+
11
19
  Change log v.0.1.4
12
20
 
13
21
  **Fix**: fixed an issue with where the WebsocketClient#on_close wouldn't be called for a renewable Websocket connection during shutdown.
data/README.md CHANGED
@@ -57,7 +57,7 @@ exit
57
57
 
58
58
  In this mode, Iodine will continue running until all the tasks have completed and than it will quite. Timer based tasks will be ignored.
59
59
 
60
- ## Simple Usage: Task polling (unreleased version)
60
+ ## Simple Usage: Task polling
61
61
 
62
62
  This mode of operation is effective if want Iodine to periodically initiates new tasks, for instance if you cannot use `cron`.
63
63
 
@@ -129,7 +129,7 @@ Iodine::Http.on_websocket WSChatServer
129
129
 
130
130
  ### Security and limits
131
131
 
132
- Nobody wants their server to crash... Security measures are a fact of life as an internet entety. It is not only the theoretical malicious attacker from which a server must protect itself, but also from the unaware user or client.
132
+ Nobody wants their server to crash... Security measures are a fact of life as an internet entity. It is not only the theoretical malicious attacker from which a server must protect itself, but also from the unaware user or client.
133
133
 
134
134
  Mostly, it is assumed that Iodine will run behind a proxy (i.e. within a Heroku Dyno or viaduct.io process), as such it is assumed that the proxy will protect the Iodine Http server from undue stress.
135
135
 
@@ -137,13 +137,13 @@ Having said that, Iodine is built with certain security measures in mind:
137
137
 
138
138
  - Iodine will not accept IO data (neither from new connections nor form existing ones) while still answering existing requests and performing tasks. This safeguards against task overloading and DoS attacks causing a global crash, allowing the server to resume normal operation once a DoS attack had run it's course (and potentially allowing legitimate requests to be answered while the attack is still underway).
139
139
 
140
- - Iodine will limit the query length (Http/1), header count and header data size as well as well as react to header overloading by immediate disconnections. Iodine's limits are hardcoded to be slightly more than double those of the common Proxies, so this counter-measure will only take effect should an attacker manage to bypass the Proxy.
140
+ - Iodine will limit the query length, header count and header data size as well as well as react to header overloading by immediate disconnections. Iodine's limits are hardcoded to be slightly more than double those of common Proxies, so this counter-measure will only take effect should an attacker manage to bypass the Proxy.
141
141
 
142
142
  - Iodine limits every Http request body-size (file upload data, form data, etc') to ~0.5GB. This setting can be changed using `Iodine::Http.max_http_buffer`. This safeguard is meant to prevent Ruby from crashing due to insufficient memory (an error Iodine cannot, and should not, recover from).
143
143
 
144
144
  It is recommended that this number will be lowered substantially whenever possible, by using `Iodine::Http.max_http_buffer = new_value`
145
145
 
146
- Do be aware that, at the moment, file uploads are passed through the memory when parsed. The parser's memory consumption will hopefully decrese in future releases, however, it is always recomended that large data be avoided when possible or handled using download/upload management protocols and services.
146
+ Do be aware that, at the moment, file upload data must passed through the memory on it's way to the temporary file. The parser's memory consumption will hopefully decrese in future releases, however, it is always recomended that large data be avoided when possible or handled using download/upload management protocols and services.
147
147
 
148
148
  ## Server Usage: Plug in your network protocol
149
149
 
@@ -7,6 +7,10 @@ Dir.chdir Root.join('..').to_s
7
7
  require "bundler/setup"
8
8
  require "iodine/http"
9
9
 
10
+
11
+ # ~/ruby/wrk/wrk -c400 -d10 -t12 http://localhost:3000/
12
+
13
+
10
14
  # Iodine.ssl = true
11
15
  # Iodine.treads = 8
12
16
  # Iodine.protocol.on_http do |req, res|
@@ -39,10 +39,19 @@ require 'openssl'
39
39
  #
40
40
  # # setting up the server is as easy as plugging in your Protocol class:
41
41
  # Iodine.protocol = MyProtocol
42
+ #
43
+ # # setting up cuncurrency can be done with threads - the default is a single thread):
44
+ # Iodine.threads = 8
45
+ #
46
+ # # setting up cuncurrency can also be done with processes (forking) - the default is a single process:
47
+ # Iodine.processes = 4
42
48
  #
43
49
  # # if you are excecuting this script from IRB, exit IRB to start Iodine.
44
50
  # exit
45
51
  #
52
+ # You can test the example using `telnet`
53
+ #
54
+ # Before you write your own protocol, make sure you learn more about the {Iodine::Protocol} and all it's got to offer.
46
55
  module Iodine
47
56
  extend self
48
57
  end
@@ -0,0 +1,12 @@
1
+ require 'iodine/http'
2
+
3
+ module Iodine
4
+ # # Connect to a remote server or network socket
5
+ # # this method is defined under the `client` API.
6
+ # def connect url, protocol, ssl=false
7
+
8
+ # end
9
+ end
10
+
11
+ Iodine.protocol = :client
12
+
@@ -95,11 +95,11 @@ module Iodine
95
95
  def send_response response
96
96
  return false if response.headers.frozen?
97
97
 
98
+ body = response.extract_body
98
99
  request = response.request
99
100
  headers = response.headers
100
- body = response.extract_body
101
101
 
102
- headers['content-length'.freeze] ||= body.to_s.bytesize
102
+ headers['content-length'.freeze] ||= body.size if body
103
103
 
104
104
  keep_alive = response.keep_alive
105
105
  if (request[:version].to_f > 1 && request['connection'.freeze].nil?) || request['connection'.freeze].to_s =~ /ke/i || (headers['connection'.freeze] && headers['connection'.freeze] =~ /^ke/i)
@@ -111,13 +111,13 @@ module Iodine
111
111
  end
112
112
 
113
113
  send_headers response
114
- return log_finished(response) if request.head?
115
- if body
116
- written = write(body)
117
- return Iodine.warn "Http/1 couldn't send response because connection was lost." unless written
114
+ return log_finished(response) && (body && body.close) if request.head? || body.nil?
115
+ until body.eof?
116
+ written = write(body.read 65_536)
117
+ return Iodine.warn("Http/1 couldn't send response because connection was lost.") && body.close unless written
118
118
  response.bytes_written += written
119
- (body.frozen? || body.clear)
120
119
  end
120
+ body.close
121
121
  close unless keep_alive
122
122
  log_finished response
123
123
  end
@@ -130,17 +130,17 @@ module Iodine
130
130
  end
131
131
  return if response.request.head?
132
132
  body = response.extract_body
133
- if body
134
- written = stream_data(body)
135
- return Iodine.warn "Http/1 couldn't send response because connection was lost." unless written
133
+ until body.eof?
134
+ written = write(body.read 65_536)
135
+ return Iodine.warn("Http/1 couldn't send response because connection was lost.") && body.close unless written
136
136
  response.bytes_written += written
137
- end
137
+ end if body
138
138
  if finish
139
139
  response.bytes_written += stream_data('')
140
140
  log_finished response
141
141
  close unless response.keep_alive
142
142
  end
143
- (body.frozen? || body.clear) if body
143
+ body.close if body
144
144
  true
145
145
  end
146
146
 
@@ -1,18 +1,6 @@
1
1
  module Iodine
2
2
  module Http
3
3
  class Http2 < ::Iodine::Protocol
4
- def initialize io, original_request = nil
5
- super(io)
6
- return unless original_request
7
- ::Iodine.warn "Http/2: upgrade handshake settings not implemented. upgrade request:\n#{original_request}"
8
- @last_stream = original_request[:stream_id] = 1
9
- original_request[:io] = self
10
- # deal with the request['http2-settings'] - NO ACK
11
- # HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
12
-
13
- # dispatch the original request
14
- ::Iodine.run original_request, &(::Iodine::Http::Http2.dispatch)
15
- end
16
4
  def on_open
17
5
  # not fully fanctional.
18
6
  ::Iodine.warn "Http/2 requested - support is still experimental."
@@ -52,6 +40,18 @@ module Iodine
52
40
  # 0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a
53
41
  # == PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
54
42
  # + SETTINGS frame
43
+
44
+ # The @options variable contains the original Http1 request, if exists.
45
+ return unless @options
46
+ ::Iodine.warn "Http/2: upgrade handshake settings not implemented. upgrade request:\n#{@options}"
47
+ @last_stream = @options[:stream_id] = 1
48
+ @options[:io] = self
49
+ # deal with the request['http2-settings'] - NO ACK
50
+ # HTTP2-Settings: <base64url encoding of HTTP/2 SETTINGS payload>
51
+
52
+ # dispatch the original request
53
+ ::Iodine.run @options, &(::Iodine::Http::Http2.dispatch)
54
+ @options = nil
55
55
  end
56
56
  def on_message data
57
57
  data = ::StringIO.new data
@@ -61,21 +61,35 @@ module Iodine
61
61
  end
62
62
 
63
63
  def send_response response
64
- request = response.request
65
- return false unless send_headers response, request
66
- return nil if request.head?
64
+ return false if response.headers.frozen?
67
65
  body = response.extract_body
68
- return (log_finished(response) && body.clear) if request.head?
69
- (response.bytes_written += emit_payload(body.to_s, request[:sid], 0, 1) ) && (body.frozen? || body.clear) if body
66
+ request = response.request
67
+ return body && body.close unless send_headers response, request
68
+ return log_finished(response) && body && body.close if request.head?
69
+ if body
70
+ until body.eof?
71
+ response.bytes_written += emit_payload(body.read(@settings[SETTINGS_MAX_FRAME_SIZE]), request[:sid], 0, (body.eof? ? 1 : 0))
72
+ end
73
+ body.close
74
+ else
75
+ emit_payload('', request[:sid], 0, 1)
76
+ end
70
77
  log_finished response
71
78
  end
79
+
72
80
  def stream_response response, finish = false
73
81
  request = response.request
74
- send_headers response, request
75
- return nil if request.head?
76
82
  body = response.extract_body
77
- # puts "should stream #{body}"
78
- (response.bytes_written += emit_payload body.to_s, request[:sid], 0, (finish ? 1 : 0) ) && (body && !body.frozen? && body.clear) if body || finish
83
+ send_headers response, request
84
+ return body && body.close if request.head?
85
+ if body
86
+ until body.eof?
87
+ response.bytes_written += emit_payload(body.read(@settings[SETTINGS_MAX_FRAME_SIZE]), request[:sid], 0, ((finish && body.eof?) ? 1 : 0))
88
+ end
89
+ body.close
90
+ elsif finish
91
+ emit_payload('', request[:sid], 0, 1)
92
+ end
79
93
  log_finished response if finish
80
94
  end
81
95
 
@@ -132,8 +146,8 @@ module Iodine
132
146
  end
133
147
 
134
148
  def send_headers response, request
149
+ return false if response.headers.frozen?
135
150
  headers = response.headers
136
- return false if headers.frozen?
137
151
  # headers[:status] = response.status.to_s
138
152
  headers['set-cookie'] = response.extract_cookies
139
153
  headers.freeze
@@ -307,27 +307,54 @@ module Iodine
307
307
  511=>"Network Authentication Required".freeze
308
308
  }
309
309
 
310
- # This will return the Body object as a String... And set the body to `nil` (seeing as it was extracted from the response).
310
+ # This will return the Body object as an IO like object, such as StringIO (or File)... And set the body to `nil` (seeing as it was extracted from the response).
311
+ #
312
+ # This method will also attempts to set headers and update the response status in relation to the body, if applicable. Call this BEFORE getting any final data about the response or sending the headers.
311
313
  def extract_body
312
- if @body.is_a?(Array)
314
+ body_io = if @body.is_a?(Array)
313
315
  return (@body = nil) if body.empty?
314
- @body = @body.join
315
- extract_body
316
+ StringIO.new @body.join
316
317
  elsif @body.is_a?(String)
317
318
  return (@body = nil) if body.empty?
318
- tmp = @body
319
- @body = nil
320
- tmp
319
+ StringIO.new @body
321
320
  elsif body.nil?
322
321
  nil
323
- elsif body.respond_to? :each
322
+ elsif @body.is_a?(File) || @body.is_a?(Tempfile) || @body.is_a?(StringIO)
323
+ @body
324
+ elsif @body.respond_to? :each
324
325
  tmp = ''
325
- body.each {|s| tmp << s}
326
- body.close if body.respond_to? :close
326
+ @body.each {|s| tmp << s}
327
+ @body.close if @body.respond_to? :close
327
328
  @body = nil
328
329
  return nil if tmp.empty?
329
- tmp
330
+ StringIO.new tmp
330
331
  end
332
+ @body = nil
333
+ body_io.rewind
334
+
335
+ if !(@headers.frozen?) && @request['range'.freeze] && @request.get? && @status == 200 && @headers['content-length'.freeze].nil?
336
+ r = @request['range'.freeze].match(/^bytes=([\d]+)\-([\d]+)?$/i)
337
+ if r
338
+ old_size = body_io.size
339
+ start_pos = r[1].to_i
340
+ end_pos = (r[2] || (old_size - 1)).to_i
341
+ read_length = end_pos-start_pos+ 1
342
+ @status = 206 unless old_size == read_length
343
+ body_io.pos = start_pos
344
+ unless end_pos == old_size-1
345
+ new_body = body_io.read(read_length)
346
+ body_io.close
347
+ body_io = StringIO.new new_body
348
+ body_io.rewind
349
+ end
350
+ @headers['content-range'.freeze] = "bytes #{start_pos}-#{end_pos}/#{old_size}"
351
+ @headers['accept-ranges'.freeze] ||= 'bytes'
352
+ else
353
+ @headers['accept-ranges'.freeze] ||= 'none'
354
+ end
355
+ end
356
+
357
+ body_io
331
358
  end
332
359
 
333
360
  # This will return an array of cookie settings to be appended to `set-cookie` headers.
@@ -258,7 +258,7 @@ module Iodine
258
258
 
259
259
  request[:ws_client_params] = options
260
260
  client = self.new(request)
261
- Iodine::Http::Websockets.new( ( ssl || socket), client, request )
261
+ Iodine::Http::Websockets.new( ( ssl || socket), handler: client, request: request )
262
262
 
263
263
  return client
264
264
 
@@ -28,9 +28,16 @@ module Iodine
28
28
  # cleanup, if needed, using this callback.
29
29
  def on_close
30
30
  end
31
+ # extra cleanup, if needed, when server is shutting down while the websocket is connected.
32
+ #
33
+ # You can use on_close unless some "going away" cleanup is required.
34
+ def on_shutdown
35
+ end
31
36
 
32
37
  # This method allows the class itself to act as the Websocket handler, usable with:
33
- # Iodine::Http.on_websocket Iodine::Http::WebsocketEchoDemo
38
+ #
39
+ # # Iodine::Http::WebsocketHandler's default implementation does nothing.
40
+ # Iodine::Http.on_websocket Iodine::Http::WebsocketHandler
34
41
  def self.call request, response
35
42
  self.new request, response
36
43
  end
@@ -43,7 +50,11 @@ module Iodine
43
50
  def write data
44
51
  # We leverage the fact that the Http response can be used to send Websocket data.
45
52
  #
46
- # you can also use Websocket#send_data or it's alias Websocket#<<
53
+ # you can also use Websocket#send_data or it's alias Websocket#<< for example:
54
+ #
55
+ # # @request[:io] contains the Websockets Protocol instance
56
+ # @request[:io] << data
57
+ #
47
58
  # do NOT use Websocket#write (which writes the data directly, bypassing the protocol).
48
59
  @response << data
49
60
  end
@@ -53,18 +64,18 @@ module Iodine
53
64
  # This implementation is limited to a single process on a single server.
54
65
  # Consider using Redis for a scalable implementation.
55
66
  def unicast id, data
56
- # @request[:io] contains the Websocket Protocol
57
- @request[:io].unicast id, data
67
+ ::Iodine::Http::Websockets.unicast id, data
58
68
  end
59
69
  # Broadcast to all Websockets, except self.
60
70
  #
61
71
  # This implementation is limited to a single process on a single server.
62
72
  # Consider using Redis for a scalable implementation.
63
73
  def broadcast data
64
- @request[:io].broadcast data
74
+ ::Iodine::Http::Websockets.broadcast data, self
65
75
  end
66
76
  # Closes the connection
67
77
  def close
78
+ # @request[:io] contains the Websockets Protocol instance
68
79
  @request[:io].go_away
69
80
  end
70
81
  end
@@ -1,15 +1,11 @@
1
1
  module Iodine
2
2
  module Http
3
3
  class Websockets < ::Iodine::Protocol
4
- # initialize the websocket protocol.
5
- def initialize io, handler, request, ws_extentions = nil
6
- @handler = handler
7
- @ws_extentions = ws_extentions
8
- request[:io] = self
9
- super(io)
10
- end
11
4
  # continue to initialize the websocket protocol.
12
5
  def on_open
6
+ @handler = @options[:handler]
7
+ @ws_extentions = @options[:ext]
8
+ @options[:request][:io] = self
13
9
  @parser = {body: '', stage: 0, step: 0, mask_key: [], len_bytes: []}
14
10
  set_timeout = self.class.default_timeout
15
11
  @handler.on_open if @handler.respond_to? :on_open
@@ -46,7 +42,7 @@ module Iodine
46
42
  # allow Http responses to be used for sending Websocket data.
47
43
  def send_response response, finish = false
48
44
  body = response.extract_body
49
- send_data body
45
+ send_data body.read
50
46
  end
51
47
  alias :stream_response :send_response
52
48
 
@@ -164,7 +160,7 @@ module Iodine
164
160
  response.session
165
161
  # Iodine.log "#{@request[:client_ip]} [#{Time.now.utc}] - #{@connection.object_id} Upgraded HTTP to WebSockets.\n"
166
162
  response.finish
167
- self.new(io.io, handler, request, ws_extentions)
163
+ self.new(io.io, handler: handler, request: request, ext: ws_extentions)
168
164
  return true
169
165
  end
170
166
 
@@ -106,6 +106,7 @@ module Iodine
106
106
  rescue => e
107
107
  fatal e.message
108
108
  fatal "Running existing tasks and exiting."
109
+ @queue << REACTOR
109
110
  Process.kill("INT", 0)
110
111
  next
111
112
  end
@@ -4,6 +4,26 @@ module Iodine
4
4
  #
5
5
  # A new protocol instance will be created for every network connection.
6
6
  #
7
+ # A new protocol might be initialized also when switching between protocols. In this use-case, the
8
+ # protocol can be initialized with an optional second `options` parameter (the first parameter MUST be the IO object used),
9
+ # allowing this data to be accessed within the {#on_open} method using the `@options` instance variable or the `options` accessor.
10
+ #
11
+ # For example, when switching protocols midstream (i.e. for implementing an Http Upgrade to another protocol such as Websockets):
12
+ #
13
+ # class MyNextProtocol
14
+ # def on_open
15
+ # @secret = options[:secret]
16
+ # end
17
+ # end
18
+ #
19
+ # # in the old protocol:
20
+ #
21
+ # class MyOriginalProtocol
22
+ # def switch_to_next_protocol
23
+ # MyNextProtocol.new @io, secret: "my secret data"
24
+ # end
25
+ # end
26
+ #
7
27
  # The recommended use is to inherit this class and override any of the following:
8
28
  # on_open:: called whenever the Protocol is initialized. Override this to initialize the Protocol object.
9
29
  # on_message(data):: called whenever data is received from the IO. Override this to implement the actual network protocol.
@@ -24,6 +44,9 @@ module Iodine
24
44
  #
25
45
  # Using one of the Protocol methods {#write}, {#read}, {#close} is prefered over direct access.
26
46
  attr_reader :io
47
+ # the argument or options Hash passed to the initializer as a second argument (the first argument MUST be the IO object).
48
+ # the value is usually `nil` unless the protocol instance was created by a different protocol while "upgrading" from one protocol to the next.
49
+ attr_reader :options
27
50
 
28
51
  # Sets the timeout in seconds for IO activity (set timeout within {#on_open}).
29
52
  #
@@ -131,11 +154,12 @@ module Iodine
131
154
  # A new Protocol instance set itself up as the IO's protocol (replacing any previous protocol).
132
155
  #
133
156
  # Normally you won't need to override this method. Override {#on_open} instead.
134
- def initialize io
157
+ def initialize io, options = nil
135
158
  @timeout ||= nil
136
159
  @send_locker = Mutex.new
137
160
  @locker = Mutex.new
138
161
  @io = io
162
+ @options = options
139
163
  touch
140
164
  @locker.synchronize do
141
165
  Iodine.switch_protocol @io.to_io, self
@@ -1,3 +1,3 @@
1
1
  module Iodine
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.5"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iodine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Boaz Segev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2015-10-25 00:00:00.000000000 Z
11
+ date: 2015-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -74,6 +74,7 @@ files:
74
74
  - bin/setup
75
75
  - iodine.gemspec
76
76
  - lib/iodine.rb
77
+ - lib/iodine/client.rb
77
78
  - lib/iodine/core.rb
78
79
  - lib/iodine/http.rb
79
80
  - lib/iodine/http/hpack.rb