m2r 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -17,8 +17,12 @@ module M2R
17
17
 
18
18
  # @param [ConnectionFactory, Connection, #connection] connection_factory
19
19
  # Factory for generating connections
20
- def initialize(connection_factory)
20
+ #
21
+ # @param [#parse] parser
22
+ # Parser of M2 requests
23
+ def initialize(connection_factory, parser)
21
24
  @connection = connection_factory.connection
25
+ @parser = parser
22
26
  end
23
27
 
24
28
  # Start processing request
@@ -44,12 +48,14 @@ module M2R
44
48
  # Callback when a request is received
45
49
  # @api public
46
50
  # @!visibility public
51
+ # @param [Request] request Request object
47
52
  def on_request(request)
48
53
  end
49
54
 
50
55
  # Override to return a response
51
56
  # @api public
52
57
  # @!visibility public
58
+ # @param [Request] request Request object
53
59
  # @return [Response, String, #to_s] Response that should be sent to
54
60
  # Mongrel2 instance
55
61
  def process(request)
@@ -60,24 +66,30 @@ module M2R
60
66
  # because client already disconnected.
61
67
  # @api public
62
68
  # @!visibility public
69
+ # @param [Request] request Request object
63
70
  def on_disconnect(request)
64
71
  end
65
72
 
66
73
  # Callback when async-upload started
67
74
  # @api public
68
75
  # @!visibility public
76
+ # @param [Request] request Request object
69
77
  def on_upload_start(request)
70
78
  end
71
79
 
72
80
  # Callback when async-upload finished
73
81
  # @api public
74
82
  # @!visibility public
83
+ # @param [Request] request Request object
75
84
  def on_upload_done(request)
76
85
  end
77
86
 
78
87
  # Callback after process_request is done
79
88
  # @api public
80
89
  # @!visibility public
90
+ # @param [Request] request Request object
91
+ # @param [Response, String, #to_s] response Response that should be sent to
92
+ # Mongrel2 instance
81
93
  def after_process(request, response)
82
94
  return response
83
95
  end
@@ -85,6 +97,9 @@ module M2R
85
97
  # Callback after sending the response back
86
98
  # @api public
87
99
  # @!visibility public
100
+ # @param [Request] request Request object
101
+ # @param [Response, String, #to_s] response Response that was sent to
102
+ # Mongrel2 instance
88
103
  def after_reply(request, response)
89
104
  end
90
105
 
@@ -93,18 +108,39 @@ module M2R
93
108
  # resources (closing files etc)
94
109
  # @api public
95
110
  # @!visibility public
111
+ # @note `response` might be nil depending on when exception occured.
112
+ # @note In case of error this callback is called before on_error
113
+ # @param [Request] request Request object
114
+ # @param [Response, String, #to_s, nil] response Response that was sent to
115
+ # Mongrel2 instance
96
116
  def after_all(request, response)
97
117
  end
98
118
 
119
+ # Callback when exception occured
120
+ # @api public
121
+ # @!visibility public
122
+ # @note `request` and/or `response` might be nil depending on when error occured
123
+ # @param [Request, nil] request Request object
124
+ # @param [Response, String, #to_s, nil] response Response that might have been sent to
125
+ # Mongrel2 instance
126
+ # @param [StandardError] error
127
+ def on_error(request, response, error)
128
+ end
129
+
99
130
  private
100
131
 
132
+ def next_request
133
+ @parser.parse @connection.receive
134
+ end
135
+
101
136
  def one_loop
102
137
  on_wait
103
138
  throw :stop if stop?
104
- request_lifecycle(@connection.receive)
139
+ response = request_lifecycle(request = next_request)
140
+ rescue => error
141
+ on_error(request, response, error)
105
142
  end
106
143
 
107
-
108
144
  def request_lifecycle(request)
109
145
  on_request(request)
110
146
 
@@ -118,6 +154,7 @@ module M2R
118
154
  @connection.reply(request, response)
119
155
 
120
156
  after_reply(request, response)
157
+ return response
121
158
  ensure
122
159
  after_all(request, response)
123
160
  end
@@ -7,7 +7,11 @@ module M2R
7
7
  include Enumerable
8
8
 
9
9
  # @param [Hash, #inject] hash Collection of headers
10
- def initialize(hash = {})
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
11
15
  @headers = hash.inject({}) do |headers,(header,value)|
12
16
  headers[transform_key(header)] = value
13
17
  headers
@@ -63,6 +67,10 @@ module M2R
63
67
  env
64
68
  end
65
69
 
70
+ def empty?
71
+ @headers.empty?
72
+ end
73
+
66
74
  protected
67
75
 
68
76
  def transform_key(key)
