diff --git a/README.md b/README.md index eb52ac3..7dd02bc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,24 @@ -This is almost verbatim from Zed Shaw's python connector for mongrel2, which you can see here: http://mongrel2.org/dir?ci=1bdfff8f050b97df&name=examples/python/mongrel2 +m2r +=== -You'll need a recent 1.9 ruby and FFI and http://github.com/chuckremes/ffi-rzmq, oh, and Zero MQ. Run: `ruby example/http_0mq.rb` \ No newline at end of file +A [mongrel2](http://mongrel2.org/index) backend handler written in Ruby, based on [Zed's Python backend handler](http://mongrel2.org/dir?ci=1bdfff8f050b97df&name=examples/python/mongrel2). + +Usage/Examples +----- + +* `examples/http_0mq.rb` is a test little servlet thing (based on what comes with mongrel2)and there is also a rack handler at +* `examples/rack_handler.rb` is a Mongrel2 ruby handler rack handler mouthful, whose variables are probably a little off +* `examples/lobster.ru` is a rackup file using the Rack handler that'll serve Rack's funny little lobster app. + +Run: +* `ruby examples/http_0mq.rb`, which with Mongrel2's test config will serve up at http://localhost:6767/handlertest +* `rackup examples/lobster.ru`, ditto, http://localhost:6767/handlertest + +Installation +------------ + +* Ruby 1.9ish (RVM saved my life here) +* [FFI](http://github.com/ffi/ffi), `gem install ffi` should be fine +* [Zero MQ](http://www.zeromq.org/area:download) +* [ffi-rzmq](http://github.com/chuckremes/ffi-rzmq), which you'll have to build. The native zmq didn't work for me, but if you want to fix it, please do! +* Rack (gem install rack) if you want to run the rack example. diff --git a/connection.rb b/connection.rb index 17f21f2..4ca6bf0 100644 --- a/connection.rb +++ b/connection.rb @@ -7,21 +7,11 @@ gem 'json' require 'ffi-rzmq' require 'json' + $: << File.dirname(__FILE__) require 'request' CTX = ZMQ::Context.new(1) -HTTP_FORMAT = "HTTP/1.1 %(code)s %(status)s\r\n%(headers)s\r\n\r\n%(body)s" - -class String - # Match python's % function - def |(args) - args.inject(self.dup) do |copy, (key, val)| - copy.gsub!(/%\(#{key.to_s}\)/, val.to_s) - copy - end - end -end module Mongrel2 # A Connection object manages the connection between your handler @@ -86,8 +76,8 @@ def reply_json(req, data) # Basic HTTP response mechanism which will take your body, # any headers you've made, and encode them so that the # browser gets them. - def reply_http(req, body, code=200, status="OK", headers={}) - self.reply(req, http_response(body, code, status, headers)) + def reply_http(req, body, code=200, headers={}) + self.reply(req, http_response(body, code, headers)) end # This lets you send a single message to many currently @@ -108,17 +98,60 @@ def deliver_json(idents, data) # Same as deliver, but builds an HTTP response, which means, yes, # you can reply to multiple connected clients waiting for an HTTP # response from one handler. Kinda cool. - def deliver_http(idents, body, code=200, status="OK", headers={}) - self.deliver(idents, http_response(body, code, status, headers)) + def deliver_http(idents, body, code=200, headers={}) + self.deliver(idents, http_response(body, code, headers)) end private - def http_response(body, code, status, headers) - payload = {'code' => code, 'status' => status, 'body' => body} + def http_response(body, code, headers) headers['Content-Length'] = body.size - payload['headers'] = headers.map{|k, v| "%s: %s" % [k,v]}.join("\r\n") + headers_s = headers.map{|k, v| "%s: %s" % [k,v]}.join("\r\n") - HTTP_FORMAT | payload + "HTTP/1.1 #{code} #{StatusMessage[code.to_i]}\r\n#{headers_s}\r\n\r\n#{body}" end + + # From WEBrick: thanks dawg. + StatusMessage = { + 100 => 'Continue', + 101 => 'Switching Protocols', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Request Range Not Satisfiable', + 417 => 'Expectation Failed', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported' + } end # class Connection end # mod Mongrel2 \ No newline at end of file diff --git a/example/lobster.ru b/example/lobster.ru new file mode 100644 index 0000000..d7d374c --- /dev/null +++ b/example/lobster.ru @@ -0,0 +1,7 @@ +require 'rack/lobster' +$: << File.dirname(__FILE__) +require 'rack_handler' + +use Rack::ShowExceptions +puts "Lobster at http://localhost:6767/handlertest" +Rack::Handler::Mongrel2Handler.run Rack::Lobster.new \ No newline at end of file diff --git a/example/rack_handler.rb b/example/rack_handler.rb new file mode 100644 index 0000000..3567e05 --- /dev/null +++ b/example/rack_handler.rb @@ -0,0 +1,57 @@ +require 'rubygems' +require 'rack' +require 'stringio' +# require 'ruby-debug' +# Debugger.start +# gem install ruby-debug19 -- --with-ruby-include=$HOME/.rvm/src/ruby-1.9.2-head + +$: << File.expand_path(File.dirname(__FILE__) + '/..') +require 'connection' + +$sender_id = "70D107AB-19F5-44AE-A2D0-2326A167D8D7" + +module Rack + module Handler + class Mongrel2Handler + def self.run(app, receive = "tcp://127.0.0.1:9997", send = "tcp://127.0.0.1:9996") + conn = Mongrel2::Connection.new($sender_id, receive, send) + while true + puts "WAITING FOR REQUEST" + + req = conn.recv + + if req.is_disconnect + puts "DICONNECT" + next + end + + env = { + "rack.version" => Rack::VERSION, + "rack.url_scheme" => "http", + "rack.input" => StringIO.new(req.body), + "rack.errors" => $stderr, + "rack.multithread" => true, + "rack.multiprocess" => true, + "rack.run_once" => false, + "REQUEST_METHOD" => req.headers["METHOD"], + "SCRIPT_NAME" => "", + "PATH_INFO" => env["PATH"], + "QUERY_STRING" => env["QUERY"] + } + + env["SERVER_NAME"], env["SERVER_PORT"] = req.headers["Host"].split(':', 2) + req.headers.each do |key, val| + unless key =~ /content_(type|length)/i + key = "HTTP_#{key.upcase}" + end + env[key] = val + end + + status, headers, rack_response = app.call(env) + conn.reply_http(req, rack_response.body.join, status, headers) + end + end + end + end +end + \ No newline at end of file