what is rack hijacking api
Post on 19-Jan-2017
402 Views
Preview:
TRANSCRIPT
Who am I?
• Kiyoshi Nomo
• @kysnm
• Web Application Engineer
• Goodpatch, Inc.
http://goodpatch.com/
https://prottapp.com/
2
Agenda
• The Basics
• About the SPEC
• About the implementation
• Take a quick look at ActionCable
• Conclusion
3
The Basics
4
Who made this API?
5
6
Why it was made?
• Rack didn't have an API that allows for IO-like streaming.
• for WebSocket
• for HTTP2
https://github.com/rack/rack/pull/481#issue-9702395
7
Similar implementation
• Golang's Hijacker interface.
• Probably, This API would made based on this interface.
https://github.com/rack/rack/pull/481#issue-9702395
8
Support Servers
• puma
• passenger
• thin
• webrick (only partial hijack is supported.)
• etc…
9
About the SPEC
10
Two mode of Hijaking
• Full hijacking
• Partial hijacking
http://www.rubydoc.info/github/rack/rack/master/file/SPEC#Hijacking
11
The timing of Full hijacking
• Request (before status)
12
The conditions of Full hijacking
env['rack.hijack?'] == true
env['rack.hijack'].respond_to?(:call) == true
env['rack.hijack'].call must returns the io
env['rack.hijack'].call is assigned the io to env['rack.hijack_io']
REQUIRED_METHOD = [:read, :write, :read_nonblock, :write_nonblock, :flush, :close, :close_read, :close_write, :closed?] REQUIRED_METHOD.all? { |m| env['rack.hijack_io'].respond_to?(m) } == true
13
Your responsibility of Full hijacking
• Outputting any HTTP headers, if applicable.
• Closing the IO object when you no longer need it.
14
class HijackWrapper include Assertion extend Forwardable
REQUIRED_METHODS = [ :read, :write, :read_nonblock, :write_nonblock, :flush, :close, :close_read, :close_write, :closed? ]
def_delegators :@io, *REQUIRED_METHODS
def initialize(io) @io = io REQUIRED_METHODS.each do |meth| assert("rack.hijack_io must respond to #{meth}") { io.respond_to? meth } end end end
https://github.com/rack/rack/blob/fd1fbab1ec8c7fc49ac805aac47b1f12d4cc5a99/lib/rack/lint.rb#L494-L511
15
def check_hijack(env) if env[RACK_IS_HIJACK] original_hijack = env[RACK_HIJACK] assert("rack.hijack must respond to call") { original_hijack.respond_to?(:call) } env[RACK_HIJACK] = proc do io = original_hijack.call HijackWrapper.new(io) env[RACK_HIJACK_IO] = HijackWrapper.new(env[RACK_HIJACK_IO]) io end else assert("rack.hijack? is false, but rack.hijack is present") { env[RACK_HIJACK].nil? } assert("rack.hijack? is false, but rack.hijack_io is present") { env[RACK_HIJACK_IO].nil? } end end
https://github.com/rack/rack/blob/fd1fbab1ec8c7fc49ac805aac47b1f12d4cc5a99/lib/rack/lint.rb#L513-L562
16
The timing of Partial hijacking
• Response (after headers)
17
The conditions of Partial hijacking
• an application may set the special header rack.hijack to an object that responds to #call accepting an argument that conforms to the rack.hijack_io protocol.
18
Your responsibility of Partial hijacking
• closing the socket when it’s no longer needed.
19
def check_hijack_response(headers, env)
headers = Rack::Utils::HeaderHash.new(headers)
if env[RACK_IS_HIJACK] && headers[RACK_HIJACK] assert('rack.hijack header must respond to #call') { headers[RACK_HIJACK].respond_to? :call } original_hijack = headers[RACK_HIJACK] headers[RACK_HIJACK] = proc do |io| original_hijack.call HijackWrapper.new(io) end else assert('rack.hijack header must not be present if server does not support hijacking') { headers[RACK_HIJACK].nil? } end end
https://github.com/rack/rack/blob/fd1fbab1ec8c7fc49ac805aac47b1f12d4cc5a99/lib/rack/lint.rb#L564-L614
20
About the implementation
21
Introduce two servers
• rack (webrick)
• puma
22
Webrick (rack)
23
Webrick is
• supported only partial hijack.
24
How to configure?
• See the test/spec_webrick.rb
25
it "support Rack partial hijack" do io_lambda = lambda{ |io| 5.times do io.write "David\r\n" end io.close }
@server.mount "/partial", Rack::Handler::WEBrick, Rack::Lint.new(lambda{ |req| [ 200, [ [ "rack.hijack", io_lambda ] ], [""] ] })
Net::HTTP.start(@host, @port){ |http| res = http.get("/partial") res.body.must_equal "David\r\nDavid\r\nDavid\r\nDavid\r\nDavid\r\n" } end
https://github.com/rack/rack/blob/cabe6b33ca4601aa6acb56317ac1c819cf6dc4bb/test/spec_webrick.rb#L162-L183
26
run lambda { |env| io_lambda = lambda { |io| i = 1 5.times do io.write "David\r\n" end io.close } [ 200, [ [ 'rack.hijack', io_lambda ] ], [''] ]}
27
Rack::Handler::Webrick::run
def self.run(app, options={}) environment = ENV['RACK_ENV'] || 'development' default_host = environment == 'development' ? 'localhost' : nil
options[:BindAddress] = options.delete(:Host) || default_host options[:Port] ||= 8080 @server = ::WEBrick::HTTPServer.new(options) @server.mount "/", Rack::Handler::WEBrick, app yield @server if block_given? @server.start end
https://github.com/rack/rack/blob/cabe6b33ca4601aa6acb56317ac1c819cf6dc4bb/lib/rack/handler/webrick.rb#L25-L35
app
[1] pry(#<Rack::Handler::WEBrick>)> app => #<Rack::ContentLength:0x007fa0fa17f2a8 @app= #<Rack::Chunked:0x007fa0fa17f2f8 @app= #<Rack::CommonLogger:0x007fa0fa17f348 @app= #<Rack::ShowExceptions:0x007fa0fb208458 @app= #<Rack::Lint:0x007fa0fb2084a8 @app= #<Rack::TempfileReaper:0x007fa0fb208520 @app=#<Proc:0x007fa0fb368c08@/tmp/rack_hijack_test/webrick/config.ru:1 (lambda)>>, @content_length=nil>>, @logger=#<IO:<STDERR>>>>>
Webrick::HTTPServer#servicesi = servlet.get_instance(self, *options) @logger.debug(format("%s is invoked.", si.class.name)) si.service(req, res)
https://github.com/ruby/ruby/blob/v2_3_3/lib/webrick/httpserver.rb#L138-L140
Webrick::HTTPServlet::AbstractServlet::get_instancedef self.get_instance(server, *options) self.new(server, *options) end
https://github.com/ruby/ruby/blob/v2_3_3/lib/webrick/httpservlet/abstract.rb#L85-L87
Rack::Handler::Webrick#initializedef initialize(server, app) super server @app = app end
https://github.com/rack/rack/blob/cabe6b33ca4601aa6acb56317ac1c819cf6dc4bb/lib/rack/handler/webrick.rb#L52-L55
Rack::Handler::Webrick#service (Take out the io_lambda)status, headers, body = @app.call(env) begin res.status = status.to_i io_lambda = nil headers.each { |k, vs| if k == RACK_HIJACK io_lambda = vs elsif k.downcase == "set-cookie" res.cookies.concat vs.split("\n") else # Since WEBrick won't accept repeated headers, # merge the values per RFC 1945 section 4.2. res[k] = vs.split("\n").join(", ") end }
https://github.com/rack/rack/blob/cabe6b33ca4601aa6acb56317ac1c819cf6dc4bb/lib/rack/handler/webrick.rb#L86-L100
Rack::Handler::Webrick#service (Calls the io_lambda) if io_lambda rd, wr = IO.pipe res.body = rd res.chunked = true io_lambda.call wr elsif body.respond_to?(:to_path) res.body = ::File.open(body.to_path, 'rb') else body.each { |part| res.body << part } end ensure body.close if body.respond_to? :close end
https://github.com/rack/rack/blob/cabe6b33ca4601aa6acb56317ac1c819cf6dc4bb/lib/rack/handler/webrick.rb#L86-L100
response
<= Recv data, 35 bytes (0x23) 0000: David 0007: David 000e: David 0015: David 001c: David == Info: transfer closed with outstanding read data remaining == Info: Curl_http_done: called premature == 1 == Info: Closing connection 0
https://gist.github.com/kysnm/ca5237d4ac96764b9cfe6ac1547710cf
puma
36
puma is
• threaded, cluster enabled server.
• supported two mode of hijacking.
37
Full hijacking example
run lambda { |env| io = env['rack.hijack'].call io.puts "HTTP/1.1 200\r\n\r\nBLAH" [-1, {}, []] }
https://github.com/puma/puma/blob/3.6.1/test/hijack.ru
38
Before Puma::Runner#start_server
=> #0 start_server <Puma::Runner#start_server()> #1 [method] start_server <Puma::Runner#start_server()> #2 [method] run <Puma::Single#run()> #3 [method] run <Puma::Launcher#run()> #4 [method] run <Puma::CLI#run()>
39
Puma::Runner#start_serverdef start_server min_t = @options[:min_threads] max_t = @options[:max_threads]
server = Puma::Server.new app, @launcher.events, @options server.min_threads = min_t server.max_threads = max_t server.inherit_binder @launcher.binder
if @options[:mode] == :tcp server.tcp_mode! end
unless development? server.leak_stack_on_error = false end
server end
https://github.com/puma/puma/blob/3.6.1/lib/puma/runner.rb#L140-L160
40
app
[1] pry(#<Puma::Server>)> app => #<Puma::Configuration::ConfigMiddleware:0x007ffaf2badc50 @app=#<Proc:0x007ffaf2badfc0@puma/hijack.ru:1 (lambda)>, @config= #<Puma::Configuration:0x007ffaf2c75110 @options= #<Puma::LeveledOptions:0x007ffaf2c74f08 @cur={}, @defaults= {:min_threads=>0, :max_threads=>16, :log_requests=>false, :debug=>false, :binds=>["tcp://0.0.0.0:9292"], :workers=>0, … snip …
41
Puma::Single#run
begin server.run.join rescue Interrupt # Swallow it end
https://github.com/puma/puma/blob/3.6.1/lib/puma/single.rb#L103-L107
42
Puma::Server#handle_servers
if io = sock.accept_nonblock client = Client.new io, @binder.env(sock) if remote_addr_value client.peerip = remote_addr_value elsif remote_addr_header client.remote_addr_header = remote_addr_header end
pool << client pool.wait_until_not_full unless queue_requests end
https://github.com/puma/puma/blob/3.6.1/lib/puma/server.rb#L333-L343
43
Before Puma::ThreadPool#spawn_thread=> #0 spawn_thread <Puma::ThreadPool#spawn_thread()> #1 [method] spawn_thread <Puma::ThreadPool#spawn_thread()> #2 [block] block in << <Puma::ThreadPool#<<(work)> #3 [method] << <Puma::ThreadPool#<<(work)> #4 [block] block in handle_servers <Puma::Server#handle_servers()> #5 [method] handle_servers <Puma::Server#handle_servers()> #6 [block] block in run <Puma::Server#run(background=?)>
44
Puma::Server#run (block)
process_client client, buffer
https://github.com/puma/puma/blob/3.6.1/lib/puma/server.rb#L275
45
Puma::Server#process_client
while true case handle_request(client, buffer) when false return when :async close_socket = false return when true return unless @queue_requests buffer.reset
https://github.com/puma/puma/blob/3.6.1/lib/puma/server.rb#L275
46
Puma::Server#handle_request (arguments)
def handle_request(req, lines) env = req.env client = req.io
normalize_env env, req
env[PUMA_SOCKET] = client
https://github.com/puma/puma/blob/3.6.1/lib/puma/server.rb#L549-L555
47
Puma::Server#handle_request (HIJACK_P, HIJACK)
env[HIJACK_P] = true env[HIJACK] = req
https://github.com/puma/puma/blob/3.6.1/lib/puma/server.rb#L561-L562
48
Puma::Client#call
# For the hijack protocol (allows us to just put the Client object # into the env) def call @hijacked = true env[HIJACK_IO] ||= @io end
https://github.com/puma/puma/blob/3.6.1/lib/puma/client.rb#L69-L74
49
Puma::Const
HIJACK_P = "rack.hijack?".freeze HIJACK = "rack.hijack".freeze HIJACK_IO = "rack.hijack_io".freeze
https://github.com/puma/puma/blob/3.6.1/lib/puma/const.rb#L249-L251
50
Puma::Server#handle_request (@app.call)
begin begin status, headers, res_body = @app.call(env)
return :async if req.hijacked
https://github.com/puma/puma/blob/3.6.1/lib/puma/server.rb#L576-L580
51
Partial hijacking example
run lambda { |env| body = lambda { |io| io.puts "BLAH\n"; io.close }
[200, { 'rack.hijack' => body }, []] }
https://github.com/puma/puma/blob/3.6.1/test/hijack2.ru
52
Puma::Server#handle_request (@app.call)
begin begin status, headers, res_body = @app.call(env)
return :async if req.hijacked
https://github.com/puma/puma/blob/3.6.1/lib/puma/server.rb#L576-L580
53
Puma::Server#handle_request (response_hijack)
response_hijack = nil
headers.each do |k, vs| case k.downcase when CONTENT_LENGTH2 content_length = vs next when TRANSFER_ENCODING allow_chunked = false content_length = nil when HIJACK response_hijack = vs next end
https://github.com/puma/puma/blob/3.6.1/lib/puma/server.rb#L653-L666
54
Puma::Server#handle_request (response_hijack.call)
if response_hijack response_hijack.call client return :async end
https://github.com/puma/puma/blob/3.6.1/lib/puma/server.rb#L705-L708
55
Take a quick look at ActionCable
56
In ActionCable::Connection::Stream
57
ActionCable::Connection::Stream#hijack_rack_socket
def hijack_rack_socket return unless @socket_object.env['rack.hijack']
@socket_object.env['rack.hijack'].call @rack_hijack_io = @socket_object.env['rack.hijack_io']
@event_loop.attach(@rack_hijack_io, self) end
https://github.com/rails/rails/blob/v5.0.0.1/actioncable/lib/action_cable/connection/stream.rb#L40-L47
58
ActionCable::Connection::Stream#clean_rack_hijack
private def clean_rack_hijack return unless @rack_hijack_io @event_loop.detach(@rack_hijack_io, self) @rack_hijack_io = nil end
https://github.com/rails/rails/blob/v5.0.0.1/actioncable/lib/action_cable/connection/stream.rb#L40-L47
59
Faye::RackStream#hijack_rack_socket 1
def hijack_rack_socket return unless @socket_object.env['rack.hijack']
@socket_object.env['rack.hijack'].call @rack_hijack_io = @socket_object.env['rack.hijack_io'] queue = Queue.new
https://github.com/faye/faye-websocket-ruby/blob/0.10.5/lib/faye/rack_stream.rb#L30-L36
60
Faye::RackStream#hijack_rack_socket 2
EventMachine.schedule do begin EventMachine.attach(@rack_hijack_io, Reader) do |reader| reader.stream = self if @rack_hijack_io @rack_hijack_io_reader = reader else reader.close_connection_after_writing end
https://github.com/faye/faye-websocket-ruby/blob/0.10.5/lib/faye/rack_stream.rb#L37-L46
61
Faye::RackStream#hijack_rack_socket 3
ensure queue.push(nil) end end
queue.pop if EventMachine.reactor_running? end
https://github.com/faye/faye-websocket-ruby/blob/0.10.5/lib/faye/rack_stream.rb#L47-L53
62
Faye::RackStream#clean_rack_hijack
def clean_rack_hijack return unless @rack_hijack_io @rack_hijack_io_reader.close_connection_after_writing @rack_hijack_io = @rack_hijack_io_reader = nil end
https://github.com/faye/faye-websocket-ruby/blob/0.10.5/lib/faye/rack_stream.rb#L55-L59
63
Conclusion
64
Limitations
•I have not tried to spec out a full IO API, and I'm not sure that we should. •I have not tried to respec all of the HTTP / anti-HTTP semantics. •There is no spec for buffering or the like.
•The intent is that this is an API to "get out the way”.
https://github.com/rack/rack/pull/481
65
What?
this is a straw man that addresses this within the confines of the rack 1.x spec. It's not an attempt to build out what I hope a 2.0 spec should be, but I am hoping that something like this will be enough to aid Rails 4s ventures, enable websockets, and a few other strategies. With HTTP2 around the corner, we'll likely want to revisit the IO API for 2.0, but we'll see how this plays out. Maybe IO wrapped around channels will be ok.
https://github.com/rack/rack/pull/481
66
Thank you.
67
Reference
• http://www.rubydoc.info/github/rack/rack/master/file/SPEC#Hijacking
• http://old.blog.phusion.nl/2013/01/23/the-new-rack-socket-hijacking-api/
• https://github.com/rack/rack/pull/481
top related