@@ -0,0 +1,30 @@
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
@@ -7,9 +7,9 @@ module M2R
7
7
  class RackHandler < Handler
8
8
  attr_accessor :app
9
9
 
10
- def initialize(app, connection_factory)
10
+ def initialize(app, connection_factory, parser)
11
11
  @app = app
12
- super(connection_factory)
12
+ super(connection_factory, parser)
13
13
 
14
14
  trap('INT') { stop }
15
15
  end
@@ -36,7 +36,7 @@ module M2R
36
36
  status, headers, body = @app.call(env)
37
37
  buffer = ""
38
38
  body.each { |part| buffer << part }
39
- return Response.new(status, headers, buffer)
39
+ return Response.new.status(status).headers(headers).body(buffer)
40
40
  end
41
41
 
42
42
  def after_all(request, response)
@@ -0,0 +1,15 @@
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
@@ -1,4 +1,6 @@
1
+ require 'set'
1
2
  require 'm2r'
3
+ require 'm2r/http/close'
2
4
  require 'm2r/request/base'
3
5
  require 'm2r/request/upload'
4
6
  require 'm2r/headers'
@@ -8,10 +10,17 @@ module M2R
8
10
  # @api public
9
11
  class Request
10
12
  # @api private
11
- TRUE_STRINGS = %w(true yes on 1).map(&:freeze).freeze
13
+ TRUE_STRINGS = Set.new(%w(true yes on 1).map(&:freeze)).freeze
14
+ # @api private
15
+ MONGREL2_BASE_HEADERS = Set.new(%w(pattern method path query url_scheme version).map(&:upcase).map(&:freeze)).freeze
16
+ # @api private
17
+ MONGREL2_UPLOAD_HEADERS = Set.new(%w(x-mongrel2-upload-start x-mongrel2-upload-done).map(&:downcase).map(&:freeze)).freeze
18
+ # @api private
19
+ MONGREL2_HEADERS = (MONGREL2_BASE_HEADERS + MONGREL2_UPLOAD_HEADERS).freeze
12
20
 
13
21
  include Base
14
22
  include Upload
23
+ include HTTP::Close
15
24
 
16
25
  # @return [String] UUID of mongrel2 origin instance
17
26
  attr_reader :sender
@@ -29,14 +38,16 @@ module M2R
29
38
  # @param [String] conn_id Mongrel2 connection id sending this request
30
39
  # @param [String] path HTTP Path of request
31
40
  # @param [M2R::Headers] headers HTTP headers of request
41
+ # @param [M2R::Headers] headers Additional mongrel2 headers
32
42
  # @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?
43
+ def initialize(sender, conn_id, path, http_headers, mongrel_headers, body)
44
+ @sender = sender
45
+ @conn_id = conn_id
46
+ @path = path
47
+ @http_headers = http_headers
48
+ @mongrel_headers = mongrel_headers
49
+ @body = body
50
+ @data = MultiJson.load(@body) if json?
40
51
  end
41
52
 
42
53
  # Parse Mongrel2 request received via ZMQ message
@@ -51,8 +62,11 @@ module M2R
51
62
 
52
63
  headers, rest = TNetstring.parse(rest)
53
64
  body, _ = TNetstring.parse(rest)
54
- headers = Headers.new MultiJson.load(headers)
55
- self.new(sender, conn_id, path, headers, body)
65
+ headers = MultiJson.load(headers)
66
+ headers, mong = split_headers(headers)
67
+ headers = Headers.new headers, true
68
+ mong = Headers.new mong, true
69
+ self.new(sender, conn_id, path, headers, mong, body)
56
70
  end
57
71
 
58
72
  # @return [M2R::Headers] HTTP headers
@@ -80,6 +94,10 @@ module M2R
80
94
  @mongrel_headers['url_scheme'] || mongrel17_scheme
81
95
  end
82
96
 
97
+ def http_version
98
+ @mongrel_headers['version']
99
+ end
100
+
83
101
  # @return [true, false] Internal mongrel2 message to handler issued when
84
102
  # message delivery is not possible because the client already
85
103
  # disconnected and there is no connection with such {#conn_id}
@@ -87,13 +105,6 @@ module M2R
87
105
  json? and @data['type'] == 'disconnect'
88
106
  end
89
107
 
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
108
  protected
98
109
 
99
110
  def mongrel17_scheme
@@ -105,25 +116,21 @@ module M2R
105
116
  (ENV['HTTPS'] || "").downcase
106
117
  end
107
118
 
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
119
  def json?
117
120
  method == 'JSON'
118
121
  end
