Permalink
Browse files

Merge branch 'hijack'

* hijack:
  ignore normal Rack response at request-time hijack
  support for Rack hijack in request and response
  • Loading branch information...
2 parents c43113e + fedb5e5 commit b73299a053b305098d5d68634fa928ec71aa4eac Eric Wong committed Jan 29, 2013
View
29 lib/unicorn/http_request.rb
@@ -91,6 +91,35 @@ def read(socket)
e[RACK_INPUT] = 0 == content_length ?
NULL_IO : @@input_class.new(socket, self)
+ hijack_setup(e, socket)
e.merge!(DEFAULTS)
end
+
+ # Rack 1.5.0 (protocol version 1.2) adds hijack request support
+ if ((Rack::VERSION[0] << 8) | Rack::VERSION[1]) >= 0x0102
+ DEFAULTS["rack.hijack?"] = true
+
+ # FIXME: asking for clarification about this in
+ # http://mid.gmane.org/20130122100802.GA28585@dcvr.yhbt.net
+ DEFAULTS["rack.version"] = [1, 2]
+
+ RACK_HIJACK = "rack.hijack".freeze
+ RACK_HIJACK_IO = "rack.hijack_io".freeze
+
+ def hijacked?
+ env.include?(RACK_HIJACK_IO)
+ end
+
+ def hijack_setup(e, socket)
+ e[RACK_HIJACK] = proc { e[RACK_HIJACK_IO] ||= socket }
+ end
+ else
+ # old Rack, do nothing.
+ def hijack_setup(e, _)
+ end
+
+ def hijacked?
+ false
+ end
+ end
end
View
40 lib/unicorn/http_response.rb
@@ -25,6 +25,7 @@ def err_response(code, response_start_sent)
def http_response_write(socket, status, headers, body,
response_start_sent=false)
status = CODES[status.to_i] || status
+ hijack = nil
http_response_start = response_start_sent ? '' : 'HTTP/1.1 '
if headers
@@ -33,19 +34,42 @@ def http_response_write(socket, status, headers, body,
"Status: #{status}\r\n" \
"Connection: close\r\n"
headers.each do |key, value|
- next if %r{\A(?:Date\z|Connection\z)}i =~ key
- if value =~ /\n/
- # avoiding blank, key-only cookies with /\n+/
- buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
+ case key
+ when %r{\A(?:Date\z|Connection\z)}i
+ next
+ when "rack.hijack"
+ # this was an illegal key in Rack < 1.5, so it should be
+ # OK to silently discard it for those older versions
+ hijack = hijack_prepare(value)
else
- buf << "#{key}: #{value}\r\n"
+ if value =~ /\n/
+ # avoiding blank, key-only cookies with /\n+/
+ buf << value.split(/\n+/).map! { |v| "#{key}: #{v}\r\n" }.join
+ else
+ buf << "#{key}: #{value}\r\n"
+ end
end
end
socket.write(buf << CRLF)
end
- body.each { |chunk| socket.write(chunk) }
- ensure
- body.respond_to?(:close) and body.close
+ if hijack
+ body = nil # ensure we do not close body
+ hijack.call(socket)
+ else
+ body.each { |chunk| socket.write(chunk) }
+ end
+ ensure
+ body.respond_to?(:close) and body.close
+ end
+
+ # Rack 1.5.0 (protocol version 1.2) adds response hijacking support
+ if ((Rack::VERSION[0] << 8) | Rack::VERSION[1]) >= 0x0102
+ def hijack_prepare(value)
+ value
+ end
+ else
+ def hijack_prepare(_)
+ end
end
end
View
8 lib/unicorn/http_server.rb
@@ -550,17 +550,21 @@ def expect_100_response
# in 3 easy steps: read request, call app, write app response
def process_client(client)
status, headers, body = @app.call(env = @request.read(client))
+ return if @request.hijacked?
if 100 == status.to_i
client.write(expect_100_response)
env.delete(Unicorn::Const::HTTP_EXPECT)
status, headers, body = @app.call(env)
+ return if @request.hijacked?
end
@request.headers? or headers = nil
http_response_write(client, status, headers, body,
@request.response_start_sent)
- client.shutdown # in case of fork() in Rack app
- client.close # flush and uncork socket immediately, no keepalive
+ unless client.closed? # rack.hijack may've close this for us
+ client.shutdown # in case of fork() in Rack app
+ client.close # flush and uncork socket immediately, no keepalive
+ end
rescue => e
handle_error(client, e)
end
View
42 t/hijack.ru
@@ -0,0 +1,42 @@
+use Rack::Lint
+use Rack::ContentLength
+use Rack::ContentType, "text/plain"
+class DieIfUsed
+ def each
+ abort "body.each called after response hijack\n"
+ end
+
+ def close
+ abort "body.close called after response hijack\n"
+ end
+end
+run lambda { |env|
+ case env["PATH_INFO"]
+ when "/hijack_req"
+ if env["rack.hijack?"]
+ io = env["rack.hijack"].call
+ if io.respond_to?(:read_nonblock) &&
+ env["rack.hijack_io"].respond_to?(:read_nonblock)
+
+ # exercise both, since we Rack::Lint may use different objects
+ env["rack.hijack_io"].write("HTTP/1.0 200 OK\r\n\r\n")
+ io.write("request.hijacked")
+ io.close
+ return [ 500, {}, DieIfUsed.new ]
+ end
+ end
+ [ 500, {}, [ "hijack BAD\n" ] ]
+ when "/hijack_res"
+ r = "response.hijacked"
+ [ 200,
+ {
+ "Content-Length" => r.bytesize.to_s,
+ "rack.hijack" => proc do |io|
+ io.write(r)
+ io.close
+ end
+ },
+ DieIfUsed.new
+ ]
+ end
+}
View
5 t/t0005-working_directory_app.rb.sh
@@ -11,7 +11,10 @@ t_begin "setup and start" && {
cat > $t_pfx.app/fooapp.rb <<\EOF
class Fooapp
def self.call(env)
- [ 200, [%w(Content-Type text/plain), %w(Content-Length 2)], %w(HI) ]
+ # Rack::Lint in 1.5.0 requires headers to be a hash
+ h = [%w(Content-Type text/plain), %w(Content-Length 2)]
+ h = Rack::Utils::HeaderHash.new(h)
+ [ 200, h, %w(HI) ]
end
end
EOF
View
27 t/t0200-rack-hijack.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+. ./test-lib.sh
+t_plan 5 "rack.hijack tests (Rack 1.5+ (Rack::VERSION >= [ 1,2]))"
+
+t_begin "setup and start" && {
+ unicorn_setup
+ unicorn -D -c $unicorn_config hijack.ru
+ unicorn_wait_start
+}
+
+t_begin "check request hijack" && {
+ test "xrequest.hijacked" = x"$(curl -sSfv http://$listen/hijack_req)"
+}
+
+t_begin "check response hijack" && {
+ test "xresponse.hijacked" = x"$(curl -sSfv http://$listen/hijack_res)"
+}
+
+t_begin "killing succeeds" && {
+ kill $unicorn_pid
+}
+
+t_begin "check stderr" && {
+ check_stderr
+}
+
+t_done

0 comments on commit b73299a

Please sign in to comment.