├── tmp └── .gitkeep ├── .yardopts ├── example ├── tmp │ └── .gitkeep ├── Procfile ├── lobster.ru ├── uploading.ru ├── mongrel2.conf └── http_0mq.rb ├── kitchen ├── roles │ └── .gitkeep ├── data_bags │ ├── README │ ├── vagrant.pub │ └── vagrant.key ├── site-cookbooks │ └── README ├── auth.cfg ├── m2r.cfg ├── cookbooks │ ├── m2r │ │ ├── README.md │ │ ├── recipes │ │ │ └── default.rb │ │ ├── metadata.rb │ │ ├── CHANGELOG.md │ │ └── metadata.json │ ├── zmq │ │ ├── README.md │ │ ├── metadata.rb │ │ ├── CHANGELOG.md │ │ ├── metadata.json │ │ └── recipes │ │ │ └── default.rb │ ├── essential │ │ ├── README.md │ │ ├── metadata.rb │ │ ├── recipes │ │ │ └── default.rb │ │ ├── CHANGELOG.md │ │ └── metadata.json │ ├── mongrel2 │ │ ├── README.md │ │ ├── metadata.rb │ │ ├── CHANGELOG.md │ │ ├── metadata.json │ │ └── recipes │ │ │ └── default.rb │ ├── ruby-build │ │ ├── README.md │ │ ├── recipes │ │ │ └── default.rb │ │ ├── metadata.rb │ │ ├── metadata.json │ │ └── definitions │ │ │ └── ruby.rb │ └── build-essential │ │ ├── metadata.rb │ │ ├── metadata.json │ │ ├── README.md │ │ └── recipes │ │ └── default.rb ├── nodes │ └── m2r.local.json └── Rakefile ├── lib ├── m2r │ ├── version.rb │ ├── reply.rb │ ├── response │ │ ├── always_close.rb │ │ ├── content_length.rb │ │ └── to_request.rb │ ├── multithread_handler.rb │ ├── request │ │ ├── base.rb │ │ └── upload.rb │ ├── http │ │ └── close.rb │ ├── parser.rb │ ├── rack_handler.rb │ ├── connection_factory.rb │ ├── headers.rb │ ├── connection.rb │ ├── request.rb │ ├── response.rb │ └── handler.rb ├── rack │ └── handler │ │ └── mongrel2.rb └── m2r.rb ├── test ├── support │ ├── capybara.rb │ ├── mongrel_helper.rb │ ├── test_user.rb │ └── test_handler.rb ├── test_helper.rb ├── unit │ ├── connection_factory_test.rb │ ├── m2r_test.rb │ ├── request_parsing_test.rb │ ├── headers_test.rb │ ├── multithread_handler_test.rb │ ├── rack_handler_test.rb │ ├── response_test.rb │ ├── handler_test.rb │ └── connection_test.rb └── acceptance │ └── examples_test.rb ├── Gemfile ├── .gitignore ├── CHANGELOG.md ├── Vagrantfile ├── Rakefile ├── .travis.yml ├── LICENSE ├── m2r.gemspec └── README.md /tmp/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | lib 2 | -------------------------------------------------------------------------------- /example/tmp/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kitchen/roles/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /kitchen/data_bags/README: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /kitchen/site-cookbooks/README: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /kitchen/auth.cfg: -------------------------------------------------------------------------------- 1 | [userinfo] 2 | ssh-config = m2r.cfg 3 | -------------------------------------------------------------------------------- /lib/m2r/version.rb: -------------------------------------------------------------------------------- 1 | module M2R 2 | # m2r gem version 3 | # @api public 4 | VERSION = '2.1.0' 5 | end 6 | -------------------------------------------------------------------------------- /test/support/capybara.rb: -------------------------------------------------------------------------------- 1 | require 'capybara' 2 | require 'capybara/mechanize' 3 | 4 | Capybara.app_host = "http://localhost:6767/" 5 | -------------------------------------------------------------------------------- /kitchen/m2r.cfg: -------------------------------------------------------------------------------- 1 | Host m2r.local 2 | HostName 172.26.66.100 3 | User vagrant 4 | IdentityFile data_bags/vagrant.key 5 | ForwardAgent true 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem "foreman", :platform => :ruby 6 | gem "jruby-openssl", :platform => :jruby 7 | -------------------------------------------------------------------------------- /kitchen/cookbooks/m2r/README.md: -------------------------------------------------------------------------------- 1 | Description 2 | =========== 3 | 4 | Requirements 5 | ============ 6 | 7 | Attributes 8 | ========== 9 | 10 | Usage 11 | ===== 12 | 13 | -------------------------------------------------------------------------------- /kitchen/cookbooks/zmq/README.md: -------------------------------------------------------------------------------- 1 | Description 2 | =========== 3 | 4 | Requirements 5 | ============ 6 | 7 | Attributes 8 | ========== 9 | 10 | Usage 11 | ===== 12 | 13 | -------------------------------------------------------------------------------- /kitchen/cookbooks/essential/README.md: -------------------------------------------------------------------------------- 1 | Description 2 | =========== 3 | 4 | Requirements 5 | ============ 6 | 7 | Attributes 8 | ========== 9 | 10 | Usage 11 | ===== 12 | 13 | -------------------------------------------------------------------------------- /kitchen/cookbooks/mongrel2/README.md: -------------------------------------------------------------------------------- 1 | Description 2 | =========== 3 | 4 | Requirements 5 | ============ 6 | 7 | Attributes 8 | ========== 9 | 10 | Usage 11 | ===== 12 | 13 | -------------------------------------------------------------------------------- /kitchen/cookbooks/ruby-build/README.md: -------------------------------------------------------------------------------- 1 | Description 2 | =========== 3 | 4 | Requirements 5 | ============ 6 | 7 | Attributes 8 | ========== 9 | 10 | Usage 11 | ===== 12 | 13 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'mocha/setup' 3 | require 'm2r' 4 | 5 | Dir[File.expand_path(File.join(__FILE__, '../support/*.rb'))].each { |m| require m } 6 | -------------------------------------------------------------------------------- /kitchen/cookbooks/ruby-build/recipes/default.rb: -------------------------------------------------------------------------------- 1 | package "git-core" do 2 | action :install 3 | end 4 | 5 | %w[git-core zlib1g-dev libssl-dev libreadline-dev libxml2-dev libxslt1-dev libmysqlclient-dev].each do |name| 6 | package name do 7 | action :install 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /kitchen/cookbooks/m2r/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: m2r 3 | # Recipe:: default 4 | # 5 | # Copyright 2012, YOUR_COMPANY_NAME 6 | # 7 | # All rights reserved - Do Not Redistribute 8 | # 9 | 10 | ruby "1.9.3" do 11 | version "1.9.3-p327" 12 | home "/home/vagrant/" 13 | end 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | example/tmp 19 | example/config.sqlite 20 | .vagrant 21 | -------------------------------------------------------------------------------- /kitchen/cookbooks/m2r/metadata.rb: -------------------------------------------------------------------------------- 1 | maintainer "YOUR_COMPANY_NAME" 2 | maintainer_email "YOUR_EMAIL" 3 | license "All rights reserved" 4 | description "Installs/Configures m2r" 5 | long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) 6 | version "0.1.0" 7 | -------------------------------------------------------------------------------- /kitchen/cookbooks/zmq/metadata.rb: -------------------------------------------------------------------------------- 1 | maintainer "YOUR_COMPANY_NAME" 2 | maintainer_email "YOUR_EMAIL" 3 | license "All rights reserved" 4 | description "Installs/Configures zmq" 5 | long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) 6 | version "0.1.0" 7 | -------------------------------------------------------------------------------- /kitchen/cookbooks/essential/metadata.rb: -------------------------------------------------------------------------------- 1 | maintainer "YOUR_COMPANY_NAME" 2 | maintainer_email "YOUR_EMAIL" 3 | license "All rights reserved" 4 | description "Installs/Configures essential" 5 | long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) 6 | version "0.1.0" 7 | -------------------------------------------------------------------------------- /kitchen/cookbooks/mongrel2/metadata.rb: -------------------------------------------------------------------------------- 1 | maintainer "YOUR_COMPANY_NAME" 2 | maintainer_email "YOUR_EMAIL" 3 | license "All rights reserved" 4 | description "Installs/Configures mongrel2" 5 | long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) 6 | version "0.1.0" 7 | -------------------------------------------------------------------------------- /kitchen/cookbooks/ruby-build/metadata.rb: -------------------------------------------------------------------------------- 1 | maintainer "Arkency" 2 | maintainer_email "michal.lomnicki@gmail.com" 3 | license "All rights reserved" 4 | description "Installs/Configures ruby-build" 5 | long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) 6 | version "0.0.1" 7 | -------------------------------------------------------------------------------- /example/Procfile: -------------------------------------------------------------------------------- 1 | mongrel2: m2sh start -name main 2 | rack_handler: bundle exec rackup -I../lib -s mongrel2 lobster.ru 3 | uploading_handler: bundle exec rackup -I../lib -s mongrel2 uploading.ru -O recv_addr=tcp://127.0.0.1:9995 -O send_addr=tcp://127.0.0.1:9994 4 | http_handler: bundle exec ruby -I../lib http_0mq.rb 5 | -------------------------------------------------------------------------------- /kitchen/cookbooks/essential/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: essential 3 | # Recipe:: default 4 | # 5 | # Copyright 2012, YOUR_COMPANY_NAME 6 | # 7 | # All rights reserved - Do Not Redistribute 8 | # 9 | 10 | package 'htop' do 11 | action :install 12 | end 13 | 14 | package 'vim' do 15 | action :install 16 | end 17 | -------------------------------------------------------------------------------- /example/lobster.ru: -------------------------------------------------------------------------------- 1 | # An example of running Rack application. 2 | # 3 | # Running this example: 4 | # 5 | # m2sh load -config mongrel2.conf 6 | # bundle exec foreman start 7 | # 8 | # Browse now to http://localhost:6767/rack to see the effect. 9 | 10 | $stdout.sync = true 11 | $stderr.sync = true 12 | 13 | require 'rack/lobster' 14 | use Rack::ContentLength 15 | run Rack::Lobster.new 16 | -------------------------------------------------------------------------------- /kitchen/cookbooks/build-essential/metadata.rb: -------------------------------------------------------------------------------- 1 | maintainer "Opscode, Inc." 2 | maintainer_email "cookbooks@opscode.com" 3 | license "Apache 2.0" 4 | description "Installs C compiler / build tools" 5 | version "1.0.0" 6 | recipe "build-essential", "Installs C compiler and build tools on Linux" 7 | 8 | %w{ fedora redhat centos ubuntu debian }.each do |os| 9 | supports os 10 | end 11 | -------------------------------------------------------------------------------- /kitchen/data_bags/vagrant.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key 2 | -------------------------------------------------------------------------------- /kitchen/nodes/m2r.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "m2r.local", 3 | "env": "development", 4 | "ipaddress": "172.26.66.100", 5 | "run_list": [ 6 | "recipe[essential]", 7 | "recipe[build-essential]", 8 | "recipe[ruby-build]", 9 | "recipe[zmq]", 10 | "recipe[mongrel2]", 11 | "recipe[m2r]" 12 | ], 13 | "ruby": "1.9.3-p286", 14 | "username":"vagrant", 15 | "home_root":"/home" 16 | } 17 | -------------------------------------------------------------------------------- /lib/m2r/reply.rb: -------------------------------------------------------------------------------- 1 | require 'm2r/response' 2 | require 'm2r/response/content_length' 3 | require 'm2r/response/to_request' 4 | 5 | module M2R 6 | # Response object to be used without any other framework 7 | # doing the job of handling content lenght and dealing with 8 | # 'Connection' header. 9 | # 10 | # @api public 11 | class Reply < Response 12 | include Response::ContentLength 13 | include Response::ToRequest 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /kitchen/cookbooks/m2r/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG for m2r 2 | 3 | This file is used to list changes made in each version of m2r. 4 | 5 | ## 0.1.0: 6 | 7 | * Initial release of m2r 8 | 9 | - - - 10 | Check the [Markdown Syntax Guide](http://daringfireball.net/projects/markdown/syntax) for help with Markdown. 11 | 12 | The [Github Flavored Markdown page](http://github.github.com/github-flavored-markdown/) describes the differences between markdown on github and standard markdown. 13 | -------------------------------------------------------------------------------- /kitchen/cookbooks/zmq/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG for zmq 2 | 3 | This file is used to list changes made in each version of zmq. 4 | 5 | ## 0.1.0: 6 | 7 | * Initial release of zmq 8 | 9 | - - - 10 | Check the [Markdown Syntax Guide](http://daringfireball.net/projects/markdown/syntax) for help with Markdown. 11 | 12 | The [Github Flavored Markdown page](http://github.github.com/github-flavored-markdown/) describes the differences between markdown on github and standard markdown. 13 | -------------------------------------------------------------------------------- /kitchen/cookbooks/essential/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG for essential 2 | 3 | This file is used to list changes made in each version of essential. 4 | 5 | ## 0.1.0: 6 | 7 | * Initial release of essential 8 | 9 | - - - 10 | Check the [Markdown Syntax Guide](http://daringfireball.net/projects/markdown/syntax) for help with Markdown. 11 | 12 | The [Github Flavored Markdown page](http://github.github.com/github-flavored-markdown/) describes the differences between markdown on github and standard markdown. 13 | -------------------------------------------------------------------------------- /kitchen/cookbooks/mongrel2/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG for mongrel2 2 | 3 | This file is used to list changes made in each version of mongrel2. 4 | 5 | ## 0.1.0: 6 | 7 | * Initial release of mongrel2 8 | 9 | - - - 10 | Check the [Markdown Syntax Guide](http://daringfireball.net/projects/markdown/syntax) for help with Markdown. 11 | 12 | The [Github Flavored Markdown page](http://github.github.com/github-flavored-markdown/) describes the differences between markdown on github and standard markdown. 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.1.0 4 | 5 | * Retry response delivery when interrupt signal received. Mark connection errors coming from signals. 6 | * Introduce handler event for interrupt signal. 7 | * Graceful stop for Rack handler. 8 | * Extract request parser from request. 9 | * Introduce MultithreadedHandler to dispatch requests in multiple threads. 10 | * Update ffi-rzmq dependency. 11 | * Remove circular require invocations. 12 | 13 | ## 2.0.2 14 | 15 | * Fixed #55 : Rack response body is closed. 16 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant::Config.run do |multi| 5 | multi.vm.define :m2r do |config| 6 | config.vm.box = "m2r" 7 | config.vm.box_url = "http://files.vagrantup.com/precise32.box" 8 | 9 | config.vm.host_name = "m2r.local" 10 | config.vm.network :hostonly, "172.26.66.100" 11 | config.vm.share_folder "v-root", "/home/vagrant/current", "." 12 | 13 | config.vm.customize ["modifyvm", :id, "--memory", 512] 14 | config.ssh.forward_agent = true 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/m2r/response/always_close.rb: -------------------------------------------------------------------------------- 1 | require 'm2r/response' 2 | 3 | module M2R 4 | class Response 5 | # Use to disable persisent connections even though 6 | # your client would prefer otherwise. 7 | # 8 | # @api public 9 | module AlwaysClose 10 | def close? 11 | true 12 | end 13 | 14 | def headers(value = GETTER) 15 | if value == GETTER 16 | h = super 17 | h['Connection'] = 'close' 18 | h 19 | else 20 | super 21 | end 22 | end 23 | end 24 | end 25 | end 26 | 27 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rake/testtask' 5 | 6 | Rake::TestTask.new('test:unit') do |test| 7 | test.pattern = 'test/unit/*_test.rb' 8 | test.libs << 'lib' << 'test' 9 | end 10 | 11 | Rake::TestTask.new('test:acceptance') do |test| 12 | test.pattern = 'test/acceptance/*_test.rb' 13 | test.libs << 'lib' << 'test' 14 | end 15 | 16 | if (ENV['HOME'] =~ /travis/ || ENV['BUNDLE_GEMFILE'] =~ /travis/) && RUBY_ENGINE =~ /jruby/ 17 | task :test => %w(test:unit) 18 | else 19 | task :test => %w(test:unit test:acceptance) 20 | end 21 | 22 | task :default => :test 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 5 | - 2.1.0-preview1 6 | - jruby-19mode 7 | - rbx-19mode 8 | 9 | env: 10 | - PATH=/opt/mongrel2-1.8.1/bin:$PATH 11 | 12 | notifications: 13 | irc: "irc.freenode.org#mongrel2-ruby" 14 | 15 | before_install: 16 | - sudo apt-get install -qq libzmq3-dev 17 | - mkdir mongrel2-1.8.1 18 | - curl -L https://github.com/zedshaw/mongrel2/archive/v1.8.1.tar.gz -o - |tar --strip-components=1 -C mongrel2-1.8.1 -zxvf - 19 | - pushd mongrel2-1.8.1 20 | - make PREFIX=/opt/mongrel2-1.8.1 21 | - sudo make install PREFIX=/opt/mongrel2-1.8.1 22 | - popd 23 | 24 | script: bundle exec rake 25 | -------------------------------------------------------------------------------- /lib/m2r/multithread_handler.rb: -------------------------------------------------------------------------------- 1 | module M2R 2 | class MultithreadHandler 3 | 4 | attr_reader :threads 5 | 6 | def initialize(singlethread_handler_factory) 7 | @singlethread_handler_factory = singlethread_handler_factory 8 | end 9 | 10 | def listen 11 | @threads = 8.times.map do 12 | Thread.new do 13 | handler = @singlethread_handler_factory.new 14 | Thread.current[:m2r_handler] = handler 15 | handler.listen 16 | end 17 | end 18 | end 19 | 20 | def stop 21 | @threads.each do |t| 22 | t[:m2r_handler].stop 23 | end 24 | end 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/m2r/request/base.rb: -------------------------------------------------------------------------------- 1 | module M2R 2 | # Logic for typical Mongrel2 request with no fancy features such as 3 | # async upload 4 | # 5 | # @private 6 | module Base 7 | # @return [StringIO] Request body encapsulated in IO compatible object 8 | # @api public 9 | def body_io 10 | @body_io ||= begin 11 | b = StringIO.new(body) 12 | b.set_encoding(Encoding::BINARY) if b.respond_to?(:set_encoding) 13 | b 14 | end 15 | end 16 | 17 | # @return [nil] Free external resources such as files or sockets 18 | # @api public 19 | def free! 20 | body_io.close 21 | end 22 | 23 | end 24 | end 25 | 26 | -------------------------------------------------------------------------------- /lib/m2r/response/content_length.rb: -------------------------------------------------------------------------------- 1 | module M2R 2 | class Response 3 | # Adds Content-Length header based on body size 4 | # This is mostly required when you use bare 5 | # Response class without any framework on top of it. 6 | # HTTP clients require such header when there is 7 | # body in response. Otherwise they hang out. 8 | # 9 | # @api public 10 | module ContentLength 11 | def headers(value = GETTER) 12 | if value == GETTER 13 | h = super 14 | h['Content-Length'] ||= body.bytesize 15 | h 16 | else 17 | super 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /kitchen/cookbooks/m2r/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "m2r", 3 | "description": "Installs/Configures m2r", 4 | "long_description": "Description\n===========\n\nRequirements\n============\n\nAttributes\n==========\n\nUsage\n=====\n\n", 5 | "maintainer": "YOUR_COMPANY_NAME", 6 | "maintainer_email": "YOUR_EMAIL", 7 | "license": "All rights reserved", 8 | "platforms": { 9 | }, 10 | "dependencies": { 11 | }, 12 | "recommendations": { 13 | }, 14 | "suggestions": { 15 | }, 16 | "conflicting": { 17 | }, 18 | "providing": { 19 | }, 20 | "replacing": { 21 | }, 22 | "attributes": { 23 | }, 24 | "groupings": { 25 | }, 26 | "recipes": { 27 | }, 28 | "version": "0.1.0" 29 | } -------------------------------------------------------------------------------- /kitchen/cookbooks/zmq/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zmq", 3 | "description": "Installs/Configures zmq", 4 | "long_description": "Description\n===========\n\nRequirements\n============\n\nAttributes\n==========\n\nUsage\n=====\n\n", 5 | "maintainer": "YOUR_COMPANY_NAME", 6 | "maintainer_email": "YOUR_EMAIL", 7 | "license": "All rights reserved", 8 | "platforms": { 9 | }, 10 | "dependencies": { 11 | }, 12 | "recommendations": { 13 | }, 14 | "suggestions": { 15 | }, 16 | "conflicting": { 17 | }, 18 | "providing": { 19 | }, 20 | "replacing": { 21 | }, 22 | "attributes": { 23 | }, 24 | "groupings": { 25 | }, 26 | "recipes": { 27 | }, 28 | "version": "0.1.0" 29 | } -------------------------------------------------------------------------------- /kitchen/cookbooks/mongrel2/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongrel2", 3 | "description": "Installs/Configures mongrel2", 4 | "long_description": "Description\n===========\n\nRequirements\n============\n\nAttributes\n==========\n\nUsage\n=====\n\n", 5 | "maintainer": "YOUR_COMPANY_NAME", 6 | "maintainer_email": "YOUR_EMAIL", 7 | "license": "All rights reserved", 8 | "platforms": { 9 | }, 10 | "dependencies": { 11 | }, 12 | "recommendations": { 13 | }, 14 | "suggestions": { 15 | }, 16 | "conflicting": { 17 | }, 18 | "providing": { 19 | }, 20 | "replacing": { 21 | }, 22 | "attributes": { 23 | }, 24 | "groupings": { 25 | }, 26 | "recipes": { 27 | }, 28 | "version": "0.1.0" 29 | } -------------------------------------------------------------------------------- /kitchen/cookbooks/essential/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "essential", 3 | "description": "Installs/Configures essential", 4 | "long_description": "Description\n===========\n\nRequirements\n============\n\nAttributes\n==========\n\nUsage\n=====\n\n", 5 | "maintainer": "YOUR_COMPANY_NAME", 6 | "maintainer_email": "YOUR_EMAIL", 7 | "license": "All rights reserved", 8 | "platforms": { 9 | }, 10 | "dependencies": { 11 | }, 12 | "recommendations": { 13 | }, 14 | "suggestions": { 15 | }, 16 | "conflicting": { 17 | }, 18 | "providing": { 19 | }, 20 | "replacing": { 21 | }, 22 | "attributes": { 23 | }, 24 | "groupings": { 25 | }, 26 | "recipes": { 27 | }, 28 | "version": "0.1.0" 29 | } -------------------------------------------------------------------------------- /kitchen/cookbooks/ruby-build/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ruby-build", 3 | "description": "Installs/Configures ruby-build", 4 | "long_description": "Description\n===========\n\nRequirements\n============\n\nAttributes\n==========\n\nUsage\n=====\n\n", 5 | "maintainer": "Arkency", 6 | "maintainer_email": "michal.lomnicki@gmail.com", 7 | "license": "All rights reserved", 8 | "platforms": { 9 | }, 10 | "dependencies": { 11 | }, 12 | "recommendations": { 13 | }, 14 | "suggestions": { 15 | }, 16 | "conflicting": { 17 | }, 18 | "providing": { 19 | }, 20 | "replacing": { 21 | }, 22 | "attributes": { 23 | }, 24 | "groupings": { 25 | }, 26 | "recipes": { 27 | }, 28 | "version": "0.0.1" 29 | } -------------------------------------------------------------------------------- /kitchen/cookbooks/build-essential/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "build-essential", 3 | "description": "Installs C compiler / build tools", 4 | "long_description": "", 5 | "maintainer": "Opscode, Inc.", 6 | "maintainer_email": "cookbooks@opscode.com", 7 | "license": "Apache 2.0", 8 | "platforms": { 9 | "fedora": ">= 0.0.0", 10 | "redhat": ">= 0.0.0", 11 | "centos": ">= 0.0.0", 12 | "ubuntu": ">= 0.0.0", 13 | "debian": ">= 0.0.0" 14 | }, 15 | "dependencies": { 16 | }, 17 | "recommendations": { 18 | }, 19 | "suggestions": { 20 | }, 21 | "conflicting": { 22 | }, 23 | "providing": { 24 | }, 25 | "replacing": { 26 | }, 27 | "attributes": { 28 | }, 29 | "groupings": { 30 | }, 31 | "recipes": { 32 | "build-essential": "Installs C compiler and build tools on Linux" 33 | }, 34 | "version": "1.0.0" 35 | } -------------------------------------------------------------------------------- /lib/m2r/response/to_request.rb: -------------------------------------------------------------------------------- 1 | module M2R 2 | class Response 3 | 4 | # Handles the logic of having response with proper 5 | # http version and 'Connection' header in relation to 6 | # given request 7 | # 8 | # @api public 9 | module ToRequest 10 | # params [Request] request Request that response handles 11 | # params [true, false] identical Whether http version in response should be identical 12 | # to the received one. 13 | # @return [self] Response object 14 | # @api public 15 | def to(request, identical = false) 16 | # http://www.ietf.org/rfc/rfc2145.txt 17 | # 2.3 Which version number to send in a message 18 | http_version(request.http_version) if identical 19 | headers['Connection'] = 'close' if request.close? 20 | self 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/m2r/http/close.rb: -------------------------------------------------------------------------------- 1 | module M2R 2 | module HTTP 3 | 4 | # Detect that whether connection should be closed 5 | # based on http protocol version and `Connection' header 6 | # We do not support persistent connections for HTTP 1.0 7 | module Close 8 | 9 | # @return [true, false] Information whether HTTP Connection should 10 | # be closed after processing the request. Happens when HTTP/1.0 11 | # or request has Connection=close header. 12 | def close? 13 | unsupported_version? || connection_close? 14 | end 15 | 16 | protected 17 | 18 | # http://en.wikipedia.org/wiki/HTTP_persistent_connection 19 | def unsupported_version? 20 | http_version != 'HTTP/1.1' 21 | end 22 | 23 | def connection_close? 24 | headers['Connection'] == 'close' 25 | end 26 | 27 | end 28 | 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /kitchen/cookbooks/build-essential/README.md: -------------------------------------------------------------------------------- 1 | DESCRIPTION 2 | =========== 3 | 4 | Installs packages required for compiling C software from source. 5 | 6 | LICENSE AND AUTHOR 7 | ================== 8 | 9 | Author:: Joshua Timberman () 10 | Author:: Seth Chisamore () 11 | 12 | Copyright 2009-2011, Opscode, Inc. 13 | 14 | Licensed under the Apache License, Version 2.0 (the "License"); 15 | you may not use this file except in compliance with the License. 16 | You may obtain a copy of the License at 17 | 18 | http://www.apache.org/licenses/LICENSE-2.0 19 | 20 | Unless required by applicable law or agreed to in writing, software 21 | distributed under the License is distributed on an "AS IS" BASIS, 22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | See the License for the specific language governing permissions and 24 | limitations under the License. 25 | -------------------------------------------------------------------------------- /test/support/mongrel_helper.rb: -------------------------------------------------------------------------------- 1 | require 'timeout' 2 | 3 | module MongrelHelper 4 | attr_accessor :pid 5 | 6 | def setup 7 | check_mongrel 8 | `cd example && m2sh load -config mongrel2.conf --db config.sqlite` 9 | self.pid = Process.spawn("bundle exec foreman start --procfile=example/Procfile", pgroup: 0, out: "/dev/null", err: "/dev/null") 10 | wait_until_mongrel_responsive 11 | end 12 | 13 | def wait_until_mongrel_responsive 14 | client = Net::HTTP.new('localhost', 6767) 15 | timeout(5) do 16 | begin 17 | client.start 18 | rescue Errno::ECONNREFUSED 19 | sleep(0.1) 20 | retry 21 | end 22 | end 23 | end 24 | 25 | def teardown 26 | Process.kill("SIGTERM", pid) if pid 27 | sleep 1 28 | end 29 | 30 | def check_mongrel 31 | skip("You must install mongrel2 to run this test") if `which mongrel2`.empty? 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /test/unit/connection_factory_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'securerandom' 3 | 4 | module M2R 5 | class ConnectionFactoryTest < MiniTest::Unit::TestCase 6 | def test_factory 7 | sender_id = "sid" 8 | request_addr = "req" 9 | response_addr = "req" 10 | 11 | pull = stub(:pull) 12 | pub = stub(:pub) 13 | context = stub(:context) 14 | 15 | context.expects(:socket).with(ZMQ::PULL).returns(pull) 16 | context.expects(:socket).with(ZMQ::PUB).returns(pub) 17 | 18 | pull.expects(:connect).with(request_addr) 19 | 20 | pub.expects(:connect).with(response_addr) 21 | pub.expects(:setsockopt).with(ZMQ::IDENTITY, sender_id) 22 | 23 | Connection.expects(:new).with(pull, pub) 24 | cf = ConnectionFactory.new ConnectionFactory::Options.new(sender_id, request_addr, response_addr), context 25 | cf.connection 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /kitchen/Rakefile: -------------------------------------------------------------------------------- 1 | COOKBOOKS_DIR = File.expand_path('../cookbooks', __FILE__) 2 | ROLE_DIR = File.expand_path('../roles', __FILE__) 3 | 4 | require 'chef' 5 | require 'posix/spawn' 6 | 7 | namespace :roles do 8 | desc "Convert ruby roles from ruby to json, creating/overwriting json files." 9 | task :to_json do 10 | Dir.glob(File.join(ROLE_DIR, '*.rb')) do |rb_file| 11 | role = Chef::Role.new 12 | role.from_file(rb_file) 13 | json_file = rb_file.sub(/\.rb$/,'.json') 14 | File.open(json_file, 'w'){|f| f.write(JSON.pretty_generate(role))} 15 | end 16 | end 17 | end 18 | 19 | namespace :metadata do 20 | desc "Convert all metadata from ruby to json." 21 | task :to_json do 22 | Dir.glob(File.join(COOKBOOKS_DIR, '*/metadata.rb')) do |rb_file| 23 | path = rb_file.split('/')[-3] 24 | recipe = rb_file.split('/')[-2] 25 | POSIX::Spawn::spawn("knife cookbook metadata -o #{path} #{recipe}") 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/support/test_user.rb: -------------------------------------------------------------------------------- 1 | require 'bbq/test' 2 | require 'bbq/test_user' 3 | require 'support/capybara' 4 | 5 | class TestUser < Bbq::TestUser 6 | include MiniTest::Assertions 7 | 8 | def initialize 9 | super(:driver => :mechanize) 10 | end 11 | 12 | def see!(*args) 13 | msg = "Expected to see %s but not found" 14 | args.each { |arg| assert has_content?(arg), msg % arg } 15 | end 16 | 17 | def generate_file 18 | File.open(@path = "./tmp/#{Time.now.to_i}", "w") do |f| 19 | f.write SecureRandom.hex(5120) 20 | end 21 | file_path 22 | end 23 | 24 | def attach_first_file(form_id, file_path) 25 | page.driver.browser.agent.current_page.form_with(id: form_id) do |form| 26 | form.file_uploads.first.file_name = file_path 27 | end 28 | end 29 | 30 | def attach_and_submit_first_file(form_id, file_path) 31 | attach_first_file(form_id, file_path).submit 32 | end 33 | 34 | def file_path 35 | @path 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/acceptance/examples_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module M2R 4 | class ExamplesTest < MiniTest::Unit::TestCase 5 | include MongrelHelper 6 | 7 | def test_rack_example 8 | user = TestUser.new 9 | user.visit("/handler") 10 | user.see!("SENDER", "PATH", "HEADERS", "x-forwarded-for", "x-forwarded-for", "BODY") 11 | end 12 | 13 | def test_handler_example 14 | user = TestUser.new 15 | user.visit("/rack") 16 | assert user.find("pre").text.include?(" {{{{{{ { ( ( ( ( (-----:=") 17 | user.click_on("flip!") 18 | assert user.find("pre").text.include?("=:-----( ( ( ( ( { {{{{{{") 19 | end 20 | 21 | def test_handler_async_uploading 22 | Dir.glob('example/tmp/upload*') {|p| File.delete(p) } 23 | user = TestUser.new 24 | user.visit("/uploading") 25 | user.attach_and_submit_first_file('uploading_form', user.generate_file) 26 | user.visit("/uploading") 27 | user.see!("Last submitted file was of size: 10240") 28 | assert_equal 0, Dir.glob('example/tmp/upload*').size 29 | end 30 | 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /kitchen/cookbooks/mongrel2/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: mongrel2 3 | # Recipe:: default 4 | # 5 | # Copyright 2012, YOUR_COMPANY_NAME 6 | # 7 | # All rights reserved - Do Not Redistribute 8 | # 9 | 10 | %w[uuid-dev uuid-runtime libsqlite3-dev sqlite3].each do |name| 11 | package name do 12 | action :install 13 | end 14 | end 15 | 16 | source = "https://github.com/zedshaw/mongrel2/tarball/v1.8.0" 17 | name = "mongrel2-v1.8.0.tar.gz" 18 | unpack = "zedshaw-mongrel2-bc721eb" 19 | 20 | cache_dir = Chef::Config[:file_cache_path] 21 | download_destination = File.join(cache_dir, name) 22 | unpack_destination = File.join(cache_dir, unpack) 23 | 24 | remote_file download_destination do 25 | source source 26 | mode "0644" 27 | action :create_if_missing 28 | end 29 | 30 | execute "Extract mongrel2 archive" do 31 | command "tar xvzf #{download_destination} -C #{cache_dir}" 32 | creates unpack_destination 33 | end 34 | 35 | execute "Install mongrel2" do 36 | command "cd #{unpack_destination} && make clean all && sudo make install" 37 | not_if { `which m2sh | wc -l`.to_i > 0} 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Pradeep Elankumaran 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /kitchen/cookbooks/zmq/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: zmq 3 | # Recipe:: default 4 | # 5 | # Copyright 2012, Arkency 6 | # 7 | # All rights reserved - Do Not Redistribute 8 | # 9 | 10 | package 'uuid-dev' 11 | 12 | zmq = node['zmq'] || {} 13 | zmq_v = zmq['version'] || '2.2.0' 14 | #zmq_v = zmq['version'] || '3.2.1-rc2' 15 | name = "zeromq-#{zmq_v}.tar.gz" 16 | unpack = 'zeromq-' + zmq_v.split("-").first 17 | 18 | cache_dir = Chef::Config[:file_cache_path] 19 | download_destination = File.join(cache_dir, name) 20 | unpack_destination = File.join(cache_dir, unpack) 21 | 22 | remote_file download_destination do 23 | source "http://download.zeromq.org/#{name}" 24 | mode "0644" 25 | action :create_if_missing 26 | end 27 | 28 | execute "Extract zmq #{zmq_v} archive" do 29 | command "tar xvzf #{download_destination} -C #{cache_dir}" 30 | creates unpack_destination 31 | end 32 | 33 | execute "Install zmq #{zmq_v} version" do 34 | command "cd #{unpack_destination} && ./configure && make && sudo make install && sudo ldconfig" 35 | not_if { `ldconfig -p | grep libzmq | wc -l`.to_i > 0} 36 | end 37 | -------------------------------------------------------------------------------- /example/uploading.ru: -------------------------------------------------------------------------------- 1 | # Running this example: 2 | # 3 | # m2sh load -config mongrel2.conf 4 | # bundle exec foreman start 5 | # 6 | # Browse now to http://localhost:6767/uploading to see the effect. 7 | # 8 | # This example is not threadsafe ! 9 | 10 | $stdout.sync = true 11 | $stderr.sync = true 12 | 13 | require 'rack' 14 | require 'pathname' 15 | 16 | use Rack::ContentLength 17 | size = 0 18 | app = Proc.new do |env| 19 | req = Rack::Request.new(env) 20 | if req.post? 21 | size = req.params["file"][:tempfile].size.to_s rescue size = 0 22 | end 23 | note = req.post?? "You submitted file of size: #{size}" : "Last submitted file was of size: #{size}" 24 | body = <<-EOF 25 | 26 | 27 |
28 | 29 | Submit 30 |
31 |

