diff --git a/Gemfile b/Gemfile index 290f4251..a2c77230 100644 --- a/Gemfile +++ b/Gemfile @@ -5,7 +5,6 @@ gem "hoe-git" gem "hoe-ignore" gem "rdoc" gem "rake-compiler" -gem "rack" gem "test-unit", "~> 3.0" gem 'minitest', '~> 4.0' diff --git a/Rakefile b/Rakefile index e5b4ca93..e2461068 100644 --- a/Rakefile +++ b/Rakefile @@ -20,7 +20,7 @@ HOE = Hoe.spec "puma" do require_ruby_version ">= 1.8.7" - dependency "rack", [">= 1.1", "< 2.0"] + dependency "rack", [">= 1.1", "< 2.0"], :development extra_dev_deps << ["rake-compiler", "~> 0.8"] end diff --git a/lib/puma/binder.rb b/lib/puma/binder.rb index d8d3f8ab..534d71f7 100644 --- a/lib/puma/binder.rb +++ b/lib/puma/binder.rb @@ -4,6 +4,8 @@ module Puma class Binder include Puma::Const + RACK_VERSION = [1,3].freeze + def initialize(events) @events = events @listeners = [] @@ -11,7 +13,7 @@ module Puma @unix_paths = [] @proto_env = { - "rack.version".freeze => Rack::VERSION, + "rack.version".freeze => RACK_VERSION, "rack.errors".freeze => events.stderr, "rack.multithread".freeze => true, "rack.multiprocess".freeze => false, @@ -87,7 +89,7 @@ module Puma logger.log "* Inherited #{str}" io = inherit_tcp_listener uri.host, uri.port, fd else - params = Rack::Utils.parse_query uri.query + params = Util.parse_query uri.query opt = params.key?('low_latency') bak = params.fetch('backlog', 1024).to_i @@ -110,7 +112,7 @@ module Puma mode = nil if uri.query - params = Rack::Utils.parse_query uri.query + params = Util.parse_query uri.query if u = params['umask'] # Use Integer() to respect the 0 prefix as octal umask = Integer(u) @@ -126,7 +128,7 @@ module Puma @listeners << [str, io] when "ssl" - params = Rack::Utils.parse_query uri.query + params = Util.parse_query uri.query require 'puma/minissl' ctx = MiniSSL::Context.new diff --git a/lib/puma/cli.rb b/lib/puma/cli.rb index 2e0ff6da..0a0cc051 100644 --- a/lib/puma/cli.rb +++ b/lib/puma/cli.rb @@ -11,8 +11,7 @@ require 'puma/util' require 'puma/single' require 'puma/cluster' -require 'rack/commonlogger' -require 'rack/utils' +require 'puma/commonlogger' module Puma class << self diff --git a/lib/puma/commonlogger.rb b/lib/puma/commonlogger.rb new file mode 100644 index 00000000..dc7c61c2 --- /dev/null +++ b/lib/puma/commonlogger.rb @@ -0,0 +1,107 @@ +module Puma + # Rack::CommonLogger forwards every request to the given +app+, and + # logs a line in the + # {Apache common log format}[http://httpd.apache.org/docs/1.3/logs.html#common] + # to the +logger+. + # + # If +logger+ is nil, CommonLogger will fall back +rack.errors+, which is + # an instance of Rack::NullLogger. + # + # +logger+ can be any class, including the standard library Logger, and is + # expected to have either +write+ or +<<+ method, which accepts the CommonLogger::FORMAT. + # According to the SPEC, the error stream must also respond to +puts+ + # (which takes a single argument that responds to +to_s+), and +flush+ + # (which is called without arguments in order to make the error appear for + # sure) + class CommonLogger + # Common Log Format: http://httpd.apache.org/docs/1.3/logs.html#common + # + # lilith.local - - [07/Aug/2006 23:58:02 -0400] "GET / HTTP/1.1" 500 - + # + # %{%s - %s [%s] "%s %s%s %s" %d %s\n} % + FORMAT = %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n} + + def initialize(app, logger=nil) + @app = app + @logger = logger + end + + def call(env) + began_at = Time.now + status, header, body = @app.call(env) + header = Util::HeaderHash.new(header) + + # If we've been hijacked, then output a special line + if env['rack.hijack_io'] + log_hijacking(env, 'HIJACK', header, began_at) + else + ary = env['rack.after_reply'] + ary << lambda { log(env, status, header, began_at) } + end + + [status, header, body] + end + + HIJACK_FORMAT = %{%s - %s [%s] "%s %s%s %s" HIJACKED -1 %0.4f\n} + + private + + def log_hijacking(env, status, header, began_at) + now = Time.now + + logger = @logger || env['rack.errors'] + logger.write HIJACK_FORMAT % [ + env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-", + env["REMOTE_USER"] || "-", + now.strftime("%d/%b/%Y %H:%M:%S"), + env["REQUEST_METHOD"], + env["PATH_INFO"], + env["QUERY_STRING"].empty? ? "" : "?"+env["QUERY_STRING"], + env["HTTP_VERSION"], + now - began_at ] + end + + PATH_INFO = 'PATH_INFO'.freeze + REQUEST_METHOD = 'REQUEST_METHOD'.freeze + SCRIPT_NAME = 'SCRIPT_NAME'.freeze + QUERY_STRING = 'QUERY_STRING'.freeze + CACHE_CONTROL = 'Cache-Control'.freeze + CONTENT_LENGTH = 'Content-Length'.freeze + CONTENT_TYPE = 'Content-Type'.freeze + + GET = 'GET'.freeze + HEAD = 'HEAD'.freeze + + + def log(env, status, header, began_at) + now = Time.now + length = extract_content_length(header) + + msg = FORMAT % [ + env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-", + env["REMOTE_USER"] || "-", + now.strftime("%d/%b/%Y:%H:%M:%S %z"), + env[REQUEST_METHOD], + env[PATH_INFO], + env[QUERY_STRING].empty? ? "" : "?"+env[QUERY_STRING], + env["HTTP_VERSION"], + status.to_s[0..3], + length, + now - began_at ] + + logger = @logger || env['rack.errors'] + # Standard library logger doesn't support write but it supports << which actually + # calls to write on the log device without formatting + if logger.respond_to?(:write) + logger.write(msg) + else + logger << msg + end + end + + def extract_content_length(headers) + value = headers[CONTENT_LENGTH] or return '-' + value.to_s == '0' ? '-' : value + end + end +end diff --git a/lib/puma/configuration.rb b/lib/puma/configuration.rb index 7db3128b..24ee2fe3 100644 --- a/lib/puma/configuration.rb +++ b/lib/puma/configuration.rb @@ -1,4 +1,4 @@ -require 'rack/builder' +require 'puma/rack/builder' module Puma @@ -79,7 +79,7 @@ module Puma if !@options[:quiet] and @options[:environment] == "development" logger = @options[:logger] || STDOUT - found = Rack::CommonLogger.new(found, logger) + found = CommonLogger.new(found, logger) end ConfigMiddleware.new(self, found) @@ -101,7 +101,7 @@ module Puma def load_rackup raise "Missing rackup file '#{rackup}'" unless File.exist?(rackup) - rack_app, rack_options = Rack::Builder.parse_file(rackup) + rack_app, rack_options = Puma::Rack::Builder.parse_file(rackup) @options.merge!(rack_options) config_ru_binds = rack_options.each_with_object([]) do |(k, v), b| diff --git a/lib/puma/const.rb b/lib/puma/const.rb index 878bc4fd..dd766b19 100644 --- a/lib/puma/const.rb +++ b/lib/puma/const.rb @@ -1,5 +1,3 @@ -require 'rack' - module Puma class UnsupportedOption < RuntimeError end @@ -8,12 +6,85 @@ module Puma # Every standard HTTP code mapped to the appropriate message. These are # used so frequently that they are placed directly in Puma for easy # access rather than Puma::Const itself. - HTTP_STATUS_CODES = Rack::Utils::HTTP_STATUS_CODES + + # Every standard HTTP code mapped to the appropriate message. + # Generated with: + # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \ + # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \ + # puts "#{m[1]} => \x27#{m[2].strip}\x27,"' + HTTP_STATUS_CODES = { + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 208 => 'Already Reported', + 226 => 'IM Used', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Payload Too Large', + 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Range Not Satisfiable', + 417 => 'Expectation Failed', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 510 => 'Not Extended', + 511 => 'Network Authentication Required' + } + + SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message| + [message.downcase.gsub(/\s|-|'/, '_').to_sym, code] + }.flatten] # For some HTTP status codes the client only expects headers. - STATUS_WITH_NO_ENTITY_BODY = Hash[Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.map { |s| - [s, true] - }] + # + + no_body = {} + ((100..199).to_a << 204 << 205 << 304).each do |code| + no_body[code] = true + end + + STATUS_WITH_NO_ENTITY_BODY = no_body # Frequently used constants when constructing requests or responses. Many times # the constant just refers to a string with the same contents. Using these constants diff --git a/lib/puma/rack/backports/uri/common_18.rb b/lib/puma/rack/backports/uri/common_18.rb new file mode 100644 index 00000000..ca3a6360 --- /dev/null +++ b/lib/puma/rack/backports/uri/common_18.rb @@ -0,0 +1,56 @@ +# :stopdoc: + +# Stolen from ruby core's uri/common.rb, with modifications to support 1.8.x +# +# https://github.com/ruby/ruby/blob/trunk/lib/uri/common.rb +# +# + +module URI + TBLENCWWWCOMP_ = {} # :nodoc: + 256.times do |i| + TBLENCWWWCOMP_[i.chr] = '%%%02X' % i + end + TBLENCWWWCOMP_[' '] = '+' + TBLENCWWWCOMP_.freeze + TBLDECWWWCOMP_ = {} # :nodoc: + 256.times do |i| + h, l = i>>4, i&15 + TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr + end + TBLDECWWWCOMP_['+'] = ' ' + TBLDECWWWCOMP_.freeze + + # Encode given +s+ to URL-encoded form data. + # + # This method doesn't convert *, -, ., 0-9, A-Z, _, a-z, but does convert SP + # (ASCII space) to + and converts others to %XX. + # + # This is an implementation of + # http://www.w3.org/TR/html5/forms.html#url-encoded-form-data + # + # See URI.decode_www_form_component, URI.encode_www_form + def self.encode_www_form_component(s) + str = s.to_s + if RUBY_VERSION < "1.9" && $KCODE =~ /u/i + str.gsub(/([^ a-zA-Z0-9_.-]+)/) do + '%' + $1.unpack('H2' * Rack::Utils.bytesize($1)).join('%').upcase + end.tr(' ', '+') + else + str.gsub(/[^*\-.0-9A-Z_a-z]/) {|m| TBLENCWWWCOMP_[m]} + end + end + + # Decode given +str+ of URL-encoded form data. + # + # This decodes + to SP. + # + # See URI.encode_www_form_component, URI.decode_www_form + def self.decode_www_form_component(str, enc=nil) + raise ArgumentError, "invalid %-encoding (#{str})" unless /\A(?:%[0-9a-fA-F]{2}|[^%])*\z/ =~ str + str.gsub(/\+|%[0-9a-fA-F]{2}/) {|m| TBLDECWWWCOMP_[m]} + end +end diff --git a/lib/puma/rack/backports/uri/common_192.rb b/lib/puma/rack/backports/uri/common_192.rb new file mode 100644 index 00000000..1a0522bf --- /dev/null +++ b/lib/puma/rack/backports/uri/common_192.rb @@ -0,0 +1,52 @@ +# :stopdoc: + +# Stolen from ruby core's uri/common.rb @32618ba to fix DoS issues in 1.9.2 +# +# https://github.com/ruby/ruby/blob/32618ba7438a2247042bba9b5d85b5d49070f5e5/lib/uri/common.rb +# +# Issue: +# http://redmine.ruby-lang.org/issues/5149 +# +# Relevant Fixes: +# https://github.com/ruby/ruby/commit/b5f91deee04aa6ccbe07c23c8222b937c22a799b +# https://github.com/ruby/ruby/commit/93177c1e5c3906abf14472ae0b905d8b5c72ce1b +# +# This should probably be removed once there is a Ruby 1.9.2 patch level that +# includes this fix. + +require 'uri/common' + +module URI + TBLDECWWWCOMP_ = {} unless const_defined?(:TBLDECWWWCOMP_) #:nodoc: + if TBLDECWWWCOMP_.empty? + 256.times do |i| + h, l = i>>4, i&15 + TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr + end + TBLDECWWWCOMP_['+'] = ' ' + TBLDECWWWCOMP_.freeze + end + + def self.decode_www_form(str, enc=Encoding::UTF_8) + return [] if str.empty? + unless /\A#{WFKV_}=#{WFKV_}(?:[;&]#{WFKV_}=#{WFKV_})*\z/o =~ str + raise ArgumentError, "invalid data of application/x-www-form-urlencoded (#{str})" + end + ary = [] + $&.scan(/([^=;&]+)=([^;&]*)/) do + ary << [decode_www_form_component($1, enc), decode_www_form_component($2, enc)] + end + ary + end + + def self.decode_www_form_component(str, enc=Encoding::UTF_8) + raise ArgumentError, "invalid %-encoding (#{str})" unless /\A[^%]*(?:%\h\h[^%]*)*\z/ =~ str + str.gsub(/\+|%\h\h/, TBLDECWWWCOMP_).force_encoding(enc) + end + + remove_const :WFKV_ if const_defined?(:WFKV_) + WFKV_ = '(?:[^%#=;&]*(?:%\h\h[^%#=;&]*)*)' # :nodoc: +end diff --git a/lib/puma/rack/backports/uri/common_193.rb b/lib/puma/rack/backports/uri/common_193.rb new file mode 100644 index 00000000..2e582033 --- /dev/null +++ b/lib/puma/rack/backports/uri/common_193.rb @@ -0,0 +1,29 @@ +# :stopdoc: + +require 'uri/common' + +# Issue: +# http://bugs.ruby-lang.org/issues/5925 +# +# Relevant commit: +# https://github.com/ruby/ruby/commit/edb7cdf1eabaff78dfa5ffedfbc2e91b29fa9ca1 + +module URI + 256.times do |i| + TBLENCWWWCOMP_[i.chr] = '%%%02X' % i + end + TBLENCWWWCOMP_[' '] = '+' + TBLENCWWWCOMP_.freeze + + 256.times do |i| + h, l = i>>4, i&15 + TBLDECWWWCOMP_['%%%X%X' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%x%X' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%X%x' % [h, l]] = i.chr + TBLDECWWWCOMP_['%%%x%x' % [h, l]] = i.chr + end + TBLDECWWWCOMP_['+'] = ' ' + TBLDECWWWCOMP_.freeze +end + +# :startdoc: diff --git a/lib/puma/rack/builder.rb b/lib/puma/rack/builder.rb new file mode 100644 index 00000000..88789f74 --- /dev/null +++ b/lib/puma/rack/builder.rb @@ -0,0 +1,298 @@ +module PumaRackCompat + BINDING = binding + + def self.const_missing(cm) + if cm == :Rack + require 'rack' + Rack + else + raise NameError, "constant undefined: #{cm}" + end + end +end + +module Puma::Rack + class Options + def parse!(args) + options = {} + opt_parser = OptionParser.new("", 24, ' ') do |opts| + opts.banner = "Usage: rackup [ruby options] [rack options] [rackup config]" + + opts.separator "" + opts.separator "Ruby options:" + + lineno = 1 + opts.on("-e", "--eval LINE", "evaluate a LINE of code") { |line| + eval line, TOPLEVEL_BINDING, "-e", lineno + lineno += 1 + } + + opts.on("-b", "--builder BUILDER_LINE", "evaluate a BUILDER_LINE of code as a builder script") { |line| + options[:builder] = line + } + + opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") { + options[:debug] = true + } + opts.on("-w", "--warn", "turn warnings on for your script") { + options[:warn] = true + } + opts.on("-q", "--quiet", "turn off logging") { + options[:quiet] = true + } + + opts.on("-I", "--include PATH", + "specify $LOAD_PATH (may be used more than once)") { |path| + (options[:include] ||= []).concat(path.split(":")) + } + + opts.on("-r", "--require LIBRARY", + "require the library, before executing your script") { |library| + options[:require] = library + } + + opts.separator "" + opts.separator "Rack options:" + opts.on("-s", "--server SERVER", "serve using SERVER (thin/puma/webrick/mongrel)") { |s| + options[:server] = s + } + + opts.on("-o", "--host HOST", "listen on HOST (default: localhost)") { |host| + options[:Host] = host + } + + opts.on("-p", "--port PORT", "use PORT (default: 9292)") { |port| + options[:Port] = port + } + + opts.on("-O", "--option NAME[=VALUE]", "pass VALUE to the server as option NAME. If no VALUE, sets it to true. Run '#{$0} -s SERVER -h' to get a list of options for SERVER") { |name| + name, value = name.split('=', 2) + value = true if value.nil? + options[name.to_sym] = value + } + + opts.on("-E", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: development)") { |e| + options[:environment] = e + } + + opts.on("-D", "--daemonize", "run daemonized in the background") { |d| + options[:daemonize] = d ? true : false + } + + opts.on("-P", "--pid FILE", "file to store PID") { |f| + options[:pid] = ::File.expand_path(f) + } + + opts.separator "" + opts.separator "Common options:" + + opts.on_tail("-h", "-?", "--help", "Show this message") do + puts opts + puts handler_opts(options) + + exit + end + + opts.on_tail("--version", "Show version") do + puts "Rack #{Rack.version} (Release: #{Rack.release})" + exit + end + end + + begin + opt_parser.parse! args + rescue OptionParser::InvalidOption => e + warn e.message + abort opt_parser.to_s + end + + options[:config] = args.last if args.last + options + end + + def handler_opts(options) + begin + info = [] + server = Rack::Handler.get(options[:server]) || Rack::Handler.default(options) + if server && server.respond_to?(:valid_options) + info << "" + info << "Server-specific options for #{server.name}:" + + has_options = false + server.valid_options.each do |name, description| + next if name.to_s.match(/^(Host|Port)[^a-zA-Z]/) # ignore handler's host and port options, we do our own. + info << " -O %-21s %s" % [name, description] + has_options = true + end + return "" if !has_options + end + info.join("\n") + rescue NameError + return "Warning: Could not find handler specified (#{options[:server] || 'default'}) to determine handler-specific options" + end + end + end + + # Rack::Builder implements a small DSL to iteratively construct Rack + # applications. + # + # Example: + # + # require 'rack/lobster' + # app = Rack::Builder.new do + # use Rack::CommonLogger + # use Rack::ShowExceptions + # map "/lobster" do + # use Rack::Lint + # run Rack::Lobster.new + # end + # end + # + # run app + # + # Or + # + # app = Rack::Builder.app do + # use Rack::CommonLogger + # run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] } + # end + # + # run app + # + # +use+ adds middleware to the stack, +run+ dispatches to an application. + # You can use +map+ to construct a Rack::URLMap in a convenient way. + + class Builder + def self.parse_file(config, opts = Options.new) + options = {} + if config =~ /\.ru$/ + cfgfile = ::File.read(config) + if cfgfile[/^#\\(.*)/] && opts + options = opts.parse! $1.split(/\s+/) + end + cfgfile.sub!(/^__END__\n.*\Z/m, '') + app = new_from_string cfgfile, config + else + require config + app = Object.const_get(::File.basename(config, '.rb').capitalize) + end + return app, options + end + + def self.new_from_string(builder_script, file="(rackup)") + eval "Puma::Rack::Builder.new {\n" + builder_script + "\n}.to_app", + PumaRackCompat::BINDING, file, 0 + end + + def initialize(default_app = nil,&block) + @use, @map, @run, @warmup = [], nil, default_app, nil + instance_eval(&block) if block_given? + end + + def self.app(default_app = nil, &block) + self.new(default_app, &block).to_app + end + + # Specifies middleware to use in a stack. + # + # class Middleware + # def initialize(app) + # @app = app + # end + # + # def call(env) + # env["rack.some_header"] = "setting an example" + # @app.call(env) + # end + # end + # + # use Middleware + # run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] } + # + # All requests through to this application will first be processed by the middleware class. + # The +call+ method in this example sets an additional environment key which then can be + # referenced in the application if required. + def use(middleware, *args, &block) + if @map + mapping, @map = @map, nil + @use << proc { |app| generate_map app, mapping } + end + @use << proc { |app| middleware.new(app, *args, &block) } + end + + # Takes an argument that is an object that responds to #call and returns a Rack response. + # The simplest form of this is a lambda object: + # + # run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] } + # + # However this could also be a class: + # + # class Heartbeat + # def self.call(env) + # [200, { "Content-Type" => "text/plain" }, ["OK"]] + # end + # end + # + # run Heartbeat + def run(app) + @run = app + end + + # Takes a lambda or block that is used to warm-up the application. + # + # warmup do |app| + # client = Rack::MockRequest.new(app) + # client.get('/') + # end + # + # use SomeMiddleware + # run MyApp + def warmup(prc=nil, &block) + @warmup = prc || block + end + + # Creates a route within the application. + # + # Rack::Builder.app do + # map '/' do + # run Heartbeat + # end + # end + # + # The +use+ method can also be used here to specify middleware to run under a specific path: + # + # Rack::Builder.app do + # map '/' do + # use Middleware + # run Heartbeat + # end + # end + # + # This example includes a piece of middleware which will run before requests hit +Heartbeat+. + # + def map(path, &block) + @map ||= {} + @map[path] = block + end + + def to_app + app = @map ? generate_map(@run, @map) : @run + fail "missing run or map statement" unless app + app = @use.reverse.inject(app) { |a,e| e[a] } + @warmup.call(app) if @warmup + app + end + + def call(env) + to_app.call(env) + end + + private + + def generate_map(default_app, mapping) + mapped = default_app ? {'/' => default_app} : {} + mapping.each { |r,b| mapped[r] = self.class.new(default_app, &b).to_app } + URLMap.new(mapped) + end + end +end diff --git a/lib/puma/rack_patch.rb b/lib/puma/rack_patch.rb index abbdd944..5e273348 100644 --- a/lib/puma/rack_patch.rb +++ b/lib/puma/rack_patch.rb @@ -1,6 +1,6 @@ -require 'rack/commonlogger' +require 'puma/rack/commonlogger' -module Rack +module Puma::Rack # Patch CommonLogger to use after_reply. # # Simply request this file and CommonLogger will be a bit more diff --git a/lib/puma/server.rb b/lib/puma/server.rb index 1a16f069..bde940a7 100644 --- a/lib/puma/server.rb +++ b/lib/puma/server.rb @@ -1,4 +1,3 @@ -require 'rack' require 'stringio' require 'puma/thread_pool' @@ -13,8 +12,6 @@ require 'puma/delegation' require 'puma/accept_nonblock' require 'puma/util' -require 'puma/rack_patch' - require 'puma/puma_http11' unless Puma.const_defined? "IOBuffer" diff --git a/lib/puma/util.rb b/lib/puma/util.rb index ee7b308e..d46015a2 100644 --- a/lib/puma/util.rb +++ b/lib/puma/util.rb @@ -1,3 +1,15 @@ +major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i } + +if major == 1 && minor < 9 + require 'puma/rack/backports/uri/common_18' +elsif major == 1 && minor == 9 && patch == 2 && RUBY_PATCHLEVEL <= 328 && RUBY_ENGINE != 'jruby' + require 'puma/rack/backports/uri/common_192' +elsif major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125 + require 'puma/rack/backports/uri/common_193' +else + require 'uri/common' +end + module Puma module Util module_function @@ -5,5 +17,116 @@ module Puma def pipe IO.pipe end + + # Unescapes a URI escaped string with +encoding+. +encoding+ will be the + # target encoding of the string returned, and it defaults to UTF-8 + if defined?(::Encoding) + def unescape(s, encoding = Encoding::UTF_8) + URI.decode_www_form_component(s, encoding) + end + else + def unescape(s, encoding = nil) + URI.decode_www_form_component(s, encoding) + end + end + module_function :unescape + + DEFAULT_SEP = /[&;] */n + + # Stolen from Mongrel, with some small modifications: + # Parses a query string by breaking it up at the '&' + # and ';' characters. You can also use this to parse + # cookies by changing the characters used in the second + # parameter (which defaults to '&;'). + def parse_query(qs, d = nil, &unescaper) + unescaper ||= method(:unescape) + + params = {} + + (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p| + next if p.empty? + k, v = p.split('=', 2).map(&unescaper) + + if cur = params[k] + if cur.class == Array + params[k] << v + else + params[k] = [cur, v] + end + else + params[k] = v + end + end + + return params + end + + # A case-insensitive Hash that preserves the original case of a + # header when set. + class HeaderHash < Hash + def self.new(hash={}) + HeaderHash === hash ? hash : super(hash) + end + + def initialize(hash={}) + super() + @names = {} + hash.each { |k, v| self[k] = v } + end + + def each + super do |k, v| + yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v) + end + end + + def to_hash + hash = {} + each { |k,v| hash[k] = v } + hash + end + + def [](k) + super(k) || super(@names[k.downcase]) + end + + def []=(k, v) + canonical = k.downcase + delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary + @names[k] = @names[canonical] = k + super k, v + end + + def delete(k) + canonical = k.downcase + result = super @names.delete(canonical) + @names.delete_if { |name,| name.downcase == canonical } + result + end + + def include?(k) + @names.include?(k) || @names.include?(k.downcase) + end + + alias_method :has_key?, :include? + alias_method :member?, :include? + alias_method :key?, :include? + + def merge!(other) + other.each { |k, v| self[k] = v } + self + end + + def merge(other) + hash = dup + hash.merge! other + end + + def replace(other) + clear + other.each { |k, v| self[k] = v } + self + end + end end end diff --git a/puma.gemspec b/puma.gemspec index 4f3ce347..741ef55e 100644 --- a/puma.gemspec +++ b/puma.gemspec @@ -36,18 +36,15 @@ Gem::Specification.new do |s| s.specification_version = 3 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then - s.add_runtime_dependency(%q, ["< 2.0", ">= 1.1"]) s.add_development_dependency(%q, ["~> 4.0"]) s.add_development_dependency(%q, ["~> 0.8.0"]) s.add_development_dependency(%q, ["~> 3.6"]) else - s.add_dependency(%q, ["< 2.0", ">= 1.1"]) s.add_dependency(%q, ["~> 4.0"]) s.add_dependency(%q, ["~> 0.8.0"]) s.add_dependency(%q, ["~> 3.6"]) end else - s.add_dependency(%q, ["< 2.0", ">= 1.1"]) s.add_dependency(%q, ["~> 4.0"]) s.add_dependency(%q, ["~> 0.8.0"]) s.add_dependency(%q, ["~> 3.6"]) diff --git a/test/test_rack_server.rb b/test/test_rack_server.rb index 4e60014a..4fd46104 100644 --- a/test/test_rack_server.rb +++ b/test/test_rack_server.rb @@ -2,8 +2,7 @@ require 'test/unit' require 'puma' require 'rack/lint' require 'test/testhelp' -require 'rack/commonlogger' -require 'puma/rack_patch' +require 'puma/commonlogger' class TestRackServer < Test::Unit::TestCase