# -*- 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 def initialize(env, input = env["rack.input"]) if trailer = env["HTTP_TRAILER"] unless trailer.split(/\s*,\s*/).grep(/\AContent-MD5\z/i)[0] trailer << (trailer.empty? ? "Content-MD5".freeze : ",Content-MD5".freeze) end else env["HTTP_TRAILER"] = "Content-MD5".freeze end env["HTTP_TRANSFER_ENCODING"] = "chunked".freeze @to_io, wr = HTTP_Spew::ChunkyPipe.new expect_md5 = env.delete("HTTP_CONTENT_MD5") expect_len = env.delete("CONTENT_LENGTH") start_write_driver(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 @bytes_digested = 0 if buf = input.read(0x4000) begin n = buf.size @bytes_digested += n wr.write("#{n.to_s(16)}\r\n") digest.update(buf) wr.write(buf << "\r\n".freeze) end while input.read(0x4000, buf) end if expect_len && expect_len.to_i != @bytes_digested raise HTTP_Spew::LengthError, "expect=#{expect_len} != got=#@bytes_digested", [] 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 rescue => e @to_io.error = e ensure wr.close end end end end