#{note}

32 | 33 | 34 | EOF 35 | [200, {'Content-Type' => 'text/html'}, [body]] 36 | end 37 | run app 38 | -------------------------------------------------------------------------------- /test/unit/m2r_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'timeout' 3 | 4 | module M2R 5 | class ModuleTest < MiniTest::Unit::TestCase 6 | def setup 7 | if context = M2R.instance_variable_get(:@zmq_context) 8 | context.send(:remove_finalizer) if context.respond_to?(:remove_finalizer) 9 | M2R.instance_variable_set(:@zmq_context, nil) 10 | end 11 | end 12 | 13 | def test_mongrel2_context_getter 14 | assert_instance_of ZMQ::Context, M2R.zmq_context 15 | end 16 | 17 | def test_mongrel2_context_setter 18 | ctx = ZMQ::Context.new(2) 19 | M2R.zmq_context = ctx 20 | assert_equal ctx, M2R.zmq_context 21 | end 22 | 23 | def test_only_1_context_created_when_race_condition 24 | threads = nil 25 | ZMQ::Context.expects(:new).returns(true).once 26 | 27 | Thread.exclusive do 28 | threads = 512.times.map do 29 | Thread.new do 30 | M2R.zmq_context 31 | end 32 | end 33 | end 34 | Timeout.timeout(5) do 35 | threads.each(&:join) 36 | end 37 | 38 | M2R.zmq_context = nil 39 | end 40 | 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/m2r/parser.rb: -------------------------------------------------------------------------------- 1 | require 'm2r/request' 2 | 3 | module M2R 4 | # Mongrel2 Request Parser 5 | # @api public 6 | class Parser 7 | # Parse Mongrel2 request received via ZMQ message 8 | # 9 | # @param [String] msg Monrel2 Request message formatted according to rules 10 | # of creating it described it m2 manual. 11 | # @return [Request] 12 | # 13 | # @api public 14 | # @threadsafe true 15 | def parse(msg) 16 | sender, conn_id, path, rest = msg.split(' ', 4) 17 | 18 | headers, rest = TNetstring.parse(rest) 19 | body, _ = TNetstring.parse(rest) 20 | headers = JSON.load(headers) 21 | headers, mong = split_headers(headers) 22 | headers = Headers.new headers, true 23 | mong = Headers.new mong, true 24 | Request.new(sender, conn_id, path, headers, mong, body) 25 | end 26 | 27 | private 28 | 29 | def split_headers(headers) 30 | http = {} 31 | mongrel = {} 32 | headers.each do |header, value| 33 | if Request::MONGREL2_HEADERS.include?(header) 34 | mongrel[header.downcase] = value 35 | else 36 | http[header] = value 37 | end 38 | end 39 | return http, mongrel 40 | end 41 | 42 | end 43 | end 44 | 45 | -------------------------------------------------------------------------------- /kitchen/cookbooks/build-essential/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Cookbook Name:: build-essential 3 | # Recipe:: default 4 | # 5 | # Copyright 2008-2009, Opscode, Inc. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | case node['platform'] 21 | when "ubuntu","debian" 22 | %w{build-essential binutils-doc}.each do |pkg| 23 | package pkg do 24 | action :install 25 | end 26 | end 27 | when "centos","redhat","fedora" 28 | %w{gcc gcc-c++ kernel-devel make}.each do |pkg| 29 | package pkg do 30 | action :install 31 | end 32 | end 33 | end 34 | 35 | package "autoconf" do 36 | action :install 37 | end 38 | 39 | package "flex" do 40 | action :install 41 | end 42 | 43 | package "bison" do 44 | action :install 45 | end 46 | -------------------------------------------------------------------------------- /example/mongrel2.conf: -------------------------------------------------------------------------------- 1 | handler_example = Handler( 2 | send_spec = "tcp://127.0.0.1:9999", 3 | send_ident = "8699e94e-ee48-4274-9461-5907fa0efc4A", 4 | recv_spec = "tcp://127.0.0.1:9998", 5 | recv_ident = "" 6 | ) 7 | 8 | rack_example = Handler( 9 | send_spec = "tcp://127.0.0.1:9997", 10 | send_ident = "14fff75f-3474-4089-af6d-bbd67735ab89", 11 | recv_spec = "tcp://127.0.0.1:9996", 12 | recv_ident = "" 13 | ) 14 | 15 | uploading_example = Handler( 16 | send_spec = "tcp://127.0.0.1:9995", 17 | send_ident = "86cce8c2-85f2-4864-ab0a-a0e28a622d30", 18 | recv_spec = "tcp://127.0.0.1:9994", 19 | recv_ident = "" 20 | ) 21 | 22 | mongrel2 = Host( 23 | name = "127.0.0.1", 24 | routes = { "/rack": rack_example, "/handler": handler_example, "/uploading": uploading_example } 25 | ) 26 | 27 | main = Server( 28 | uuid="2f62bd5-9e59-49cd-993c-3b6013c28f05", 29 | access_log = "/tmp/access.log", 30 | error_log = "/tmp/error.log", 31 | chroot = "./", 32 | pid_file = "/tmp/mongrel2.pid", 33 | default_host = "127.0.0.1", 34 | name = "main", 35 | port = 6767, 36 | hosts = [mongrel2] 37 | ) 38 | 39 | settings = {"zeromq.threads": 1, "upload.temp_store": 40 | "./tmp/upload.XXXXXX", 41 | "upload.temp_store_mode": "0666", 42 | "limits.content_length": 5120 43 | 44 | } 45 | 46 | servers = [main] 47 | 48 | -------------------------------------------------------------------------------- /lib/m2r/rack_handler.rb: -------------------------------------------------------------------------------- 1 | require 'm2r' 2 | require 'rack' 3 | 4 | module M2R 5 | # Handle Mongrel2 requests using Rack application 6 | # @private 7 | class RackHandler < Handler 8 | attr_accessor :app 9 | 10 | def initialize(app, connection_factory, parser) 11 | @app = app 12 | super(connection_factory, parser) 13 | end 14 | 15 | def process(request) 16 | script_name = request.pattern.split('(', 2).first.gsub(/\/$/, '') 17 | 18 | env = { 19 | 'REQUEST_METHOD' => request.method, 20 | 'SCRIPT_NAME' => script_name, 21 | 'PATH_INFO' => request.path.gsub(script_name, ''), 22 | 'QUERY_STRING' => request.query || "", 23 | 'rack.version' => ::Rack::VERSION, 24 | 'rack.errors' => $stderr, 25 | 'rack.multithread' => false, 26 | 'rack.multiprocess' => true, 27 | 'rack.run_once' => false, 28 | 'rack.url_scheme' => request.scheme, 29 | 'rack.input' => request.body_io 30 | } 31 | env['SERVER_NAME'], env['SERVER_PORT'] = request.headers['Host'].split(':', 2) 32 | request.headers.rackify(env) 33 | 34 | status, headers, body = @app.call(env) 35 | buffer = "" 36 | body.each { |part| buffer << part } 37 | body.close if body.respond_to?(:close) 38 | return Response.new.status(status).headers(headers).body(buffer) 39 | end 40 | 41 | def after_all(request, response) 42 | request.free! 43 | end 44 | 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/m2r/request/upload.rb: -------------------------------------------------------------------------------- 1 | module M2R 2 | # Logic for Mongrel2 request delivered using async-upload feature 3 | # Contains methods for recognizing such requests and reading them. 4 | # @private 5 | module Upload 6 | # @return [true,false] True if this is async-upload related request 7 | # @api public 8 | def upload? 9 | !!@mongrel_headers['x-mongrel2-upload-start'] 10 | end 11 | 12 | # @return [true,false] True if this is async-upload start notification 13 | # @api public 14 | def upload_start? 15 | upload? and not upload_path 16 | end 17 | 18 | # @return [true,false] True if this is final async-upload request 19 | # @api public 20 | def upload_done? 21 | upload? and upload_path 22 | end 23 | 24 | # @return [String] Relative path to file containing body of HTTP 25 | # request. 26 | # @api public 27 | def upload_path 28 | @mongrel_headers['x-mongrel2-upload-done'] 29 | end 30 | 31 | # @return [File] Request body encapsulated in IO compatible object 32 | # @api public 33 | def body_io 34 | return super unless upload_done? 35 | @body_io ||= begin 36 | f = File.open(upload_path, "r+b") 37 | f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding) 38 | f 39 | end 40 | end 41 | 42 | # @return [nil] Free external resources such as files or sockets 43 | # @api public 44 | def free! 45 | super 46 | File.delete(body_io.path) if upload_done? 47 | end 48 | end 49 | end 50 | 51 | -------------------------------------------------------------------------------- /test/support/test_handler.rb: -------------------------------------------------------------------------------- 1 | require 'thread' 2 | require 'm2r/handler' 3 | 4 | class TestHandler < M2R::Handler 5 | attr_reader :called_methods 6 | def initialize(connection_factory, parser) 7 | super 8 | @mutex = Mutex.new 9 | @called_methods = [] 10 | Thread.current[:called_methods] = [] 11 | end 12 | 13 | def on_wait() 14 | unless Thread.current[:called_methods].empty? 15 | stop 16 | return 17 | end 18 | called_method :wait 19 | end 20 | 21 | def on_request(request) 22 | called_method :request 23 | end 24 | 25 | def process(request) 26 | called_method :process 27 | return "response" 28 | end 29 | 30 | def on_disconnect(request) 31 | called_method :disconnect 32 | end 33 | 34 | def on_upload_start(request) 35 | called_method :start 36 | end 37 | 38 | def on_upload_done(request) 39 | called_method :done 40 | end 41 | 42 | def after_process(request, response) 43 | called_method :after 44 | return response 45 | end 46 | 47 | def after_reply(request, response) 48 | called_method :reply 49 | end 50 | 51 | def after_all(request, response) 52 | called_method :all 53 | end 54 | 55 | def on_error(request, response, error) 56 | called_method :error 57 | end 58 | 59 | def on_interrupted 60 | called_method :interrupted 61 | end 62 | 63 | private 64 | 65 | def called_method(mth) 66 | Thread.current[:called_methods] << mth 67 | @mutex.synchronize do 68 | @called_methods << mth 69 | end 70 | end 71 | 72 | end 73 | -------------------------------------------------------------------------------- /test/unit/request_parsing_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module M2R 4 | class TestRequestParsing < MiniTest::Unit::TestCase 5 | def test_parse 6 | data = %q[FAKESENDER 0 / 97:{"PATH":"/","host":"default","METHOD":"HEAD","VERSION":"HTTP/1.1","URI":"/","URL_SCHEME":"https"},0:,] 7 | request = Parser.new.parse(data) 8 | 9 | assert_equal "/", request.path 10 | assert_equal "default", request.headers['Host'] 11 | assert_equal "HEAD", request.method 12 | assert_equal nil, request.query 13 | assert_equal nil, request.pattern 14 | assert_equal "https", request.scheme 15 | assert_equal false, request.close? 16 | end 17 | 18 | def test_scheme 19 | data = %q[FAKESENDER 0 / 96:{"PATH":"/","host":"default","METHOD":"HEAD","VERSION":"HTTP/1.1","URI":"/","URL_SCHEME":"http"},0:,] 20 | request = Parser.new.parse(data) 21 | assert_equal "http", request.scheme 22 | end 23 | 24 | def test_http_scheme_in_mongrel17 25 | data = %q[FAKESENDER 0 / 76:{"PATH":"/","host":"default","METHOD":"HEAD","VERSION":"HTTP/1.1","URI":"/"},0:,] 26 | request = Parser.new.parse(data) 27 | assert_equal "http", request.scheme 28 | end 29 | 30 | def test_https_scheme_in_mongrel17_set_via_env 31 | ENV['HTTPS']='true' 32 | data = %q[FAKESENDER 0 / 76:{"PATH":"/","host":"default","METHOD":"HEAD","VERSION":"HTTP/1.1","URI":"/"},0:,] 33 | request = Parser.new.parse(data) 34 | assert_equal "https", request.scheme 35 | ensure 36 | ENV.delete('HTTPS') 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/m2r/connection_factory.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module M2R 4 | # {Connection} factory so that every thread can use it generate its own 5 | # {Connection} for communication with Mongrel2. 6 | # 7 | # @api public 8 | class ConnectionFactory 9 | class Options < Struct.new(:sender_id, :recv_addr, :send_addr) 10 | # @param [String, nil] sender_id {ZMQ::IDENTITY} option for response socket 11 | # @param [String] recv_addr ZMQ connection address. This is the 12 | # send_spec option from Handler configuration in mongrel2.conf 13 | # @param [String] send_addr ZMQ connection address. This is the 14 | # recv_spec option from Handler configuration in mongrel2.conf 15 | def initialize(sender_id, recv_addr, send_addr) 16 | super 17 | end 18 | end 19 | 20 | # @param [Options] options ZMQ connections options 21 | # @param [ZMQ::Context] context Context for creating new ZMQ sockets 22 | def initialize(options, context = M2R.zmq_context) 23 | @options = options 24 | @context = context 25 | end 26 | 27 | # Builds new Connection which can be used to receive 28 | # Mongrel2 requests and send responses. 29 | # 30 | # @return [Connection] 31 | def connection 32 | request_socket = @context.socket(ZMQ::PULL) 33 | request_socket.connect(@options.recv_addr) 34 | 35 | response_socket = @context.socket(ZMQ::PUB) 36 | response_socket.connect(@options.send_addr) 37 | response_socket.setsockopt(ZMQ::IDENTITY, @options.sender_id) if @options.sender_id 38 | 39 | Connection.new(request_socket, response_socket) 40 | end 41 | 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/rack/handler/mongrel2.rb: -------------------------------------------------------------------------------- 1 | require 'rack/handler' 2 | require 'm2r/rack_handler' 3 | require 'securerandom' 4 | require 'ostruct' 5 | 6 | module Rack 7 | module Handler 8 | class Mongrel2 9 | DEFAULT_OPTIONS = { 10 | 'recv_addr' => 'tcp://127.0.0.1:9997', 11 | 'send_addr' => 'tcp://127.0.0.1:9996', 12 | 'sender_id' => SecureRandom.uuid 13 | } 14 | 15 | def self.run(app, options = {}) 16 | options = OpenStruct.new( DEFAULT_OPTIONS.merge(options) ) 17 | threadsafe_parser = M2R::Parser.new 18 | adapter = M2R::RackHandler.new(app, connection_factory(options), threadsafe_parser) 19 | graceful = Proc.new { adapter.stop } 20 | trap("INT", &graceful) 21 | trap("TERM", &graceful) 22 | adapter.listen 23 | M2R.zmq_context.terminate 24 | end 25 | 26 | def self.valid_options 27 | { 28 | 'recv_addr=RECV_ADDR' => 'Receive address', 29 | 'send_addr=SEND_ADDR' => 'Send address', 30 | 'sender_id=UUID' => 'Sender UUID' 31 | } 32 | end 33 | 34 | def self.connection_factory(options) 35 | klass = if custom = options.connection_factory 36 | begin 37 | M2R::ConnectionFactory.const_get(custom.classify) 38 | rescue NameError 39 | require "m2r/connection_factory/#{custom.underscore}" 40 | M2R::ConnectionFactory.const_get(custom.classify) 41 | end 42 | else 43 | M2R::ConnectionFactory 44 | end 45 | klass.new(options) 46 | end 47 | end 48 | 49 | register :mongrel2, ::Rack::Handler::Mongrel2 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/m2r.rb: -------------------------------------------------------------------------------- 1 | require 'ffi-rzmq' 2 | require 'json' 3 | require 'tnetstring' 4 | require 'thread' 5 | 6 | # Allows you to easily interact with Mongrel2 webserver from 7 | # your ruby code. 8 | # @api public 9 | module M2R 10 | class << self 11 | 12 | # Sets ZMQ context used by M2R to create sockets 13 | # @param [ZMQ::Context] value Context to by used by M2R 14 | # @see #zmq_context 15 | # @api public 16 | attr_writer :zmq_context 17 | 18 | # Gets (or sets if not existing) ZMQ context used by M2R 19 | # to create sockets. 20 | # 21 | # @note This method is thread-safe 22 | # but it uses Thread.exclusive to achive that. 23 | # However it is unlikely that it affects the performance as you probably 24 | # do not create more than a dozen of sockets in your code. 25 | # 26 | # @param [Fixnum] zmq_io_threads Size of the ZMQ thread pool to handle I/O operations. 27 | # The rule of thumb is to make it equal to the number gigabits per second 28 | # that the application will produce. 29 | # 30 | # @return [ZMQ::Context] 31 | # @see #zmq_context= 32 | # @api public 33 | def zmq_context(zmq_io_threads = 1) 34 | Thread.exclusive do 35 | @zmq_context ||= ZMQ::Context.new(zmq_io_threads) 36 | end 37 | end 38 | end 39 | end 40 | 41 | # @deprecated: Use M2R instead 42 | # Namespace used in the past in 0.0.* gem releases 43 | Mongrel2 = M2R 44 | 45 | require 'm2r/version' 46 | require 'm2r/request' 47 | require 'm2r/parser' 48 | require 'm2r/response' 49 | require 'm2r/reply' 50 | require 'm2r/connection' 51 | require 'm2r/connection_factory' 52 | require 'm2r/handler' 53 | require 'm2r/multithread_handler' 54 | -------------------------------------------------------------------------------- /m2r.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/m2r/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Colin Curtin", "Pradeep Elankumaran", "Pawel Pacana", "Robert Pankowecki"] 6 | gem.email = ["colin.t.curtin+m2r@gmail.com", "pawel.pacana+m2r@gmail.com", "robert.pankowecki+m2r@gmail.com"] 7 | gem.description = "Mongrel2 Rack handler and pure handler. Works with Rack, so it works with Rails!" 8 | gem.homepage = "http://github.com/perplexes/m2r" 9 | gem.summary = "Mongrel2 interface and handler library for Ruby." 10 | gem.license = "MIT" 11 | 12 | gem.files = `git ls-files`.split($\) 13 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 14 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 15 | gem.extra_rdoc_files = ["LICENSE", "README.md" ] 16 | 17 | gem.name = "m2r" 18 | gem.require_paths = ["lib"] 19 | gem.version = M2R::VERSION 20 | 21 | gem.add_dependency "ffi-rzmq", ">= 1.0.1" 22 | gem.add_dependency "ffi", ">= 1.0.0" 23 | gem.add_dependency "tnetstring" 24 | 25 | gem.add_development_dependency "rack" 26 | gem.add_development_dependency "rake" 27 | gem.add_development_dependency "minitest", "= 3.2.0" 28 | gem.add_development_dependency "mocha", ">= 0.14.0" 29 | gem.add_development_dependency "bbq", "= 0.0.4" 30 | gem.add_development_dependency "capybara-mechanize", "= 0.3.0" 31 | gem.add_development_dependency "activesupport", "~> 3.2.7" 32 | gem.add_development_dependency "yard", "~> 0.8.2" 33 | gem.add_development_dependency "kramdown", "~> 0.13.7" 34 | 35 | end 36 | -------------------------------------------------------------------------------- /kitchen/data_bags/vagrant.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzI 3 | w+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoP 4 | kcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2 5 | hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NO 6 | Td0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcW 7 | yLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQIBIwKCAQEA4iqWPJXtzZA68mKd 8 | ELs4jJsdyky+ewdZeNds5tjcnHU5zUYE25K+ffJED9qUWICcLZDc81TGWjHyAqD1 9 | Bw7XpgUwFgeUJwUlzQurAv+/ySnxiwuaGJfhFM1CaQHzfXphgVml+fZUvnJUTvzf 10 | TK2Lg6EdbUE9TarUlBf/xPfuEhMSlIE5keb/Zz3/LUlRg8yDqz5w+QWVJ4utnKnK 11 | iqwZN0mwpwU7YSyJhlT4YV1F3n4YjLswM5wJs2oqm0jssQu/BT0tyEXNDYBLEF4A 12 | sClaWuSJ2kjq7KhrrYXzagqhnSei9ODYFShJu8UWVec3Ihb5ZXlzO6vdNQ1J9Xsf 13 | 4m+2ywKBgQD6qFxx/Rv9CNN96l/4rb14HKirC2o/orApiHmHDsURs5rUKDx0f9iP 14 | cXN7S1uePXuJRK/5hsubaOCx3Owd2u9gD6Oq0CsMkE4CUSiJcYrMANtx54cGH7Rk 15 | EjFZxK8xAv1ldELEyxrFqkbE4BKd8QOt414qjvTGyAK+OLD3M2QdCQKBgQDtx8pN 16 | CAxR7yhHbIWT1AH66+XWN8bXq7l3RO/ukeaci98JfkbkxURZhtxV/HHuvUhnPLdX 17 | 3TwygPBYZFNo4pzVEhzWoTtnEtrFueKxyc3+LjZpuo+mBlQ6ORtfgkr9gBVphXZG 18 | YEzkCD3lVdl8L4cw9BVpKrJCs1c5taGjDgdInQKBgHm/fVvv96bJxc9x1tffXAcj 19 | 3OVdUN0UgXNCSaf/3A/phbeBQe9xS+3mpc4r6qvx+iy69mNBeNZ0xOitIjpjBo2+ 20 | dBEjSBwLk5q5tJqHmy/jKMJL4n9ROlx93XS+njxgibTvU6Fp9w+NOFD/HvxB3Tcz 21 | 6+jJF85D5BNAG3DBMKBjAoGBAOAxZvgsKN+JuENXsST7F89Tck2iTcQIT8g5rwWC 22 | P9Vt74yboe2kDT531w8+egz7nAmRBKNM751U/95P9t88EDacDI/Z2OwnuFQHCPDF 23 | llYOUI+SpLJ6/vURRbHSnnn8a/XG+nzedGH5JGqEJNQsz+xT2axM0/W/CRknmGaJ 24 | kda/AoGANWrLCz708y7VYgAtW2Uf1DPOIYMdvo6fxIB5i9ZfISgcJ/bbCUkFrhoH 25 | +vq/5CIWxCPp0f85R4qxxQ5ihxJ0YDQT9Jpx4TMss4PSavPaBH3RXow5Ohe+bYoQ 26 | NE5OgEXk2wVfZczCZpigBKbKZHNYcelXtTt/nP3rsCuGcM4h53s= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /example/http_0mq.rb: -------------------------------------------------------------------------------- 1 | # An example handler from the Mongrel2 manual. You can spin up many 2 | # of these, Mongrel2 will then round-robin requests to each one. 3 | # 4 | # Running this example: 5 | # 6 | # m2sh load -config mongrel2.conf 7 | # bundle exec foreman start 8 | # 9 | # Browse now to http://localhost:6767/handler to see the effect. 10 | 11 | $stdout.sync = true 12 | $stderr.sync = true 13 | 14 | require 'm2r' 15 | 16 | class Http0MQHandler < M2R::Handler 17 | def on_wait 18 | puts "WAITING FOR REQUEST" 19 | end 20 | 21 | def on_disconnect(request) 22 | puts "DISCONNECT" 23 | end 24 | 25 | def on_error(request, response, error) 26 | puts "ERROR:" 27 | puts error.message 28 | puts *error.backtrace 29 | end 30 | 31 | def process(request) 32 | body = < 34 | SENDER: #{request.sender} 35 | IDENT: #{request.conn_id} 36 | PATH: #{request.path} 37 | HEADERS: #{JSON.dump(request.headers.inject({}) {|hash,(h,v)| hash[h]=v; hash }, :pretty => true)} 38 | PATTERN: #{request.pattern} 39 | VERSION: #{request.http_version} 40 | METHOD: #{request.method} 41 | QUERY: #{request.query} 42 | SCHEME: #{request.scheme} 43 | BODY: #{request.body.inspect} 44 | 45 | EOF 46 | response = M2R::Reply.new.to(request).body(body) 47 | return response 48 | end 49 | end 50 | 51 | sender_id = "34f9ceee-cd52-4b7f-b197-88bf2f0ec378" 52 | pull_port = "tcp://127.0.0.1:9999" 53 | pub_port = "tcp://127.0.0.1:9998" 54 | 55 | factory = M2R::ConnectionFactory.new M2R::ConnectionFactory::Options.new(sender_id, pull_port, pub_port) 56 | handler = Http0MQHandler.new(factory, M2R::Request) 57 | graceful = Proc.new { handler.stop } 58 | trap("INT", &graceful) 59 | trap("TERM", &graceful) 60 | handler.listen 61 | M2R.zmq_context.terminate 62 | 63 | -------------------------------------------------------------------------------- /test/unit/headers_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module M2R 4 | class HeadersTest < MiniTest::Unit::TestCase 5 | def test_case_insensitivity 6 | headers = Headers.new({"Content-Type" => "CT"}) 7 | assert_equal "CT", headers['content-type'] 8 | assert_equal "CT", headers['Content-Type'] 9 | assert_equal "CT", headers['Content-type'] 10 | end 11 | 12 | def test_underscore 13 | headers = Headers.new({"URL_SCHEME" => "https"}) 14 | assert_equal "https", headers['url_scheme'] 15 | end 16 | 17 | def test_symbols_as_keys 18 | headers = Headers.new({"type" => "Ty"}) 19 | assert_equal "Ty", headers[:type] 20 | end 21 | 22 | def test_rackify 23 | headers = Headers.new({ 24 | "Content-Type" => "CT", 25 | "type" => "Ty", 26 | "Accept-Charset" => "utf8", 27 | "cOnTenT-LeNgTh" => "123" 28 | }) 29 | env = {"rack.version" => [1,1]} 30 | headers.rackify(env) 31 | assert_equal({ 32 | "rack.version" => [1,1], 33 | "CONTENT_TYPE" => "CT", 34 | "HTTP_TYPE" => "Ty", 35 | "HTTP_ACCEPT_CHARSET" => "utf8", 36 | "CONTENT_LENGTH" => "123" 37 | }, env) 38 | end 39 | 40 | def test_rackify_empty_headers 41 | headers = Headers.new({}) 42 | env = {"rack.something" => "value"} 43 | headers.rackify(env) 44 | assert_equal({ 45 | "rack.something" => "value", 46 | }, env) 47 | end 48 | 49 | def test_compatibility_trust 50 | headers = Headers.new({"Content-Type" => "CT"}, true) 51 | assert_equal nil, headers['content-type'] 52 | end 53 | 54 | def test_compatibility_direct_access 55 | headers = Headers.new(source = {"content-type" => "CT"}, true) 56 | assert_equal "CT", headers['content-type'] 57 | headers['Content-type'] = "NEW" 58 | assert_equal "NEW", headers['content-Type'] 59 | assert_equal "NEW", source['content-type'] 60 | end 61 | 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /kitchen/cookbooks/ruby-build/definitions/ruby.rb: -------------------------------------------------------------------------------- 1 | define :ruby do 2 | version = params[:version] 3 | home_dir = params[:home] 4 | ruby_dir = "#{home_dir}/#{version}" 5 | ruby_build_dir = "#{home_dir}/ruby-build" 6 | rubygems = params[:rubygems] 7 | owner = params[:owner] 8 | bin_dir = "#{ruby_dir}/bin" 9 | ruby_bin = "#{bin_dir}/ruby" 10 | gem_bin = "#{bin_dir}/gem" 11 | 12 | if params[:exports] 13 | hash = params[:exports].inject(node.set){|memo, step| memo[step] } 14 | hash['ruby_computed'] = { 15 | 'ruby_dir' => ruby_dir, 16 | 'bin_dir' => bin_dir, 17 | 'gem_bin' => gem_bin, 18 | 'ruby_bin' => ruby_bin, 19 | } 20 | end 21 | 22 | git ruby_build_dir do 23 | repository "https://github.com/sstephenson/ruby-build.git" 24 | reference "master" 25 | action :sync 26 | user owner 27 | group owner 28 | end 29 | 30 | execute "install ruby #{ruby_dir}" do 31 | command "#{ruby_build_dir}/bin/ruby-build #{version} #{ruby_dir}" 32 | user owner 33 | group owner 34 | not_if { File.exists?(ruby_dir) } 35 | end 36 | 37 | profile_file = "#{home_dir}/.bashrc" 38 | ruby_block "append ruby path #{ruby_dir}" do 39 | path_definition = "export PATH=$HOME/#{version}/bin:$PATH" 40 | block do 41 | original_content = File.open(profile_file, 'r').read 42 | File.open(profile_file, 'w') do |f| 43 | f.puts "# Generated by chef" 44 | f.puts path_definition 45 | f.puts original_content 46 | end 47 | end 48 | not_if { File.read(profile_file).include?(path_definition) } 49 | end 50 | 51 | if rubygems 52 | execute "install rubygems - #{bin_dir}" do 53 | user owner 54 | cwd home_dir 55 | command "#{bin_dir}/gem update --system #{rubygems}" 56 | not_if %Q{test $(#{bin_dir}/gem --version) = "#{rubygems}"} 57 | end 58 | end 59 | 60 | execute "#{bin_dir}/gem install bundler --no-ri --no-rdoc" do 61 | user owner 62 | not_if "#{bin_dir}/gem list | grep -q bundler" 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /test/unit/multithread_handler_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module M2R 4 | class MultithreadHandlerTest < MiniTest::Unit::TestCase 5 | SLEEP_TIME = 3 6 | MARGIN_TIME = 1 7 | WAIT_TIME = 1 8 | 9 | class ThreadTestHandler < TestHandler 10 | def process(request) 11 | sleep(SLEEP_TIME) 12 | super 13 | return Thread.current.object_id.to_s 14 | end 15 | end 16 | 17 | class TestSinglethreadHandlerFactory 18 | def initialize(connection_factory, parser) 19 | @connection_factory = connection_factory 20 | @parser = parser 21 | end 22 | 23 | def new 24 | ThreadTestHandler.new(@connection_factory, @parser) 25 | end 26 | end 27 | 28 | def setup 29 | @request_addr = "inproc://#{SecureRandom.hex}" 30 | @response_addr = "inproc://#{SecureRandom.hex}" 31 | 32 | @push = M2R.zmq_context.socket(ZMQ::PUSH) 33 | assert_equal 0, @push.bind(@request_addr), "Could not bind push socket in tests" 34 | 35 | @sub = M2R.zmq_context.socket(ZMQ::SUB) 36 | assert_equal 0, @sub.bind(@response_addr), "Could not bind sub socket in tests" 37 | @sub.setsockopt(ZMQ::SUBSCRIBE, "") 38 | end 39 | 40 | def teardown 41 | @push.close if @push 42 | @sub.close if @sub 43 | end 44 | 45 | def test_threads_are_processing_request_simultaneously 46 | cf = ConnectionFactory.new(ConnectionFactory::Options.new(nil, @request_addr, @response_addr)) 47 | par = Parser.new 48 | mth = MultithreadHandler.new(TestSinglethreadHandlerFactory.new(cf, par)) 49 | mth.listen 50 | sleep(WAIT_TIME) 51 | 52 | start = Time.now 53 | 8.times do |i| 54 | @push.send_string(msg = "1c5fd481-1121-49d8-a706-69127975db1a ebb407b2-49aa-48a5-9f96-9db12105148#{i} / 2:{},1:#{i},", ZMQ::NonBlocking) 55 | end 56 | responses = 16.times.map do 57 | @sub.recv_string(msg = "") 58 | msg 59 | end 60 | finish = Time.now 61 | 62 | mth.threads.each do |t| 63 | t.join 64 | end 65 | 66 | blob = responses.join("\n") 67 | mth.threads.each do |t| 68 | assert blob.include?(", #{t.object_id}") 69 | end 70 | 71 | delta = finish - start 72 | assert_in_delta(SLEEP_TIME, delta, MARGIN_TIME) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/m2r/headers.rb: -------------------------------------------------------------------------------- 1 | require 'delegate' 2 | 3 | module M2R 4 | # Normalize headers access so that it is not case-sensitive 5 | # @api public 6 | class Headers 7 | include Enumerable 8 | 9 | # @param [Hash, #inject] hash Collection of headers 10 | # @param [true, false] compatible Whether the hash already contains 11 | # downcased strings only. If so it is going to be directly as 12 | # container for the headers. 13 | def initialize(hash = {}, compatible = false) 14 | @headers = hash and return if compatible 15 | @headers = hash.inject({}) do |headers,(header,value)| 16 | headers[transform_key(header)] = value 17 | headers 18 | end 19 | end 20 | 21 | # Get header 22 | # @param [String, Symbol, #to_s] header HTTP Header key 23 | # @return [String, nil] Value of given Header, nil when not present 24 | def [](header) 25 | @headers[transform_key(header)] 26 | end 27 | 28 | # Set header 29 | # @param [String, Symbol, #to_s] header HTTP Header key 30 | # @param [String] value HTTP Header value 31 | # @return [String] Set value 32 | def []=(header, value) 33 | @headers[transform_key(header)] = value 34 | end 35 | 36 | # Delete header 37 | # @param [String, Symbol, #to_s] header HTTP Header key 38 | # @return [String, nil] Value of deleted header, nil when was not present 39 | def delete(header) 40 | @headers.delete(transform_key(header)) 41 | end 42 | 43 | # Iterate over headers 44 | # @yield HTTP header and its value 45 | # @yieldparam [String] header HTTP Header name (downcased) 46 | # @yieldparam [String] value HTTP Header value 47 | # @return [Hash, Enumerator] 48 | def each(&proc) 49 | @headers.each(&proc) 50 | end 51 | 52 | # Fill Hash with Headers compatibile with Rack standard. 53 | # Every header except for Content-Length and Content-Type 54 | # is capitalized, underscored, and prefixed with HTTP. 55 | # Content-Length and Content-Type are not prefixed 56 | # (according to the spec) 57 | # 58 | # @param [Hash] env Hash representing Rack Env or compatible 59 | # @return [Hash] same Hash as provided as argument. 60 | def rackify(env = {}) 61 | @headers.each do |header, value| 62 | key = "HTTP_" + header.upcase.gsub("-", "_") 63 | env[key] = value 64 | end 65 | env["CONTENT_LENGTH"] = env.delete("HTTP_CONTENT_LENGTH") if env.key?("HTTP_CONTENT_LENGTH") 66 | env["CONTENT_TYPE"] = env.delete("HTTP_CONTENT_TYPE") if env.key?("HTTP_CONTENT_TYPE") 67 | env 68 | end 69 | 70 | def empty? 71 | @headers.empty? 72 | end 73 | 74 | protected 75 | 76 | def transform_key(key) 77 | key.to_s.downcase 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/m2r/connection.rb: -------------------------------------------------------------------------------- 1 | module M2R 2 | # Connection for exchanging data with mongrel2 3 | class Connection 4 | class Error < StandardError 5 | attr_accessor :errno 6 | def signal? 7 | errno == ZMQ::EINTR 8 | end 9 | end 10 | 11 | # @param [ZMQ::Socket] request_socket socket for receiving requests 12 | # from Mongrel2 13 | # @param [ZMQ::Socket] response_socket socket for sending responses 14 | # to Mongrel2 15 | # @api public 16 | def initialize(request_socket, response_socket) 17 | @request_socket = request_socket 18 | @response_socket = response_socket 19 | end 20 | 21 | # For compatibility with {M2R::ConnectionFactory} 22 | # 23 | # @return [Connection] self 24 | # @api public 25 | def connection 26 | self 27 | end 28 | 29 | # Returns Mongrel2 request 30 | # 31 | # @note This is blocking call 32 | # @return [String] M2 request message 33 | # @api public 34 | def receive 35 | ret = @request_socket.recv_string(msg = "") 36 | if ret < 0 37 | e = Error.new "Unable to receive message: #{ZMQ::Util.error_string}" 38 | e.errno = ZMQ::Util.errno 39 | raise e 40 | end 41 | return msg 42 | end 43 | 44 | # Sends response to Mongrel2 for given request 45 | # 46 | # @param [Response, #to_s] response_or_string Response 47 | # for the request. Anything convertable to [String] 48 | # @return [String] M2 response message 49 | # @api public 50 | def reply(request, response_or_string) 51 | deliver(request.sender, request.conn_id, response_or_string.to_s) 52 | deliver(request.sender, request.conn_id, "") if close?(request, response_or_string) 53 | end 54 | 55 | # Delivers data to multiple mongrel2 connections. 56 | # Useful for streaming. 57 | # 58 | # @param [String] uuid Mongrel2 instance uuid 59 | # @param [Array, String] connection_ids Mongrel2 connections ids 60 | # @param [String] data Data that should be delivered to the connections 61 | # @return [String] M2 response message 62 | # 63 | # @api public 64 | def deliver(uuid, connection_ids, data, trial = 1) 65 | msg = "#{uuid} #{TNetstring.dump([*connection_ids].join(' '))} #{data}" 66 | ret = @response_socket.send_string(msg, ZMQ::NonBlocking) 67 | if ret < 0 68 | e = Error.new "Unable to deliver message: #{ZMQ::Util.error_string}" 69 | e.errno = ZMQ::Util.errno 70 | raise e 71 | end 72 | return msg 73 | rescue Connection::Error => er 74 | raise if trial >= 3 75 | raise unless er.signal? 76 | deliver(uuid, connection_ids, data, trial + 1) 77 | end 78 | 79 | # Closes ZMQ sockets 80 | # 81 | # @api public 82 | def close 83 | @request_socket.close 84 | @response_socket.close 85 | end 86 | 87 | private 88 | 89 | def close?(request, response_or_string) 90 | if response_or_string.respond_to?(:close?) 91 | response_or_string.close? 92 | else 93 | request.close? 94 | end 95 | end 96 | 97 | attr_reader :request_socket 98 | attr_reader :response_socket 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /test/unit/rack_handler_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'm2r/rack_handler' 3 | require 'm2r/connection_factory' 4 | require 'tempfile' 5 | 6 | class HelloWorld 7 | def call(env) 8 | return [200, {'Content-Type' => 'text/plain'}, ["Hello world!"]] 9 | end 10 | end 11 | 12 | class HelloWorldFile 13 | def call(env) 14 | Thread.current[:tempfile] = t = Tempfile.new("asd") 15 | t << "Hello world!\n" 16 | t.rewind 17 | return [201, {'Content-Type' => 'text/plain'}, t] 18 | end 19 | end 20 | 21 | module M2R 22 | class ConnectionFactory 23 | class Custom 24 | def initialize(*) 25 | end 26 | end 27 | end 28 | 29 | class RackHandlerTest < MiniTest::Unit::TestCase 30 | def test_discoverability 31 | handler = ::Rack::Handler.get(:mongrel2) 32 | assert_equal ::Rack::Handler::Mongrel2, handler 33 | 34 | handler = ::Rack::Handler.get('Mongrel2') 35 | assert_equal ::Rack::Handler::Mongrel2, handler 36 | end 37 | 38 | def test_options 39 | require 'rack/handler/mongrel2' 40 | handler = ::Rack::Handler::Mongrel2 41 | options = { 42 | 'recv_addr' => recv = 'tcp://1.2.3.4:1234', 43 | 'send_addr' => send = 'tcp://1.2.3.4:4321', 44 | 'sender_id' => id = SecureRandom.uuid 45 | } 46 | cf = stub() 47 | cf.stubs(:connection => cf, :close => nil) 48 | ConnectionFactory.expects(:new).with(responds_with(:sender_id, id)).returns(cf) 49 | RackHandler.any_instance.stubs(:stop? => true) 50 | M2R.zmq_context.expects(:terminate) 51 | handler.run(HelloWorld.new, options) 52 | end 53 | 54 | def test_lint_rack_adapter 55 | factory = stub(:connection) 56 | handler = RackHandler.new(app, factory, Request) 57 | response = handler.process(root_request) 58 | 59 | assert_equal "Hello world!", response.body 60 | assert_equal 200, response.status 61 | end 62 | 63 | def test_file_closed 64 | factory = stub(:connection) 65 | handler = RackHandler.new(file_app, factory, Request) 66 | response = handler.process(root_request) 67 | 68 | assert_equal "Hello world!\n", response.body 69 | assert_equal 201, response.status 70 | assert Thread.current[:tempfile].closed? 71 | end 72 | 73 | def test_custom_connection_factory 74 | require 'rack/handler/mongrel2' 75 | handler = ::Rack::Handler::Mongrel2 76 | options = { 77 | 'connection_factory' => 'custom' 78 | } 79 | cf = stub() 80 | cf.stubs(:connection => cf, :close => nil) 81 | ConnectionFactory::Custom.expects(:new).with(responds_with(:connection_factory, 'custom')).returns(cf) 82 | RackHandler.any_instance.stubs(:stop? => true) 83 | M2R.zmq_context.expects(:terminate) 84 | handler.run(HelloWorld.new, options) 85 | end 86 | 87 | 88 | private 89 | 90 | 91 | def root_request 92 | data = %q("1c5fd481-1121-49d8-a706-69127975db1a ebb407b2-49aa-48a5-9f96-9db121051484 / 96:{"PATH":"/","host":"127.0.0.1:6767","PATTERN":"/","METHOD":"GET","VERSION":"HTTP/1.1","URI":"/"},0:,) 93 | Request.parse(data) 94 | end 95 | 96 | def app 97 | Rack::Lint.new(HelloWorld.new) 98 | end 99 | 100 | def file_app 101 | Rack::Lint.new(HelloWorldFile.new) 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/m2r/request.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | require 'm2r/http/close' 3 | require 'm2r/request/base' 4 | require 'm2r/request/upload' 5 | require 'm2r/headers' 6 | 7 | module M2R 8 | # Abstraction over Mongrel 2 request 9 | # @api public 10 | class Request 11 | # @api private 12 | TRUE_STRINGS = Set.new(%w(true yes on 1).map(&:freeze)).freeze 13 | # @api private 14 | MONGREL2_BASE_HEADERS = Set.new(%w(pattern method path query url_scheme version).map(&:upcase).map(&:freeze)).freeze 15 | # @api private 16 | MONGREL2_UPLOAD_HEADERS = Set.new(%w(x-mongrel2-upload-start x-mongrel2-upload-done).map(&:downcase).map(&:freeze)).freeze 17 | # @api private 18 | MONGREL2_HEADERS = (MONGREL2_BASE_HEADERS + MONGREL2_UPLOAD_HEADERS).freeze 19 | 20 | include Base 21 | include Upload 22 | include HTTP::Close 23 | 24 | # @return [String] UUID of mongrel2 origin instance 25 | attr_reader :sender 26 | 27 | # @return [String] Mongrel2 connection id sending this request 28 | attr_reader :conn_id 29 | 30 | # @return [String] HTTP Path of request 31 | attr_reader :path 32 | 33 | # @return [String] HTTP Body of request 34 | attr_reader :body 35 | 36 | # @param [String] sender UUID of mongrel2 origin instance 37 | # @param [String] conn_id Mongrel2 connection id sending this request 38 | # @param [String] path HTTP Path of request 39 | # @param [M2R::Headers] headers HTTP headers of request 40 | # @param [M2R::Headers] headers Additional mongrel2 headers 41 | # @param [String] body HTTP Body of request 42 | def initialize(sender, conn_id, path, http_headers, mongrel_headers, body) 43 | @sender = sender 44 | @conn_id = conn_id 45 | @path = path 46 | @http_headers = http_headers 47 | @mongrel_headers = mongrel_headers 48 | @body = body 49 | @data = JSON.load(@body) if json? 50 | end 51 | 52 | # Parse Mongrel2 request received via ZMQ message 53 | # 54 | # @param [String] msg Monrel2 Request message formatted according to rules 55 | # of creating it described it m2 manual. 56 | # @return [Request] 57 | # 58 | # @api public 59 | # @deprecated 60 | def self.parse(msg) 61 | Parser.new.parse(msg) 62 | end 63 | 64 | # @return [M2R::Headers] HTTP headers 65 | def headers 66 | @http_headers 67 | end 68 | 69 | # @return [String] Mongrel2 pattern used to match this request 70 | def pattern 71 | @mongrel_headers['pattern'] 72 | end 73 | 74 | # @return [String] HTTP method 75 | def method 76 | @mongrel_headers['method'] 77 | end 78 | 79 | # @return [String] Request query string 80 | def query 81 | @mongrel_headers['query'] 82 | end 83 | 84 | # return [String] URL scheme 85 | def scheme 86 | @mongrel_headers['url_scheme'] || mongrel17_scheme 87 | end 88 | 89 | def http_version 90 | @mongrel_headers['version'] 91 | end 92 | 93 | # @return [true, false] Internal mongrel2 message to handler issued when 94 | # message delivery is not possible because the client already 95 | # disconnected and there is no connection with such {#conn_id} 96 | def disconnect? 97 | json? and @data['type'] == 'disconnect' 98 | end 99 | 100 | protected 101 | 102 | def mongrel17_scheme 103 | return 'https' if TRUE_STRINGS.include? env_https 104 | return 'http' 105 | end 106 | 107 | def env_https 108 | (ENV['HTTPS'] || "").downcase 109 | end 110 | 111 | def json? 112 | method == 'JSON' 113 | end 114 | 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/unit/response_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module M2R 4 | class ResponseTest < MiniTest::Unit::TestCase 5 | def test_response_with_nil_body 6 | ok = Response.new.status(200).headers({"Transfer-Encoding" => "chunked"}).body(nil) 7 | http = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" 8 | assert_equal http, ok.to_s 9 | end 10 | 11 | def test_response_with_empty_body 12 | ok = Response.new.status(200).headers({"Transfer-Encoding" => "chunked"}).body("") 13 | http = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" 14 | assert_equal http, ok.to_s 15 | end 16 | 17 | def test_response_with_no_body 18 | ok = Response.new.status(200).headers({"Transfer-Encoding" => "chunked"}) 19 | http = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" 20 | assert_equal http, ok.to_s 21 | end 22 | 23 | def test_response_with_content_length 24 | ok = Response.new.body('data') 25 | ok.extend Response::ContentLength 26 | http = "HTTP/1.1 200 OK\r\ncontent-length: 4\r\n\r\ndata" 27 | assert_equal http, ok.to_s 28 | end 29 | 30 | def test_response_with_header 31 | ok = Response.new.body('data').header('X-Man', 'Wolverine') 32 | ok.extend Response::ContentLength 33 | http = "HTTP/1.1 200 OK\r\nx-man: Wolverine\r\ncontent-length: 4\r\n\r\ndata" 34 | assert_equal http, ok.to_s 35 | end 36 | 37 | def test_response_with_old_version 38 | ok = Response.new.http_version('HTTP/1.0').body('data') 39 | ok.extend Response::ContentLength 40 | http = "HTTP/1.0 200 OK\r\ncontent-length: 4\r\n\r\ndata" 41 | assert_equal http, ok.to_s 42 | end 43 | 44 | def test_getters 45 | ok = Response.new.http_version(v = 'HTTP/1.0').status(s = 300).headers({'X-Man' => xman = 'Summers'}).header("X-Angel", angel = "Warren").body(data = 'data') 46 | assert_equal v, ok.http_version 47 | assert_equal s, ok.status 48 | assert_equal xman, ok.header("X-Man") 49 | assert_equal angel, ok.header("X-Angel") 50 | assert_equal data , ok.body 51 | assert_equal 2, ok.headers.size 52 | end 53 | 54 | def test_default_close 55 | ok = Response.new 56 | refute ok.close? 57 | end 58 | 59 | def test_http10_close 60 | ok = Response.new.http_version('HTTP/1.0') 61 | assert ok.close? 62 | end 63 | 64 | def test_header_close 65 | ok = Response.new.header('Connection', 'close') 66 | assert ok.close? 67 | end 68 | 69 | def test_response_to_http10_identical 70 | ok = Response.new 71 | ok.extend(Response::ToRequest) 72 | ok.to(stub(http_version: 'HTTP/1.0', close?:true), true) 73 | assert_equal 'HTTP/1.0', ok.http_version 74 | assert_equal 'close', ok.header('Connection') 75 | end 76 | 77 | def test_response_to_http10_rfc2145 78 | ok = Response.new 79 | ok.extend(Response::ToRequest) 80 | ok.to(stub(http_version: 'HTTP/1.0', close?:true)) 81 | assert_equal 'HTTP/1.1', ok.http_version 82 | assert_equal 'close', ok.header('Connection') 83 | end 84 | 85 | def test_response_to_http11 86 | ok = Response.new 87 | ok.extend(Response::ToRequest) 88 | ok.to(stub(http_version: 'HTTP/1.1', close?:false)) 89 | assert_equal 'HTTP/1.1', ok.http_version 90 | assert_equal nil, ok.header('Connection') 91 | end 92 | 93 | def test_response_to_http11_with_close 94 | ok = Response.new 95 | ok.extend(Response::ToRequest) 96 | ok.to(stub(http_version: 'HTTP/1.1', close?:true)) 97 | assert_equal 'HTTP/1.1', ok.http_version 98 | assert_equal 'close', ok.header('Connection') 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /test/unit/handler_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module M2R 4 | class HandlerTest < MiniTest::Unit::TestCase 5 | 6 | def test_lifecycle_for_disconnect 7 | connection = stub(:receive => "", :close => nil) 8 | connection.stubs(:connection).returns(connection) 9 | parser = stub(:parse => disconnect_request) 10 | h = TestHandler.new(connection, parser) 11 | h.listen 12 | assert_equal [:wait, :request, :disconnect, :all], h.called_methods 13 | end 14 | 15 | def test_lifecycle_for_upload_start 16 | connection = stub(:receive => "", :close => nil) 17 | connection.stubs(:connection).returns(connection) 18 | parser = stub(:parse => upload_start_request) 19 | h = TestHandler.new(connection, parser) 20 | h.listen 21 | assert_equal [:wait, :request, :start, :all], h.called_methods 22 | end 23 | 24 | def test_lifecycle_for_upload_done 25 | connection = stub(:receive => "", :reply => nil, :close => nil) 26 | connection.stubs(:connection).returns(connection) 27 | parser = stub(:parse => upload_done_request) 28 | h = TestHandler.new(connection, parser) 29 | h.listen 30 | assert_equal [:wait, :request, :done, :process, :after, :reply, :all], h.called_methods 31 | end 32 | 33 | def test_lifecycle_for_exception_when_getting_request 34 | connection = stub(:close => nil) 35 | connection.stubs(:receive).raises(StandardError) 36 | connection.stubs(:connection).returns(connection) 37 | h = TestHandler.new(connection, nil) 38 | h.listen 39 | assert_equal [:wait, :error], h.called_methods 40 | end 41 | 42 | def test_lifecycle_for_exception_when_processing 43 | connection = stub(:receive => "", :reply => nil, :close => nil) 44 | connection.stubs(:connection).returns(connection) 45 | parser = stub(:parse => request) 46 | h = TestHandler.new(connection, parser) 47 | h.extend(Module.new(){ 48 | def process(request) 49 | super 50 | raise StandardError 51 | end 52 | }) 53 | h.listen 54 | assert_equal [:wait, :request, :process, :all, :error], h.called_methods 55 | end 56 | 57 | def test_signal_when_receive 58 | e = Connection::Error.new.tap{|x| x.errno = 4} 59 | connection = stub(:reply => nil, :close => nil) 60 | connection.stubs(:connection).returns(connection) 61 | connection.expects(:receive).raises(e).then.returns("").twice 62 | parser = stub(:parse => request) 63 | h = TestHandler.new(connection, parser) 64 | h.extend(Module.new(){ 65 | def on_wait 66 | if @called_methods.size > 2 67 | stop 68 | return 69 | end 70 | @called_methods << :wait 71 | end 72 | }) 73 | h.listen 74 | assert_equal [:wait, :interrupted, :wait, :request, :process, :after, :reply, :all], h.called_methods 75 | end 76 | 77 | def test_connection_closed 78 | connection = mock(:close => nil) 79 | connection.expects(:connection).returns(connection) 80 | h = TestHandler.new(connection, nil) 81 | h.stop 82 | h.listen 83 | end 84 | 85 | 86 | private 87 | 88 | 89 | def disconnect_request 90 | Request.new("sender", "conn_id", "/path", Headers.new({}), Headers.new({"METHOD" => "JSON"}), '{"type":"disconnect"}') 91 | end 92 | 93 | def upload_start_request 94 | Request.new("sender", "conn_id", "/path", Headers.new({}), Headers.new({"x-mongrel2-upload-start" => "/tmp/file"}), '') 95 | end 96 | 97 | def upload_done_request 98 | Request.new("sender", "conn_id", "/path", Headers.new({}), Headers.new({"x-mongrel2-upload-start" => "/tmp/file", "x-mongrel2-upload-done" => "/tmp/file"}), '') 99 | end 100 | 101 | def request 102 | Request.new("sender", "conn_id", "/path", Headers.new({}), Headers.new({}), '') 103 | end 104 | 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/m2r/response.rb: -------------------------------------------------------------------------------- 1 | require 'm2r/http/close' 2 | require 'm2r/response/content_length' 3 | require 'm2r/response/to_request' 4 | 5 | module M2R 6 | # Simplest possible abstraction layer over HTTP request 7 | # 8 | # @api public 9 | class Response 10 | include HTTP::Close 11 | 12 | # @private 13 | VERSION = "HTTP/1.1".freeze 14 | 15 | # @private 16 | CRLF = "\r\n".freeze 17 | 18 | # @private 19 | STATUS_CODES = { 20 | 100 => 'Continue', 21 | 101 => 'Switching Protocols', 22 | 102 => 'Processing', 23 | 200 => 'OK', 24 | 201 => 'Created', 25 | 202 => 'Accepted', 26 | 203 => 'Non-Authoritative Information', 27 | 204 => 'No Content', 28 | 205 => 'Reset Content', 29 | 206 => 'Partial Content', 30 | 207 => 'Multi-Status', 31 | 226 => 'IM Used', 32 | 300 => 'Multiple Choices', 33 | 301 => 'Moved Permanently', 34 | 302 => 'Found', 35 | 303 => 'See Other', 36 | 304 => 'Not Modified', 37 | 305 => 'Use Proxy', 38 | 306 => 'Reserved', 39 | 307 => 'Temporary Redirect', 40 | 400 => 'Bad Request', 41 | 401 => 'Unauthorized', 42 | 402 => 'Payment Required', 43 | 403 => 'Forbidden', 44 | 404 => 'Not Found', 45 | 405 => 'Method Not Allowed', 46 | 406 => 'Not Acceptable', 47 | 407 => 'Proxy Authentication Required', 48 | 408 => 'Request Timeout', 49 | 409 => 'Conflict', 50 | 410 => 'Gone', 51 | 411 => 'Length Required', 52 | 412 => 'Precondition Failed', 53 | 413 => 'Request Entity Too Large', 54 | 414 => 'Request-URI Too Long', 55 | 415 => 'Unsupported Media Type', 56 | 416 => 'Requested Range Not Satisfiable', 57 | 417 => 'Expectation Failed', 58 | 418 => "I'm a Teapot", 59 | 422 => 'Unprocessable Entity', 60 | 423 => 'Locked', 61 | 424 => 'Failed Dependency', 62 | 426 => 'Upgrade Required', 63 | 500 => 'Internal Server Error', 64 | 501 => 'Not Implemented', 65 | 502 => 'Bad Gateway', 66 | 503 => 'Service Unavailable', 67 | 504 => 'Gateway Timeout', 68 | } 69 | STATUS_CODES.freeze 70 | 71 | attr_reader :reason 72 | 73 | def initialize 74 | status(200) 75 | headers(Headers.new) 76 | body("") 77 | http_version(VERSION) 78 | end 79 | 80 | # @param [Fixnum, #to_i] value HTTP status code 81 | def status(value = GETTER) 82 | if value == GETTER 83 | @status 84 | else 85 | @status = value.to_i 86 | @reason = STATUS_CODES[@status] 87 | self 88 | end 89 | end 90 | 91 | # @param [Hash] value HTTP headers 92 | def headers(value = GETTER) 93 | if value == GETTER 94 | @headers 95 | else 96 | @headers = value 97 | self 98 | end 99 | end 100 | 101 | # @param [Hash] header HTTP header key 102 | # @param [Hash] value HTTP header value 103 | def header(header, value = GETTER) 104 | if value == GETTER 105 | @headers[header] 106 | else 107 | @headers[header] = value 108 | self 109 | end 110 | end 111 | 112 | # @param [String, nil] value HTTP body 113 | def body(value = GETTER) 114 | if value == GETTER 115 | @body 116 | else 117 | @body = value 118 | self 119 | end 120 | end 121 | 122 | # @param [String, nil] version HTTP body 123 | def http_version(value = GETTER) 124 | if value == GETTER 125 | @version 126 | else 127 | @version = value 128 | self 129 | end 130 | end 131 | 132 | # @return [String] HTTP Response 133 | def to_s 134 | response = "#{http_version} #{status} #{reason}#{CRLF}" 135 | unless headers.empty? 136 | response << headers.map { |h, v| "#{h}: #{v}" }.join(CRLF) << CRLF 137 | end 138 | response << CRLF 139 | response << body.to_s 140 | response 141 | end 142 | 143 | protected 144 | 145 | GETTER = Object.new 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/m2r/handler.rb: -------------------------------------------------------------------------------- 1 | module M2R 2 | 3 | # Basic handler, scaffold for your own Handler. 4 | # Overwrite hook methods to define behavior. 5 | # After calling #listen the Handler will block 6 | # waiting for request from connection generated 7 | # by {M2R::Handler#connection_factory}, process them and send 8 | # reponses back. 9 | # 10 | # @api public 11 | # @abstract Subclass and override method hooks to implement your own Handler 12 | class Handler 13 | # @return [Connection] used for receiving requests and sending responses 14 | attr_accessor :connection 15 | 16 | # @param [ConnectionFactory, Connection, #connection] connection_factory 17 | # Factory for generating connections 18 | # 19 | # @param [#parse] parser 20 | # Parser of M2 requests 21 | def initialize(connection_factory, parser) 22 | @connection = connection_factory.connection 23 | @parser = parser 24 | end 25 | 26 | # Start processing request 27 | def listen 28 | catch(:stop) do 29 | loop { one_loop } 30 | end 31 | @connection.close 32 | end 33 | 34 | # Schedule stop after processing request 35 | def stop 36 | @stop = true 37 | end 38 | 39 | protected 40 | 41 | # Callback executed when waiting for a request 42 | # @api public 43 | # @!visibility public 44 | def on_wait() 45 | end 46 | 47 | # Callback when a request is received 48 | # @api public 49 | # @!visibility public 50 | # @param [Request] request Request object 51 | def on_request(request) 52 | end 53 | 54 | # Override to return a response 55 | # @api public 56 | # @!visibility public 57 | # @param [Request] request Request object 58 | # @return [Response, String, #to_s] Response that should be sent to 59 | # Mongrel2 instance 60 | def process(request) 61 | raise NotImplementedError 62 | end 63 | 64 | # Callback executed when response could not be delivered by Mongrel2 65 | # because client already disconnected. 66 | # @api public 67 | # @!visibility public 68 | # @param [Request] request Request object 69 | def on_disconnect(request) 70 | end 71 | 72 | # Callback when async-upload started 73 | # @api public 74 | # @!visibility public 75 | # @param [Request] request Request object 76 | def on_upload_start(request) 77 | end 78 | 79 | # Callback when async-upload finished 80 | # @api public 81 | # @!visibility public 82 | # @param [Request] request Request object 83 | def on_upload_done(request) 84 | end 85 | 86 | # Callback after process_request is done 87 | # @api public 88 | # @!visibility public 89 | # @param [Request] request Request object 90 | # @param [Response, String, #to_s] response Response that should be sent to 91 | # Mongrel2 instance 92 | def after_process(request, response) 93 | return response 94 | end 95 | 96 | # Callback after sending the response back 97 | # @api public 98 | # @!visibility public 99 | # @param [Request] request Request object 100 | # @param [Response, String, #to_s] response Response that was sent to 101 | # Mongrel2 instance 102 | def after_reply(request, response) 103 | end 104 | 105 | # Callback after request is processed that is executed 106 | # even when execption occured. Useful for releasing 107 | # resources (closing files etc) 108 | # @api public 109 | # @!visibility public 110 | # @note `response` might be nil depending on when exception occured. 111 | # @note In case of error this callback is called before on_error 112 | # @param [Request] request Request object 113 | # @param [Response, String, #to_s, nil] response Response that was sent to 114 | # Mongrel2 instance 115 | def after_all(request, response) 116 | end 117 | 118 | # Callback when exception occured 119 | # @api public 120 | # @!visibility public 121 | # @note `request` and/or `response` might be nil depending on when error occured 122 | # @param [Request, nil] request Request object 123 | # @param [Response, String, #to_s, nil] response Response that might have been sent to 124 | # Mongrel2 instance 125 | # @param [StandardError] error 126 | def on_error(request, response, error) 127 | end 128 | 129 | # Callback when ZMQ interrupted by signal 130 | # @api public 131 | # @!visibility public 132 | def on_interrupted 133 | end 134 | 135 | private 136 | 137 | def next_request 138 | @parser.parse @connection.receive 139 | end 140 | 141 | def one_loop 142 | on_wait 143 | throw :stop if stop? 144 | response = request_lifecycle(request = next_request) 145 | rescue => error 146 | return on_interrupted if Connection::Error === error && error.signal? 147 | on_error(request, response, error) 148 | end 149 | 150 | def request_lifecycle(request) 151 | on_request(request) 152 | 153 | return on_disconnect(request) if request.disconnect? 154 | return on_upload_start(request) if request.upload_start? 155 | on_upload_done(request) if request.upload_done? 156 | 157 | response = process(request) 158 | response = after_process(request, response) 159 | 160 | @connection.reply(request, response) 161 | 162 | after_reply(request, response) 163 | return response 164 | ensure 165 | after_all(request, response) 166 | end 167 | 168 | def stop? 169 | @stop 170 | end 171 | 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /test/unit/connection_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'securerandom' 3 | 4 | module M2R 5 | class ConnectionTest < MiniTest::Unit::TestCase 6 | 7 | def setup 8 | @request_addr = "inproc://#{SecureRandom.hex}" 9 | @response_addr = "inproc://#{SecureRandom.hex}" 10 | 11 | @push = M2R.zmq_context.socket(ZMQ::PUSH) 12 | assert_equal 0, @push.bind(@request_addr), "Could not bind push socket in tests" 13 | 14 | @request_socket = M2R.zmq_context.socket(ZMQ::PULL) 15 | @request_socket.connect(@request_addr) 16 | 17 | @response_socket = M2R.zmq_context.socket(ZMQ::PUB) 18 | @response_socket.bind(@response_addr) 19 | @response_socket.setsockopt(ZMQ::IDENTITY, @sender_id = SecureRandom.uuid) 20 | 21 | @sub = M2R.zmq_context.socket(ZMQ::SUB) 22 | assert_equal 0, @sub.connect(@response_addr), "Could not connect sub socket in tests" 23 | @sub.setsockopt(ZMQ::SUBSCRIBE, "") 24 | end 25 | 26 | def teardown 27 | @request_socket.close if @request_socket 28 | @response_socket.close if @response_socket 29 | @push.close if @push 30 | @sub.close if @sub 31 | end 32 | 33 | def test_receive_message 34 | connection = Connection.new(@request_socket, @response_socket) 35 | @push.send_string(msg = "1c5fd481-1121-49d8-a706-69127975db1a ebb407b2-49aa-48a5-9f96-9db121051484 / 2:{},0:,", ZMQ::NonBlocking) 36 | assert_equal msg, connection.receive 37 | end 38 | 39 | def test_deliver_message 40 | connection = Connection.new(@request_socket, @response_socket) 41 | connection.deliver('uuid', ['conn1', 'conn2'], 'ddaattaa') 42 | assert @sub.recv_string(msg = "") > 0 43 | assert_equal "uuid 11:conn1 conn2, ddaattaa", msg 44 | end 45 | 46 | def test_string_reply_non_close 47 | connection = Connection.new(@request_socket, @response_socket) 48 | connection.reply( stub(sender: 'uuid', conn_id: 'conn1', close?: false), 'ddaattaa') 49 | assert @sub.recv_string(msg = "") > 0 50 | assert_equal "uuid 5:conn1, ddaattaa", msg 51 | assert_equal -1, @sub.recv_string(msg = "", ZMQ::NonBlocking) 52 | end 53 | 54 | def test_string_reply_close 55 | connection = Connection.new(@request_socket, @response_socket) 56 | connection.reply( stub(sender: 'uuid', conn_id: 'conn1', close?: true), 'ddaattaa') 57 | assert @sub.recv_string(msg = "") > 0 58 | assert_equal "uuid 5:conn1, ddaattaa", msg 59 | assert @sub.recv_string(msg = "") > 0 60 | assert_equal "uuid 5:conn1, ", msg 61 | end 62 | 63 | def test_response_reply_non_close 64 | connection = Connection.new(@request_socket, @response_socket) 65 | connection.reply( stub(sender: 'uuid', conn_id: 'conn1'), mock(to_s: 'ddaattaa', close?: false)) 66 | assert @sub.recv_string(msg = "") > 0 67 | assert_equal "uuid 5:conn1, ddaattaa", msg 68 | assert_equal -1, @sub.recv_string(msg = "", ZMQ::NonBlocking) 69 | end 70 | 71 | def test_response_reply_close 72 | connection = Connection.new(@request_socket, @response_socket) 73 | connection.reply( stub(sender: 'uuid', conn_id: 'conn1'), mock(to_s: 'ddaattaa', close?: true)) 74 | assert @sub.recv_string(msg = "") > 0 75 | assert_equal "uuid 5:conn1, ddaattaa", msg 76 | assert @sub.recv_string(msg = "") > 0 77 | assert_equal "uuid 5:conn1, ", msg 78 | end 79 | 80 | def test_exception_when_receiving 81 | request_socket = mock(:recv_string => -1) 82 | connection = Connection.new request_socket, nil 83 | assert_raises(Connection::Error) { connection.receive } 84 | end 85 | 86 | def test_exception_erron_when_receiving 87 | request_socket = mock(:recv_string => -1) 88 | ZMQ::Util.expects(:errno).at_least_once.returns(4) 89 | connection = Connection.new request_socket, nil 90 | begin 91 | connection.receive 92 | flunk "exception expected" 93 | rescue => er 94 | assert_equal 4, er.errno 95 | assert er.signal? 96 | end 97 | end 98 | 99 | def test_exception_when_deliverying 100 | ZMQ::Util.expects(:errno).at_least_once.returns(1) 101 | response_socket = mock(:send_string => -1) 102 | connection = Connection.new nil, response_socket 103 | assert_raises(Connection::Error) { connection.deliver('uuid', ['connection_ids'], 'data') } 104 | end 105 | 106 | def test_exception_signal_retry 107 | ZMQ::Util.expects(:errno).at_least_once.returns(4) 108 | response_socket = mock 109 | response_socket.expects(:send_string).times(3).returns(-1) 110 | connection = Connection.new nil, response_socket 111 | assert_raises(Connection::Error) { connection.deliver('uuid', ['connection_ids'], 'data') } 112 | end 113 | 114 | def test_exception_signal_retry_ok 115 | ZMQ::Util.expects(:errno).at_least_once.returns(4) 116 | response_socket = mock 117 | response_socket.expects(:send_string).twice.returns(-1).then.returns(0) 118 | connection = Connection.new nil, response_socket 119 | assert connection.deliver('uuid', ['connection_ids'], 'data').size > 0 120 | end 121 | 122 | def test_exception_when_deliverying 123 | response_socket = mock(:send_string => -1) 124 | connection = Connection.new nil, response_socket 125 | assert_raises(Connection::Error) { connection.deliver('uuid', ['connection_ids'], 'data') } 126 | end 127 | 128 | def test_exception_when_replying 129 | response_socket = mock(:send_string => -1) 130 | connection = Connection.new nil, response_socket 131 | assert_raises(Connection::Error) { connection.reply( Struct.new(:sender, :conn_id).new('sender', 'conn_id') , 'data' ) } 132 | end 133 | 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | m2r 2 | === 3 | 4 | A [Mongrel2](http://mongrel2.org/) [™](#legal) backend handler written in Ruby. Also includes Rack adpater to get you up and running quickly. 5 | 6 | [![Build Status](https://secure.travis-ci.org/perplexes/m2r.png)](http://travis-ci.org/perplexes/m2r) [![Dependency Status](https://gemnasium.com/perplexes/m2r.png)](https://gemnasium.com/perplexes/m2r) 7 | 8 | Documentation 9 | ------------- 10 | 11 | * [Tutorial](http://documentup.com/perplexes/m2r/recompile) 12 | * [API](http://www.rubydoc.info/gems/m2r/frames) 13 | 14 | Installation 15 | ------------ 16 | 17 | You'll need following prerequisites first: 18 | 19 | * [Mongrel2](http://mongrel2.org/downloads) 20 | * [ØMQ](http://www.zeromq.org/area:download) 21 | 22 | Next, in your `Gemfile` add: 23 | 24 | ```ruby 25 | gem 'm2r' 26 | ``` 27 | 28 | And finally run: 29 | 30 | ``` 31 | bundle install 32 | ``` 33 | 34 | Guides 35 | ------ 36 | 37 | ### Running Rack Application 38 | 39 | #### Gemfile 40 | 41 | Add `m2r` to `Gemfile` and run `bundle install` 42 | 43 | #### Mongrel 2 44 | 45 | [Configure `Handler`](http://mongrel2.org/static/book-finalch4.html#x6-260003.4) for your application: 46 | 47 | ``` 48 | rack_example = Handler( 49 | send_spec = "tcp://127.0.0.1:9997", 50 | send_ident = "14fff75f-3474-4089-af6d-bbd67735ab89", 51 | recv_spec = "tcp://127.0.0.1:9996", 52 | recv_ident = "" 53 | ) 54 | ``` 55 | 56 | #### Start 57 | 58 | ```bash 59 | [bundle exec] rackup -s mongrel2 application.ru 60 | ``` 61 | 62 | Add `-O option_name` to provide options for m2r handler: 63 | 64 | ```bash 65 | [bundle exec] rackup -s mongrel2 another.ru -O recv_addr=tcp://127.0.0.1:9995 -O send_addr=tcp://127.0.0.1:9994 66 | ``` 67 | 68 | #### Options 69 | 70 | * `recv_addr` - This is the `send_spec` option from `Handler` configuration in `mongrel2.conf`. Default: `tcp://127.0.0.1:9997` 71 | * `send_addr` - This is the `recv_spec` option from `Handler` configuration in your `mongrel2.conf`. Default: `tcp://127.0.0.1:9996` 72 | * `factory` - Use it to load custom `ConnectionFactory` that implements rules for ZMQ connections to Mongrel 2. 73 | 74 | #### Custom Connection Factory 75 | 76 | ZMQ allows to set multiple options and connect to large number of endpoints. Providing every ZMQ option for handler connections 77 | would be troublesome. Instead you can use your custom implementation that deals only with that fact. 78 | 79 | ##### Automatic require of custom connection factory 80 | 81 | The first way to do it is to implement custom class in a file that can be required with `m2r/connection_factory/custom_name`. 82 | The location of such file might depends on how `$LOAD_PATH` is configured but for standard Rails application or gem that 83 | would like to depend on `m2r` it would be: `lib/m2r/connection_factory/custom_name`. 84 | 85 | Implement the Factory in the file: 86 | 87 | ```ruby 88 | module M2R 89 | class ConnectionFactory 90 | # Just exemplary implementation ... 91 | class CustomName < ConnectionFactory 92 | def initialize(options) 93 | # OpenStruct with rackup options for the handler (added with -O) 94 | @options = options 95 | end 96 | 97 | def connection 98 | request_socket = @context.socket(ZMQ::PULL) 99 | request_socket.connect("tcp://127.0.0.1:2222") 100 | request_socket.setsockopt(ZMQ::RECONNECT_IVL, 5) 101 | 102 | response_socket = @context.socket(ZMQ::PUB) 103 | response_socket.connect("tcp://127.0.0.1:3333") 104 | response_socket.setsockopt(ZMQ::HWM, 100) 105 | response_socket.setsockopt(ZMQ::RECONNECT_IVL, 5) 106 | 107 | Connection.new(request_socket, response_socket) 108 | end 109 | end 110 | end 111 | end 112 | ``` 113 | 114 | Use `connection_factory` option to select it. 115 | 116 | ```bash 117 | [bundle exec] rackup -s mongrel2 another.ru -O connection_factory=custom_name 118 | ``` 119 | 120 | ##### Manual require of factory 121 | 122 | Implement custom factory in a file like in a previous paragraph. 123 | 124 | Load the file using `-r` option for `rackup` and use `connection_factory` option. 125 | 126 | ```bash 127 | [bundle exec] rackup -r custom_name.rb -s mongrel2 another.ru -O connection_factory=custom_name 128 | ``` 129 | 130 | #### Processing HTTPS requests from Mongrel2 1.7 131 | 132 | Set `HTTPS` env to `true`. 133 | 134 | ```bash 135 | HTTPS=true [bundle exec] rackup -s mongrel2 application.ru 136 | ``` 137 | 138 | For Mongrel2 1.8 and newer this is not necessary. 139 | 140 | ### Developing custom bare Handler 141 | 142 | TBD 143 | 144 | Versioning 145 | ---------- 146 | 147 | Starting from version `0.1.0` this gem follows [semantic versioning](http://semver.org) policy. 148 | 149 | Usage/Examples 150 | ----- 151 | 152 | * [examples/http\_0mq.rb](https://github.com/perplexes/m2r/blob/master/example/http_0mq.rb) is a test little servlet thing (based on what comes with mongrel2) 153 | * [examples/lobster.ru](https://github.com/perplexes/m2r/blob/master/example/lobster.ru) is a rackup file using the Rack handler that'll serve Rack's funny little lobster app 154 | 155 | 156 | Contributing 157 | ------------ 158 | 159 | In the spirit of [free software][free-sw], **everyone** is encouraged to help 160 | improve this project. 161 | 162 | [free-sw]: http://www.fsf.org/licensing/essays/free-sw.html 163 | 164 | Here are some ways *you* can contribute: 165 | 166 | * by using alpha, beta, and prerelease versions 167 | * by reporting bugs 168 | * by suggesting new features 169 | * by writing or editing documentation 170 | * by writing tests 171 | * by writing code (**no patch is too small**: fix typos, add comments, clean up 172 | inconsistent whitespace) 173 | * by refactoring code 174 | * by closing [issues][] 175 | * by reviewing patches 176 | 177 | [issues]: https://github.com/perplexes/m2r/issues 178 | 179 | [Read Contributing page](https://github.com/perplexes/m2r/wiki/Contributing) before sending Pull Request :) 180 | 181 | Submitting an Issue 182 | ------------------- 183 | 184 | We use the [GitHub issue tracker][issues] to track bugs and features. Before 185 | submitting a bug report or feature request, check to make sure it hasn't 186 | already been submitted. When submitting a bug report, please include a [Gist][] 187 | that includes a stack trace and any details that may be necessary to reproduce 188 | the bug, including your gem version, Ruby version, and operating system. 189 | Ideally, a bug report should include a pull request with failing tests. 190 | 191 | [gist]: https://gist.github.com/ 192 | 193 | Submitting a Pull Request 194 | ------------------------- 195 | 0. [Read Contributing page](https://github.com/perplexes/m2r/wiki/Contributing) 196 | 1. [Fork the repository.][fork] 197 | 2. [Create a topic branch.][branch] 198 | 3. Add tests for your unimplemented feature or bug fix. 199 | 4. Run `bundle exec rake`. If your test pass, return to step 3. 200 | 5. Implement your feature or bug fix. 201 | 6. Run `bundle exec rake`. If your tests fail, return to step 5. 202 | 7. Add, commit, and push your changes. 203 | 8. [Submit a pull request.][pr] 204 | 205 | [fork]: http://help.github.com/fork-a-repo/ 206 | [branch]: http://learn.github.com/p/branching.html 207 | [pr]: http://help.github.com/send-pull-requests/ 208 | 209 | 210 | Supported Ruby Versions 211 | ----------------------- 212 | 213 | This library aims to support and is [tested against](http://travis-ci.org/perplexes/m2r) the following Ruby implementations: 214 | 215 | - Ruby 1.9.2 216 | - Ruby 1.9.3 217 | - JRuby 218 | - Rubinius 219 | 220 | 221 | legal 222 | ----------------------- 223 | 224 | Mongrel2 is a registered trademark of [Zed A. Shaw](http://zedshaw.com/) who wrote it. And it is awesome. 225 | --------------------------------------------------------------------------------