merb-core 0.9.10 → 0.9.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Rakefile +6 -0
- data/lib/merb-core.rb +11 -4
- data/lib/merb-core/autoload.rb +1 -0
- data/lib/merb-core/bootloader.rb +44 -11
- data/lib/merb-core/config.rb +7 -0
- data/lib/merb-core/controller/abstract_controller.rb +1 -1
- data/lib/merb-core/controller/merb_controller.rb +0 -1
- data/lib/merb-core/controller/mixins/controller.rb +1 -1
- data/lib/merb-core/core_ext/kernel.rb +30 -24
- data/lib/merb-core/dispatch/cookies.rb +2 -2
- data/lib/merb-core/dispatch/request.rb +8 -229
- data/lib/merb-core/dispatch/request_parsers.rb +236 -0
- data/lib/merb-core/dispatch/router/route.rb +1 -1
- data/lib/merb-core/dispatch/session/cookie.rb +2 -2
- data/lib/merb-core/rack.rb +0 -1
- data/lib/merb-core/rack/adapter/abstract.rb +3 -1
- data/lib/merb-core/rack/middleware/static.rb +2 -2
- data/lib/merb-core/server.rb +1 -1
- data/lib/merb-core/tasks/gem_management.rb +2 -1
- data/lib/merb-core/test/helpers.rb +2 -1
- data/lib/merb-core/test/helpers/cookie_jar.rb +106 -0
- data/lib/merb-core/test/helpers/mock_request_helper.rb +2 -2
- data/lib/merb-core/test/helpers/request_helper.rb +48 -32
- data/lib/merb-core/test/matchers/request_matchers.rb +20 -0
- data/lib/merb-core/test/matchers/route_matchers.rb +1 -1
- data/lib/merb-core/test/test_ext/rspec.rb +5 -2
- data/lib/merb-core/version.rb +1 -1
- metadata +4 -3
- data/lib/merb-core/rack/middleware/csrf.rb +0 -73
@@ -0,0 +1,236 @@
|
|
1
|
+
module Merb
|
2
|
+
module Parse
|
3
|
+
|
4
|
+
# ==== Parameters
|
5
|
+
# query_string<String>:: The query string.
|
6
|
+
# delimiter<String>:: The query string divider. Defaults to "&".
|
7
|
+
# preserve_order<Boolean>:: Preserve order of args. Defaults to false.
|
8
|
+
#
|
9
|
+
# ==== Returns
|
10
|
+
# Mash:: The parsed query string (Dictionary if preserve_order is set).
|
11
|
+
#
|
12
|
+
# ==== Examples
|
13
|
+
# Merb::Parse.query("bar=nik&post[body]=heya")
|
14
|
+
# # => { :bar => "nik", :post => { :body => "heya" } }
|
15
|
+
#
|
16
|
+
# @api plugin
|
17
|
+
def self.query(query_string, delimiter = '&;', preserve_order = false)
|
18
|
+
query = preserve_order ? Dictionary.new : {}
|
19
|
+
for pair in (query_string || '').split(/[#{delimiter}] */n)
|
20
|
+
key, value = unescape(pair).split('=',2)
|
21
|
+
next if key.nil?
|
22
|
+
if key.include?('[')
|
23
|
+
normalize_params(query, key, value)
|
24
|
+
else
|
25
|
+
query[key] = value
|
26
|
+
end
|
27
|
+
end
|
28
|
+
preserve_order ? query : query.to_mash
|
29
|
+
end
|
30
|
+
|
31
|
+
NAME_REGEX = /Content-Disposition:.* name="?([^\";]*)"?/ni.freeze
|
32
|
+
CONTENT_TYPE_REGEX = /Content-Type: (.*)\r\n/ni.freeze
|
33
|
+
FILENAME_REGEX = /Content-Disposition:.* filename="?([^\";]*)"?/ni.freeze
|
34
|
+
CRLF = "\r\n".freeze
|
35
|
+
EOL = CRLF
|
36
|
+
|
37
|
+
# ==== Parameters
|
38
|
+
# request<IO>:: The raw request.
|
39
|
+
# boundary<String>:: The boundary string.
|
40
|
+
# content_length<Fixnum>:: The length of the content.
|
41
|
+
#
|
42
|
+
# ==== Raises
|
43
|
+
# ControllerExceptions::MultiPartParseError:: Failed to parse request.
|
44
|
+
#
|
45
|
+
# ==== Returns
|
46
|
+
# Hash:: The parsed request.
|
47
|
+
#
|
48
|
+
# @api plugin
|
49
|
+
def self.multipart(request, boundary, content_length)
|
50
|
+
boundary = "--#{boundary}"
|
51
|
+
paramhsh = {}
|
52
|
+
buf = ""
|
53
|
+
input = request
|
54
|
+
input.binmode if defined? input.binmode
|
55
|
+
boundary_size = boundary.size + EOL.size
|
56
|
+
bufsize = 16384
|
57
|
+
content_length -= boundary_size
|
58
|
+
# status is boundary delimiter line
|
59
|
+
status = input.read(boundary_size)
|
60
|
+
return {} if status == nil || status.empty?
|
61
|
+
raise ControllerExceptions::MultiPartParseError, "bad content body:\n'#{status}' should == '#{boundary + EOL}'" unless status == boundary + EOL
|
62
|
+
# second argument to Regexp.quote is for KCODE
|
63
|
+
rx = /(?:#{EOL})?#{Regexp.quote(boundary,'n')}(#{EOL}|--)/
|
64
|
+
loop {
|
65
|
+
head = nil
|
66
|
+
body = ''
|
67
|
+
filename = content_type = name = nil
|
68
|
+
read_size = 0
|
69
|
+
until head && buf =~ rx
|
70
|
+
i = buf.index("\r\n\r\n")
|
71
|
+
if( i == nil && read_size == 0 && content_length == 0 )
|
72
|
+
content_length = -1
|
73
|
+
break
|
74
|
+
end
|
75
|
+
if !head && i
|
76
|
+
head = buf.slice!(0, i+2) # First \r\n
|
77
|
+
buf.slice!(0, 2) # Second \r\n
|
78
|
+
|
79
|
+
# String#[] with 2nd arg here is returning
|
80
|
+
# a group from match data
|
81
|
+
filename = head[FILENAME_REGEX, 1]
|
82
|
+
content_type = head[CONTENT_TYPE_REGEX, 1]
|
83
|
+
name = head[NAME_REGEX, 1]
|
84
|
+
|
85
|
+
if filename && !filename.empty?
|
86
|
+
body = Tempfile.new(:Merb)
|
87
|
+
body.binmode if defined? body.binmode
|
88
|
+
end
|
89
|
+
next
|
90
|
+
end
|
91
|
+
|
92
|
+
# Save the read body part.
|
93
|
+
if head && (boundary_size+4 < buf.size)
|
94
|
+
body << buf.slice!(0, buf.size - (boundary_size+4))
|
95
|
+
end
|
96
|
+
|
97
|
+
read_size = bufsize < content_length ? bufsize : content_length
|
98
|
+
if( read_size > 0 )
|
99
|
+
c = input.read(read_size)
|
100
|
+
raise ControllerExceptions::MultiPartParseError, "bad content body" if c.nil? || c.empty?
|
101
|
+
buf << c
|
102
|
+
content_length -= c.size
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Save the rest.
|
107
|
+
if i = buf.index(rx)
|
108
|
+
# correct value of i for some edge cases
|
109
|
+
if (i > 2) && (j = buf.index(rx, i-2)) && (j < i)
|
110
|
+
i = j
|
111
|
+
end
|
112
|
+
body << buf.slice!(0, i)
|
113
|
+
buf.slice!(0, boundary_size+2)
|
114
|
+
|
115
|
+
content_length = -1 if $1 == "--"
|
116
|
+
end
|
117
|
+
|
118
|
+
if filename && !filename.empty?
|
119
|
+
body.rewind
|
120
|
+
data = {
|
121
|
+
:filename => File.basename(filename),
|
122
|
+
:content_type => content_type,
|
123
|
+
:tempfile => body,
|
124
|
+
:size => File.size(body.path)
|
125
|
+
}
|
126
|
+
else
|
127
|
+
data = body
|
128
|
+
end
|
129
|
+
paramhsh = normalize_params(paramhsh,name,data)
|
130
|
+
break if buf.empty? || content_length == -1
|
131
|
+
}
|
132
|
+
paramhsh
|
133
|
+
end
|
134
|
+
|
135
|
+
# ==== Parameters
|
136
|
+
# value<Array, Hash, Dictionary ~to_s>:: The value for the query string.
|
137
|
+
# prefix<~to_s>:: The prefix to add to the query string keys.
|
138
|
+
#
|
139
|
+
# ==== Returns
|
140
|
+
# String:: The query string.
|
141
|
+
#
|
142
|
+
# ==== Alternatives
|
143
|
+
# If the value is a string, the prefix will be used as the key.
|
144
|
+
#
|
145
|
+
# ==== Examples
|
146
|
+
# params_to_query_string(10, "page")
|
147
|
+
# # => "page=10"
|
148
|
+
# params_to_query_string({ :page => 10, :word => "ruby" })
|
149
|
+
# # => "page=10&word=ruby"
|
150
|
+
# params_to_query_string({ :page => 10, :word => "ruby" }, "search")
|
151
|
+
# # => "search[page]=10&search[word]=ruby"
|
152
|
+
# params_to_query_string([ "ice-cream", "cake" ], "shopping_list")
|
153
|
+
# # => "shopping_list[]=ice-cream&shopping_list[]=cake"
|
154
|
+
#
|
155
|
+
# @api plugin
|
156
|
+
def self.params_to_query_string(value, prefix = nil)
|
157
|
+
case value
|
158
|
+
when Array
|
159
|
+
value.map { |v|
|
160
|
+
params_to_query_string(v, "#{prefix}[]")
|
161
|
+
} * "&"
|
162
|
+
when Hash, Dictionary
|
163
|
+
value.map { |k, v|
|
164
|
+
params_to_query_string(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
|
165
|
+
} * "&"
|
166
|
+
else
|
167
|
+
"#{prefix}=#{escape(value)}"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# ==== Parameters
|
172
|
+
# s<String>:: String to URL escape.
|
173
|
+
#
|
174
|
+
# ==== returns
|
175
|
+
# String:: The escaped string.
|
176
|
+
#
|
177
|
+
# @api public
|
178
|
+
def self.escape(s)
|
179
|
+
s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
180
|
+
'%'+$1.unpack('H2'*$1.size).join('%').upcase
|
181
|
+
}.tr(' ', '+')
|
182
|
+
end
|
183
|
+
|
184
|
+
# ==== Parameter
|
185
|
+
# s<String>:: String to URL unescape.
|
186
|
+
#
|
187
|
+
# ==== returns
|
188
|
+
# String:: The unescaped string.
|
189
|
+
#
|
190
|
+
# @api public
|
191
|
+
def self.unescape(s)
|
192
|
+
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){
|
193
|
+
[$1.delete('%')].pack('H*')
|
194
|
+
}
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
# Converts a query string snippet to a hash and adds it to existing
|
200
|
+
# parameters.
|
201
|
+
#
|
202
|
+
# ==== Parameters
|
203
|
+
# parms<Hash>:: Parameters to add the normalized parameters to.
|
204
|
+
# name<String>:: The key of the parameter to normalize.
|
205
|
+
# val<String>:: The value of the parameter.
|
206
|
+
#
|
207
|
+
# ==== Returns
|
208
|
+
# Hash:: Normalized parameters
|
209
|
+
#
|
210
|
+
# @api private
|
211
|
+
def self.normalize_params(parms, name, val=nil)
|
212
|
+
name =~ %r([\[\]]*([^\[\]]+)\]*)
|
213
|
+
key = $1 || ''
|
214
|
+
after = $' || ''
|
215
|
+
|
216
|
+
if after == ""
|
217
|
+
parms[key] = val
|
218
|
+
elsif after == "[]"
|
219
|
+
(parms[key] ||= []) << val
|
220
|
+
elsif after =~ %r(^\[\]\[([^\[\]]+)\]$)
|
221
|
+
child_key = $1
|
222
|
+
parms[key] ||= []
|
223
|
+
if parms[key].last.is_a?(Hash) && !parms[key].last.key?(child_key)
|
224
|
+
parms[key].last.update(child_key => val)
|
225
|
+
else
|
226
|
+
parms[key] << { child_key => val }
|
227
|
+
end
|
228
|
+
else
|
229
|
+
parms[key] ||= {}
|
230
|
+
parms[key] = normalize_params(parms[key], after, val)
|
231
|
+
end
|
232
|
+
parms
|
233
|
+
end
|
234
|
+
|
235
|
+
end
|
236
|
+
end
|
@@ -223,7 +223,7 @@ module Merb
|
|
223
223
|
|
224
224
|
ruby << " query_params.delete_if { |key, value| value.nil? }\n"
|
225
225
|
ruby << " unless query_params.empty?\n"
|
226
|
-
ruby << ' url << "?#{Merb::
|
226
|
+
ruby << ' url << "?#{Merb::Parse.params_to_query_string(query_params)}"' << "\n"
|
227
227
|
ruby << " end\n"
|
228
228
|
ruby << ' url << "##{fragment}" if fragment' << "\n"
|
229
229
|
ruby << " url\n"
|
@@ -126,7 +126,7 @@ module Merb
|
|
126
126
|
def to_cookie
|
127
127
|
unless self.empty?
|
128
128
|
data = self.serialize
|
129
|
-
value = Merb::
|
129
|
+
value = Merb::Parse.escape "#{data}--#{generate_digest(data)}"
|
130
130
|
if value.size > MAX
|
131
131
|
msg = "Cookies have limit of 4K. Session contents: #{data.inspect}"
|
132
132
|
Merb.logger.error!(msg)
|
@@ -164,7 +164,7 @@ module Merb
|
|
164
164
|
if cookie.blank?
|
165
165
|
{}
|
166
166
|
else
|
167
|
-
data, digest = Merb::
|
167
|
+
data, digest = Merb::Parse.unescape(cookie).split('--')
|
168
168
|
return {} if data.blank? || digest.blank?
|
169
169
|
unless digest == generate_digest(data)
|
170
170
|
clear
|
data/lib/merb-core/rack.rb
CHANGED
@@ -21,7 +21,6 @@ module Merb
|
|
21
21
|
autoload :Tracer, 'merb-core/rack/middleware/tracer'
|
22
22
|
autoload :ContentLength, 'merb-core/rack/middleware/content_length'
|
23
23
|
autoload :ConditionalGet, 'merb-core/rack/middleware/conditional_get'
|
24
|
-
autoload :Csrf, 'merb-core/rack/middleware/csrf'
|
25
24
|
autoload :StreamWrapper, 'merb-core/rack/stream_wrapper'
|
26
25
|
autoload :Helpers, 'merb-core/rack/helpers'
|
27
26
|
end # Rack
|
@@ -168,7 +168,9 @@ module Merb
|
|
168
168
|
# If it was not fork_for_class_load, we already set up
|
169
169
|
# ctrl-c handlers in the master thread.
|
170
170
|
elsif Merb::Config[:fork_for_class_load]
|
171
|
-
Merb
|
171
|
+
if Merb::Config[:console_trap]
|
172
|
+
Merb::Server.add_irb_trap
|
173
|
+
end
|
172
174
|
end
|
173
175
|
|
174
176
|
# In daemonized mode or not, support HUPing the process to
|
@@ -33,14 +33,14 @@ module Merb
|
|
33
33
|
# ==== Returns
|
34
34
|
# Boolean:: True if file exists under the server root and is readable.
|
35
35
|
def file_exist?(path)
|
36
|
-
full_path = ::File.join(@static_server.root, ::Merb::
|
36
|
+
full_path = ::File.join(@static_server.root, ::Merb::Parse.unescape(path))
|
37
37
|
::File.file?(full_path) && ::File.readable?(full_path)
|
38
38
|
end
|
39
39
|
|
40
40
|
# ==== Parameters
|
41
41
|
# env<Hash>:: Environment variables to pass on to the server.
|
42
42
|
def serve_static(env)
|
43
|
-
env[Merb::Const::PATH_INFO] = ::Merb::
|
43
|
+
env[Merb::Const::PATH_INFO] = ::Merb::Parse.unescape(env[Merb::Const::PATH_INFO])
|
44
44
|
@static_server.call(env)
|
45
45
|
end
|
46
46
|
|
data/lib/merb-core/server.rb
CHANGED
@@ -266,7 +266,7 @@ module Merb
|
|
266
266
|
Merb.fatal! "Failed to store Merb logs in #{File.dirname(file)}, " \
|
267
267
|
"permission denied. ", e
|
268
268
|
end
|
269
|
-
Merb.logger.warn! "Storing #{
|
269
|
+
Merb.logger.warn! "Storing pid #{Process.pid} file to #{file}..." if Merb::Config[:verbose]
|
270
270
|
begin
|
271
271
|
File.open(file, 'w'){ |f| f.write(Process.pid.to_s) }
|
272
272
|
rescue Errno::EACCES => e
|
@@ -292,7 +292,7 @@ module GemManagement
|
|
292
292
|
end
|
293
293
|
end
|
294
294
|
end
|
295
|
-
|
295
|
+
|
296
296
|
private
|
297
297
|
|
298
298
|
def executable_wrapper(spec, bin_file_name, minigems = true)
|
@@ -316,6 +316,7 @@ end
|
|
316
316
|
if File.directory?(gems_dir = File.join(Dir.pwd, 'gems')) ||
|
317
317
|
File.directory?(gems_dir = File.join(File.dirname(__FILE__), '..', 'gems'))
|
318
318
|
$BUNDLE = true; Gem.clear_paths; Gem.path.unshift(gems_dir)
|
319
|
+
ENV["PATH"] = "\#{File.dirname(__FILE__)}:\#{gems_dir}/bin:\#{ENV["PATH"]}"
|
319
320
|
if (local_gem = Dir[File.join(gems_dir, "specifications", "#{spec.name}-*.gemspec")].last)
|
320
321
|
version = File.basename(local_gem)[/-([\\.\\d]+)\\.gemspec$/, 1]
|
321
322
|
end
|
@@ -2,9 +2,10 @@
|
|
2
2
|
# testing helpers
|
3
3
|
module Merb::Test::Helpers; end
|
4
4
|
|
5
|
+
require "merb-core/test/helpers/cookie_jar"
|
5
6
|
require "merb-core/test/helpers/mock_request_helper"
|
7
|
+
require "merb-core/test/helpers/route_helper"
|
6
8
|
require "merb-core/test/helpers/request_helper"
|
7
9
|
require "merb-core/test/helpers/multipart_request_helper"
|
8
10
|
require "merb-core/test/helpers/controller_helper"
|
9
|
-
require "merb-core/test/helpers/route_helper"
|
10
11
|
require "merb-core/test/helpers/view_helper"
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Merb
|
4
|
+
module Test
|
5
|
+
class Cookie
|
6
|
+
|
7
|
+
attr_reader :name, :value
|
8
|
+
|
9
|
+
def initialize(raw, default_host)
|
10
|
+
# separate the name / value pair from the cookie options
|
11
|
+
@name_value_raw, options = raw.split(/[;,] */n, 2)
|
12
|
+
|
13
|
+
@name, @value = Merb::Parse.query(@name_value_raw, ';').to_a.first
|
14
|
+
@options = Merb::Parse.query(options, ';')
|
15
|
+
|
16
|
+
@options.delete_if { |k, v| !v || v.empty? }
|
17
|
+
|
18
|
+
@options["domain"] ||= default_host
|
19
|
+
end
|
20
|
+
|
21
|
+
def raw
|
22
|
+
@name_value_raw
|
23
|
+
end
|
24
|
+
|
25
|
+
def empty?
|
26
|
+
@value.nil? || @value.empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def domain
|
30
|
+
@options["domain"]
|
31
|
+
end
|
32
|
+
|
33
|
+
def path
|
34
|
+
@options["path"] || "/"
|
35
|
+
end
|
36
|
+
|
37
|
+
def expires
|
38
|
+
Time.parse(@options["expires"]) if @options["expires"]
|
39
|
+
end
|
40
|
+
|
41
|
+
def expired?
|
42
|
+
expires && expires < Time.now
|
43
|
+
end
|
44
|
+
|
45
|
+
def valid?(uri)
|
46
|
+
uri.host =~ Regexp.new("#{Regexp.escape(domain)}$") &&
|
47
|
+
uri.path =~ Regexp.new("^#{Regexp.escape(path)}")
|
48
|
+
end
|
49
|
+
|
50
|
+
def matches?(uri)
|
51
|
+
! expired? && valid?(uri)
|
52
|
+
end
|
53
|
+
|
54
|
+
def <=>(other)
|
55
|
+
# Orders the cookies from least specific to most
|
56
|
+
[name, path, domain.reverse] <=> [other.name, other.path, other.domain.reverse]
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
class CookieJar
|
62
|
+
|
63
|
+
def initialize
|
64
|
+
@jars = {}
|
65
|
+
end
|
66
|
+
|
67
|
+
def update(jar, uri, raw_cookies)
|
68
|
+
return unless raw_cookies
|
69
|
+
# Initialize all the the received cookies
|
70
|
+
cookies = []
|
71
|
+
raw_cookies.each do |raw|
|
72
|
+
c = Cookie.new(raw, uri.host)
|
73
|
+
cookies << c if c.valid?(uri)
|
74
|
+
end
|
75
|
+
|
76
|
+
@jars[jar] ||= []
|
77
|
+
|
78
|
+
# Remove all the cookies that will be updated
|
79
|
+
@jars[jar].delete_if do |existing|
|
80
|
+
cookies.find { |c| [c.name, c.domain, c.path] == [existing.name, existing.domain, existing.path] }
|
81
|
+
end
|
82
|
+
|
83
|
+
@jars[jar].concat cookies
|
84
|
+
|
85
|
+
@jars[jar].sort!
|
86
|
+
end
|
87
|
+
|
88
|
+
def for(jar, uri)
|
89
|
+
cookies = {}
|
90
|
+
|
91
|
+
@jars[jar] ||= []
|
92
|
+
# The cookies are sorted by most specific first. So, we loop through
|
93
|
+
# all the cookies in order and add it to a hash by cookie name if
|
94
|
+
# the cookie can be sent to the current URI. It's added to the hash
|
95
|
+
# so that when we are done, the cookies will be unique by name and
|
96
|
+
# we'll have grabbed the most specific to the URI.
|
97
|
+
@jars[jar].each do |cookie|
|
98
|
+
cookies[cookie.name] = cookie.raw if cookie.matches?(uri)
|
99
|
+
end
|
100
|
+
|
101
|
+
cookies.values.join
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|