merb-core 0.9.10 → 0.9.11
Sign up to get free protection for your applications and to get access to all the features.
- 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
|