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