what is rack hijacking api

Post on 19-Jan-2017

402 Views

Category:

Engineering

1 Downloads

Preview:

Click to see full reader

TRANSCRIPT

What is Rack Hijacking API

2016-12-03 at rubyconf.tw

1

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