iodine 0.1.0 → 0.1.1
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 +4 -4
- data/CHANGELOG.md +48 -0
- data/README.md +18 -0
- data/bin/hello_world +2 -2
- data/lib/iodine/http.rb +12 -1
- data/lib/iodine/http/http1.rb +36 -10
- data/lib/iodine/http/http2.rb +13 -1
- data/lib/iodine/http/rack_support.rb +2 -2
- data/lib/iodine/http/request.rb +86 -57
- data/lib/iodine/http/response.rb +5 -3
- data/lib/iodine/http/websocket_client.rb +56 -10
- data/lib/iodine/protocol.rb +3 -2
- data/lib/iodine/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 08fe1f8f8ebb9d46760123f8ca97f9811e350190
|
4
|
+
data.tar.gz: bccdc5e606be4dcb98a27f81b39f115d6ee3c37e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b203fafc5965ae514600798dac5ac96ed61f9a54e5ef55b32bad1335c79844dbf1a967e0603530f68d48fadd919c4f4d81257917cdea0934c9d8ee9232573186
|
7
|
+
data.tar.gz: cb90e413302e828682192bc349c2cadcd1e817b9b3a5c3cb41139153deddcd08ea259e561d6b8c75a30742afb4536b9d74bf41e0ecf27f57b35c970ac0e9e7c7
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Iodine
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/iodine.svg)](https://badge.fury.io/rb/iodine)
|
3
|
+
[![Inline docs](http://inch-ci.org/github/boazsegev/iodine.svg?branch=master)](http://www.rubydoc.info/github/boazsegev/iodine/master/frames)
|
4
|
+
|
5
|
+
Please notice that this change log contains changes for upcoming releases as well. Please refer to the current gem version to review the current release.
|
6
|
+
|
7
|
+
## Changes:
|
8
|
+
|
9
|
+
***
|
10
|
+
|
11
|
+
Change log v.0.1.1
|
12
|
+
|
13
|
+
**Fix**: Fixed an issue where slow processing of Http/1 requests could cause timeout disconnections to occur while the request is being processed.
|
14
|
+
|
15
|
+
**Change/Security**: Uploads now use temporary files. Aceessing the data for file uploads should be done throught the `:file` property of the params hash (i.e. `params[:upload_field_name][:file]`). Using the `:data` property (old API) would cause the whole file to be dumped to the memory and the file's content will be returned as a String.
|
16
|
+
|
17
|
+
**Change/Security**: Http upload limits are now enforced. The current default limit is about ~0.5GB.
|
18
|
+
|
19
|
+
**Feature**: WebsocketClient now supports both an auto-connection-renewal and a polling machanism built in to the `WebsocketClient.connect` API. The polling feature is mostly a handy helper for testing, as it is assumed that connection renewal and pub/sub offer a better design than polling.
|
20
|
+
|
21
|
+
**Logging**: Better Http error logging and recognition.
|
22
|
+
|
23
|
+
***
|
24
|
+
|
25
|
+
Change log v.0.1.0
|
26
|
+
|
27
|
+
**First actual release**:
|
28
|
+
|
29
|
+
We learn, we evolve, we change... but we remember our past and do our best to help with the transition and make it worth the toll it takes on our resources.
|
30
|
+
|
31
|
+
I took much of the code used for GRHttp and GReactor, changed it, morphed it and united it into the singular Iodine gem. This includes Major API changes, refactoring of code, bug fixes and changes to the core approach of how a task/io based application should behave or be constructed.
|
32
|
+
|
33
|
+
For example, Iodine kicks in automatically when the setup script is done, so that all code is run from within tasks and IO connections and no code is run in parallel to the Iodine engine.
|
34
|
+
|
35
|
+
Another example, Iodine now favors Object Oriented code, so that some actions - such as writing a network service - require classes of objects to be declared or inherited (i.e. the Protocol class).
|
36
|
+
|
37
|
+
This allows objects to manage their data as if they were in a single thread environment, unless the objects themselves are calling asynchronous code. For example, the Protocol class makes sure that the `on_open` and `on_message(data)` callbacks are excecuted within a Mutex (`on_close` is an exception to the rule since it is assumed that objects should be prepared to loose network connection at any moment).
|
38
|
+
|
39
|
+
Another example is that real-life deployemnt preferences were favored over adjustability or features. This means that some command-line arguments are automatically recognized (such as the `-p <port>` argument) and thet Iodine assumes a single web service per script/process (whereas GReactor and GRHttp allowed multiple listening sockets).
|
40
|
+
|
41
|
+
I tested this new gem during the 0.0.x version releases, and I feel that version 0.1.0 is stable enough to work with. For instance, I left the Iodine server running all night under stress (repeatedly benchmarking it)... millions of requests later, under heavey load, a restart wasn't required and memory consumption didn't show any increase after the warmup period.
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
## License
|
46
|
+
|
47
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
48
|
+
|
data/README.md
CHANGED
@@ -127,6 +127,24 @@ end
|
|
127
127
|
Iodine::Http.on_websocket WSChatServer
|
128
128
|
```
|
129
129
|
|
130
|
+
### Security and limits
|
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.
|
133
|
+
|
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
|
+
|
136
|
+
Having said that, Iodine is built with certain security measures in mind:
|
137
|
+
|
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
|
+
|
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.
|
141
|
+
|
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
|
+
|
144
|
+
It is recommended that this number will be lowered substantially whenever possible, by using `Iodine::Http.max_http_buffer = new_value`
|
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.
|
147
|
+
|
130
148
|
## Server Usage: Plug in your network protocol
|
131
149
|
|
132
150
|
Iodine is designed to help write network services (Servers) where each script is intended to implement a single server.
|
data/bin/hello_world
CHANGED
@@ -17,7 +17,7 @@ require "iodine/http"
|
|
17
17
|
|
18
18
|
# Iodine.processes = 4
|
19
19
|
|
20
|
-
Iodine.
|
20
|
+
Iodine::Http.on_http { "Hello World!" }
|
21
21
|
|
22
22
|
|
23
23
|
class WSChatServer < Iodine::Http::WebsocketHandler
|
@@ -46,7 +46,7 @@ Process.fork do
|
|
46
46
|
|
47
47
|
Iodine.ssl = true
|
48
48
|
Iodine.port = 3030
|
49
|
-
Iodine.
|
49
|
+
Iodine::Http.on_http do |req, res|
|
50
50
|
res.session[:count] ||= 0
|
51
51
|
res.session[:count] += 1
|
52
52
|
res['content-type'] = 'text/plain'
|
data/lib/iodine/http.rb
CHANGED
@@ -7,6 +7,7 @@ require 'uri'
|
|
7
7
|
require 'tmpdir'
|
8
8
|
require 'zlib'
|
9
9
|
require 'securerandom'
|
10
|
+
require 'tempfile.rb'
|
10
11
|
|
11
12
|
require 'iodine/http/request'
|
12
13
|
require 'iodine/http/response'
|
@@ -94,11 +95,20 @@ module Iodine
|
|
94
95
|
def session_token= token
|
95
96
|
@session_token = token
|
96
97
|
end
|
97
|
-
#
|
98
|
+
# Gets the session token for the Http server (String). Defaults to the name of the script.
|
98
99
|
def session_token
|
99
100
|
@session_token
|
100
101
|
end
|
101
102
|
|
103
|
+
# Sets the maximum bytes allowed for an HTTP's body request (file upload data). Defaults to the command line `-limit` argument, or ~0.25GB.
|
104
|
+
def max_http_buffer= size
|
105
|
+
@max_http_buffer = size
|
106
|
+
end
|
107
|
+
# Gets the maximum bytes allowed for an HTTP's body request (file upload data). Defaults to the command line `-limit` argument, or ~0.25GB.
|
108
|
+
def max_http_buffer
|
109
|
+
@max_http_buffer
|
110
|
+
end
|
111
|
+
|
102
112
|
# Sets whether Iodine will allow connections to the experiemntal Http2 protocol. Defaults to false unless the `http2` command line flag is present.
|
103
113
|
def http2= allow
|
104
114
|
@http2 = allow && true
|
@@ -149,6 +159,7 @@ module Iodine
|
|
149
159
|
protected
|
150
160
|
|
151
161
|
@http2 = (ARGV.index('http2') && true)
|
162
|
+
@max_http_buffer = ((ARGV.index('-limit') && ARGV[ARGV.index('-limit') + 1]) || 536_870_912).to_i
|
152
163
|
|
153
164
|
@websocket_app = @http_app = NOT_IMPLEMENTED = Proc.new { |i,o| false }
|
154
165
|
@session_token = "#{File.basename($0, '.*')}_uuid"
|
data/lib/iodine/http/http1.rb
CHANGED
@@ -16,9 +16,14 @@ module Iodine
|
|
16
16
|
request = (@request ||= ::Iodine::Http::Request.new(self))
|
17
17
|
unless request[:method]
|
18
18
|
l = data.gets.strip
|
19
|
+
if l.bytesize > 16_384
|
20
|
+
write "HTTP/1.0 414 Request-URI Too Long\r\ncontent-length: 20\r\n\r\nRequest URI too Long"
|
21
|
+
Iodine.warn "Http/1 URI too long, closing connection."
|
22
|
+
return close
|
23
|
+
end
|
19
24
|
next if l.empty?
|
20
25
|
request[:method], request[:query], request[:version] = l.split(/[\s]+/, 3)
|
21
|
-
return (Iodine.warn('Protocol Error, closing connection.') && close) unless request[:method] =~ HTTP_METHODS_REGEXP
|
26
|
+
return (Iodine.warn('Htt1 Protocol Error, closing connection.') && close) unless request[:method] =~ HTTP_METHODS_REGEXP
|
22
27
|
request[:version] = (request[:version] || '1.1'.freeze).match(/[\d\.]+/)[0]
|
23
28
|
request[:time_recieved] = Time.now
|
24
29
|
end
|
@@ -27,6 +32,8 @@ module Iodine
|
|
27
32
|
# n = l.slice!(0, l.index(':')); l.slice! 0
|
28
33
|
# n.strip! ; n.downcase!; n.freeze
|
29
34
|
# request[n] ? (request[n].is_a?(Array) ? (request[n] << l) : request[n] = [request[n], l ]) : (request[n] = l)
|
35
|
+
request[:headers_size] ||= 0
|
36
|
+
request[:headers_size] += l.bytesize
|
30
37
|
l = l.strip.split(/:[\s]?/, 2)
|
31
38
|
l[0].strip! ; l[0].downcase!;
|
32
39
|
request[l[0]] ? (request[l[0]].is_a?(Array) ? (request[l[0]] << l[1]) : request[l[0]] = [request[l[0]], l[1] ]) : (request[l[0]] = l[1])
|
@@ -37,6 +44,10 @@ module Iodine
|
|
37
44
|
Iodine.warn 'Protocol Error, closing connection.'
|
38
45
|
return close
|
39
46
|
end
|
47
|
+
if request.length > 2096 || request[:headers_size] > 262_144
|
48
|
+
write "HTTP/1.0 431 Request Header Fields Too Large\r\ncontent-length: 31\r\n\r\nRequest Header Fields Too Large"
|
49
|
+
return (Iodine.warn('Http1 header overloading, closing connection.') && close)
|
50
|
+
end
|
40
51
|
end
|
41
52
|
until request[:body_complete] && request[:headers_complete]
|
42
53
|
if request['transfer-coding'.freeze] == 'chunked'.freeze
|
@@ -48,7 +59,7 @@ module Iodine
|
|
48
59
|
return (Iodine.warn('Protocol Error, closing connection.') && close) unless @parser[:length]
|
49
60
|
request[:body_complete] = true && break if @parser[:length] == 0
|
50
61
|
@parser[:act_length] = 0
|
51
|
-
request[:body] ||= ''
|
62
|
+
request[:body] ||= Tempfile.new('iodine'.freeze, :encoding => 'binary'.freeze)
|
52
63
|
end
|
53
64
|
chunk = data.read(@parser[:length] - @parser[:act_length])
|
54
65
|
return false unless chunk
|
@@ -56,21 +67,27 @@ module Iodine
|
|
56
67
|
@parser[:act_length] += chunk.bytesize
|
57
68
|
(@parser[:act_length] = @parser[:length] = 0) && (data.gets) if @parser[:act_length] >= @parser[:length]
|
58
69
|
elsif request['content-length'.freeze] && request['content-length'.freeze].to_i != 0
|
59
|
-
request[:body] ||= ''
|
60
|
-
packet = data.read(request['content-length'.freeze].to_i - request[:body].
|
70
|
+
request[:body] ||= Tempfile.new('iodine'.freeze, :encoding => 'binary'.freeze)
|
71
|
+
packet = data.read(request['content-length'.freeze].to_i - request[:body].size)
|
61
72
|
return false unless packet
|
62
73
|
request[:body] << packet
|
63
|
-
request[:body_complete] = true if request['content-length'.freeze].to_i - request[:body].
|
74
|
+
request[:body_complete] = true if request['content-length'.freeze].to_i - request[:body].size <= 0
|
64
75
|
elsif request['content-type'.freeze]
|
65
76
|
Iodine.warn 'Body type protocol error.' unless request[:body]
|
66
77
|
line = data.gets
|
67
78
|
return false unless line
|
68
|
-
(request[:body] ||= '') << line
|
79
|
+
(request[:body] ||= Tempfile.new('iodine'.freeze, :encoding => 'binary'.freeze) ) << line
|
69
80
|
request[:body_complete] = true if line =~ EOHEADERS
|
70
81
|
else
|
71
82
|
request[:body_complete] = true
|
72
83
|
end
|
73
84
|
end
|
85
|
+
if request[:body] && request[:body].size > ::Iodine::Http.max_http_buffer
|
86
|
+
Iodine.warn("Http1 message body too big, closing connection (Iodine::Http.max_http_buffer == #{::Iodine::Http.max_http_buffer} bytes) - #{request[:body].size} bytes.")
|
87
|
+
request.delete(:body).tap {|f| f.close unless f.closed? } rescue false
|
88
|
+
write "HTTP/1.0 413 Payload Too Large\r\ncontent-length: 17\r\n\r\nPayload Too Large"
|
89
|
+
return close
|
90
|
+
end
|
74
91
|
(@request = ::Iodine::Http::Request.new(self)) && ( (::Iodine::Http.http2 && ::Iodine::Http::Http2.handshake(request, self, data)) || dispatch(request, data) ) if request.delete :body_complete
|
75
92
|
end
|
76
93
|
end
|
@@ -95,7 +112,12 @@ module Iodine
|
|
95
112
|
|
96
113
|
send_headers response
|
97
114
|
return log_finished(response) if request.head?
|
98
|
-
|
115
|
+
if body
|
116
|
+
written = write(body)
|
117
|
+
return Iodine.warn "Http/1 couldn't send response because connection was lost." unless written
|
118
|
+
response.bytes_written += written
|
119
|
+
(body.frozen? || body.clear)
|
120
|
+
end
|
99
121
|
close unless keep_alive
|
100
122
|
log_finished response
|
101
123
|
end
|
@@ -108,9 +130,13 @@ module Iodine
|
|
108
130
|
end
|
109
131
|
return if response.request.head?
|
110
132
|
body = response.extract_body
|
111
|
-
|
133
|
+
if body
|
134
|
+
written = stream_data(body)
|
135
|
+
return Iodine.warn "Http/1 couldn't send response because connection was lost." unless written
|
136
|
+
response.bytes_written += written
|
137
|
+
end
|
112
138
|
if finish
|
113
|
-
response.bytes_written += stream_data('')
|
139
|
+
response.bytes_written += stream_data('')
|
114
140
|
log_finished response
|
115
141
|
close unless response.keep_alive
|
116
142
|
end
|
@@ -183,7 +209,7 @@ module Iodine
|
|
183
209
|
response.raw_cookies.freeze
|
184
210
|
end
|
185
211
|
def stream_data data = nil
|
186
|
-
write("#{data.to_s.bytesize.to_s(16)}\r\n#{data.to_s}\r\n")
|
212
|
+
write("#{data.to_s.bytesize.to_s(16)}\r\n#{data.to_s}\r\n")
|
187
213
|
end
|
188
214
|
|
189
215
|
def log_finished response
|
data/lib/iodine/http/http2.rb
CHANGED
@@ -290,9 +290,15 @@ module Iodine
|
|
290
290
|
|
291
291
|
@header_buffer << frame[:body]
|
292
292
|
|
293
|
+
frame[:stream][:headers_size] ||= 0
|
294
|
+
frame[:stream][:headers_size] += frame[:body].bytesize
|
295
|
+
|
296
|
+
return (Iodine.warn('Http2 header overloading, closing connection.') && connection_error( ENHANCE_YOUR_CALM ) ) if frame[:stream][:headers_size] > 262_144
|
297
|
+
|
293
298
|
return unless frame[:flags][2] == 1 # fin
|
294
299
|
|
295
300
|
frame[:stream].update @hpack.decode(@header_buffer) # this is where HPACK comes in
|
301
|
+
return (Iodine.warn('Http2 header overloading, closing connection.') && connection_error( ENHANCE_YOUR_CALM ) ) if frame[:stream].length > 2096
|
296
302
|
frame[:stream][:time_recieved] ||= Time.now
|
297
303
|
frame[:stream][:version] ||= '2'.freeze
|
298
304
|
|
@@ -312,7 +318,13 @@ module Iodine
|
|
312
318
|
frame[:body] = frame[:body][1...(0 - frame[:body][0].ord)]
|
313
319
|
end
|
314
320
|
|
315
|
-
frame[:stream][:body]
|
321
|
+
(frame[:stream][:body] ||= Tempfile.new('iodine'.freeze, :encoding => 'binary'.freeze) ) << frame[:body]
|
322
|
+
|
323
|
+
# check request size
|
324
|
+
if frame[:stream][:body].size > ::Iodine::Http.max_http_buffer
|
325
|
+
Iodine.warn("Http2 payload (message size) too big (Iodine::Http.max_http_buffer == #{::Iodine::Http.max_http_buffer} bytes) - #{frame[:stream][:body].size} bytes.")
|
326
|
+
return connection_error( ENHANCE_YOUR_CALM )
|
327
|
+
end
|
316
328
|
|
317
329
|
process_request(@open_streams.delete frame[:sid]) if frame[:flags][0] == 1
|
318
330
|
end
|
@@ -41,7 +41,7 @@ module Iodine
|
|
41
41
|
# env.each {|k, v| env[k] = @request[v] if v.is_a?(Symbol)}
|
42
42
|
RACK_ADDON.each {|k, v| env[k] = (request[v].is_a?(String) ? ( request[v].frozen? ? request[v].dup.force_encoding('ASCII-8BIT') : request[v].force_encoding('ASCII-8BIT') ): request[v])}
|
43
43
|
request.each {|k, v| env["HTTP_#{k.upcase.tr('-', '_')}"] = v if k.is_a?(String) }
|
44
|
-
env['rack.input'.freeze] ||= StringIO.new(''.force_encoding('ASCII-8BIT'.freeze))
|
44
|
+
env['rack.input'.freeze] ||= request[:body] || StringIO.new(''.force_encoding('ASCII-8BIT'.freeze))
|
45
45
|
env['CONTENT_LENGTH'.freeze] = env.delete 'HTTP_CONTENT_LENGTH'.freeze if env['HTTP_CONTENT_LENGTH'.freeze]
|
46
46
|
env['CONTENT_TYPE'.freeze] = env.delete 'HTTP_CONTENT_TYPE'.freeze if env['HTTP_CONTENT_TYPE'.freeze]
|
47
47
|
env['HTTP_VERSION'.freeze] = "HTTP/#{request[:version].to_s}"
|
@@ -64,7 +64,7 @@ module Iodine
|
|
64
64
|
# 'gr.cookies' => :cookies,
|
65
65
|
'REQUEST_METHOD' => :method,
|
66
66
|
'rack.url_scheme' => :scheme,
|
67
|
-
'rack.input' => :
|
67
|
+
'rack.input' => :body
|
68
68
|
}
|
69
69
|
|
70
70
|
RACK_DICTIONARY = {
|
data/lib/iodine/http/request.rb
CHANGED
@@ -206,6 +206,9 @@ module Iodine
|
|
206
206
|
# end
|
207
207
|
# end
|
208
208
|
return request if request[:client_ip]
|
209
|
+
|
210
|
+
request.delete :headers_size
|
211
|
+
|
209
212
|
request[:client_ip] = request['x-forwarded-for'.freeze].to_s.split(/,[\s]?/)[0] || (request[:io].io.to_io.remote_address.ip_address) rescue 'unknown IP'.freeze
|
210
213
|
request[:version] ||= '1'
|
211
214
|
|
@@ -262,7 +265,7 @@ module Iodine
|
|
262
265
|
end
|
263
266
|
|
264
267
|
# Adds paramaters to a Hash object, according to the Iodine's server conventions.
|
265
|
-
def self.add_param_to_hash name, value, target
|
268
|
+
def self.add_param_to_hash name, value, target, &block
|
266
269
|
begin
|
267
270
|
c = target
|
268
271
|
val = rubyfy! value
|
@@ -284,6 +287,7 @@ module Iodine
|
|
284
287
|
else
|
285
288
|
c[n] = val
|
286
289
|
end
|
290
|
+
c.default_proc = block if block
|
287
291
|
else
|
288
292
|
if c[n]
|
289
293
|
c[n].is_a?(Array) ? (c[n] << val) : (c[n] = [c[n], val])
|
@@ -340,80 +344,105 @@ module Iodine
|
|
340
344
|
# read the body's data and parse any incoming data.
|
341
345
|
def self.read_body request
|
342
346
|
# save body for Rack, if applicable
|
343
|
-
request[:rack_input] =
|
347
|
+
# request[:rack_input] = request[:body] if ::Iodine::Http.on_http == ::Iodine::Http::Rack
|
344
348
|
# parse content
|
349
|
+
request[:body].rewind
|
345
350
|
case request['content-type'.freeze].to_s
|
346
351
|
when /x-www-form-urlencoded/
|
347
|
-
extract_params request
|
352
|
+
extract_params request[:body].read.split(/[&;]/), request[:params] #, :form # :uri
|
348
353
|
when /multipart\/form-data/
|
349
|
-
read_multipart request, request
|
354
|
+
read_multipart request, request
|
350
355
|
when /text\/xml/
|
351
356
|
# to-do support xml?
|
352
|
-
make_utf8! request[:body]
|
357
|
+
# request[:xml] = make_utf8! request[:body].read
|
353
358
|
nil
|
354
359
|
when /application\/json/
|
355
|
-
JSON.parse(make_utf8! request[:body]).each {|k, v| add_param_to_hash k, v, request[:params]} rescue true
|
360
|
+
JSON.parse(make_utf8! request[:body].read).each {|k, v| add_param_to_hash k, v, request[:params]} rescue true
|
356
361
|
end
|
362
|
+
request[:body].rewind if request[:body]
|
357
363
|
end
|
358
364
|
|
359
365
|
# parse a mime/multipart body or part.
|
360
|
-
def self.read_multipart request, headers,
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
366
|
+
def self.read_multipart request, headers = {}, boundary = [], name_prefix = ''
|
367
|
+
body = request[:body]
|
368
|
+
return unless headers['content-type'].to_s =~ /multipart/i
|
369
|
+
part_headers = {}
|
370
|
+
extract_header headers['content-type'].split(/[;,][\s]?/), part_headers
|
371
|
+
boundary << part_headers[:boundary]
|
372
|
+
if part_headers[:name]
|
373
|
+
if name_prefix.empty?
|
374
|
+
name_prefix << part_headers[:name]
|
375
|
+
else
|
376
|
+
name_prefix << "[#{part_headers[:name]}]"
|
377
|
+
end
|
378
|
+
end
|
379
|
+
part_headers.delete :name
|
380
|
+
part_headers.clear
|
381
|
+
line = nil
|
382
|
+
boundary_length = nil
|
383
|
+
true until ( (line = body.gets) ) && line =~ /\A--(#{boundary.join '|'})(--)?[\r]?\n/
|
384
|
+
until body.eof?
|
385
|
+
return if line =~ /--[\r]?\n/
|
386
|
+
return boundary.pop if boundary.count > 1 && line.match(/--(#{boundary.join '|'})/)[1] != boundary.last
|
387
|
+
boundary_length = line.bytesize
|
388
|
+
line = body.gets until line.nil? || line =~ /\:/
|
389
|
+
until line.nil? || line =~ /^[\r]?\n/
|
390
|
+
tmp = line.strip.split ':', 2
|
391
|
+
return Iodine.error "Http multipart parsing error (multipart header data malformed): #{line}" unless tmp && tmp.count == 2
|
392
|
+
tmp[0].strip!; tmp[0].downcase!; tmp[1].strip!;
|
393
|
+
part_headers[tmp[0]] = tmp[1]
|
394
|
+
line = body.gets
|
395
|
+
end
|
396
|
+
return if line.nil?
|
397
|
+
if !part_headers['content-disposition'.freeze]
|
398
|
+
Iodine.error "Wrong multipart format with headers: #{part_headers}"
|
399
|
+
return
|
400
|
+
end
|
401
|
+
extract_header part_headers['content-disposition'.freeze].split(/[;,][\s]?/), part_headers
|
402
|
+
if name_prefix.empty?
|
403
|
+
name = part_headers[:name][1..-2]
|
404
|
+
else
|
405
|
+
name = "#{name_prefix}[part_headers[:name][1..-2]}]"
|
371
406
|
end
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
407
|
+
part_headers.delete :name
|
408
|
+
|
409
|
+
start_part_pos = body.pos
|
410
|
+
tmp = /\A--(#{boundary.join '|'})(--)?[\r]?\n/
|
411
|
+
line.clear until ( (line = body.gets) && line =~ tmp)
|
412
|
+
end_part_pos = (body.pos - line.bytesize) - 2
|
413
|
+
new_part_pos = body.pos
|
414
|
+
body.pos = end_part_pos
|
415
|
+
end_part_pos += 1 unless body.getc == "\r"
|
416
|
+
|
417
|
+
if part_headers['content-type'.freeze]
|
418
|
+
if part_headers['content-type'.freeze] =~ /multipart/i
|
419
|
+
body.pos = start_part_pos
|
420
|
+
read_multipart request, part_headers, boundary, name_prefix
|
421
|
+
else
|
422
|
+
part_headers.delete 'content-disposition'.freeze
|
423
|
+
add_param_to_hash "#{name}[type]", make_utf8!(part_headers['content-type'.freeze]), request[:params]
|
424
|
+
part_headers.each {|k,v| add_param_to_hash "#{name}[#{k.to_s}]", make_utf8!(v[0] == '"' ? v[1..-2].to_s : v), request[:params] if v}
|
425
|
+
|
426
|
+
tmp = Tempfile.new 'upload', encoding: 'binary'
|
427
|
+
body.pos = start_part_pos
|
428
|
+
((end_part_pos - start_part_pos)/65_536).to_i.times {tmp << body.read(65_536)}
|
429
|
+
tmp << body.read(end_part_pos - body.pos)
|
430
|
+
add_param_to_hash "#{name}[size]", tmp.size, request[:params]
|
431
|
+
add_param_to_hash "#{name}[file]", tmp, request[:params] do |hash, key|
|
432
|
+
if key == :data || key == "data" && hash.has_key?(:file) && hash[:file].is_a?(::Tempfile)
|
433
|
+
hash[:file].rewind
|
434
|
+
(hash[:data] = hash[:file].read)
|
435
|
+
end
|
382
436
|
end
|
383
|
-
|
384
|
-
read_multipart request, h, p, name_prefix
|
437
|
+
tmp.rewind
|
385
438
|
end
|
439
|
+
else
|
440
|
+
body.pos = start_part_pos
|
441
|
+
add_param_to_hash name, uri_decode!( body.read(end_part_pos - start_part_pos) ), request[:params]
|
386
442
|
end
|
387
|
-
|
388
|
-
end
|
389
|
-
|
390
|
-
# require a part body to exist (data exists) for parsing
|
391
|
-
return true if part.to_s.empty?
|
392
|
-
|
393
|
-
# convert part to `charset` if charset is defined?
|
394
|
-
|
395
|
-
if !headers['content-disposition'.freeze]
|
396
|
-
Iodine.error "Wrong multipart format with headers: #{headers} and body: #{part}"
|
397
|
-
return
|
443
|
+
body.pos = new_part_pos
|
398
444
|
end
|
399
445
|
|
400
|
-
cd = {}
|
401
|
-
|
402
|
-
extract_header headers['content-disposition'.freeze].split(/[;,][\s]?/), cd
|
403
|
-
|
404
|
-
if name_prefix.empty?
|
405
|
-
name = cd[:name][1..-2]
|
406
|
-
else
|
407
|
-
name = "#{name_prefix}[cd[:name][1..-2]}]"
|
408
|
-
end
|
409
|
-
if headers['content-type'.freeze]
|
410
|
-
add_param_to_hash "#{name}[data]", part, request[:params]
|
411
|
-
add_param_to_hash "#{name}[type]", make_utf8!(headers['content-type'.freeze]), request[:params]
|
412
|
-
cd.each {|k,v| add_param_to_hash "#{name}[#{k.to_s}]", make_utf8!(v[1..-2].to_s), request[:params] unless k == :name || !v}
|
413
|
-
else
|
414
|
-
add_param_to_hash name, uri_decode!(part), request[:params]
|
415
|
-
end
|
416
|
-
true
|
417
446
|
end
|
418
447
|
|
419
448
|
end
|
data/lib/iodine/http/response.rb
CHANGED
@@ -238,6 +238,7 @@ module Iodine
|
|
238
238
|
# attempts to write a non-streaming response to the IO. This can be done only once and will quitely fail subsequently.
|
239
239
|
def finish
|
240
240
|
request[:io].send_response self
|
241
|
+
request.delete(:body).tap {|f| f.close unless f.respond_to?(:close) && f.closed? rescue false } if request[:body] && @http_sblocks_count.to_i == 0
|
241
242
|
end
|
242
243
|
|
243
244
|
# Returns the connection's UUID.
|
@@ -347,9 +348,9 @@ module Iodine
|
|
347
348
|
# response.cookies.set_response nil
|
348
349
|
@flash.freeze
|
349
350
|
end
|
350
|
-
[]
|
351
|
-
|
352
|
-
|
351
|
+
arr = []
|
352
|
+
@cookies.each {|k, v| arr << "#{k.to_s}=#{v.to_s}"}
|
353
|
+
arr
|
353
354
|
end
|
354
355
|
|
355
356
|
protected
|
@@ -373,6 +374,7 @@ module Iodine
|
|
373
374
|
def finish_streaming
|
374
375
|
return unless @http_sblocks_count == 0
|
375
376
|
request[:io].stream_response self, true
|
377
|
+
request.delete(:body).tap {|f| f.close unless f.respond_to?(:close) && f.closed? rescue false } if request[:body]
|
376
378
|
end
|
377
379
|
end
|
378
380
|
end
|
@@ -8,16 +8,17 @@ module Iodine
|
|
8
8
|
# Use {Iodine::Http::WebsocketClient.connect} to initialize a client with all the callbacks needed.
|
9
9
|
class WebsocketClient
|
10
10
|
|
11
|
-
attr_accessor :response, :request
|
11
|
+
attr_accessor :response, :request, :params
|
12
12
|
|
13
13
|
def initialize request
|
14
14
|
@response = nil
|
15
15
|
@request = request
|
16
|
-
params = request[:ws_client_params]
|
17
|
-
@on_message = params[:on_message]
|
16
|
+
@params = request[:ws_client_params]
|
17
|
+
@on_message = @params[:on_message]
|
18
18
|
raise "Websocket client must have an #on_message Proc or handler." unless @on_message && @on_message.respond_to?(:call)
|
19
|
-
@on_open = params[:on_open]
|
20
|
-
@on_close = params[:on_close]
|
19
|
+
@on_open = @params[:on_open]
|
20
|
+
@on_close = @params[:on_close]
|
21
|
+
@renew = @params[:renew].to_i
|
21
22
|
end
|
22
23
|
|
23
24
|
def on event_name, &block
|
@@ -41,16 +42,49 @@ module Iodine
|
|
41
42
|
instance_exec( data, &@on_message)
|
42
43
|
end
|
43
44
|
|
44
|
-
def on_open
|
45
|
-
raise 'The on_open even is invalid at this point.' if
|
45
|
+
def on_open
|
46
|
+
raise 'The on_open even is invalid at this point.' if block_given?
|
46
47
|
@io = @request[:io]
|
47
48
|
Iodine::Http::Request.parse @request
|
48
49
|
instance_exec(&@on_open) if @on_open
|
50
|
+
if request[:ws_client_params][:every] && @params[:send]
|
51
|
+
raise TypeError, "Websocket Client `:send` should be either a String or a Proc object." unless @params[:send].is_a?(String) || @params[:send].is_a?(Proc)
|
52
|
+
Iodine.run_every @params[:every], self, @params do |ws, client_params, timer|
|
53
|
+
if ws.closed?
|
54
|
+
timer.stop!
|
55
|
+
next
|
56
|
+
end
|
57
|
+
if client_params[:send].is_a?(String)
|
58
|
+
ws.write client_params[:send]
|
59
|
+
elsif client_params[:send].is_a?(Proc)
|
60
|
+
ws.instance_exec(&client_params[:send])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
49
64
|
end
|
50
65
|
|
51
66
|
def on_close(&block)
|
52
|
-
@on_close = block if block
|
53
|
-
|
67
|
+
return @on_close = block if block
|
68
|
+
if @renew > 0
|
69
|
+
renew_proc = Proc.new do
|
70
|
+
begin
|
71
|
+
Iodine::Http::WebsocketClient.connect(@params[:url], @params)
|
72
|
+
rescue
|
73
|
+
@renew -= 1
|
74
|
+
if @renew <= 0
|
75
|
+
Iodine.fatal "WebsocketClient renewal FAILED for #{@params[:url]}"
|
76
|
+
instance_exec(&@on_close) if @on_close
|
77
|
+
else
|
78
|
+
Iodine.run_after 2, &renew_proc
|
79
|
+
Iodine.warn "WebsocketClient renewal failed for #{@params[:url]}, #{@renew} attempts left"
|
80
|
+
end
|
81
|
+
false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
renew_proc.call
|
85
|
+
else
|
86
|
+
instance_exec(&@on_close) if @on_close
|
87
|
+
end
|
54
88
|
end
|
55
89
|
|
56
90
|
# Sends data through the socket. a shortcut for ws_client.response <<
|
@@ -96,13 +130,23 @@ module Iodine
|
|
96
130
|
# Acceptable options are:
|
97
131
|
# on_open:: the on_open callback. Must be an objects that answers `call(ws)`, usually a Proc.
|
98
132
|
# on_message:: the on_message callback. Must be an objects that answers `call(ws)`, usually a Proc.
|
99
|
-
# on_close:: the on_close callback. Must be an objects that answers `call(ws)`, usually a Proc.
|
133
|
+
# on_close:: the on_close callback - this will ONLY be called if the connection WASN'T renewed. Must be an objects that answers `call(ws)`, usually a Proc.
|
100
134
|
# headers:: a Hash of custom HTTP headers to be sent with the request. Header data, including cookie headers, should be correctly encoded.
|
101
135
|
# cookies:: a Hash of cookies to be sent with the request. cookie data will be encoded before being sent.
|
102
136
|
# timeout:: the number of seconds to wait before the connection is established. Defaults to 5 seconds.
|
137
|
+
# every:: this option, together with `:send` and `:renew`, implements a polling websocket. :every is the number of seconds between each polling event. without `:send`, this option will be ignored. defaults to nil.
|
138
|
+
# send:: a String to be sent or a Proc to be performed each polling interval. This option, together with `:every` and `:renew`, implements a polling websocket. without `:every`, this option will be ignored. defaults to nil. If `:send` is a Proc, it will be executed within the context of the websocket client object, with acess to the websocket client's instance variables and methods.
|
139
|
+
# renew:: the number of times to attempt to renew the connection if the connection is terminated by the remote server. Attempts are made in 2 seconds interval. The default for a polling websocket is 5 attempts to renew. For all other clients, the default is 0 (no renewal).
|
103
140
|
#
|
104
141
|
# The method will block until the connection is established or until 5 seconds have passed (the timeout). The method will either return a WebsocketClient instance object or raise an exception it the connection was unsuccessful.
|
105
142
|
#
|
143
|
+
# Use Iodine::Http.ws_connect for a non-blocking initialization.
|
144
|
+
#
|
145
|
+
# An #on_close callback will only be called if the connection isn't or cannot be renewed. If the connection is renewed,
|
146
|
+
# the #on_open callback will be called again for a new Websocket client instance - but the #on_close callback will NOT be called.
|
147
|
+
#
|
148
|
+
# Due to this design, the #on_open and #on_close methods should NOT be used for opening IO resources (i.e. file handles) nor for cleanup IF the `:renew` option is enabled.
|
149
|
+
#
|
106
150
|
# An on_message Proc must be defined, or the method will fail.
|
107
151
|
#
|
108
152
|
# The on_message Proc can be defined using the optional block:
|
@@ -136,6 +180,8 @@ module Iodine
|
|
136
180
|
options[:on_message] ||= block
|
137
181
|
raise "No #on_message handler defined! please pass a block or define an #on_message handler!" unless options[:on_message]
|
138
182
|
url = URI.parse(url) unless url.is_a?(URI)
|
183
|
+
options[:url] = url
|
184
|
+
options[:renew] ||= 5 if options[:every] && options[:send]
|
139
185
|
|
140
186
|
ssl = url.scheme == "https" || url.scheme == "wss"
|
141
187
|
|
data/lib/iodine/protocol.rb
CHANGED
@@ -49,9 +49,9 @@ module Iodine
|
|
49
49
|
end
|
50
50
|
|
51
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
|
+
# The default implementation closes the connection unless the protocol is still processing information received before timeout occurred.
|
53
53
|
def ping
|
54
|
-
close
|
54
|
+
close unless @locker.locked?
|
55
55
|
end
|
56
56
|
|
57
57
|
#############
|
@@ -98,6 +98,7 @@ module Iodine
|
|
98
98
|
r
|
99
99
|
end
|
100
100
|
rescue => e
|
101
|
+
# Iodine.info e.message
|
101
102
|
close
|
102
103
|
end
|
103
104
|
end
|
data/lib/iodine/version.rb
CHANGED
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
|
+
version: 0.1.1
|
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-
|
11
|
+
date: 2015-10-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -61,6 +61,7 @@ extra_rdoc_files: []
|
|
61
61
|
files:
|
62
62
|
- ".gitignore"
|
63
63
|
- ".travis.yml"
|
64
|
+
- CHANGELOG.md
|
64
65
|
- Gemfile
|
65
66
|
- LICENSE.txt
|
66
67
|
- README.md
|