119
122
 
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)
123
+ def self.split_headers(headers)
124
+ http = {}
125
+ mongrel = {}
126
+ headers.each do |header, value|
127
+ if MONGREL2_HEADERS.include?(header)
128
+ mongrel[header.downcase] = value
129
+ else
130
+ http[header] = value
131
+ end
125
132
  end
126
- return headers, mongrel
133
+ return http, mongrel
127
134
  end
128
135
  end
129
136
  end
@@ -4,8 +4,6 @@ module M2R
4
4
  #
5
5
  # @private
6
6
  module Base
7
- MONGREL2_BASE_HEADERS = %w(pattern method path query url_scheme).map(&:freeze).freeze
8
-
9
7
  # @return [StringIO] Request body encapsulated in IO compatible object
10
8
  # @api public
11
9
  def body_io
@@ -22,12 +20,6 @@ module M2R
22
20
  body_io.close
23
21
  end
24
22
 
25
- protected
26
-
27
- def mongrel_headers
28
- MONGREL2_BASE_HEADERS
29
- end
30
-
31
23
  end
32
24
  end
33
25
 
@@ -3,10 +3,6 @@ module M2R
3
3
  # Contains methods for recognizing such requests and reading them.
4
4
  # @private
5
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
6
  # @return [true,false] True if this is async-upload related request
11
7
  # @api public
12
8
  def upload?
@@ -49,12 +45,6 @@ module M2R
49
45
  super
50
46
  File.delete(body_io.path) if upload_done?
51
47
  end
52
-
53
- protected
54
-
55
- def mongrel_headers
56
- super + MONGREL2_UPLOAD_HEADERS
57
- end
58
48
  end
59
49
  end
60
50
 
@@ -1,11 +1,14 @@
1
1
  require 'm2r'
2
+ require 'm2r/http/close'
2
3
  require 'm2r/response/content_length'
4
+ require 'm2r/response/to_request'
3
5
 
4
6
  module M2R
5
7
  # Simplest possible abstraction layer over HTTP request
6
8
  #
7
9
  # @api public
8
10
  class Response
11
+ include HTTP::Close
9
12
 
10
13
  # @private
11
14
  VERSION = "HTTP/1.1".freeze
@@ -66,37 +69,80 @@ module M2R
66
69
  }
67
70
  STATUS_CODES.freeze
68
71
 
69
- # @return [Fixnum] HTTP Status code
70
- attr_reader :status
72
+ attr_reader :reason
73
+
74
+ def initialize
75
+ status(200)
76
+ headers(Headers.new)
77
+ body("")
78
+ http_version(VERSION)
79
+ end
71
80
 
72
- # @return [Hash, Headers] Collection of response HTTP Headers
73
- attr_reader :headers
81
+ # @param [Fixnum, #to_i] value HTTP status code
82
+ def status(value = GETTER)
83
+ if value == GETTER
84
+ @status
85
+ else
86
+ @status = value.to_i
87
+ @reason = STATUS_CODES[@status]
88
+ self
89
+ end
90
+ end
74
91
 
75
- # @return [String] HTTP Body
76
- attr_reader :body
92
+ # @param [Hash] value HTTP headers
93
+ def headers(value = GETTER)
94
+ if value == GETTER
95
+ @headers
96
+ else
97
+ @headers = value
98
+ self
99
+ end
100
+ end
77
101
 
78
- # @return [String] HTTP Status code description
79
- attr_reader :reason
102
+ # @param [Hash] header HTTP header key
103
+ # @param [Hash] value HTTP header value
104
+ def header(header, value = GETTER)
105
+ if value == GETTER
106
+ @headers[header]
107
+ else
108
+ @headers[header] = value
109
+ self
110
+ end
111
+ end
112
+
113
+ # @param [String, nil] value HTTP body
114
+ def body(value = GETTER)
115
+ if value == GETTER
116
+ @body
117
+ else
118
+ @body = value
119
+ self
120
+ end
121
+ end
80
122
 
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]
123
+ # @param [String, nil] version HTTP body
124
+ def http_version(value = GETTER)
125
+ if value == GETTER
126
+ @version
127
+ else
128
+ @version = value
129
+ self
130
+ end
89
131
  end
90
132
 
91
133
  # @return [String] HTTP Response
92
134
  def to_s
93
- response = "#{VERSION} #{status} #{reason}#{CRLF}"
135
+ response = "#{http_version} #{status} #{reason}#{CRLF}"
94
136
  unless headers.empty?
95
137
  response << headers.map { |h, v| "#{h}: #{v}" }.join(CRLF) << CRLF
96
138
  end
97
139
  response << CRLF
98
- response << body
140
+ response << body.to_s
99
141
  response
100
142
  end
143
+
144
+ protected
145
+
146
+ GETTER = Object.new
101
147
  end
102
148
  end