121925
From ae0065be15b3253042b65baa54f2953f3a6e6926 Mon Sep 17 00:00:00 2001
121925
From: usa <usa@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>
121925
Date: Wed, 28 Mar 2018 14:47:30 +0000
121925
Subject: [PATCH] merge revision(s) 62960-62965:
121925
MIME-Version: 1.0
121925
Content-Type: text/plain; charset=UTF-8
121925
Content-Transfer-Encoding: 8bit
121925
121925
	webrick: use IO.copy_stream for multipart response
121925
121925
	Use the new Proc response body feature to generate a multipart
121925
	range response dynamically.  We use a flat array to minimize
121925
	object overhead as much as possible; as many ranges may fit
121925
	into an HTTP request header.
121925
121925
	* lib/webrick/httpservlet/filehandler.rb (multipart_body): new method
121925
	  (make_partial_content): use multipart_body
121925
	------------------------------------------------------------------------
121925
	r62960 | normal | 2018-03-28 17:06:23 +0900 (水, 28 3 2018) | 13 lines
121925
121925
	webrick/httprequest: limit request headers size
121925
121925
	We use the same 112 KB limit started (AFAIK) by Mongrel, Thin,
121925
	and Puma to prevent malicious users from using up all the memory
121925
	with a single request.  This also limits the damage done by
121925
	excessive ranges in multipart Range: requests.
121925
121925
	Due to the way we rely on IO#gets and the desire to keep
121925
	the code simple, the actual maximum header may be 4093 bytes
121925
	larger than 112 KB, but we're splitting hairs at that point.
121925
121925
	* lib/webrick/httprequest.rb: define MAX_HEADER_LENGTH
121925
	  (read_header): raise when headers exceed max length
121925
	------------------------------------------------------------------------
121925
	r62961 | normal | 2018-03-28 17:06:28 +0900 (水, 28 3 2018) | 9 lines
121925
121925
	webrick/httpservlet/cgihandler: reduce memory use
121925
121925
	WEBrick::HTTPRequest#body can be passed a block to process the
121925
	body in chunks.  Use this feature to avoid building a giant
121925
	string in memory.
121925
121925
	* lib/webrick/httpservlet/cgihandler.rb (do_GET):
121925
	  avoid reading entire request body into memory
121925
	  (do_POST is aliased to do_GET, so it handles bodies)
121925
	------------------------------------------------------------------------
121925
	r62962 | normal | 2018-03-28 17:06:34 +0900 (水, 28 3 2018) | 7 lines
121925
121925
	webrick/httprequest: raise correct exception
121925
121925
	"BadRequest" alone does not resolve correctly, it is in the
121925
	HTTPStatus namespace.
121925
121925
	* lib/webrick/httprequest.rb (read_chunked): use correct exception
121925
	* test/webrick/test_httpserver.rb (test_eof_in_chunk): new test
121925
	------------------------------------------------------------------------
121925
	r62963 | normal | 2018-03-28 17:06:39 +0900 (水, 28 3 2018) | 9 lines
121925
121925
	webrick/httprequest: use InputBufferSize for chunked requests
121925
121925
	While WEBrick::HTTPRequest#body provides a Proc interface
121925
	for streaming large request bodies, clients must not force
121925
	the server to use an excessively large chunk size.
121925
121925
	* lib/webrick/httprequest.rb (read_chunk_size): limit each
121925
	  read and block.call to :InputBufferSize in config.
121925
	* test/webrick/test_httpserver.rb (test_big_chunks): new test
121925
	------------------------------------------------------------------------
121925
	r62964 | normal | 2018-03-28 17:06:44 +0900 (水, 28 3 2018) | 9 lines
121925
121925
	webrick: add test for Digest auth-int
121925
121925
	No changes to the actual code, this is a new test for
121925
	a feature for which no tests existed.  I don't understand
121925
	the Digest authentication code well at all, but this is
121925
	necessary for the subsequent change.
121925
121925
	* test/webrick/test_httpauth.rb (test_digest_auth_int): new test
121925
	  (credentials_for_request): support bodies with POST
121925
	------------------------------------------------------------------------
121925
	r62965 | normal | 2018-03-28 17:06:49 +0900 (水, 28 3 2018) | 18 lines
121925
121925
	webrick/httpauth/digestauth: stream req.body
121925
121925
	WARNING! WARNING! WARNING!  LIKELY BROKEN CHANGE
121925
121925
	Pass a proc to WEBrick::HTTPRequest#body to avoid reading a
121925
	potentially large request body into memory during
121925
	authentication.
121925
121925
	WARNING! this will break apps completely which want to do
121925
	something with the body besides calculating the MD5 digest
121925
	of it.
121925
121925
	Also, keep in mind that probably nobody uses "auth-int".
121925
	Servers such as Apache, lighttpd, nginx don't seem to
