cgi_process.rb 7.7 KB
Newer Older
1
require 'action_controller/cgi_ext'
2
require 'action_controller/session/cookie_store'
D
Initial  
David Heinemeier Hansson 已提交
3 4 5 6 7 8 9

module ActionController #:nodoc:
  class Base
    # Process a request extracted from an CGI object and return a response. Pass false as <tt>session_options</tt> to disable
    # sessions (large performance increase if sessions are not needed). The <tt>session_options</tt> are the same as for CGI::Session:
    #
    # * <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
D
Initial  
David Heinemeier Hansson 已提交
11 12
    #   lib/action_controller/session.
    # * <tt>:session_key</tt> - the parameter name used for the session id. Defaults to '_session_id'.
13 14
    # * <tt>:session_id</tt> - the session id to use.  If not provided, then it is retrieved from the +session_key+ cookie, or 
    #   automatically generated for a new session.
D
Initial  
David Heinemeier Hansson 已提交
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,
D
Initial  
David Heinemeier Hansson 已提交
17 18 19 20 21 22 23
    #   an ArgumentError is raised.
    # * <tt>:session_expires</tt> - the time the current session expires, as a +Time+ object.  If not set, the session will continue
    #   indefinitely.
    # * <tt>:session_domain</tt> -  the hostname domain for which this session is valid. If not set, defaults to the hostname of the
    #   server.
    # * <tt>:session_secure</tt> - if +true+, this session will only work over HTTPS.
    # * <tt>:session_path</tt> - the path for which this session applies.  Defaults to the directory of the CGI script.
24 25
    # * <tt>:cookie_only</tt> - if +true+ (the default), session IDs will only be accepted from cookies and not from
    #   the query string or POST parameters. This protects against session fixation attacks.
26
    def self.process_cgi(cgi = CGI.new, session_options = {})
D
Initial  
David Heinemeier Hansson 已提交
27 28
      new.process_cgi(cgi, session_options)
    end
29

D
Initial  
David Heinemeier Hansson 已提交
30 31 32 33 34 35
    def process_cgi(cgi, session_options = {}) #:nodoc:
      process(CgiRequest.new(cgi, session_options), CgiResponse.new(cgi)).out
    end
  end

  class CgiRequest < AbstractRequest #:nodoc:
36
    attr_accessor :cgi, :session_options
37
    class SessionFixationAttempt < StandardError; end #:nodoc:
D
Initial  
David Heinemeier Hansson 已提交
38

39
    DEFAULT_SESSION_OPTIONS = {
40 41
      :database_manager => CGI::Session::CookieStore, # store data in cookie
      :prefix           => "ruby_sess.",    # prefix session file names
42
      :session_path     => "/",             # available to all paths in app
43
      :session_key      => "_session_id",
44
      :cookie_only      => true
45
    } unless const_defined?(:DEFAULT_SESSION_OPTIONS)
D
Initial  
David Heinemeier Hansson 已提交
46 47 48 49

    def initialize(cgi, session_options = {})
      @cgi = cgi
      @session_options = session_options
50
      @env = @cgi.send!(:env_table)
D
Initial  
David Heinemeier Hansson 已提交
51 52 53
      super()
    end

54
    def query_string
55
      qs = @cgi.query_string if @cgi.respond_to?(:query_string)
56
      if !qs.blank?
57
        qs
58
      else
59
        super
60
      end
61 62
    end

63 64 65 66 67 68 69 70 71 72
    # The request body is an IO input stream. If the RAW_POST_DATA environment
    # variable is already set, wrap it in a StringIO.
    def body
      if raw_post = env['RAW_POST_DATA']
        StringIO.new(raw_post)
      else
        @cgi.stdinput
      end
    end

D
Initial  
David Heinemeier Hansson 已提交
73
    def query_parameters
74
      @query_parameters ||= self.class.parse_query_parameters(query_string)
D
Initial  
David Heinemeier Hansson 已提交
75 76 77
    end

    def request_parameters
78
      @request_parameters ||= parse_formatted_request_parameters
D
Initial  
David Heinemeier Hansson 已提交
79
    end
80

D
Initial  
David Heinemeier Hansson 已提交
81 82 83 84
    def cookies
      @cgi.cookies.freeze
    end

85
    def host_with_port_without_standard_port_handling
86 87 88 89 90 91 92 93 94 95 96
      if forwarded = env["HTTP_X_FORWARDED_HOST"]
        forwarded.split(/,\s?/).last
      elsif http_host = env['HTTP_HOST']
        http_host
      elsif server_name = env['SERVER_NAME']
        server_name
      else
        "#{env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
      end
    end

