From 5c9f871771e1be7fb6a917dfff21bda31aa56693 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Fri, 17 Jul 2015 10:52:06 -0700 Subject: [PATCH] Pull over and use Rack::URLMap. Fixes #741 --- lib/puma/rack/builder.rb | 2 + lib/puma/rack/urlmap.rb | 90 ++++++++++++++++++++++++++++++++++++++++ test/hello-map.ru | 3 ++ 3 files changed, 95 insertions(+) create mode 100644 lib/puma/rack/urlmap.rb create mode 100644 test/hello-map.ru diff --git a/lib/puma/rack/builder.rb b/lib/puma/rack/builder.rb index 88789f74..1ee8f1ac 100644 --- a/lib/puma/rack/builder.rb +++ b/lib/puma/rack/builder.rb @@ -290,6 +290,8 @@ module Puma::Rack private def generate_map(default_app, mapping) + require 'puma/rack/urlmap' + mapped = default_app ? {'/' => default_app} : {} mapping.each { |r,b| mapped[r] = self.class.new(default_app, &b).to_app } URLMap.new(mapped) diff --git a/lib/puma/rack/urlmap.rb b/lib/puma/rack/urlmap.rb new file mode 100644 index 00000000..a82c7ee8 --- /dev/null +++ b/lib/puma/rack/urlmap.rb @@ -0,0 +1,90 @@ +module Puma::Rack + # Rack::URLMap takes a hash mapping urls or paths to apps, and + # dispatches accordingly. Support for HTTP/1.1 host names exists if + # the URLs start with http:// or https://. + # + # URLMap modifies the SCRIPT_NAME and PATH_INFO such that the part + # relevant for dispatch is in the SCRIPT_NAME, and the rest in the + # PATH_INFO. This should be taken care of when you need to + # reconstruct the URL in order to create links. + # + # URLMap dispatches in such a way that the longest paths are tried + # first, since they are most specific. + + class URLMap + NEGATIVE_INFINITY = -1.0 / 0.0 + INFINITY = 1.0 / 0.0 + + def initialize(map = {}) + remap(map) + end + + def remap(map) + @mapping = map.map { |location, app| + if location =~ %r{\Ahttps?://(.*?)(/.*)} + host, location = $1, $2 + else + host = nil + end + + unless location[0] == ?/ + raise ArgumentError, "paths need to start with /" + end + + location = location.chomp('/') + match = Regexp.new("^#{Regexp.quote(location).gsub('/', '/+')}(.*)", nil, 'n') + + [host, location, match, app] + }.sort_by do |(host, location, _, _)| + [host ? -host.size : INFINITY, -location.size] + end + end + + def call(env) + path = env['PATH_INFO'] + script_name = env['SCRIPT_NAME'] + hHost = env['HTTP_HOST'] + sName = env['SERVER_NAME'] + sPort = env['SERVER_PORT'] + + @mapping.each do |host, location, match, app| + unless casecmp?(hHost, host) \ + || casecmp?(sName, host) \ + || (!host && (casecmp?(hHost, sName) || + casecmp?(hHost, sName+':'+sPort))) + next + end + + next unless m = match.match(path.to_s) + + rest = m[1] + next unless !rest || rest.empty? || rest[0] == ?/ + + env['SCRIPT_NAME'] = (script_name + location) + env['PATH_INFO'] = rest + + return app.call(env) + end + + [404, {'Content-Type' => "text/plain", "X-Cascade" => "pass"}, ["Not Found: #{path}"]] + + ensure + env['PATH_INFO'] = path + env['SCRIPT_NAME'] = script_name + end + + private + def casecmp?(v1, v2) + # if both nil, or they're the same string + return true if v1 == v2 + + # if either are nil... (but they're not the same) + return false if v1.nil? + return false if v2.nil? + + # otherwise check they're not case-insensitive the same + v1.casecmp(v2).zero? + end + end +end + diff --git a/test/hello-map.ru b/test/hello-map.ru new file mode 100644 index 00000000..11401d48 --- /dev/null +++ b/test/hello-map.ru @@ -0,0 +1,3 @@ +map "/foo" do + run lambda { |env| [200, {"Content-Type" => "text/plain"}, ["Hello World"]] } +end