121925
	support it; nor does curl when using POST/PUT bodies;
121925
	and we didn't have tests for it until now...
121925
121925
	* lib/webrick/httpauth/digestauth.rb (_authenticate): stream req.body
121925
121925
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/branches/ruby_2_2@63021 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
121925
---
121925
 lib/webrick/httpauth/digestauth.rb    |  8 ++-
121925
 lib/webrick/httprequest.rb            | 23 +++++--
121925
 lib/webrick/httpservlet/cgihandler.rb |  4 +-
121925
 test/webrick/test_httpauth.rb         | 90 ++++++++++++++++++++++++++-
121925
 test/webrick/test_httpserver.rb       | 67 ++++++++++++++++++++
121925
 5 files changed, 178 insertions(+), 14 deletions(-)
121925
121925
diff --git a/lib/webrick/httpauth/digestauth.rb b/lib/webrick/httpauth/digestauth.rb
121925
index 78ad45b233..2a2319e9b1 100644
121925
--- a/lib/webrick/httpauth/digestauth.rb
121925
+++ b/lib/webrick/httpauth/digestauth.rb
121925
@@ -235,9 +235,11 @@ module WEBrick
121925
           ha2 = hexdigest(req.request_method, auth_req['uri'])
121925
           ha2_res = hexdigest("", auth_req['uri'])
121925
         elsif auth_req['qop'] == "auth-int"
121925
-          ha2 = hexdigest(req.request_method, auth_req['uri'],
121925
-                          hexdigest(req.body))
121925
-          ha2_res = hexdigest("", auth_req['uri'], hexdigest(res.body))
121925
+          body_digest = @h.new
121925
+          req.body { |chunk| body_digest.update(chunk) }
121925
+          body_digest = body_digest.hexdigest
121925
+          ha2 = hexdigest(req.request_method, auth_req['uri'], body_digest)
121925
+          ha2_res = hexdigest("", auth_req['uri'], body_digest)
121925
         end
121925
 
121925
         if auth_req['qop'] == "auth" || auth_req['qop'] == "auth-int"
121925
diff --git a/lib/webrick/httprequest.rb b/lib/webrick/httprequest.rb
121925
index 76420730b1..b3bcea7b3d 100644
121925
--- a/lib/webrick/httprequest.rb
121925
+++ b/lib/webrick/httprequest.rb
121925
@@ -412,9 +412,13 @@ module WEBrick
121925
 
121925
     MAX_URI_LENGTH = 2083 # :nodoc:
121925
 
121925
+    # same as Mongrel, Thin and Puma
121925
+    MAX_HEADER_LENGTH = (112 * 1024) # :nodoc:
121925
+
121925
     def read_request_line(socket)
121925
       @request_line = read_line(socket, MAX_URI_LENGTH) if socket
121925
-      if @request_line.bytesize >= MAX_URI_LENGTH and @request_line[-1, 1] != LF
121925
+      @request_bytes = @request_line.bytesize
121925
+      if @request_bytes >= MAX_URI_LENGTH and @request_line[-1, 1] != LF
121925
         raise HTTPStatus::RequestURITooLarge
121925
       end
121925
       @request_time = Time.now
121925
@@ -433,6 +437,9 @@ module WEBrick
121925
       if socket
121925
         while line = read_line(socket)
121925
           break if /\A(#{CRLF}|#{LF})\z/om =~ line
121925
+          if (@request_bytes += line.bytesize) > MAX_HEADER_LENGTH
121925
+            raise HTTPStatus::RequestEntityTooLarge, 'headers too large'
121925
+          end
121925
           @raw_header << line
121925
         end
121925
       end
121925
@@ -500,12 +507,16 @@ module WEBrick
121925
     def read_chunked(socket, block)
121925
       chunk_size, = read_chunk_size(socket)
121925
       while chunk_size > 0
121925
-        data = read_data(socket, chunk_size) # read chunk-data
121925
-        if data.nil? || data.bytesize != chunk_size
121925
-          raise BadRequest, "bad chunk data size."
121925
-        end
121925
+        begin
121925
+          sz = [ chunk_size, @buffer_size ].min
121925
+          data = read_data(socket, sz) # read chunk-data
121925
+          if data.nil? || data.bytesize != sz
121925
+            raise HTTPStatus::BadRequest, "bad chunk data size."
121925
+          end
121925
+          block.call(data)
121925
+        end while (chunk_size -= sz) > 0
121925
+
121925
         read_line(socket)                    # skip CRLF
121925
-        block.call(data)
121925
         chunk_size, = read_chunk_size(socket)
121925
       end
121925
       read_header(socket)                    # trailer + CRLF
121925
diff --git a/lib/webrick/httpservlet/cgihandler.rb b/lib/webrick/httpservlet/cgihandler.rb
121925
index 7c012ca64b..d5ba756437 100644
121925
--- a/lib/webrick/httpservlet/cgihandler.rb
121925
+++ b/lib/webrick/httpservlet/cgihandler.rb
121925
@@ -66,9 +66,7 @@ module WEBrick
121925
           cgi_in.write("%8d" % dump.bytesize)
121925
           cgi_in.write(dump)
121925
 
121925
-          if req.body and req.body.bytesize > 0
121925
-            cgi_in.write(req.body)
121925
-          end
121925
+          req.body { |chunk| cgi_in.write(chunk) }
121925
         ensure
121925
           cgi_in.close
121925
           status = $?.exitstatus
121925
diff --git a/test/webrick/test_httpauth.rb b/test/webrick/test_httpauth.rb
121925
index 2414be9096..842668f54e 100644
121925
--- a/test/webrick/test_httpauth.rb
121925
+++ b/test/webrick/test_httpauth.rb
121925
@@ -3,6 +3,7 @@ require "net/http"
121925
 require "tempfile"
121925
 require "webrick"
121925
 require "webrick/httpauth/basicauth"
121925
+require "stringio"
121925
 require_relative "utils"
121925
 
121925
 class TestWEBrickHTTPAuth < Test::Unit::TestCase
121925
@@ -182,12 +183,97 @@ class TestWEBrickHTTPAuth < Test::Unit::TestCase
121925
     }
