what is rack hijacking api

68
What is Rack Hijacking API 2016-12-03 at rubyconf.tw 1

Upload: nomo-kiyoshi

Post on 19-Jan-2017

410 views

Category:

Engineering


1 download

TRANSCRIPT

Page 1: What is Rack Hijacking API

What is Rack Hijacking API

2016-12-03 at rubyconf.tw

1

Page 2: What is Rack Hijacking API

Who am I?

• Kiyoshi Nomo

• @kysnm

• Web Application Engineer

• Goodpatch, Inc.

http://goodpatch.com/

https://prottapp.com/

2

Page 3: What is Rack Hijacking API

Agenda

• The Basics

• About the SPEC

• About the implementation

• Take a quick look at ActionCable

• Conclusion

3

Page 4: What is Rack Hijacking API

The Basics

4

Page 5: What is Rack Hijacking API

Who made this API?

5

Page 6: What is Rack Hijacking API

6

Page 7: What is Rack Hijacking API

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

Page 8: What is Rack Hijacking API

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

Page 9: What is Rack Hijacking API

Support Servers

• puma

• passenger

• thin

• webrick (only partial hijack is supported.)

• etc…

9

Page 10: What is Rack Hijacking API

About the SPEC

10

Page 11: What is Rack Hijacking API

Two mode of Hijaking

• Full hijacking

• Partial hijacking

http://www.rubydoc.info/github/rack/rack/master/file/SPEC#Hijacking

11

Page 12: What is Rack Hijacking API

The timing of Full hijacking

• Request (before status)

12

Page 13: What is Rack Hijacking API

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

Page 14: What is Rack Hijacking API

Your responsibility of Full hijacking

• Outputting any HTTP headers, if applicable.

• Closing the IO object when you no longer need it.

14

Page 15: What is Rack Hijacking API

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

Page 16: What is Rack Hijacking API

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

Page 17: What is Rack Hijacking API

The timing of Partial hijacking

• Response (after headers)

17

Page 18: What is Rack Hijacking API

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

Page 19: What is Rack Hijacking API

Your responsibility of Partial hijacking

• closing the socket when it’s no longer needed.

19

Page 20: What is Rack Hijacking API

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

Page 21: What is Rack Hijacking API

About the implementation

21

Page 22: What is Rack Hijacking API

Introduce two servers

• rack (webrick)

• puma

22

Page 23: What is Rack Hijacking API

Webrick (rack)

23

Page 24: What is Rack Hijacking API

Webrick is

• supported only partial hijack.

24

Page 25: What is Rack Hijacking API

How to configure?

• See the test/spec_webrick.rb

25

Page 26: What is Rack Hijacking API

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

Page 27: What is Rack Hijacking API

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

Page 28: What is Rack Hijacking API

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

Page 29: What is Rack Hijacking API

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>>>>>

Page 30: What is Rack Hijacking API

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

Page 31: What is Rack Hijacking API

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

Page 32: What is Rack Hijacking API

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

Page 33: What is Rack Hijacking API

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

Page 34: What is Rack Hijacking API

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

Page 35: What is Rack Hijacking API

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

Page 36: What is Rack Hijacking API

puma

36

Page 37: What is Rack Hijacking API

puma is

• threaded, cluster enabled server.

• supported two mode of hijacking.

37

Page 38: What is Rack Hijacking API

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

Page 39: What is Rack Hijacking API

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

Page 40: What is Rack Hijacking API

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

Page 41: What is Rack Hijacking API

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

Page 42: What is Rack Hijacking API

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

Page 43: What is Rack Hijacking API

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

Page 44: What is Rack Hijacking API

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

Page 45: What is Rack Hijacking API

Puma::Server#run (block)

process_client client, buffer

https://github.com/puma/puma/blob/3.6.1/lib/puma/server.rb#L275

45

Page 46: What is Rack Hijacking API

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

Page 47: What is Rack Hijacking API

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

Page 48: What is Rack Hijacking API

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

Page 49: What is Rack Hijacking API

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

Page 50: What is Rack Hijacking API

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

Page 51: What is Rack Hijacking API

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

Page 52: What is Rack Hijacking API

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

Page 53: What is Rack Hijacking API

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

Page 54: What is Rack Hijacking API

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

Page 55: What is Rack Hijacking API

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

Page 56: What is Rack Hijacking API

Take a quick look at ActionCable

56

Page 57: What is Rack Hijacking API

In ActionCable::Connection::Stream

57

Page 58: What is Rack Hijacking API

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

Page 59: What is Rack Hijacking API

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

Page 60: What is Rack Hijacking API

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

Page 61: What is Rack Hijacking API

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

Page 62: What is Rack Hijacking API

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

Page 63: What is Rack Hijacking API

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

Page 64: What is Rack Hijacking API

Conclusion

64

Page 65: What is Rack Hijacking API

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

Page 66: What is Rack Hijacking API

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

Page 67: What is Rack Hijacking API

Thank you.

67

Page 68: What is Rack Hijacking API

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