【问题标题】:Is it possible to access the compressed data before decompression in HttpClient?在HttpClient中解压前是否可以访问压缩数据?
【发布时间】:2020-02-18 12:29:40
【问题描述】:

我正在处理Google Cloud Storage .NET client library。共有三个功能(在.NET、我的客户端 库和存储服务)组合在一个 不愉快的方式:

  • 下载文件时(Google Cloud Storage 中的对象) 术语),服务器包括存储数据的哈希值。我的 客户端代码然后根据它的数据验证该哈希 已下载。

  • Google 云存储的一个单独功能是用户可以 设置对象的 Content-Encoding,并将其包含为 下载时标头,当请求包含匹配时 接受编码。 (目前,让我们忽略当 请求不包括那个...)

  • HttpClientHandler可以解压gzip(或deflate)内容 自动透明。

当所有这三个结合在一起时,我们就会遇到麻烦。这是一个 简短但完整的程序证明了这一点,但没有使用我的 客户端库(并点击可公开访问的文件):

using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string url = "https://www.googleapis.com/download/storage/v1/b/"
            + "storage-library-test-bucket/o/gzipped-text.txt?alt=media";
        var handler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.GZip
        };
        var client = new HttpClient(handler);

        var response = await client.GetAsync(url);
        byte[] content = await response.Content.ReadAsByteArrayAsync();
        string text = Encoding.UTF8.GetString(content);
        Console.WriteLine($"Content: {text}");

        var hashHeader = response.Headers.GetValues("X-Goog-Hash").FirstOrDefault();
        Console.WriteLine($"Hash header: {hashHeader}");

        using (var md5 = MD5.Create())
        {
            var md5Hash = md5.ComputeHash(content);
            var md5HashBase64 = Convert.ToBase64String(md5Hash);
            Console.WriteLine($"MD5 of content: {md5HashBase64}");
        }
    }
}

.NET Core 项目文件:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <LangVersion>7.1</LangVersion>
  </PropertyGroup>
</Project>

输出:

Content: hello world
Hash header: crc32c=T1s5RQ==,md5=xhF4M6pNFRDQnvaRRNVnkA==
MD5 of content: XrY7u+Ae7tCTyyK7j1rNww==

如您所见,内容的 MD5 与 MD5 不同 X-Goog-Hash 标头的一部分。 (在我的客户端库中,我使用的是 crc32c 哈希,但显示相同的行为。)

这不是 HttpClientHandler 中的错误 - 这是意料之中的,但很痛苦 当我想验证哈希时。基本上,我需要在 解压后的内容。我找不到任何方法 这样做。

为了稍微澄清我的要求,我知道如何防止HttpClient 中的解压缩,而是在从流中读取之后解压缩 - 但我需要能够在不更改任何使用生成的@987654330 的代码的情况下做到这一点@来自HttpClient。 (处理响应的代码很多,我只想在一个中心位置进行更改。)

我有一个计划,我已经制作了原型,并且可以尽我所能 到目前为止发现,但有点难看。它涉及创建一个三层 处理程序:

  • HttpClientHandler 禁用自动解压。
  • 用新的Stream 子类替换内容流的新处理程序 它委托给原始内容流,但在读取数据时对其进行哈希处理。
  • 仅解压缩处理程序,基于 Microsoft DecompressionHandler 代码。

虽然这可行,但它有以下缺点:

  • 开源许可:准确检查我需要做的事情 根据 MIT 许可在我的仓库中创建一个新文件 微软代码
  • 有效地分叉 MS 代码,这意味着我可能应该 定期检查是否发现任何错误
  • Microsoft 代码使用程序集的内部成员,因此它 没有尽可能干净地移植。

如果微软将DecompressionHandler 公开,那将有助于 很多 - 但这可能比我需要的时间更长。

如果可能的话,我正在寻找一种替代方法 - 我错过了一些让我之前了解内容的东西 减压。我不想重新发明HttpClient - 回复 例如,经常被分块,我不想进入 事情的那一面。这是一个非常具体的拦截点 我在找。