D
Initial  
David Heinemeier Hansson 已提交
97
    def host
98
      host_with_port_without_standard_port_handling.sub(/:\d+$/, '')
99
    end
100

101
    def port
102
      if host_with_port_without_standard_port_handling =~ /:(\d+)$/
103 104 105 106
        $1.to_i
      else
        standard_port
      end
D
Initial  
David Heinemeier Hansson 已提交
107
    end
108

D
Initial  
David Heinemeier Hansson 已提交
109
    def session
110
      unless defined?(@session)
111
        if @session_options == false
112
          @session = Hash.new
113
        else
114
          stale_session_check! do
115
            if cookie_only? && query_parameters[session_options_with_string_keys['session_key']]
116 117
              raise SessionFixationAttempt
            end
118 119 120 121 122 123 124 125 126 127 128 129
            case value = session_options_with_string_keys['new_session']
              when true
                @session = new_session
              when false
                begin
                  @session = CGI::Session.new(@cgi, session_options_with_string_keys)
                # CGI::Session raises ArgumentError if 'new_session' == false
                # and no session cookie or query param is present.
                rescue ArgumentError
                  @session = Hash.new
                end
              when nil
130
                @session = CGI::Session.new(@cgi, session_options_with_string_keys)
131 132
              else
                raise ArgumentError, "Invalid new_session option: #{value}"
133 134
            end
            @session['__valid_session']
135
          end
136
        end
D
Initial  
David Heinemeier Hansson 已提交
137
      end
138
      @session
D
Initial  
David Heinemeier Hansson 已提交
139
    end
140

D
Initial  
David Heinemeier Hansson 已提交
141
    def reset_session
142
      @session.delete if defined?(@session) && @session.is_a?(CGI::Session)
143
      @session = new_session
D
Initial  
David Heinemeier Hansson 已提交
144 145 146
    end

    def method_missing(method_id, *arguments)
147
      @cgi.send!(method_id, *arguments) rescue super
D
Initial  
David Heinemeier Hansson 已提交
148 149 150
    end

    private
151
      # Delete an old session if it exists then create a new one.
D
Initial  
David Heinemeier Hansson 已提交
152
      def new_session
153
        if @session_options == false
154
          Hash.new
155 156 157 158
        else
          CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => false)).delete rescue nil
          CGI::Session.new(@cgi, session_options_with_string_keys.merge("new_session" => true))
        end
D
Initial  
David Heinemeier Hansson 已提交
159
      end
160

161 162 163 164
      def cookie_only?
        session_options_with_string_keys['cookie_only']
      end

165
      def stale_session_check!
166
        yield
167
      rescue ArgumentError => argument_error
168
        if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
169
          begin
170 171
            # Note that the regexp does not allow $1 to end with a ':'
            $1.constantize
172
          rescue LoadError, NameError => const_error
173
            raise ActionController::SessionRestoreError, <<-end_msg
174
Session contains objects whose class definition isn\'t available.
175 176 177 178 179 180 181 182 183 184 185
Remember to require the classes for all objects kept in the session.
(Original exception: #{const_error.message} [#{const_error.class}])
end_msg
          end

          retry
        else
          raise
        end
      end

186
      def session_options_with_string_keys
J
Jeremy Kemper 已提交
187
        @session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).stringify_keys
188
      end
D
Initial  
David Heinemeier Hansson 已提交
189 190 191 192 193 194 195 196
  end

  class CgiResponse < AbstractResponse #:nodoc:
    def initialize(cgi)
      @cgi = cgi
      super()
    end

197 198 199
    def out(output = $stdout)
      output.binmode      if output.respond_to?(:binmode)
      output.sync = false if output.respond_to?(:sync=)
200

201
      begin
202
        output.write(@cgi.header(@headers))
203

204
        if @cgi.send!(:env_table)['REQUEST_METHOD'] == 'HEAD'
205 206
          return
        elsif @body.respond_to?(:call)
207 208 209
          # Flush the output now in case the @body Proc uses
          # #syswrite.
          output.flush if output.respond_to?(:flush)
210
          @body.call(self, output)
211
        else
212
          output.write(@body)
213
        end
214 215

        output.flush if output.respond_to?(:flush)
216 217
      rescue Errno::EPIPE, Errno::ECONNRESET
        # lost connection to parent process, ignore output
D
Initial  
David Heinemeier Hansson 已提交
218 219 220
      end
    end
  end
221
end