m2r 0.0.3 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/Gemfile +6 -0
  2. data/README.md +141 -35
  3. data/Rakefile +13 -45
  4. data/example/Procfile +4 -0
  5. data/example/config.sqlite +0 -0
  6. data/example/http_0mq.rb +37 -19
  7. data/example/lobster.ru +14 -6
  8. data/example/mongrel2.conf +47 -0
  9. data/example/tmp/access.log +505 -0
  10. data/example/uploading.ru +37 -0
  11. data/lib/m2r.rb +49 -3
  12. data/lib/m2r/connection.rb +66 -0
  13. data/lib/m2r/connection_factory.rb +41 -0
  14. data/lib/m2r/handler.rb +130 -0
  15. data/lib/m2r/headers.rb +72 -0
  16. data/lib/m2r/rack_handler.rb +47 -0
  17. data/lib/m2r/request.rb +129 -0
  18. data/lib/m2r/request/base.rb +33 -0
  19. data/lib/m2r/request/upload.rb +60 -0
  20. data/lib/m2r/response.rb +102 -0
  21. data/lib/m2r/response/content_length.rb +18 -0
  22. data/lib/m2r/version.rb +5 -0
  23. data/lib/rack/handler/mongrel2.rb +33 -0
  24. data/m2r.gemspec +30 -63
  25. data/test/acceptance/examples_test.rb +32 -0
  26. data/test/support/capybara.rb +4 -0
  27. data/test/support/mongrel_helper.rb +40 -0
  28. data/test/support/test_handler.rb +51 -0
  29. data/test/support/test_user.rb +37 -0
  30. data/test/test_helper.rb +5 -0
  31. data/test/unit/connection_factory_test.rb +29 -0
  32. data/test/unit/connection_test.rb +49 -0
  33. data/test/unit/handler_test.rb +41 -0
  34. data/test/unit/headers_test.rb +50 -0
  35. data/test/unit/m2r_test.rb +40 -0
  36. data/test/unit/rack_handler_test.rb +52 -0
  37. data/test/unit/request_test.rb +38 -0
  38. data/test/unit/response_test.rb +30 -0
  39. metadata +310 -105
  40. data/.document +0 -5
  41. data/.gitignore +0 -21
  42. data/ISSUES +0 -62
  43. data/VERSION +0 -1
  44. data/benchmarks/jruby +0 -60
  45. data/example/rack_handler.rb +0 -69
  46. data/lib/connection.rb +0 -158
  47. data/lib/fiber_handler.rb +0 -43
  48. data/lib/handler.rb +0 -66
  49. data/lib/request.rb +0 -44
  50. data/test/helper.rb +0 -10
  51. data/test/test_m2r.rb +0 -7
@@ -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
+
@@ -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
@@ -0,0 +1,5 @@
1
+ module M2R
2
+ # m2r gem version
3
+ # @api public
4
+ VERSION = '1.0.0'
5
+ end
@@ -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
@@ -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 |s|
7
- s.name = %q{m2r}
8
- s.version = "0.0.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 = "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
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Colin Curtin", "Pradeep Elankumaran"]
12
- s.date = %q{2010-10-23}
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
- if s.respond_to? :specification_version then
51
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
52
- s.specification_version = 3
16
+ gem.name = "m2r"
17
+ gem.date = "2010-10-23"
18
+ gem.require_path = "lib"
19
+ gem.version = M2R::VERSION
53
20
 
54
- if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
55
- s.add_runtime_dependency(%q<ffi>, [">= 0"])
56
- s.add_runtime_dependency(%q<ffi-rzmq>, [">= 0"])
57
- s.add_runtime_dependency(%q<json>, [">= 0"])
58
- else
59
- s.add_dependency(%q<ffi>, [">= 0"])
60
- s.add_dependency(%q<ffi-rzmq>, [">= 0"])
61
- s.add_dependency(%q<json>, [">= 0"])
62
- end
63
- else
64
- s.add_dependency(%q<ffi>, [">= 0"])
65
- s.add_dependency(%q<ffi-rzmq>, [">= 0"])
66
- s.add_dependency(%q<json>, [">= 0"])
67
- end
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