121925
   end
121925
 
121925
+  def test_digest_auth_int
121925
+    log_tester = lambda {|log, access_log|
121925
+      log.reject! {|line| /\A\s*\z/ =~ line }
121925
+      pats = [
121925
+        /ERROR Digest wb auth-int realm: no credentials in the request\./,
121925
+        /ERROR WEBrick::HTTPStatus::Unauthorized/,
121925
+        /ERROR Digest wb auth-int realm: foo: digest unmatch\./
121925
+      ]
121925
+      pats.each {|pat|
121925
+        assert(!log.grep(pat).empty?, "webrick log doesn't have expected error: #{pat.inspect}")
121925
+        log.reject! {|line| pat =~ line }
121925
+      }
121925
+      assert_equal([], log)
121925
+   }
121925
+    TestWEBrick.start_httpserver({}, log_tester) {|server, addr, port, log|
121925
+      realm = "wb auth-int realm"
121925
+      path = "/digest_auth_int"
121925
+
121925
+      Tempfile.create("test_webrick_auth_int") {|tmpfile|
121925
+        tmpfile.close
121925
+        tmp_pass = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path)
121925
+        tmp_pass.set_passwd(realm, "foo", "Hunter2")
121925
+        tmp_pass.flush
121925
+
121925
+        htdigest = WEBrick::HTTPAuth::Htdigest.new(tmpfile.path)
121925
+        users = []
121925
+        htdigest.each{|user, pass| users << user }
121925
+        assert_equal %w(foo), users
121925
+
121925
+        auth = WEBrick::HTTPAuth::DigestAuth.new(
121925
+          :Realm => realm, :UserDB => htdigest,
121925
+          :Algorithm => 'MD5',
121925
+          :Logger => server.logger,
121925
+          :Qop => %w(auth-int),
121925
+        )
121925
+        server.mount_proc(path){|req, res|
121925
+          auth.authenticate(req, res)
121925
+          res.body = "bbb"
121925
+        }
121925
+        Net::HTTP.start(addr, port) do |http|
121925
+          post = Net::HTTP::Post.new(path)
121925
+          params = {}
121925
+          data = 'hello=world'
121925
+          body = StringIO.new(data)
121925
+          post.content_length = data.bytesize
121925
+          post['Content-Type'] = 'application/x-www-form-urlencoded'
121925
+          post.body_stream = body
121925
+
121925
+          http.request(post) do |res|
121925
+            assert_equal('401', res.code, log.call)
121925
+            res["www-authenticate"].scan(DIGESTRES_) do |key, quoted, token|
121925
+              params[key.downcase] = token || quoted.delete('\\')
121925
+            end
121925
+             params['uri'] = "http://#{addr}:#{port}#{path}"
121925
+          end
121925
+
121925
+          body.rewind
121925
+          cred = credentials_for_request('foo', 'Hunter3', params, body)
121925
+          post['Authorization'] = cred
121925
+          post.body_stream = body
121925
+          http.request(post){|res|
121925
+            assert_equal('401', res.code, log.call)
121925
+            assert_not_equal("bbb", res.body, log.call)
121925
+          }
121925
+
121925
+          body.rewind
121925
+          cred = credentials_for_request('foo', 'Hunter2', params, body)
121925
+          post['Authorization'] = cred
121925
+          post.body_stream = body
121925
+          http.request(post){|res| assert_equal("bbb", res.body, log.call)}
121925
+        end
121925
+      }
121925
+    }
121925
+  end
121925
+
121925
   private