【问题讨论】:

  • 在我看来,这里的压缩部分,就存储方面而言,有点像这样“我确实有一个未压缩的文件,但如果我可以将它压缩存储,那就太好了让浏览器的解压部分自动解压”。如果是这样,那么存储/提供解压缩内容的哈希是否有意义?听起来这只是服务器空间和cpu优化,避免服务器端的压缩步骤。我在这里想念什么?很多客户端库不会因此而出现完全相同的问题吗?
  • @LasseVågsætherKarlsen:如果响应可以同时包含压缩数据的散列未压缩数据(您不希望客户端必须解压缩它),那就太好了只是为了散列,如果他们想让它保持压缩,否则我怀疑我是否能够通过这种变化。是的,其他一些客户端库可能确实有同样的问题 - 但我正在与谷歌官方的维护者保持联系,他们正在检查它:)
  • @LasseVågsætherKarlsen:如果您仍在从 GCS 获取数据,那不是 HttpClientHandler 这样做 - 那是 GCS。如果您请求一个 Content-Encoding 为 gzip 的文件,但您没有指定 Accept-Encoding: gzip,它会为您解压缩,提供没有 Content-Encoding 标头的解压缩内容。 (并且仍然包括压缩文件的哈希值。我知道,这是有问题的......我不想进入 all 这个问题中可能存在的怪癖,但如果你认为我应该提到这一点。)
  • 简而言之,这个哈希似乎被设计无法验证,这对我来说听起来毫无意义。
  • @zaitsman:我通常比源代码更相信我在网络上看到的内容 :) 我一直在 .NET Core 上运行我的大部分测试,但在 Windows 上 - 这就是绝对可以禁用压缩。

标签: c# .net-core google-cloud-storage md5 dotnet-httpclient


【解决方案1】:

看看@Michael 的所作所为给了我我所缺少的提示。获得压缩内容后,您可以使用CryptoStreamGZipStreamStreamReader 来读取响应,而无需将其加载到内存中超过需要的部分。 CryptoStream 将在解压缩和读取压缩内容时对其进行哈希处理。将StreamReader 替换为FileStream,您可以将数据写入内存占用最少的文件:)

using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string url = "https://www.googleapis.com/download/storage/v1/b/"
            + "storage-library-test-bucket/o/gzipped-text.txt?alt=media";
        var handler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.None
        };
        var client = new HttpClient(handler);
        client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip");

        var response = await client.GetAsync(url);
        var hashHeader = response.Headers.GetValues("X-Goog-Hash").FirstOrDefault();
        Console.WriteLine($"Hash header: {hashHeader}");
        string text = null;
        using (var md5 = MD5.Create())
        {
            using (var cryptoStream = new CryptoStream(await response.Content.ReadAsStreamAsync(), md5, CryptoStreamMode.Read))
            {
                using (var gzipStream = new GZipStream(cryptoStream, CompressionMode.Decompress))
                {
                    using (var streamReader = new StreamReader(gzipStream, Encoding.UTF8))
                    {
                        text = streamReader.ReadToEnd();
                    }
                }
                Console.WriteLine($"Content: {text}");
                var md5HashBase64 = Convert.ToBase64String(md5.Hash);
                Console.WriteLine($"MD5 of content: {md5HashBase64}");
            }
        }
    }
}

输出:

Hash header: crc32c=T1s5RQ==,md5=xhF4M6pNFRDQnvaRRNVnkA==
Content: hello world
MD5 of content: xhF4M6pNFRDQnvaRRNVnkA==

答案V2

阅读 Jon 的回复和更新的答案后,我得到了以下版本。几乎相同的想法,但我将流转移到我注入的特殊HttpContent 中。不完全漂亮,但想法就在那里。

