e8ea3704a88a0dcc9fd14f2fe5ccb3932bb7d283
[depot.git] / rack_process.rb
1 require 'action_controller/cgi_ext'
2 require 'action_controller/session/cookie_store'
3
4 module ActionController #:nodoc:
5 class RackRequest < AbstractRequest #:nodoc:
6 attr_accessor :session_options
7 attr_reader :cgi
8
9 class SessionFixationAttempt < StandardError #:nodoc:
10 end
11
12 DEFAULT_SESSION_OPTIONS = {
13 :database_manager => CGI::Session::CookieStore, # store data in cookie
14 :prefix => "ruby_sess.", # prefix session file names
15 :session_path => "/", # available to all paths in app
16 :session_key => "_session_id",
17 :cookie_only => true,
18 :session_http_only=> true
19 }
20
21 def initialize(env, session_options = DEFAULT_SESSION_OPTIONS)
22 @session_options = session_options
23 @env = env
24 @cgi = CGIWrapper.new(self)
25 super()
26 end
27
28 %w[ AUTH_TYPE GATEWAY_INTERFACE PATH_INFO
29 PATH_TRANSLATED REMOTE_HOST
30 REMOTE_IDENT REMOTE_USER SCRIPT_NAME
31 SERVER_NAME SERVER_PROTOCOL
32
33 HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
34 HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
35 HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env|
36 define_method(env.sub(/^HTTP_/n, '').downcase) do
37 @env[env]
38 end
39 end
40
41 def query_string
42 qs = super
43 if !qs.blank?
44 qs
45 else
46 @env['QUERY_STRING']
47 end
48 end
49
50 def body_stream #:nodoc:
51 @env['rack.input']
52 end
53
54 def key?(key)
55 @env.key?(key)
56 end
57
58 def cookies
59 return {} unless @env["HTTP_COOKIE"]
60
61 unless @env["rack.request.cookie_string"] == @env["HTTP_COOKIE"]
62 @env["rack.request.cookie_string"] = @env["HTTP_COOKIE"]
63 @env["rack.request.cookie_hash"] = CGI::Cookie::parse(@env["rack.request.cookie_string"])
64 end
65
66 @env["rack.request.cookie_hash"]
67 end
68
69 def server_port
70 @env['SERVER_PORT'].to_i
71 end
72
73 def server_software
74 @env['SERVER_SOFTWARE'].split("/").first
75 end
76
77 def session
78 unless defined?(@session)
79 if @session_options == false
80 @session = Hash.new
81 else
82 stale_session_check! do
83 if cookie_only? && query_parameters[session_options_with_string_keys['session_key']]
84 raise SessionFixationAttempt
85 end
86 case value = session_options_with_string_keys['new_session']
87 when true
88 @session = new_session
89 when false
90 begin
91 @session = CGI::Session.new(@cgi, session_options_with_string_keys)
92 # CGI::Session raises ArgumentError if 'new_session' == false
93 # and no session cookie or query param is present.
94 rescue ArgumentError
95 @session = Hash.new
96 end
97 when nil
98 @session = CGI::Session.new(@cgi, session_options_with_string_keys)
99 else
100 raise ArgumentError, "Invalid new_session option: #{value}"
101 end
102 @session['__valid_session']
103 end
104 end
105 end
106 @session
107 end
108
109 def reset_session
110 @session.delete if defined?(@session) && @session.is_a?(CGI::Session)
111 @session = new_session
112 end
113
114 private
115 # Delete an old session if it exists then create a new one.
116 def new_session
117 if @session_options == false
118 Hash.new
119 else
120 CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => false)).delete rescue nil
121 CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => true))
122 end
123 end
124
125 def cookie_only?
126 session_options_with_string_keys['cookie_only']
127 end
128
129 def stale_session_check!
130 yield
131 rescue ArgumentError => argument_error
132 if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
133 begin
134 # Note that the regexp does not allow $1 to end with a ':'
135 $1.constantize
136 rescue LoadError, NameError => const_error
137 raise ActionController::SessionRestoreError, <<-end_msg
138 Session contains objects whose class definition isn\'t available.
139 Remember to require the classes for all objects kept in the session.
140 (Original exception: #{const_error.message} [#{const_error.class}])
141 end_msg
142 end
143
144 retry
145 else
146 raise
147 end
148 end
149
150 def session_options_with_string_keys
151 @session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).stringify_keys
152 end
153 end
154
155 class RackResponse < AbstractResponse #:nodoc:
156 def initialize(request)
157 @cgi = request.cgi
158 @writer = lambda { |x| @body << x }
159 @block = nil
160 super()
161 end
162
163 # Retrieve status from instance variable if has already been delete
164 def status
165 @status || super
166 end
167
168 def out(output = $stdout, &block)
169 # Nasty hack because CGI sessions are closed after the normal
170 # prepare! statement
171 set_cookies!
172
173 @block = block
174 @status = headers.delete("Status")
175 if [204, 304].include?(status.to_i)
176 headers.delete("Content-Type")
177 [status, headers.to_hash, []]
178 else
179 [status, headers.to_hash, self]
180 end
181 end
182 alias to_a out
183
184 def each(&callback)
185 if @body.respond_to?(:call)
186 @writer = lambda { |x| callback.call(x) }
187 @body.call(self, self)
188 elsif @body.is_a?(String)
189 @body.each_line(&callback)
190 else
191 @body.each(&callback)
192 end
193
194 @writer = callback
195 @block.call(self) if @block
196 end
197
198 def write(str)
199 @writer.call str.to_s
200 str
201 end
202
203 def close
204 @body.close if @body.respond_to?(:close)
205 end
206
207 def empty?
208 @block == nil && @body.empty?
209 end
210
211 def prepare!
212 super
213
214 convert_language!
215 convert_expires!
216 set_status!
217 # set_cookies!
218 end
219
220 private
221 def convert_language!
222 headers["Content-Language"] = headers.delete("language") if headers["language"]
223 end
224
225 def convert_expires!
226 headers["Expires"] = headers.delete("") if headers["expires"]
227 end
228
229 def convert_content_type!
230 super
231 headers['Content-Type'] = headers.delete('type') || "text/html"
232 headers['Content-Type'] += "; charset=" + headers.delete('charset') if headers['charset']
233 end
234
235 def set_content_length!
236 super
237 headers["Content-Length"] = headers["Content-Length"].to_s if headers["Content-Length"]
238 end
239
240 def set_status!
241 self.status ||= "200 OK"
242 end
243
244 def set_cookies!
245 # Convert 'cookie' header to 'Set-Cookie' headers.
246 # Because Set-Cookie header can appear more the once in the response body,
247 # we store it in a line break separated string that will be translated to
248 # multiple Set-Cookie header by the handler.
249 if cookie = headers.delete('cookie')
250 cookies = []
251
252 case cookie
253 when Array then cookie.each { |c| cookies << c.to_s }
254 when Hash then cookie.each { |_, c| cookies << c.to_s }
255 else cookies << cookie.to_s
256 end
257
258 @cgi.output_cookies.each { |c| cookies << c.to_s } if @cgi.output_cookies
259
260 headers['Set-Cookie'] = [headers['Set-Cookie'], cookies].flatten.compact
261 end
262 end
263 end
264
265 class CGIWrapper < ::CGI
266 attr_reader :output_cookies
267
268 def initialize(request, *args)
269 @request = request
270 @args = *args
271 @input = request.body
272
273 super *args
274 end
275
276 def params
277 @params ||= @request.params
278 end
279
280 def cookies
281 @request.cookies
282 end
283
284 def query_string
285 @request.query_string
286 end
287
288 # Used to wrap the normal args variable used inside CGI.
289 def args
290 @args
291 end
292
293 # Used to wrap the normal env_table variable used inside CGI.
294 def env_table
295 @request.env
296 end
297
298 # Used to wrap the normal stdinput variable used inside CGI.
299 def stdinput
300 @input
301 end
302 end
303 end