rainbows 4.4.3 → 4.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/.document +1 -0
  2. data/.gitignore +1 -0
  3. data/Documentation/rainbows.1.txt +8 -4
  4. data/GIT-VERSION-GEN +34 -35
  5. data/GNUmakefile +4 -2
  6. data/HACKING +72 -0
  7. data/bin/rainbows +5 -0
  8. data/lib/rainbows/const.rb +3 -3
  9. data/lib/rainbows/coolio/client.rb +18 -6
  10. data/lib/rainbows/coolio/thread_client.rb +2 -0
  11. data/lib/rainbows/epoll/client.rb +35 -12
  12. data/lib/rainbows/ev_core.rb +5 -4
  13. data/lib/rainbows/event_machine/client.rb +9 -4
  14. data/lib/rainbows/process_client.rb +7 -3
  15. data/lib/rainbows/response.rb +54 -18
  16. data/lib/rainbows/revactor/client/methods.rb +1 -1
  17. data/lib/rainbows/stream_response_epoll.rb +34 -15
  18. data/lib/rainbows/stream_response_epoll/client.rb +11 -3
  19. data/lib/rainbows/writer_thread_pool/client.rb +2 -0
  20. data/lib/rainbows/xepoll/client.rb +0 -3
  21. data/lib/rainbows/xepoll_thread_pool/client.rb +1 -1
  22. data/lib/rainbows/xepoll_thread_spawn/client.rb +1 -1
  23. data/rainbows.gemspec +6 -3
  24. data/t/GNUmakefile +10 -3
  25. data/t/byte-range-common.sh +1 -1
  26. data/t/hijack.ru +56 -0
  27. data/t/t0000-simple-http.sh +9 -9
  28. data/t/t0001-unix-http.sh +8 -8
  29. data/t/t0003-reopen-logs.sh +8 -8
  30. data/t/t0004-heartbeat-timeout.sh +3 -3
  31. data/t/t0005-large-file-response.sh +1 -1
  32. data/t/t0010-keepalive-timeout-effective.sh +2 -2
  33. data/t/t0011-close-on-exec-set.sh +2 -2
  34. data/t/t0017-keepalive-timeout-zero.sh +2 -2
  35. data/t/t0024-pipelined-sendfile-response.sh +2 -2
  36. data/t/t0027-nil-copy_stream.sh +1 -1
  37. data/t/t0030-fast-pipe-response.sh +1 -1
  38. data/t/t0034-pipelined-pipe-response.sh +2 -2
  39. data/t/t0035-kgio-pipe-response.sh +1 -1
  40. data/t/t0040-keepalive_requests-setting.sh +4 -4
  41. data/t/t0043-quit-keepalive-disconnect.sh +3 -3
  42. data/t/t0044-autopush.sh +2 -2
  43. data/t/t0045-client_max_header_size.sh +2 -2
  44. data/t/t0100-rack-input-hammer-chunked.sh +4 -4
  45. data/t/t0100-rack-input-hammer-content-length.sh +4 -4
  46. data/t/t0106-rack-input-keepalive.sh +6 -6
  47. data/t/t0200-async-response.sh +5 -5
  48. data/t/t0202-async-response-one-oh.sh +5 -5
  49. data/t/t0300-async_sinatra.sh +5 -5
  50. data/t/t0400-em-async-app.sh +2 -2
  51. data/t/t0401-em-async-tailer.sh +2 -2
  52. data/t/t0402-async-keepalive.sh +23 -23
  53. data/t/t0500-cramp-streaming.sh +3 -3
  54. data/t/t0600-rack-fiber_pool.sh +1 -1
  55. data/t/t0700-app-deferred.sh +2 -2
  56. data/t/t0800-rack-hijack.sh +27 -0
  57. data/t/t9000-rack-app-pool.sh +3 -3
  58. data/t/t9001-sendfile-to-path.sh +1 -1
  59. data/t/t9100-thread-timeout.sh +1 -1
  60. data/t/t9101-thread-timeout-threshold.sh +1 -1
  61. data/t/test-lib.sh +15 -0
  62. data/t/test_isolate.rb +4 -3
  63. metadata +26 -6
  64. data/t/bin/utee +0 -12
