fix: malformed charset param (#2263)
[rack.git] / lib / rack / urlmap.rb
blob99c4d82365a298bfe709b8dab0f8edb2e7756ddc
1 # frozen_string_literal: true
3 require 'set'
5 require_relative 'constants'
7 module Rack
8   # Rack::URLMap takes a hash mapping urls or paths to apps, and
9   # dispatches accordingly.  Support for HTTP/1.1 host names exists if
10   # the URLs start with <tt>http://</tt> or <tt>https://</tt>.
11   #
12   # URLMap modifies the SCRIPT_NAME and PATH_INFO such that the part
13   # relevant for dispatch is in the SCRIPT_NAME, and the rest in the
14   # PATH_INFO.  This should be taken care of when you need to
15   # reconstruct the URL in order to create links.
16   #
17   # URLMap dispatches in such a way that the longest paths are tried
18   # first, since they are most specific.
20   class URLMap
21     def initialize(map = {})
22       remap(map)
23     end
25     def remap(map)
26       @known_hosts = Set[]
27       @mapping = map.map { |location, app|
28         if location =~ %r{\Ahttps?://(.*?)(/.*)}
29           host, location = $1, $2
30           @known_hosts << host
31         else
32           host = nil
33         end
35         unless location[0] == ?/
36           raise ArgumentError, "paths need to start with /"
37         end
39         location = location.chomp('/')
40         match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", Regexp::NOENCODING)
42         [host, location, match, app]
43       }.sort_by do |(host, location, _, _)|
44         [host ? -host.size : Float::INFINITY, -location.size]
45       end
46     end
48     def call(env)
49       path        = env[PATH_INFO]
50       script_name = env[SCRIPT_NAME]
51       http_host   = env[HTTP_HOST]
52       server_name = env[SERVER_NAME]
53       server_port = env[SERVER_PORT]
55       is_same_server = casecmp?(http_host, server_name) ||
56                        casecmp?(http_host, "#{server_name}:#{server_port}")
58       is_host_known = @known_hosts.include? http_host
60       @mapping.each do |host, location, match, app|
61         unless casecmp?(http_host, host) \
62             || casecmp?(server_name, host) \
63             || (!host && is_same_server) \
64             || (!host && !is_host_known) # If we don't have a matching host, default to the first without a specified host
65           next
66         end
68         next unless m = match.match(path.to_s)
70         rest = m[1]
71         next unless !rest || rest.empty? || rest[0] == ?/
73         env[SCRIPT_NAME] = (script_name + location)
74         env[PATH_INFO] = rest
76         return app.call(env)
77       end
79       [404, { CONTENT_TYPE => "text/plain", "x-cascade" => "pass" }, ["Not Found: #{path}"]]
81     ensure
82       env[PATH_INFO]   = path
83       env[SCRIPT_NAME] = script_name
84     end
86     private
87     def casecmp?(v1, v2)
88       # if both nil, or they're the same string
89       return true if v1 == v2
91       # if either are nil... (but they're not the same)
92       return false if v1.nil?
93       return false if v2.nil?
95       # otherwise check they're not case-insensitive the same
96       v1.casecmp(v2).zero?
97     end
98   end
99 end