using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string url = "https://www.googleapis.com/download/storage/v1/b/"
            + "storage-library-test-bucket/o/gzipped-text.txt?alt=media";
        var handler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.None
        };
        var client = new HttpClient(new Intercepter(handler));
        client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip");

        var response = await client.GetAsync(url);
        var hashHeader = response.Headers.GetValues("X-Goog-Hash").FirstOrDefault();
        Console.WriteLine($"Hash header: {hashHeader}");
        HttpContent content1 = response.Content;
        byte[] content = await content1.ReadAsByteArrayAsync();
        string text = Encoding.UTF8.GetString(content);
        Console.WriteLine($"Content: {text}");
        var md5Hash = ((HashingContent)content1).Hash;
        var md5HashBase64 = Convert.ToBase64String(md5Hash);
        Console.WriteLine($"MD5 of content: {md5HashBase64}");
    }

    public class Intercepter : DelegatingHandler
    {
        public Intercepter(HttpMessageHandler innerHandler) : base(innerHandler)
        {
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var response = await base.SendAsync(request, cancellationToken);
            response.Content = new HashingContent(await response.Content.ReadAsStreamAsync());
            return response;
        }
    }

    public sealed class HashingContent : HttpContent
    {
        private readonly StreamContent streamContent;
        private readonly MD5 mD5;
        private readonly CryptoStream cryptoStream;
        private readonly GZipStream gZipStream;

        public HashingContent(Stream content)
        {
            mD5 = MD5.Create();
            cryptoStream = new CryptoStream(content, mD5, CryptoStreamMode.Read);
            gZipStream = new GZipStream(cryptoStream, CompressionMode.Decompress);
            streamContent = new StreamContent(gZipStream);
        }

        protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => streamContent.CopyToAsync(stream, context);
        protected override bool TryComputeLength(out long length)
        {
            length = 0;
            return false;
        }

        protected override Task<Stream> CreateContentReadStreamAsync() => streamContent.ReadAsStreamAsync();

        protected override void Dispose(bool disposing)
        {
            try
            {
                if (disposing)
                {
                    streamContent.Dispose();
                    gZipStream.Dispose();
                    cryptoStream.Dispose();
                    mD5.Dispose();
                }
            }
            finally
            {
                base.Dispose(disposing);
            }
        }

        public byte[] Hash => mD5.Hash;
    }
}

【讨论】:

  • 如果我的代码都在读取数据就好了——但事实并非如此。 (或者至少,它在非常不同的地方这样做。)我真的需要保持 API 相同,使用 HttpClient 并在读取数据时截取数据:( 当我有机会制作要求更清晰。
  • @JonSkeet 你是个棘手的客户!这次我想我明白了:)
  • 对,这实际上是我描述的解决方法,除了没有散列和解压缩之间的分离 - 并且没有 DecompressionHandler 所做的标头复制。我很高兴我们最终来到了大致相同的地方,即使它不像我希望的那样无创。
  • 重要的区别是我不使用任何内部的东西:)
  • @shmulie:是的,但是通过重新实现位 - 正如我计划做的那样。 (也有标题等。)
【解决方案2】:

我设法让 headerhash 正确:

  • 创建一个继承 HttpClientHandler 的自定义处理程序
  • 覆盖SendAsync
  • 使用base.SendAsync将响应读取为字节
  • 使用 GZipStream 压缩它
  • 将 Gzip Md5 散列到 base64(使用您的代码)

这个问题,正如你所说的“解压前”在这里并没有得到真正的尊重

我们的想法是让这个if 按你的意愿工作 https://github.com/dotnet/corefx/blob/master/src/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseParser.cs#L80-L91

匹配

class Program
{
    const string url = "https://www.googleapis.com/download/storage/v1/b/storage-library-test-bucket/o/gzipped-text.txt?alt=media";

    static async Task Main()
    {
        //await HashResponseContent(CreateHandler(DecompressionMethods.None));
        //await HashResponseContent(CreateHandler(DecompressionMethods.GZip));
        await HashResponseContent(new MyHandler());

        Console.ReadLine();
    }

    private static HttpClientHandler CreateHandler(DecompressionMethods decompressionMethods)
    {
        return new HttpClientHandler { AutomaticDecompression = decompressionMethods };
    }

