From 537bc21593182cd9c4c0079a3936d05b1f91fe14 Mon Sep 17 00:00:00 2001 From: Evan Phoenix Date: Tue, 14 Jul 2015 10:28:59 -0700 Subject: [PATCH] Remove rack dependency. Fixes #705 Because frameworks like rails dependent on rack, if puma truly wants to be able to reload new code and thus new versions of rails, it has to be able to reload rack as well. Having a dependency on rack held by puma prevented that from happening and so that dependency has been removed. --- Gemfile | 1 - Rakefile | 2 +- lib/puma/binder.rb | 10 +- lib/puma/cli.rb | 3 +- lib/puma/commonlogger.rb | 107 ++++++++ lib/puma/configuration.rb | 6 +- lib/puma/const.rb | 83 +++++- lib/puma/rack/backports/uri/common_18.rb | 56 ++++ lib/puma/rack/backports/uri/common_192.rb | 52 ++++ lib/puma/rack/backports/uri/common_193.rb | 29 +++ lib/puma/rack/builder.rb | 298 ++++++++++++++++++++++ lib/puma/rack_patch.rb | 4 +- lib/puma/server.rb | 3 - lib/puma/util.rb | 123 +++++++++ puma.gemspec | 3 - test/test_rack_server.rb | 3 +- 16 files changed, 756 insertions(+), 27 deletions(-) create mode 100644 lib/puma/commonlogger.rb create mode 100644 lib/puma/rack/backports/uri/common_18.rb create mode 100644 lib/puma/rack/backports/uri/common_192.rb create mode 100644 lib/puma/rack/backports/uri/common_193.rb create mode 100644 lib/puma/rack/builder.rb 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