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

module ActionController #:nodoc:
  class Base
6
    # 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 已提交
7 8 9
    # 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
    #   an ArgumentError is raised.
P
Pratik Naik 已提交
18
    # * <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 已提交
19
    #   indefinitely.
20
    # * <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 已提交
21 22 23
    #   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 38
    class SessionFixationAttempt < StandardError #:nodoc:
    end
D
Initial  
David Heinemeier Hansson 已提交
39

40
    DEFAULT_SESSION_OPTIONS = {
41 42
      :database_manager => CGI::Session::CookieStore, # store data in cookie
      :prefix           => "ruby_sess.",    # prefix session file names
43
      :session_path     => "/",             # available to all paths in app
44
      :session_key      => "_session_id",
45
      :cookie_only      => true
46
    } unless const_defined?(:DEFAULT_SESSION_OPTIONS)
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 66 67
    # 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']
68
        raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
69 70 71 72 73 74
        StringIO.new(raw_post)
      else
        @cgi.stdinput
      end
    end

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

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

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

87
    def host_with_port_without_standard_port_handling
88 89 90 91 92 93 94 95 96 97 98
      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 已提交
99
    def host
100
      host_with_port_without_standard_port_handling.sub(/:\d+$/, '')
101
    end
102

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

D
Initial  
David Heinemeier Hansson 已提交
111
    def session
112
      unless defined?(@session)
113
        if @session_options == false
114
          @session = Hash.new
115
        else
116
          stale_session_check! do
117
            if cookie_only? && query_parameters[session_options_with_string_keys['session_key']]
118 119
              raise SessionFixationAttempt
            end
120 121 122 123 124 125 126 127 128 129 130 131
            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
132
                @session = CGI::Session.new(@cgi, session_options_with_string_keys)
133 134
              else
                raise ArgumentError, "Invalid new_session option: #{value}"
135 136
            end
            @session['__valid_session']
137
          end
138
        end
D
Initial  
David Heinemeier Hansson 已提交
139
      end
140
      @session
D
Initial  
David Heinemeier Hansson 已提交
141
    end
142

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

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

    private
153
      # Delete an old session if it exists then create a new one.
D
Initial  
David Heinemeier Hansson 已提交
154
      def new_session
155
        if @session_options == false
156
          Hash.new
157 158 159 160
        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 已提交
161
      end
162

163 164 165 166
      def cookie_only?
        session_options_with_string_keys['cookie_only']
      end

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

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

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

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

203
      begin
204
        output.write(@cgi.header(@headers))
205

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

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