rack/rack — pull request #481 by raggi (2013-01-05 04:42:21+00:00)
Title: Hijack

## this pr is intended for discussion before merge

Hi all!

I had some time on the plane with no WiFi and some holiday left over, so...

@tenderlove please tell me if this matches up with the stuff you were describing to me at rubyconf
@tarcieri please take a look as you're also familiar with these things
@evanphx as with Tony, if you wouldn't mind
@chneukirchen @rkh you know why :-)

Please also highlight anyone else who would be worthwhile, or at least send them a link. I'd appreciate feedback.

## What?

We still don't have an API that allows for IO-like streaming, and 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.

## How?

There are two modes, one for "full hijacking", which is what you'd want for websockets and various hacks, and another mode which I think is closer to what @tenderlove really wants, which is a post-headers hijack. In both cases, the application assumes control of the socket after it has been hijacked, and there is currently no API for "giving sockets back to the server", so it's recommended that folks use Connection:close with this mechanism.

## Examples?

I've implemented this spec in Thin (oddly enough) because it was the only reasonable server I had checked out on my machine as I as writing it. I suspect the Puma implementation will be even simpler. It's all in this branch: https://github.com/raggi/thin/commits/hijack

Specifically the server piece lives here: https://github.com/raggi/thin/blob/e04855459cb42fd98a0a483075f8337cafe6d949/lib/thin/connection.rb

There are two examples, one for each of the hijack "modes" (pre headers and post headers):
* https://github.com/raggi/thin/blob/e04855459cb42fd98a0a483075f8337cafe6d949/example/hijack.ru
* https://github.com/raggi/thin/blob/e04855459cb42fd98a0a483075f8337cafe6d949/example/hijack_response.ru

The examples implement a trivial HTTP -> telnet chat interface. You make any HTTP request to "join" and the response dumps you into the single global line buffered chat room.

## 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".

## Similar implementations

http://golang.org/pkg/net/http/#Hijacker

## Additional example

``` ruby
class ChunkyIO
class IOWrapper
extend Forwardable
def_delegators :@io, :close_read, :read, :read_nonblock

def initialize(io)
@io = io
@terminated = false
end

def write(data)
@io.write chunk_header(data) + data + "\r\n"
end

def write_nonblock(data)
# XXX bugs can occur here, if a partial write happens!
@io.write_nonblock chunk_header(data) + data + "\r\n"
end

def chunk_header(data)
"#{data.size.to_s(16)}\r\n"
end

def close_write
terminate
@io.close_write
end

def close
terminate
@io.close
end

def terminate
return if @terminated
write ""
@terminated = true
end
end

def initialize(app)
@app = app
end

def call(env)
return app.call(env) unless env['rack.hijack?']

handle_request env
response = app.call(env)
handle_response response[1]
response
end

def handle_request(env)
orig = env['rack.hijack']
env['rack.hijack'] = proc do
orig.call
env['rack.hijack_io'] = IOWrapper.new(env['rack.hijack_io'])
end
end

def handle_response(headers)
if orig = headers['rack.hijack']
headers['rack.hijack'] = proc do |io|
orig.call IOWrapper.new(io)
end
end
end
end
```