121925
-  def credentials_for_request(user, password, params)
121925
+  def credentials_for_request(user, password, params, body = nil)
121925
     cnonce = "hoge"
121925
     nonce_count = 1
121925
     ha1 = "#{user}:#{params['realm']}:#{password}"
121925
-    ha2 = "GET:#{params['uri']}"
121925
+    if body
121925
+      dig = Digest::MD5.new
121925
+      while buf = body.read(16384)
121925
+        dig.update(buf)
121925
+      end
121925
+      body.rewind
121925
+      ha2 = "POST:#{params['uri']}:#{dig.hexdigest}"
121925
+    else
121925
+      ha2 = "GET:#{params['uri']}"
121925
+    end
121925
+
121925
     request_digest =
121925
       "#{Digest::MD5.hexdigest(ha1)}:" \
121925
       "#{params['nonce']}:#{'%08x' % nonce_count}:#{cnonce}:#{params['qop']}:" \
121925
diff --git a/test/webrick/test_httpserver.rb b/test/webrick/test_httpserver.rb
121925
index ffebf7e843..f1d58b40f5 100644
121925
--- a/test/webrick/test_httpserver.rb
121925
+++ b/test/webrick/test_httpserver.rb
121925
@@ -366,4 +366,71 @@ class TestWEBrickHTTPServer < Test::Unit::TestCase
121925
     }
121925
     assert_equal(requested, 1)
121925
   end
121925
+
121925
+  def test_gigantic_request_header
121925
+    log_tester = lambda {|log, access_log|
121925
+      assert_equal 1, log.size
121925
+      assert log[0].include?('ERROR headers too large')
121925
+    }
121925
+    TestWEBrick.start_httpserver({}, log_tester){|server, addr, port, log|
121925
+      server.mount('/', WEBrick::HTTPServlet::FileHandler, __FILE__)
121925
+      TCPSocket.open(addr, port) do |c|
121925
+        c.write("GET / HTTP/1.0\r\n")
121925
+        junk = "X-Junk: #{' ' * 1024}\r\n"
121925
+        assert_raise(Errno::ECONNRESET, Errno::EPIPE) do
121925
+          loop { c.write(junk) }
121925
+        end
121925
+      end
121925
+    }
121925
+  end
121925
+
121925
+  def test_eof_in_chunk
121925
+    log_tester = lambda do |log, access_log|
121925
+      assert_equal 1, log.size
121925
+      assert log[0].include?('ERROR bad chunk data size')
121925
+    end
121925
+    TestWEBrick.start_httpserver({}, log_tester){|server, addr, port, log|
121925
+      server.mount_proc('/', ->(req, res) { res.body = req.body })
121925
+      TCPSocket.open(addr, port) do |c|
121925
+        c.write("POST / HTTP/1.1\r\nHost: example.com\r\n" \
121925
+                "Transfer-Encoding: chunked\r\n\r\n5\r\na")
121925
+        c.shutdown(Socket::SHUT_WR) # trigger EOF in server
121925
+        res = c.read
121925
+        assert_match %r{\AHTTP/1\.1 400 }, res
121925
+      end
121925
+    }
121925
+  end
121925
+
121925
+  def test_big_chunks
121925
+    nr_out = 3
121925
+    buf = 'big' # 3 bytes is bigger than 2!
121925
+    config = { :InputBufferSize => 2 }.freeze
121925
+    total = 0
121925
+    all = ''
121925
+    TestWEBrick.start_httpserver(config){|server, addr, port, log|
121925
+      server.mount_proc('/', ->(req, res) {
121925
+        err = []
121925
+        ret = req.body do |chunk|
121925
+          n = chunk.bytesize
121925
+          n > config[:InputBufferSize] and err << "#{n} > :InputBufferSize"
121925
+          total += n
121925
+          all << chunk
121925
+        end
121925
+        ret.nil? or err << 'req.body should return nil'
121925
+        (buf * nr_out) == all or err << 'input body does not match expected'
121925
+        res.header['connection'] = 'close'
121925
+        res.body = err.join("\n")
121925
+      })
121925
+      TCPSocket.open(addr, port) do |c|
121925
+        c.write("POST / HTTP/1.1\r\nHost: example.com\r\n" \
121925
+                "Transfer-Encoding: chunked\r\n\r\n")
121925
+        chunk = "#{buf.bytesize.to_s(16)}\r\n#{buf}\r\n"
121925
+        nr_out.times { c.write(chunk) }
121925
+        c.write("0\r\n\r\n")
121925
+        head, body = c.read.split("\r\n\r\n")
121925
+        assert_match %r{\AHTTP/1\.1 200 OK}, head
121925
+        assert_nil body
121925
+      end
121925
+    }
121925
+  end
121925
 end
121925
-- 
121925
2.17.1
121925