aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/http/request.rb
blob: ab654b9a5059171474137dcb1af36331f1fa6b9e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
require 'tempfile'
require 'stringio'
require 'strscan'

require 'active_support/memoizable'

module ActionDispatch
  class Request < Rack::Request

    %w[ AUTH_TYPE GATEWAY_INTERFACE
        PATH_TRANSLATED REMOTE_HOST
        REMOTE_IDENT REMOTE_USER REMOTE_ADDR
        SERVER_NAME SERVER_PROTOCOL

        HTTP_ACCEPT HTTP_ACCEPT_CHARSET HTTP_ACCEPT_ENCODING
        HTTP_ACCEPT_LANGUAGE HTTP_CACHE_CONTROL HTTP_FROM
        HTTP_NEGOTIATE HTTP_PRAGMA HTTP_REFERER HTTP_USER_AGENT ].each do |env|
      define_method(env.sub(/^HTTP_/n, '').downcase) do
        @env[env]
      end
    end

    def key?(key)
      @env.key?(key)
    end

    HTTP_METHODS = %w(get head put post delete options)
    HTTP_METHOD_LOOKUP = HTTP_METHODS.inject({}) { |h, m| h[m] = h[m.upcase] = m.to_sym; h }

    # Returns the true HTTP request \method as a lowercase symbol, such as
    # <tt>:get</tt>. If the request \method is not listed in the HTTP_METHODS
    # constant above, an UnknownHttpMethod exception is raised.
    def request_method
      @request_method ||= HTTP_METHOD_LOOKUP[super] || raise(ActionController::UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
    end

    # Returns the HTTP request \method used for action processing as a
    # lowercase symbol, such as <tt>:post</tt>. (Unlike #request_method, this
    # method returns <tt>:get</tt> for a HEAD request because the two are
    # functionally equivalent from the application's perspective.)
    def method
      request_method == :head ? :get : request_method
    end

    # Is this a GET (or HEAD) request?  Equivalent to <tt>request.method == :get</tt>.
    def get?
      method == :get
    end

    # Is this a POST request?  Equivalent to <tt>request.method == :post</tt>.
    def post?
      request_method == :post
    end

    # Is this a PUT request?  Equivalent to <tt>request.method == :put</tt>.
    def put?
      request_method == :put
    end

    # Is this a DELETE request?  Equivalent to <tt>request.method == :delete</tt>.
    def delete?
      request_method == :delete
    end

    # Is this a HEAD request? Since <tt>request.method</tt> sees HEAD as <tt>:get</tt>,
    # this \method checks the actual HTTP \method directly.
    def head?
      request_method == :head
    end

    # Provides access to the request's HTTP headers, for example:
    #
    #   request.headers["Content-Type"] # => "text/plain"
    def headers
      Http::Headers.new(@env)
    end

    # Returns the content length of the request as an integer.
    def content_length
      super.to_i
    end

    # The MIME type of the HTTP request, such as Mime::XML.
    #
    # For backward compatibility, the post \format is extracted from the
    # X-Post-Data-Format HTTP header if present.
    def content_type
      @content_type ||= begin
        if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/
          Mime::Type.lookup($1.strip.downcase)
        else
          nil
        end
      end
    end

    def media_type
      content_type.to_s
    end

    # Returns the accepted MIME type for the request.
    def accepts
      @accepts ||= begin
        header = @env['HTTP_ACCEPT'].to_s.strip

        fallback = xhr? ? Mime::JS : Mime::HTML

        if header.empty?
          [content_type, fallback, Mime::ALL].compact
        else
          ret = Mime::Type.parse(header)
          if ret.last == Mime::ALL
            ret.insert(-2, fallback)
          end
          ret
        end
      end
    end
    
    def if_modified_since
      if since = env['HTTP_IF_MODIFIED_SINCE']
        Time.rfc2822(since) rescue nil
      end
    end

    def if_none_match
      env['HTTP_IF_NONE_MATCH']
    end

    def not_modified?(modified_at)
      if_modified_since && modified_at && if_modified_since >= modified_at
    end

    def etag_matches?(etag)
      if_none_match && if_none_match == etag
    end

    # Check response freshness (Last-Modified and ETag) against request
    # If-Modified-Since and If-None-Match conditions. If both headers are
    # supplied, both must match, or the request is not considered fresh.
    def fresh?(response)
      case
      when if_modified_since && if_none_match
        not_modified?(response.last_modified) && etag_matches?(response.etag)
      when if_modified_since
        not_modified?(response.last_modified)
      when if_none_match
        etag_matches?(response.etag)
      else
        false
      end
    end

    ONLY_ALL = [Mime::ALL].freeze

    # Returns the Mime type for the \format used in the request.
    #
    #   GET /posts/5.xml   | request.format => Mime::XML
    #   GET /posts/5.xhtml | request.format => Mime::HTML
    #   GET /posts/5       | request.format => Mime::HTML or MIME::JS, or request.accepts.first depending on the value of <tt>ActionController::Base.use_accept_header</tt>

    def format(view_path = [])
      @format ||=
        if parameters[:format]
                        Mime[parameters[:format]]
        elsif ActionController::Base.use_accept_header && !(accepts == ONLY_ALL)
                        accepts.first
        elsif xhr? then Mime::JS
        else            Mime::HTML
        end
    end

    def formats
      @formats = 
        if ActionController::Base.use_accept_header
          Array(Mime[parameters[:format]] || accepts)
        else
          [format]
        end
    end

    # Sets the \format by string extension, which can be used to force custom formats
    # that are not controlled by the extension.
    #
    #   class ApplicationController < ActionController::Base
    #     before_filter :adjust_format_for_iphone
    #
    #     private
    #       def adjust_format_for_iphone
    #         request.format = :iphone if request.env["HTTP_USER_AGENT"][/iPhone/]
    #       end
    #   end
    def format=(extension)
      parameters[:format] = extension.to_s
      @format = Mime::Type.lookup_by_extension(parameters[:format])
    end

    # Returns a symbolized version of the <tt>:format</tt> parameter of the request.
    # If no \format is given it returns <tt>:js</tt>for Ajax requests and <tt>:html</tt>
    # otherwise.
    def template_format
      parameter_format = parameters[:format]

      if parameter_format
        parameter_format
      elsif xhr?
        :js
      else
        :html
      end
    end

    def cache_format
      parameters[:format]
    end

    # Returns true if the request's "X-Requested-With" header contains
    # "XMLHttpRequest". (The Prototype Javascript library sends this header with
    # every Ajax request.)
    def xml_http_request?
      !(@env['HTTP_X_REQUESTED_WITH'] !~ /XMLHttpRequest/i)
    end
    alias xhr? :xml_http_request?

    # Which IP addresses are "trusted proxies" that can be stripped from
    # the right-hand-side of X-Forwarded-For
    TRUSTED_PROXIES = /^127\.0\.0\.1$|^(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\./i

    # Determines originating IP address.  REMOTE_ADDR is the standard
    # but will fail if the user is behind a proxy.  HTTP_CLIENT_IP and/or
    # HTTP_X_FORWARDED_FOR are set by proxies so check for these if
    # REMOTE_ADDR is a proxy.  HTTP_X_FORWARDED_FOR may be a comma-
    # delimited list in the case of multiple chained proxies; the last
    # address which is not trusted is the originating IP.
    def remote_ip
      remote_addr_list = @env['REMOTE_ADDR'] && @env['REMOTE_ADDR'].scan(/[^,\s]+/)

      unless remote_addr_list.blank?
        not_trusted_addrs = remote_addr_list.reject {|addr| addr =~ TRUSTED_PROXIES}
        return not_trusted_addrs.first unless not_trusted_addrs.empty?
      end
      remote_ips = @env['HTTP_X_FORWARDED_FOR'] && @env['HTTP_X_FORWARDED_FOR'].split(',')

      if @env.include? 'HTTP_CLIENT_IP'
        if ActionController::Base.ip_spoofing_check && remote_ips && !remote_ips.include?(@env['HTTP_CLIENT_IP'])
          # We don't know which came from the proxy, and which from the user
          raise ActionController::ActionControllerError.new(<<EOM)
IP spoofing attack?!
HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}
HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}
EOM
        end

        return @env['HTTP_CLIENT_IP']
      end

      if remote_ips
        while remote_ips.size > 1 && TRUSTED_PROXIES =~ remote_ips.last.strip
          remote_ips.pop
        end

        return remote_ips.last.strip
      end

      @env['REMOTE_ADDR']
    end

    # Returns the lowercase name of the HTTP server software.
    def server_software
      (@env['SERVER_SOFTWARE'] && /^([a-zA-Z]+)/ =~ @env['SERVER_SOFTWARE']) ? $1.downcase : nil
    end

    # Returns the complete URL used for this request.
    def url
      protocol + host_with_port + request_uri
    end

    # Returns 'https://' if this is an SSL request and 'http://' otherwise.
    def protocol
      ssl? ? 'https://' : 'http://'
    end

    # Is this an SSL request?
    def ssl?
      @env['HTTPS'] == 'on' || @env['HTTP_X_FORWARDED_PROTO'] == 'https'
    end

    # Returns the \host for this request, such as "example.com".
    def raw_host_with_port
      if forwarded = env["HTTP_X_FORWARDED_HOST"]
        forwarded.split(/,\s?/).last
      else
        env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
      end
    end

    # Returns the host for this request, such as example.com.
    def host
      raw_host_with_port.sub(/:\d+$/, '')
    end

    # Returns a \host:\port string for this request, such as "example.com" or
    # "example.com:8080".
    def host_with_port
      "#{host}#{port_string}"
    end

    # Returns the port number of this request as an integer.
    def port
      if raw_host_with_port =~ /:(\d+)$/
        $1.to_i
      else
        standard_port
      end
    end

    # Returns the standard \port number for this request's protocol.
    def standard_port
      case protocol
        when 'https://' then 443
        else 80
      end
    end

    # Returns a \port suffix like ":8080" if the \port number of this request
    # is not the default HTTP \port 80 or HTTPS \port 443.
    def port_string
      port == standard_port ? '' : ":#{port}"
    end

    # Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
    # a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
    def domain(tld_length = 1)
      return nil unless named_host?(host)

      host.split('.').last(1 + tld_length).join('.')
    end

    # Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
    # returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
    # such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
    # in "www.rubyonrails.co.uk".
    def subdomains(tld_length = 1)
      return [] unless named_host?(host)
      parts = host.split('.')
      parts[0..-(tld_length+2)]
    end

    # Returns the query string, accounting for server idiosyncrasies.
    def query_string
      @env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '')
    end

    # Returns the request URI, accounting for server idiosyncrasies.
    # WEBrick includes the full URL. IIS leaves REQUEST_URI blank.
    def request_uri
      if uri = @env['REQUEST_URI']
        # Remove domain, which webrick puts into the request_uri.
        (%r{^\w+\://[^/]+(/.*|$)$} =~ uri) ? $1 : uri
      else
        # Construct IIS missing REQUEST_URI from SCRIPT_NAME and PATH_INFO.
        uri = @env['PATH_INFO'].to_s

        if script_filename = @env['SCRIPT_NAME'].to_s.match(%r{[^/]+$})
          uri = uri.sub(/#{script_filename}\//, '')
        end

        env_qs = @env['QUERY_STRING'].to_s
        uri += "?#{env_qs}" unless env_qs.empty?

        if uri.blank?
          @env.delete('REQUEST_URI')
        else
          @env['REQUEST_URI'] = uri
        end
      end
    end

    # Returns the interpreted \path to requested resource after all the installation
    # directory of this application was taken into account.
    def path
      path = request_uri.to_s[/\A[^\?]*/]
      path.sub!(/\A#{ActionController::Base.relative_url_root}/, '')
      path
    end

    # Read the request \body. This is useful for web services that need to
    # work with raw requests directly.
    def raw_post
      unless @env.include? 'RAW_POST_DATA'
        @env['RAW_POST_DATA'] = body.read(@env['CONTENT_LENGTH'].to_i)
        body.rewind if body.respond_to?(:rewind)
      end
      @env['RAW_POST_DATA']
    end

    # Returns both GET and POST \parameters in a single hash.
    def parameters
      @parameters ||= request_parameters.merge(query_parameters).update(path_parameters).with_indifferent_access
    end
    alias_method :params, :parameters

    def path_parameters=(parameters) #:nodoc:
      @env["action_dispatch.request.path_parameters"] = parameters
      @symbolized_path_parameters = @parameters = nil
    end

    # The same as <tt>path_parameters</tt> with explicitly symbolized keys.
    def symbolized_path_parameters
      @symbolized_path_parameters ||= path_parameters.symbolize_keys
    end

    # Returns a hash with the \parameters used to form the \path of the request.
    # Returned hash keys are strings:
    #
    #   {'action' => 'my_action', 'controller' => 'my_controller'}
    #
    # See <tt>symbolized_path_parameters</tt> for symbolized keys.
    def path_parameters
      @env["action_dispatch.request.path_parameters"] ||= {}
    end

    # 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']
        raw_post.force_encoding(Encoding::BINARY) if raw_post.respond_to?(:force_encoding)
        StringIO.new(raw_post)
      else
        @env['rack.input']
      end
    end

    def form_data?
      FORM_DATA_MEDIA_TYPES.include?(content_type.to_s)
    end

    # Override Rack's GET method to support indifferent access
    def GET
      @env["action_controller.request.query_parameters"] ||= normalize_parameters(super)
    end
    alias_method :query_parameters, :GET

    # Override Rack's POST method to support indifferent access
    def POST
      @env["action_controller.request.request_parameters"] ||= normalize_parameters(super)
    end
    alias_method :request_parameters, :POST

    def body_stream #:nodoc:
      @env['rack.input']
    end

    def session
      @env['rack.session'] ||= {}
    end

    def session=(session) #:nodoc:
      @env['rack.session'] = session
    end

    def reset_session
      @env['rack.session.options'].delete(:id)
      @env['rack.session'] = {}
    end

    def session_options
      @env['rack.session.options'] ||= {}
    end

    def session_options=(options)
      @env['rack.session.options'] = options
    end

    def server_port
      @env['SERVER_PORT'].to_i
    end

    private
      def named_host?(host)
        !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
      end

      module UploadedFile
        def self.extended(object)
          object.class_eval do
            attr_accessor :original_path, :content_type
            alias_method :local_path, :path
          end
        end

        # Take the basename of the upload's original filename.
        # This handles the full Windows paths given by Internet Explorer
        # (and perhaps other broken user agents) without affecting
        # those which give the lone filename.
        # The Windows regexp is adapted from Perl's File::Basename.
        def original_filename
          unless defined? @original_filename
            @original_filename =
              unless original_path.blank?
                if original_path =~ /^(?:.*[:\\\/])?(.*)/m
                  $1
                else
                  File.basename original_path
                end
              end
          end
          @original_filename
        end
      end

      # Convert nested Hashs to HashWithIndifferentAccess and replace
      # file upload hashs with UploadedFile objects
      def normalize_parameters(value)
        case value
        when Hash
          if value.has_key?(:tempfile)
            upload = value[:tempfile]
            upload.extend(UploadedFile)
            upload.original_path = value[:filename]
            upload.content_type = value[:type]
            upload
          else
            h = {}
            value.each { |k, v| h[k] = normalize_parameters(v) }
            h.with_indifferent_access
          end
        when Array
          value.map { |e| normalize_parameters(e) }
        else
          value
        end
      end
  end
end