    public static async Task HashResponseContent(HttpClientHandler handler)
    {
        //Console.WriteLine($"Using AutomaticDecompression : '{handler.AutomaticDecompression}'");
        //Console.WriteLine($"Using SupportsAutomaticDecompression : '{handler.SupportsAutomaticDecompression}'");
        //Console.WriteLine($"Using Properties : '{string.Join('\n', handler.Properties.Keys.ToArray())}'");

        var client = new HttpClient(handler);

        var response = await client.GetAsync(url);
        byte[] content = await response.Content.ReadAsByteArrayAsync();
        string text = Encoding.UTF8.GetString(content);
        Console.WriteLine($"Content: {text}");

        var hashHeader = response.Headers.GetValues("X-Goog-Hash").FirstOrDefault();
        Console.WriteLine($"Hash header: {hashHeader}");
        byteArrayToMd5(content);

        Console.WriteLine($"=====================================================================");
    }

    public static string byteArrayToMd5(byte[] content)
    {
        using (var md5 = MD5.Create())
        {
            var md5Hash = md5.ComputeHash(content);
            return Convert.ToBase64String(md5Hash);
        }
    }

    public static byte[] Compress(byte[] contentToGzip)
    {
        using (MemoryStream resultStream = new MemoryStream())
        {
            using (MemoryStream contentStreamToGzip = new MemoryStream(contentToGzip))
            {
                using (GZipStream compressionStream = new GZipStream(resultStream, CompressionMode.Compress))
                {
                    contentStreamToGzip.CopyTo(compressionStream);
                }
            }

            return resultStream.ToArray();
        }
    }
}

public class MyHandler : HttpClientHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var response = await base.SendAsync(request, cancellationToken);
        var responseContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);

        Program.byteArrayToMd5(responseContent);

        var compressedResponse = Program.Compress(responseContent);
        var compressedResponseMd5 = Program.byteArrayToMd5(compressedResponse);

        Console.WriteLine($"recompressed response to md5 : {compressedResponseMd5}");

        return response;
    }
}

【讨论】:

  • 这行得通,因为我碰巧使用 .NET 开始使用默认设置压缩该文件的内容。但是有几种不同的压缩内容的方法,最终会创建不同的哈希值。如果 gzip 是稳定的(即压缩相同的输入总是给出相同的输出)这将是可行的 - 但它不适用于这种情况:(
  • 这很奇怪,因为默认值(以及调试时的窥探)似乎用false 调用if 语句,所以实际上它不应该解压缩user-images.githubusercontent.com/2266487/…
  • 如果库不发送 Accept-Encoding,服务器会即时解压缩内容。我怀疑这就是这种情况下发生的事情 - 然后你使用与原始压缩相同的设置重新压缩它,所以你最终得到相同的哈希。
【解决方案3】:

如果禁用自动解压,手动添加Accept-Encoding标头,然后在哈希验证后解压?

private static async Task Test2()
{
    var url = @"https://www.googleapis.com/download/storage/v1/b/storage-library-test-bucket/o/gzipped-text.txt?alt=media";
    var handler = new HttpClientHandler
    {
        AutomaticDecompression = DecompressionMethods.None
    };
    var client = new HttpClient(handler);
    client.DefaultRequestHeaders.Add("Accept-Encoding", "gzip");

    var response = await client.GetAsync(url);
    var raw = await response.Content.ReadAsByteArrayAsync();

    var hashHeader = response.Headers.GetValues("X-Goog-Hash").FirstOrDefault();
    Debug.WriteLine($"Hash header: {hashHeader}");

    bool match = false;
    using (var md5 = MD5.Create())
    {
        var md5Hash = md5.ComputeHash(raw);
        var md5HashBase64 = Convert.ToBase64String(md5Hash);
        match = hashHeader.EndsWith(md5HashBase64);
        Debug.WriteLine($"MD5 of content: {md5HashBase64}");
    }

    if (match)
    {
        var memInput = new MemoryStream(raw);
        var gz = new GZipStream(memInput, CompressionMode.Decompress);
        var memOutput = new MemoryStream();
        gz.CopyTo(memOutput);
        var text = Encoding.UTF8.GetString(memOutput.ToArray());
        Console.WriteLine($"Content: {text}");
    }
}

【讨论】:

  • 这基本上是我原型的一个更简单但效率较低的版本。问题是它将整个流保存在内存中 - 当这些文件可能是数 GB 时。我需要在从内容返回的流中插入散列:(
  • 如果我们谈论的是千兆字节,那么这个方法是不可用的,sry :(
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-06-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多