1 require 'action_controller/cgi_ext'
2 require 'action_controller/session/cookie_store'
4 module ActionController #:nodoc:
6 # Process a request extracted from an CGI object and return a response. Pass false as <tt>session_options</tt> to disable
7 # sessions (large performance increase if sessions are not needed). The <tt>session_options</tt> are the same as for CGI::Session:
9 # * <tt>:database_manager</tt> - standard options are CGI::Session::FileStore, CGI::Session::MemoryStore, and CGI::Session::PStore
10 # (default). Additionally, there is CGI::Session::DRbStore and CGI::Session::ActiveRecordStore. Read more about these in
11 # lib/action_controller/session.
12 # * <tt>:session_key</tt> - the parameter name used for the session id. Defaults to '_session_id'.
13 # * <tt>:session_id</tt> - the session id to use. If not provided, then it is retrieved from the +session_key+ cookie, or
14 # automatically generated for a new session.
15 # * <tt>:new_session</tt> - if true, force creation of a new session. If not set, a new session is only created if none currently
16 # exists. If false, a new session is never created, and if none currently exists and the +session_id+ option is not set,
17 # an ArgumentError is raised.
18 # * <tt>:session_expires</tt> - the time the current session expires, as a +Time+ object. If not set, the session will continue
20 # * <tt>:session_domain</tt> - the hostname domain for which this session is valid. If not set, defaults to the hostname of the
22 # * <tt>:session_secure</tt> - if +true+, this session will only work over HTTPS.
23 # * <tt>:session_path</tt> - the path for which this session applies. Defaults to the directory of the CGI script.
24 # * <tt>:cookie_only</tt> - if +true+ (the default), session IDs will only be accepted from cookies and not from
25 # the query string or POST parameters. This protects against session fixation attacks.
26 def self.process_cgi(cgi = CGI.new, session_options = {})
27 new.process_cgi(cgi, session_options)
30 def process_cgi(cgi, session_options = {}) #:nodoc:
31 process(CgiRequest.new(cgi, session_options), CgiResponse.new(cgi)).out
35 class CgiRequest < AbstractRequest #:nodoc:
36 attr_accessor :cgi, :session_options
37 class SessionFixationAttempt < StandardError; end #:nodoc:
39 DEFAULT_SESSION_OPTIONS = {
40 :database_manager => CGI::Session::CookieStore, # store data in cookie
41 :prefix => "ruby_sess.", # prefix session file names
42 :session_path => "/", # available to all paths in app
43 :session_key => "_session_id",
45 } unless const_defined?(:DEFAULT_SESSION_OPTIONS)
47 def initialize(cgi, session_options = {})
49 @session_options = session_options
50 @env = @cgi.send!(:env_table)
55 qs = @cgi.query_string if @cgi.respond_to?(:query_string)
63 # The request body is an IO input stream. If the RAW_POST_DATA environment
64 # variable is already set, wrap it in a StringIO.
66 if raw_post = env['RAW_POST_DATA']
67 StringIO.new(raw_post)
74 @query_parameters ||= self.class.parse_query_parameters(query_string)
77 def request_parameters
78 @request_parameters ||= parse_formatted_request_parameters
85 def host_with_port_without_standard_port_handling
86 if forwarded = env["HTTP_X_FORWARDED_HOST"]
87 forwarded.split(/,\s?/).last
88 elsif http_host = env['HTTP_HOST']
90 elsif server_name = env['SERVER_NAME']
93 "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
98 host_with_port_without_standard_port_handling.sub(/:\d+$/, '')
102 if host_with_port_without_standard_port_handling =~ /:(\d+)$/
110 unless defined?(@session)
111 if @session_options == false
114 stale_session_check! do
115 if cookie_only? && query_parameters[session_options_with_string_keys['session_key']]
116 raise SessionFixationAttempt
118 case value = session_options_with_string_keys['new_session']
120 @session = new_session
123 @session = CGI::Session.new(@cgi, session_options_with_string_keys)
124 # CGI::Session raises ArgumentError if 'new_session' == false
125 # and no session cookie or query param is present.
130 @session = CGI::Session.new(@cgi, session_options_with_string_keys)
132 raise ArgumentError, "Invalid new_session option: #{value}"
134 @session['__valid_session']
142 @session.delete if defined?(@session) && @session.is_a?(CGI::Session)
143 @session = new_session
146 def method_missing(method_id, *arguments)
147 @cgi.send!(method_id, *arguments) rescue super
151 # Delete an old session if it exists then create a new one.
153 if @session_options == false
156 CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => false)).delete rescue nil
157 CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => true))
162 session_options_with_string_keys['cookie_only']
165 def stale_session_check!
167 rescue ArgumentError => argument_error
168 if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
170 # Note that the regexp does not allow $1 to end with a ':'
172 rescue LoadError, NameError => const_error
173 raise ActionController::SessionRestoreError, <<-end_msg
174 Session contains objects whose class definition isn\'t available.
175 Remember to require the classes for all objects kept in the session.
176 (Original exception: #{const_error.message} [#{const_error.class}])
186 def session_options_with_string_keys
187 @session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).stringify_keys
191 class CgiResponse < AbstractResponse #:nodoc:
197 def out(output = $stdout)
198 output.binmode if output.respond_to?(:binmode)
199 output.sync = false if output.respond_to?(:sync=)
202 output.write(@cgi.header(@headers))
204 if @cgi.send!(:env_table)['REQUEST_METHOD'] == 'HEAD'
206 elsif @body.respond_to?(:call)
207 # Flush the output now in case the @body Proc uses
209 output.flush if output.respond_to?(:flush)
210 @body.call(self, output)
215 output.flush if output.respond_to?(:flush)
216 rescue Errno::EPIPE, Errno::ECONNRESET
217 # lost connection to parent process, ignore output