@@ -10,6 +10,7 @@ class Rainbows::EventMachine::Client < EM::Connection
10
10
  end
11
11
 
12
12
  alias write send_data
13
+ alias hijacked detach
13
14
 
14
15
  def receive_data(data)
15
16
  # To avoid clobbering the current streaming response
@@ -37,9 +38,11 @@ class Rainbows::EventMachine::Client < EM::Connection
37
38
  @env[REMOTE_ADDR] = @_io.kgio_addr
38
39
  @env[ASYNC_CALLBACK] = method(:write_async_response)
39
40
  @env[ASYNC_CLOSE] = EM::DefaultDeferrable.new
41
+ @hp.hijack_setup(@env, @_io)
40
42
  status, headers, body = catch(:async) {
41
43
  APP.call(@env.merge!(RACK_DEFAULTS))
42
44
  }
45
+ return hijacked if @hp.hijacked?
43
46
 
44
47
  if (nil == status || -1 == status)
45
48
  @deferred = true
@@ -67,8 +70,8 @@ class Rainbows::EventMachine::Client < EM::Connection
67
70
  def ev_write_response(status, headers, body, alive)
68
71
  @state = :headers if alive
69
72
  if body.respond_to?(:errback) && body.respond_to?(:callback)
73
+ write_headers(status, headers, alive, body) or return hijacked
70
74
  @deferred = body
71
- write_headers(status, headers, alive)
72
75
  write_body_each(body)
73
76
  deferred_errback(body)
74
77
  deferred_callback(body, alive)
@@ -77,21 +80,22 @@ class Rainbows::EventMachine::Client < EM::Connection
77
80
  st = File.stat(path = body.to_path)
78
81
 
79
82
  if st.file?
80
- write_headers(status, headers, alive)
83
+ write_headers(status, headers, alive, body) or return hijacked
81
84
  @deferred = stream_file_data(path)
82
85
  deferred_errback(body)
83
86
  deferred_callback(body, alive)
84
87
  return
85
88
  elsif st.socket? || st.pipe?
89
+ chunk = stream_response_headers(status, headers, alive, body)
90
+ return hijacked if nil == chunk
86
91
  io = body_to_io(@deferred = body)
87
- chunk = stream_response_headers(status, headers, alive)
88
92
  m = chunk ? Rainbows::EventMachine::ResponseChunkPipe :
89
93
  Rainbows::EventMachine::ResponsePipe
90
94
  return EM.watch(io, m, self).notify_readable = true
91
95
  end
92
96
  # char or block device... WTF? fall through to body.each
93
97
  end
94
- write_response(status, headers, body, alive)
98
+ write_response(status, headers, body, alive) or return hijacked
95
99
  if alive
96
100
  if @deferred.nil?
97
101
  if @buf.empty?
@@ -112,6 +116,7 @@ class Rainbows::EventMachine::Client < EM::Connection
112
116
  end
113
117
 
114
118
  def unbind
119
+ return if @hp.hijacked?
115
120
  async_close = @env[ASYNC_CLOSE] and async_close.succeed
116
121
  @deferred.respond_to?(:fail) and @deferred.fail
117
122
  begin
@@ -40,6 +40,7 @@ module Rainbows::ProcessClient
40
40
 
41
41
  set_input(env, hp)
42
42
  env[REMOTE_ADDR] = kgio_addr
43
+ hp.hijack_setup(env, to_io)
43
44
  status, headers, body = APP.call(env.merge!(RACK_DEFAULTS))
44
45
 
45
46
  if 100 == status.to_i
@@ -47,7 +48,8 @@ module Rainbows::ProcessClient
47
48
  env.delete(HTTP_EXPECT)
