m2r 0.0.3 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +6 -0
- data/README.md +141 -35
- data/Rakefile +13 -45
- data/example/Procfile +4 -0
- data/example/config.sqlite +0 -0
- data/example/http_0mq.rb +37 -19
- data/example/lobster.ru +14 -6
- data/example/mongrel2.conf +47 -0
- data/example/tmp/access.log +505 -0
- data/example/uploading.ru +37 -0
- data/lib/m2r.rb +49 -3
- data/lib/m2r/connection.rb +66 -0
- data/lib/m2r/connection_factory.rb +41 -0
- data/lib/m2r/handler.rb +130 -0
- data/lib/m2r/headers.rb +72 -0
- data/lib/m2r/rack_handler.rb +47 -0
- data/lib/m2r/request.rb +129 -0
- data/lib/m2r/request/base.rb +33 -0
- data/lib/m2r/request/upload.rb +60 -0
- data/lib/m2r/response.rb +102 -0
- data/lib/m2r/response/content_length.rb +18 -0
- data/lib/m2r/version.rb +5 -0
- data/lib/rack/handler/mongrel2.rb +33 -0
- data/m2r.gemspec +30 -63
- data/test/acceptance/examples_test.rb +32 -0
- data/test/support/capybara.rb +4 -0
- data/test/support/mongrel_helper.rb +40 -0
- data/test/support/test_handler.rb +51 -0
- data/test/support/test_user.rb +37 -0
- data/test/test_helper.rb +5 -0
- data/test/unit/connection_factory_test.rb +29 -0
- data/test/unit/connection_test.rb +49 -0
- data/test/unit/handler_test.rb +41 -0
- data/test/unit/headers_test.rb +50 -0
- data/test/unit/m2r_test.rb +40 -0
- data/test/unit/rack_handler_test.rb +52 -0
- data/test/unit/request_test.rb +38 -0
- data/test/unit/response_test.rb +30 -0
- metadata +310 -105
- data/.document +0 -5
- data/.gitignore +0 -21
- data/ISSUES +0 -62
- data/VERSION +0 -1
- data/benchmarks/jruby +0 -60
- data/example/rack_handler.rb +0 -69
- data/lib/connection.rb +0 -158
- data/lib/fiber_handler.rb +0 -43
- data/lib/handler.rb +0 -66
- data/lib/request.rb +0 -44
- data/test/helper.rb +0 -10
- data/test/test_m2r.rb +0 -7
data/lib/m2r/request.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'm2r'
|
2
|
+
require 'm2r/request/base'
|
3
|
+
require 'm2r/request/upload'
|
4
|
+
require 'm2r/headers'
|
5
|
+
|
6
|
+
module M2R
|
7
|
+
# Abstraction over Mongrel 2 request
|
8
|
+
# @api public
|
9
|
+
class Request
|
10
|
+
# @api private
|
11
|
+
TRUE_STRINGS = %w(true yes on 1).map(&:freeze).freeze
|
12
|
+
|
13
|
+
include Base
|
14
|
+
include Upload
|
15
|
+
|
16
|
+
# @return [String] UUID of mongrel2 origin instance
|
17
|
+
attr_reader :sender
|
18
|
+
|
19
|
+
# @return [String] Mongrel2 connection id sending this request
|
20
|
+
attr_reader :conn_id
|
21
|
+
|
22
|
+
# @return [String] HTTP Path of request
|
23
|
+
attr_reader :path
|
24
|
+
|
25
|
+
# @return [String] HTTP Body of request
|
26
|
+
attr_reader :body
|
27
|
+
|
28
|
+
# @param [String] sender UUID of mongrel2 origin instance
|
29
|
+
# @param [String] conn_id Mongrel2 connection id sending this request
|
30
|
+
# @param [String] path HTTP Path of request
|
31
|
+
# @param [M2R::Headers] headers HTTP headers of request
|
32
|
+
# @param [String] body HTTP Body of request
|
33
|
+
def initialize(sender, conn_id, path, headers, body)
|
34
|
+
@http_headers, @mongrel_headers = split_headers(headers)
|
35
|
+
@sender = sender
|
36
|
+
@conn_id = conn_id
|
37
|
+
@path = path
|
38
|
+
@body = body
|
39
|
+
@data = MultiJson.load(@body) if json?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Parse Mongrel2 request received via ZMQ message
|
43
|
+
#
|
44
|
+
# @param [String] msg Monrel2 Request message formatted according to rules
|
45
|
+
# of creating it described it m2 manual.
|
46
|
+
# @return [Request]
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def self.parse(msg)
|
50
|
+
sender, conn_id, path, rest = msg.split(' ', 4)
|
51
|
+
|
52
|
+
headers, rest = TNetstring.parse(rest)
|
53
|
+
body, _ = TNetstring.parse(rest)
|
54
|
+
headers = Headers.new MultiJson.load(headers)
|
55
|
+
self.new(sender, conn_id, path, headers, body)
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return [M2R::Headers] HTTP headers
|
59
|
+
def headers
|
60
|
+
@http_headers
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [String] Mongrel2 pattern used to match this request
|
64
|
+
def pattern
|
65
|
+
@mongrel_headers['pattern']
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [String] HTTP method
|
69
|
+
def method
|
70
|
+
@mongrel_headers['method']
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [String] Request query string
|
74
|
+
def query
|
75
|
+
@mongrel_headers['query']
|
76
|
+
end
|
77
|
+
|
78
|
+
# return [String] URL scheme
|
79
|
+
def scheme
|
80
|
+
@mongrel_headers['url_scheme'] || mongrel17_scheme
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [true, false] Internal mongrel2 message to handler issued when
|
84
|
+
# message delivery is not possible because the client already
|
85
|
+
# disconnected and there is no connection with such {#conn_id}
|
86
|
+
def disconnect?
|
87
|
+
json? and @data['type'] == 'disconnect'
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [true, false] Information whether HTTP Connection should
|
91
|
+
# be closed after processing the request. Happens when HTTP/1.0
|
92
|
+
# or request has Connection=close header.
|
93
|
+
def close?
|
94
|
+
unsupported_version? or connection_close?
|
95
|
+
end
|
96
|
+
|
97
|
+
protected
|
98
|
+
|
99
|
+
def mongrel17_scheme
|
100
|
+
return 'https' if TRUE_STRINGS.include? env_https
|
101
|
+
return 'http'
|
102
|
+
end
|
103
|
+
|
104
|
+
def env_https
|
105
|
+
(ENV['HTTPS'] || "").downcase
|
106
|
+
end
|
107
|
+
|
108
|
+
def unsupported_version?
|
109
|
+
@http_headers['version'] != 'HTTP/1.1'
|
110
|
+
end
|
111
|
+
|
112
|
+
def connection_close?
|
113
|
+
@http_headers['connection'] == 'close'
|
114
|
+
end
|
115
|
+
|
116
|
+
def json?
|
117
|
+
method == 'JSON'
|
118
|
+
end
|
119
|
+
|
120
|
+
def split_headers(headers)
|
121
|
+
mongrel = Headers.new
|
122
|
+
mongrel_headers.each do |header|
|
123
|
+
next unless headers[header]
|
124
|
+
mongrel[header] = headers.delete(header)
|
125
|
+
end
|
126
|
+
return headers, mongrel
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,33 @@
|
|
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
|
+
MONGREL2_BASE_HEADERS = %w(pattern method path query url_scheme).map(&:freeze).freeze
|
8
|
+
|
9
|
+
# @return [StringIO] Request body encapsulated in IO compatible object
|
10
|
+
# @api public
|
11
|
+
def body_io
|
12
|
+
@body_io ||= begin
|
13
|
+
b = StringIO.new(body)
|
14
|
+
b.set_encoding(Encoding::BINARY) if b.respond_to?(:set_encoding)
|
15
|
+
b
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [nil] Free external resources such as files or sockets
|
20
|
+
# @api public
|
21
|
+
def free!
|
22
|
+
body_io.close
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def mongrel_headers
|
28
|
+
MONGREL2_BASE_HEADERS
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,60 @@
|
|
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
|
+
# Headers related to async-upload feature
|
7
|
+
# @private
|
8
|
+
MONGREL2_UPLOAD_HEADERS = %w(x-mongrel2-upload-start x-mongrel2-upload-done).map(&:freeze).freeze
|
9
|
+
|
10
|
+
# @return [true,false] True if this is async-upload related request
|
11
|
+
# @api public
|
12
|
+
def upload?
|
13
|
+
!!@mongrel_headers['x-mongrel2-upload-start']
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [true,false] True if this is async-upload start notification
|
17
|
+
# @api public
|
18
|
+
def upload_start?
|
19
|
+
upload? and not upload_path
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [true,false] True if this is final async-upload request
|
23
|
+
# @api public
|
24
|
+
def upload_done?
|
25
|
+
upload? and upload_path
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [String] Relative path to file containing body of HTTP
|
29
|
+
# request.
|
30
|
+
# @api public
|
31
|
+
def upload_path
|
32
|
+
@mongrel_headers['x-mongrel2-upload-done']
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [File] Request body encapsulated in IO compatible object
|
36
|
+
# @api public
|
37
|
+
def body_io
|
38
|
+
return super unless upload_done?
|
39
|
+
@body_io ||= begin
|
40
|
+
f = File.open(upload_path, "r+b")
|
41
|
+
f.set_encoding(Encoding::BINARY) if f.respond_to?(:set_encoding)
|
42
|
+
f
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [nil] Free external resources such as files or sockets
|
47
|
+
# @api public
|
48
|
+
def free!
|
49
|
+
super
|
50
|
+
File.delete(body_io.path) if upload_done?
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
|
55
|
+
def mongrel_headers
|
56
|
+
super + MONGREL2_UPLOAD_HEADERS
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
data/lib/m2r/response.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'm2r'
|
2
|
+
require 'm2r/response/content_length'
|
3
|
+
|
4
|
+
module M2R
|
5
|
+
# Simplest possible abstraction layer over HTTP request
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
class Response
|
9
|
+
|
10
|
+
# @private
|
11
|
+
VERSION = "HTTP/1.1".freeze
|
12
|
+
|
13
|
+
# @private
|
14
|
+
CRLF = "\r\n".freeze
|
15
|
+
|
16
|
+
# @private
|
17
|
+
STATUS_CODES = {
|
18
|
+
100 => 'Continue',
|
19
|
+
101 => 'Switching Protocols',
|
20
|
+
102 => 'Processing',
|
21
|
+
200 => 'OK',
|
22
|
+
201 => 'Created',
|
23
|
+
202 => 'Accepted',
|
24
|
+
203 => 'Non-Authoritative Information',
|
25
|
+
204 => 'No Content',
|
26
|
+
205 => 'Reset Content',
|
27
|
+
206 => 'Partial Content',
|
28
|
+
207 => 'Multi-Status',
|
29
|
+
226 => 'IM Used',
|
30
|
+
300 => 'Multiple Choices',
|
31
|
+
301 => 'Moved Permanently',
|
32
|
+
302 => 'Found',
|
33
|
+
303 => 'See Other',
|
34
|
+
304 => 'Not Modified',
|
35
|
+
305 => 'Use Proxy',
|
36
|
+
306 => 'Reserved',
|
37
|
+
307 => 'Temporary Redirect',
|
38
|
+
400 => 'Bad Request',
|
39
|
+
401 => 'Unauthorized',
|
40
|
+
402 => 'Payment Required',
|
41
|
+
403 => 'Forbidden',
|
42
|
+
404 => 'Not Found',
|
43
|
+
405 => 'Method Not Allowed',
|
44
|
+
406 => 'Not Acceptable',
|
45
|
+
407 => 'Proxy Authentication Required',
|
46
|
+
408 => 'Request Timeout',
|
47
|
+
409 => 'Conflict',
|
48
|
+
410 => 'Gone',
|
49
|
+
411 => 'Length Required',
|
50
|
+
412 => 'Precondition Failed',
|
51
|
+
413 => 'Request Entity Too Large',
|
52
|
+
414 => 'Request-URI Too Long',
|
53
|
+
415 => 'Unsupported Media Type',
|
54
|
+
416 => 'Requested Range Not Satisfiable',
|
55
|
+
417 => 'Expectation Failed',
|
56
|
+
418 => "I'm a Teapot",
|
57
|
+
422 => 'Unprocessable Entity',
|
58
|
+
423 => 'Locked',
|
59
|
+
424 => 'Failed Dependency',
|
60
|
+
426 => 'Upgrade Required',
|
61
|
+
500 => 'Internal Server Error',
|
62
|
+
501 => 'Not Implemented',
|
63
|
+
502 => 'Bad Gateway',
|
64
|
+
503 => 'Service Unavailable',
|
65
|
+
504 => 'Gateway Timeout',
|
66
|
+
}
|
67
|
+
STATUS_CODES.freeze
|
68
|
+
|
69
|
+
# @return [Fixnum] HTTP Status code
|
70
|
+
attr_reader :status
|
71
|
+
|
72
|
+
# @return [Hash, Headers] Collection of response HTTP Headers
|
73
|
+
attr_reader :headers
|
74
|
+
|
75
|
+
# @return [String] HTTP Body
|
76
|
+
attr_reader :body
|
77
|
+
|
78
|
+
# @return [String] HTTP Status code description
|
79
|
+
attr_reader :reason
|
80
|
+
|
81
|
+
# @param [Fixnum, #to_i] status HTTP status code
|
82
|
+
# @param [Hash] headers HTTP headers
|
83
|
+
# @param [String, nil] body HTTP body
|
84
|
+
def initialize(status, headers, body = nil)
|
85
|
+
@status = status.to_i
|
86
|
+
@headers = headers
|
87
|
+
@body = body || ""
|
88
|
+
@reason = STATUS_CODES[status.to_i]
|
89
|
+
end
|
90
|
+
|
91
|
+
# @return [String] HTTP Response
|
92
|
+
def to_s
|
93
|
+
response = "#{VERSION} #{status} #{reason}#{CRLF}"
|
94
|
+
unless headers.empty?
|
95
|
+
response << headers.map { |h, v| "#{h}: #{v}" }.join(CRLF) << CRLF
|
96
|
+
end
|
97
|
+
response << CRLF
|
98
|
+
response << body
|
99
|
+
response
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'm2r/response'
|
2
|
+
|
3
|
+
module M2R
|
4
|
+
class Response
|
5
|
+
# Adds Content-Length header based on body size
|
6
|
+
# This is mostly required when you use bare
|
7
|
+
# Response class without any framework on top of it.
|
8
|
+
# HTTP clients require such header when there is
|
9
|
+
# body in response. Otherwise they hang out.
|
10
|
+
#
|
11
|
+
# @api public
|
12
|
+
module ContentLength
|
13
|
+
def headers
|
14
|
+
super.merge('Content-Length' => body.bytesize)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/m2r/version.rb
ADDED
@@ -0,0 +1,33 @@
|
|
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
|
+
factory = M2R::ConnectionFactory.new(options.sender_id, options.recv_addr, options.send_addr)
|
18
|
+
adapter = M2R::RackHandler.new(app, factory)
|
19
|
+
adapter.listen
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.valid_options
|
23
|
+
{
|
24
|
+
'recv_addr=RECV_ADDR' => 'Receive address',
|
25
|
+
'send_addr=SEND_ADDR' => 'Send address',
|
26
|
+
'sender_id=UUID' => 'Sender UUID'
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
register :mongrel2, ::Rack::Handler::Mongrel2
|
32
|
+
end
|
33
|
+
end
|
data/m2r.gemspec
CHANGED
@@ -1,69 +1,36 @@
|
|
1
|
-
# Generated by jeweler
|
2
|
-
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
-
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
1
|
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/m2r/version', __FILE__)
|
5
3
|
|
6
|
-
Gem::Specification.new do |
|
7
|
-
|
8
|
-
|
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 = "A Mongrel2 interface and handler library for JRuby, and hopefully other Ruby implementations in the future. Works with Rack, so it works with Rails! (Rails installation guide forthcoming)."
|
8
|
+
gem.homepage = "http://github.com/perplexes/m2r"
|
9
|
+
gem.summary = "Mongrel2 interface and handler library for JRuby."
|
10
|
+
gem.license = "MIT"
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
s.description = %q{A Mongrel2 interface and handler library for JRuby, and hopefully other Ruby implementations in the future. Works with Rack, so it works with Rails! (Rails installation guide forthcoming.)}
|
14
|
-
s.email = %q{colin.t.curtin+m2r@gmail.com}
|
15
|
-
s.extra_rdoc_files = [
|
16
|
-
"LICENSE",
|
17
|
-
"README.md"
|
18
|
-
]
|
19
|
-
s.files = [
|
20
|
-
".document",
|
21
|
-
".gitignore",
|
22
|
-
"ISSUES",
|
23
|
-
"LICENSE",
|
24
|
-
"README.md",
|
25
|
-
"Rakefile",
|
26
|
-
"VERSION",
|
27
|
-
"benchmarks/jruby",
|
28
|
-
"example/http_0mq.rb",
|
29
|
-
"example/lobster.ru",
|
30
|
-
"example/rack_handler.rb",
|
31
|
-
"lib/connection.rb",
|
32
|
-
"lib/fiber_handler.rb",
|
33
|
-
"lib/handler.rb",
|
34
|
-
"lib/m2r.rb",
|
35
|
-
"lib/request.rb",
|
36
|
-
"m2r.gemspec",
|
37
|
-
"test/helper.rb",
|
38
|
-
"test/test_m2r.rb"
|
39
|
-
]
|
40
|
-
s.homepage = %q{http://github.com/perplexes/m2r}
|
41
|
-
s.rdoc_options = ["--charset=UTF-8"]
|
42
|
-
s.require_paths = ["lib"]
|
43
|
-
s.rubygems_version = %q{1.3.6}
|
44
|
-
s.summary = %q{Mongrel2 interface and handler library for JRuby}
|
45
|
-
s.test_files = [
|
46
|
-
"test/helper.rb",
|
47
|
-
"test/test_m2r.rb"
|
48
|
-
]
|
12
|
+
gem.files = Dir.glob("{lib,example,test}/**/*") + %w(LICENSE README.md Rakefile Gemfile m2r.gemspec)
|
13
|
+
gem.test_files = Dir.glob("test/**/*")
|
14
|
+
gem.extra_rdoc_files = ["LICENSE", "README.md" ]
|
49
15
|
|
50
|
-
|
51
|
-
|
52
|
-
|
16
|
+
gem.name = "m2r"
|
17
|
+
gem.date = "2010-10-23"
|
18
|
+
gem.require_path = "lib"
|
19
|
+
gem.version = M2R::VERSION
|
53
20
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
end
|
21
|
+
gem.add_dependency "ffi-rzmq", "~> 0.9.3"
|
22
|
+
gem.add_dependency "ffi", ">= 1.0.0"
|
23
|
+
gem.add_dependency "multi_json"
|
24
|
+
gem.add_dependency "tnetstring"
|
25
|
+
|
26
|
+
gem.add_development_dependency "rack"
|
27
|
+
gem.add_development_dependency "rake"
|
28
|
+
gem.add_development_dependency "minitest", "= 3.2.0"
|
29
|
+
gem.add_development_dependency "mocha", "~> 0.12.1"
|
30
|
+
gem.add_development_dependency "bbq", "= 0.0.4"
|
31
|
+
gem.add_development_dependency "capybara-mechanize", "= 0.3.0"
|
32
|
+
gem.add_development_dependency "activesupport", "~> 3.2.7"
|
33
|
+
gem.add_development_dependency "yard", "~> 0.8.2"
|
34
|
+
gem.add_development_dependency "kramdown", "~> 0.13.7"
|
69
35
|
|
36
|
+
end
|