# -*- encoding: binary -*- require "digest/md5" # this uses a pipe internally so it can use IO.tee in the "io_splice" RubyGem class HTTP_Spew::ContentMD5 attr_reader :to_io attr_reader :content_md5 attr_reader :bytes_digested CRLF = "\r\n" # :nodoc: def initialize(env) if trailer = env["HTTP_TRAILER"] unless trailer.split(/\s*,\s*/).grep(/\AContent-MD5\z/i)[0] trailer << (trailer.empty? ? "Content-MD5" : ",Content-MD5") end else env["HTTP_TRAILER"] = "Content-MD5" end env["HTTP_TRANSFER_ENCODING"] = "chunked" @to_io, wr = HTTP_Spew::ChunkyPipe.new expect_md5 = env.delete("HTTP_CONTENT_MD5") expect_len = env.delete("CONTENT_LENGTH") start_write_driver(env["rack.input"], wr, expect_md5, expect_len) end # compatible with IO#read and Rack::InputWrapper#read def read(length, buffer = "") # calls HTTP_Spew::ChunkyPipe#read @to_io.read(length, buffer) end def start_write_driver(input, wr, expect_md5, expect_len) # :nodoc: Thread.new do begin digest = Digest::MD5.new buf = "" bytes = 0 while input.read(0x4000, buf) n = buf.size bytes += n wr.write("#{n.to_s(16)}\r\n") digest.update(buf) wr.write(buf << CRLF) end if expect_len && expect_len.to_i != bytes raise HTTP_Spew::LengthError, "expect=#{expect_len} != got=#{bytes}", [] end md5 = [ digest.digest ].pack("m0") if expect_md5 && expect_md5.strip != md5 raise HTTP_Spew::ChecksumError, "expect=#{expect_md5} != got=#{md5}", [] end wr.write "0\r\nContent-MD5: #{md5}\r\n\r\n" @content_md5 = md5 @bytes_digested = bytes rescue => e @to_io.error = e ensure wr.close end end end end