48
49
  status, headers, body = APP.call(env)
49
50
  end
50
- write_response(status, headers, body, alive = @hp.next?)
51
+ return if hp.hijacked?
52
+ write_response(status, headers, body, alive = hp.next?) or return
51
53
  end while alive
52
54
  # if we get any error, try to write something back to the client
53
55
  # assuming we haven't closed the socket, but don't get hung up
@@ -56,7 +58,7 @@ module Rainbows::ProcessClient
56
58
  rescue => e
57
59
  handle_error(e)
58
60
  ensure
59
- close unless closed?
61
+ close unless closed? || hp.hijacked?
60
62
  end
61
63
 
62
64
  def handle_error(e)
@@ -71,13 +73,15 @@ module Rainbows::ProcessClient
71
73
  begin
72
74
  set_input(env, hp)
73
75
  env[REMOTE_ADDR] = kgio_addr
76
+ hp.hijack_setup(env, to_io)
74
77
  status, headers, body = APP.call(env.merge!(RACK_DEFAULTS))
75
78
  if 100 == status.to_i
76
79
  write(EXPECT_100_RESPONSE)
77
80
  env.delete(HTTP_EXPECT)
78
81
  status, headers, body = APP.call(env)
79
82
  end
80
- write_response(status, headers, body, alive = hp.next?)
83
+ return if hp.hijacked?
84
+ write_response(status, headers, body, alive = hp.next?) or return
81
85
  end while alive && pipeline_ready(hp)
82
86
  alive or close
83
87
  rescue => e
@@ -19,23 +19,56 @@ module Rainbows::Response
19
19
  Rainbows::HttpParser.keepalive_requests = 0
20
20
  end
21
21
 
22
- def write_headers(status, headers, alive)
23
- @hp.headers? or return
22
+ # Rack 1.5.0 (protocol version 1.2) adds response hijacking support
23
+ if ((Rack::VERSION[0] << 8) | Rack::VERSION[1]) >= 0x0102
24
+ RACK_HIJACK = "rack.hijack"
25
+
26
+ def hijack_prepare(value)
27
+ value
28
+ end
29
+
30
+ def hijack_socket
31
+ @hp.env[RACK_HIJACK].call
32
+ end
33
+ else
34
+ def hijack_prepare(_)
35
+ end
36
+ end
37
+
38
+ # returns the original body on success
39
+ # returns nil if the headers hijacked the response body
40
+ def write_headers(status, headers, alive, body)
41
+ @hp.headers? or return body
42
+ hijack = nil
24
43
  status = CODES[status.to_i] || status
25
44
  buf = "HTTP/1.1 #{status}\r\n" \
26
45
  "Date: #{httpdate}\r\n" \
27
- "Status: #{status}\r\n" \
28
- "Connection: #{alive ? KeepAlive : Close}\r\n"
46
+ "Status: #{status}\r\n"
29
47
  headers.each do |key, value|
30
- next if %r{\A(?:Date\z|Connection\z)}i =~ key
31
- if value =~ /\n/
32
- # avoiding blank, key-only cookies with /\n+/
33
- buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
48
+ case key
49
+ when %r{\A(?:Date\z|Connection\z)}i
50
+ next
51
+ when "rack.hijack"
52
+ # this was an illegal key in Rack < 1.5, so it should be
53
+ # OK to silently discard it for those older versions
54
+ hijack = hijack_prepare(value)
55
+ alive = false # No persistent connections for hijacking
34
56
  else
35
- buf << "#{key}: #{value}\r\n"
57
+ if /\n/ =~ value
58
+ # avoiding blank, key-only cookies with /\n+/
59
+ buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
60
+ else
61
+ buf << "#{key}: #{value}\r\n"
62
+ end
36
63
  end
37
64
  end
38
- write(buf << CRLF)
65
+ write(buf << "Connection: #{alive ? KeepAlive : Close}\r\n\r\n")
66
+
67
+ if hijack
68
+ body = nil # ensure caller does not close body
69
+ hijack.call(hijack_socket)
70
+ end
71
+ body
39
72
  end
