cgi_process.rb 6.6 KB
Newer Older
1
require 'action_controller/cgi_ext'
D
Initial  
David Heinemeier Hansson 已提交
2 3 4

module ActionController #:nodoc:
  class Base
5
    # Process a request extracted from a CGI object and return a response. Pass false as <tt>session_options</tt> to disable
D
Initial  
David Heinemeier Hansson 已提交
6 7 8
    # 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
9
    #   (default). Additionally, there is CGI::Session::DRbStore and CGI::Session::ActiveRecordStore. Read more about these in
D
Initial  
David Heinemeier Hansson 已提交
10 11
    #   lib/action_controller/session.
    # * <tt>:session_key</tt> - the parameter name used for the session id. Defaults to '_session_id'.
12 13
    # * <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 已提交
14
    # * <tt>:new_session</tt> - if true, force creation of a new session.  If not set, a new session is only created if none currently
15
    #   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 已提交
16
    #   an ArgumentError is raised.
P
Pratik Naik 已提交
17
    # * <tt>:session_expires</tt> - the time the current session expires, as a Time object.  If not set, the session will continue
D
Initial  
David Heinemeier Hansson 已提交
18
    #   indefinitely.
19
    # * <tt>:session_domain</tt> - the hostname domain for which this session is valid. If not set, defaults to the hostname of the
D
Initial  
David Heinemeier Hansson 已提交
20 21 22
    #   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.
23 24
    # * <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.
25
    def self.process_cgi(cgi = CGI.new, session_options = {})
D
Initial  
David Heinemeier Hansson 已提交
26 27
      new.process_cgi(cgi, session_options)
    end
28

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

  class CgiRequest < AbstractRequest #:nodoc:
35
    attr_accessor :cgi, :session_options
36 37
    class SessionFixationAttempt < StandardError #:nodoc:
    end
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 45
      :cookie_only      => true,
      :session_http_only=> true
46
    }
D
Initial  
David Heinemeier Hansson 已提交
47 48 49 50

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

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

64 65
    def body_stream #:nodoc:
      @cgi.stdinput
D
Initial  
David Heinemeier Hansson 已提交
66
    end
67

D
Initial  
David Heinemeier Hansson 已提交
68 69 70 71 72
    def cookies
      @cgi.cookies.freeze
    end

    def session
73
      unless defined?(@session)
74
        if @session_options == false
75
          @session = Hash.new
76
        else
77
          stale_session_check! do
78
            if cookie_only? && query_parameters[session_options_with_string_keys['session_key']]
79 80
              raise SessionFixationAttempt
            end
81 82 83 84 85 86 87 88 89 90 91 92
            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
93
                @session = CGI::Session.new(@cgi, session_options_with_string_keys)
94 95
              else
                raise ArgumentError, "Invalid new_session option: #{value}"
96 97
            end
            @session['__valid_session']
98
          end
99
        end
D
Initial  
David Heinemeier Hansson 已提交
100
      end
101
      @session
D
Initial  
David Heinemeier Hansson 已提交
102
    end
103

D
Initial  
David Heinemeier Hansson 已提交
104
    def reset_session
105
      @session.delete if defined?(@session) && @session.is_a?(CGI::Session)
106
      @session = new_session
D
Initial  
David Heinemeier Hansson 已提交
107 108 109
    end

    def method_missing(method_id, *arguments)
110
      @cgi.__send__(method_id, *arguments) rescue super
D
Initial  
David Heinemeier Hansson 已提交
111 112 113
    end

    private
114
      # Delete an old session if it exists then create a new one.
D
Initial  
David Heinemeier Hansson 已提交
115
      def new_session
116
        if @session_options == false
117
          Hash.new
118 119 120 121
        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 已提交
122
      end
123

124 125 126 127
      def cookie_only?
        session_options_with_string_keys['cookie_only']
      end

128
      def stale_session_check!
129
        yield
130
      rescue ArgumentError => argument_error
131
        if argument_error.message =~ %r{undefined class/module ([\w:]*\w)}
132
          begin
133 134
            # Note that the regexp does not allow $1 to end with a ':'
            $1.constantize
135
          rescue LoadError, NameError => const_error
136
            raise ActionController::SessionRestoreError, <<-end_msg
137
Session contains objects whose class definition isn\'t available.
138 139 140 141 142 143 144 145 146 147 148
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

149
      def session_options_with_string_keys
J
Jeremy Kemper 已提交
150
        @session_options_with_string_keys ||= DEFAULT_SESSION_OPTIONS.merge(@session_options).stringify_keys
151
      end
D
Initial  
David Heinemeier Hansson 已提交
152 153 154 155 156 157 158 159
  end

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

160 161 162
    def out(output = $stdout)
      output.binmode      if output.respond_to?(:binmode)
      output.sync = false if output.respond_to?(:sync=)
163

164
      begin
165
        output.write(@cgi.header(@headers))
166

167
        if @cgi.__send__(:env_table)['REQUEST_METHOD'] == 'HEAD'
168 169
          return
        elsif @body.respond_to?(:call)
170 171 172
          # Flush the output now in case the @body Proc uses
          # #syswrite.
          output.flush if output.respond_to?(:flush)
173
          @body.call(self, output)
174
        else
175
          output.write(@body)
176
        end
177 178

        output.flush if output.respond_to?(:flush)
179 180
      rescue Errno::EPIPE, Errno::ECONNRESET
        # lost connection to parent process, ignore output
D
Initial  
David Heinemeier Hansson 已提交
181 182 183
      end
    end
  end
184
end