40
73
 
41
74
  def close_if_private(io)
@@ -70,8 +103,9 @@ module Rainbows::Response
70
103
  # generic response writer, used for most dynamically-generated responses
71
104
  # and also when copy_stream and/or IO#trysendfile is unavailable
72
105
  def write_response(status, headers, body, alive)
73
- write_headers(status, headers, alive)
74
- write_body_each(body)
106
+ body = write_headers(status, headers, alive, body)
107
+ write_body_each(body) if body
108
+ body
75
109
  ensure
76
110
  body.close if body.respond_to?(:close)
77
111
  end
@@ -166,21 +200,23 @@ module Rainbows::Response
166
200
  if File.file?(body.to_path)
167
201
  if r = sendfile_range(status, headers)
168
202
  status, headers, range = r
169
- write_headers(status, headers, alive)
170
- write_body_file(body, range) if range
203
+ body = write_headers(status, headers, alive, body)
204
+ write_body_file(body, range) if body && range
171
205
  else
172
- write_headers(status, headers, alive)
173
- write_body_file(body, nil)
206
+ body = write_headers(status, headers, alive, body)
207
+ write_body_file(body, nil) if body
174
208
  end
175
209
  else
176
- write_headers(status, headers, alive)
177
- write_body_stream(body)
210
+ body = write_headers(status, headers, alive, body)
211
+ write_body_stream(body) if body
178
212
  end
213
+ body
179
214
  ensure
180
215
  body.close if body.respond_to?(:close)
181
216
  end
182
217
 
183
218
  module ToPath
219
+ # returns nil if hijacked
184
220
  def write_response(status, headers, body, alive)
185
221
  if body.respond_to?(:to_path)
186
222
  write_response_path(status, headers, body, alive)
@@ -36,7 +36,7 @@ module Rainbows::Revactor::Client::Methods
36
36
  end
37
37
 
38
38
  def write_response(status, headers, body, alive)
39
- super(status, headers, body, alive)
39
+ super(status, headers, body, alive) or return
40
40
  alive && @ts and @hp.buf << @ts.leftover
41
41
  end
42
42
 
@@ -26,18 +26,24 @@ module Rainbows::StreamResponseEpoll
26
26
 
27
27
  def http_response_write(socket, status, headers, body)
28
28
  status = CODES[status.to_i] || status
29
- ep_client = false
29
+ hijack = ep_client = false
30
30
 
31
31
  if headers
32
32
  # don't set extra headers here, this is only intended for
33
33
  # consuming by nginx.
34
34
  buf = "HTTP/1.0 #{status}\r\nStatus: #{status}\r\n"
35
35
  headers.each do |key, value|
36
- if value =~ /\n/
37
- # avoiding blank, key-only cookies with /\n+/
38
- buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
36
+ case key
37
+ when "rack.hijack"
38
+ hijack = hijack_prepare(value)
39
+ body = nil # ensure we do not close body
39
40
  else
40
- buf << "#{key}: #{value}\r\n"
41
+ if /\n/ =~ value
42
+ # avoiding blank, key-only cookies with /\n+/
43
+ buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
44
+ else
45
+ buf << "#{key}: #{value}\r\n"
46
+ end
41
47
  end
42
48
  end
43
49
  buf << HEADER_END
@@ -48,11 +54,22 @@ module Rainbows::StreamResponseEpoll
48
54
  buf = rv
49
55
  when :wait_writable
50
56
  ep_client = Client.new(socket, buf)
51
- body.each { |chunk| ep_client.write(chunk) }
52
- return ep_client.close
57
+ if hijack
58
+ ep_client.hijack(hijack)
59
+ else
60
+ body.each { |chunk| ep_client.write(chunk) }
61
+ ep_client.close
62
+ end
63
+ # body is nil on hijack, in which case ep_client is never closed by us
64
+ return
53
65
  end while true
54
66
  end
55
67
 
68
+ if hijack
69
+ hijack.call(socket)
70
+ return
71
+ end
72
+
56
73
  body.each do |chunk|
57
74
  if ep_client
58
75
  ep_client.write(chunk)
@@ -67,14 +84,15 @@ module Rainbows::StreamResponseEpoll
67
84
  end while true
68
85
  end
69
86
  end
70
- ensure
71
- body.respond_to?(:close) and body.close
72
- if ep_client
73
- ep_client.close
74
- else
75
- socket.shutdown
76
- socket.close
77
- end
87
+ ensure
88
+ return if hijack
89
+ body.respond_to?(:close) and body.close
90
+ if ep_client
91
+ ep_client.close
92
+ else
93
+ socket.shutdown
94
+ socket.close
95
+ end
78
96
  end
79
97
 
80
98
  # once a client is accepted, it is processed in its entirety here
@@ -88,6 +106,7 @@ module Rainbows::StreamResponseEpoll
88
106
  status, headers, body = @app.call(env)
89
107
  end
90
108
  @request.headers? or headers = nil
109
+ return if @request.hijacked?
91
110
  http_response_write(client, status, headers, body)
92
111
  rescue => e
93
112
  handle_error(client, e)
@@ -18,7 +18,7 @@ class Rainbows::StreamResponseEpoll::Client
18
18
  attr_reader :to_io
19
19
 
20
20
  def initialize(io, unwritten)
21
- @closed = false
21
+ @finish = false
22
22
  @to_io = io
23
23
  @wr_queue = [ unwritten.dup ]
24
24
  EP.set(self, OUT)
@@ -29,7 +29,11 @@ class Rainbows::StreamResponseEpoll::Client
29
29
  end
30
30
 
31
31
  def close
32
- @closed = true
32
+ @finish = true
33
+ end
34
+
35
+ def hijack(hijack)
36
+ @finish = hijack
33
37
  end
34
38
 
35
39
  def epoll_run
@@ -49,10 +53,14 @@ class Rainbows::StreamResponseEpoll::Client
49
53
  end
50
54
 
51
55
  def on_write_complete
52
- if @closed
56
+ if true == @finish
53
57
  @to_io.shutdown
54
58
  @to_io.close
55
59
  N.decr(0, 1)
60
+ elsif @finish.respond_to?(:call) # hijacked
61
+ EP.delete(self)
62
+ N.decr(0, 1)
63
+ @finish.call(@to_io)
56
64
  end
57
65
  end
58
66
  end
@@ -8,11 +8,13 @@ class Rainbows::WriterThreadPool::Client < Struct.new(:to_io, :q)
8
8
 
9
9
  module Methods
10
10
  def write_body_each(body)
11
+ return if @hp.hijacked?
11
12
  q << [ to_io, :write_body_each, body ]
12
13
  end
13
14
 
14
15
  def write_response_close(status, headers, body, alive)
15
16
  to_io.instance_variable_set(:@hp, @hp) # XXX ugh
17
+ return if @hp.hijacked?
16
18
  Rainbows::SyncClose.new(body) { |sync_body|
17
19
  q << [ to_io, :write_response, status, headers, sync_body, alive ]
18
20
  }
@@ -27,9 +27,6 @@ module Rainbows::XEpoll::Client
27
27
  def self.loop
28
28
  begin
29
29
  EP.wait(nil, 1000) { |_, obj| obj.epoll_run }
30
- while obj = ReRun.shift
31
- obj.epoll_run
32
- end
33
30
  Rainbows::Epoll::Client.expire
34
31
  rescue Errno::EINTR
35
32
  rescue => e
@@ -37,7 +37,7 @@ module Rainbows::XEpollThreadPool::Client
37
37
 
38
38
  ep = SleepyPenguin::Epoll
39
39
  EP = ep.new
40
- IN = ep::IN | ep::ET | ep::ONESHOT
40
+ IN = ep::IN | ep::ONESHOT
41
41
  KATO = {}
42
42
  KATO.compare_by_identity if KATO.respond_to?(:compare_by_identity)
43
43
  LOCK = Mutex.new
@@ -26,7 +26,7 @@ module Rainbows::XEpollThreadSpawn::Client
26
26
 
27
27
  ep = SleepyPenguin::Epoll
28
28
  EP = ep.new
29
- IN = ep::IN | ep::ET | ep::ONESHOT
29
+ IN = ep::IN | ep::ONESHOT
30
30
  KATO = {}
31
31
  KATO.compare_by_identity if KATO.respond_to?(:compare_by_identity)
32
32
  LOCK = Mutex.new
@@ -28,7 +28,9 @@ Gem::Specification.new do |s|
28
28
  s.add_dependency(%q<kgio>, ['~> 2.5'])
29
29
 
30
30
  # we need Unicorn for the HTTP parser and process management
31
- s.add_dependency(%q<unicorn>, ["~> 4.1"])
31
+ # 4.6.0+ supports hijacking, 4.6.2 fixes the chunk parser (for Ruby 2.0.0)
32
+ s.add_dependency(%q<unicorn>, ["~> 4.6", ">= 4.6.2"])
33
+
32
34
  s.add_development_dependency(%q<isolate>, "~> 3.1")
33
35
  s.add_development_dependency(%q<wrongdoc>, "~> 1.6")
34
36
 
@@ -53,6 +55,7 @@ Gem::Specification.new do |s|
53
55
  # NeverBlock, currently only available on http://gems.github.com/
54
56
  # s.add_dependency(%q<espace-neverblock>, ["~> 0.1.6.1"])
55
57
 
56
- # accessor not compatible with older RubyGems
57
- # s.licenses = %w(GPLv3 GPLv2 Ruby)
58
+ # We inherited the Ruby 1.8 license from Mongrel, so we're stuck with it.
59
+ # GPLv3 is preferred.
60
+ s.licenses = ["GPLv2", "GPLv3", "Ruby 1.8"]
58
61
  end
@@ -30,13 +30,12 @@ models += WriterThreadSpawn
30
30
  models += ThreadPool
31
31
  models += ThreadSpawn
32
32
  models += Coolio
33
- models += EventMachine
34
- models += NeverBlock
33
+
35
34
  models += StreamResponseEpoll
36
35
 
37
36
  ifeq ($(RUBY_ENGINE),ruby)
38
37
  rp := )
39
- ONENINE := $(shell case $(RUBY_VERSION) in 1.9.*$(rp) echo true;;esac)
38
+ ONENINE := $(shell case $(RUBY_VERSION) in 1.9.*|2.0.*$(rp) echo true;;esac)
40
39
  ifeq ($(ONENINE),true)
41
40
  ifeq ($(RUBY_VERSION),1.9.2)
42
41
  models += Revactor
@@ -46,6 +45,14 @@ ifeq ($(RUBY_ENGINE),ruby)
46
45
  models += CoolioThreadPool
47
46
  models += CoolioThreadSpawn
48
47
  models += CoolioFiberSpawn
48
+
49
+ # EventMachine 1.0.0 currently does not build on Ruby 2.0.0
50
+ # NeverBlock depends on 2.0.0
51
+ RBTWO := $(shell case $(RUBY_VERSION) in 2.0.*$(rp) echo true;;esac)
52
+ ifeq ($(RBTWO),)
53
+ models += EventMachine
54
+ models += NeverBlock
55
+ endif
49
56
  endif
50
57
  endif
51
58
 
@@ -1,5 +1,5 @@
1
1
  t_begin "byte-range setup vars" && {
2
- random_blob_size=$(wc -c < random_blob)
2
+ random_blob_size=$(count_bytes < random_blob)
3
3
  rb_1=$(( $random_blob_size - 1 ))
4
4
  range_head=-r-365
5
5
  range_tail=-r155-