Programing

HttpClient에서 압축 해제 전에 압축 된 데이터에 액세스 할 수 있습니까?

lottogame 2020. 11. 19. 07:43
반응형

HttpClient에서 압축 해제 전에 압축 된 데이터에 액세스 할 수 있습니까?


저는 Google Cloud Storage .NET 클라이언트 라이브러리 에서 작업하고 있습니다 . 불쾌한 방식으로 결합되는 세 가지 기능 (.NET, 내 클라이언트 라이브러리 및 스토리지 서비스 간)이 있습니다.

  • 파일 (Google Cloud Storage 용어의 객체)을 다운로드 할 때 서버에는 저장된 데이터의 해시가 포함됩니다. 그런 다음 내 클라이언트 코드는 다운로드 한 데이터에 대해 해당 해시의 유효성을 검사합니다.

  • Google Cloud Storage의 별도 기능은 사용자가 객체의 Content-Encoding을 설정할 수 있으며, 요청에 일치하는 Accept-Encoding이 포함 된 경우 다운로드 할 때 헤더로 포함된다는 것입니다. (당분간 요청에 포함되지 않은 동작은 무시합시다 ...)

  • HttpClientHandler gzip (또는 압축) 콘텐츠를 자동으로 투명하게 압축 해제 할 수 있습니다.

이 세 가지가 모두 결합되면 문제가 발생합니다. 다음은 내 클라이언트 라이브러리를 사용하지 않고 공개적으로 액세스 할 수있는 파일을 사용하지 않고이를 보여주는 짧지 만 완전한 프로그램입니다.

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는 X-Goog-Hash헤더 의 MD5 부분과 동일하지 않습니다 . (내 클라이언트 라이브러리에서 crc32c 해시를 사용하고 있지만 동일한 동작을 보여줍니다.)

이것은 버그가 아닙니다. HttpClientHandler예상되는 일이지만 해시를 검증하고 싶을 때 고통 스럽습니다. 기본적으로, 나는 전에 내용에 필요 하고 압축 해제 후. 그리고 나는 그것을 할 방법을 찾을 수 없습니다.

내 요구 사항을 다소 명확히하기 위해 HttpClient스트림에서 읽을 때 압축 해제를 방지하고 나중에 압축을 해제하는 방법을 알고 있지만 .NET Framework의 결과를 사용하는 코드를 변경하지 않고이를 수행 할 수 있어야 HttpResponseMessage합니다 HttpClient. (응답을 처리하는 많은 코드가 있으며 한 곳에서만 변경하고 싶습니다.)

나는 내가 프로토 타입을 만들고 지금까지 내가 찾은 한 작동하지만 약간 못생긴 계획을 가지고있다. 여기에는 3 계층 처리기 생성이 포함됩니다.

  • HttpClientHandler 자동 압축 해제가 비활성화 된 상태입니다.
  • 콘텐츠 스트림을 Stream원래 콘텐츠 스트림에 위임 하는 새 하위 클래스로 대체하는 새 처리기 이지만 읽은대로 데이터를 해시합니다.
  • Microsoft DecompressionHandler코드를 기반으로하는 압축 해제 전용 처리기 입니다.

이것이 작동하는 동안 다음과 같은 단점이 있습니다.

  • 오픈 소스 라이선싱 : MIT 라이선싱 Microsoft 코드를 기반으로 리포지토리에 새 파일을 생성하기 위해 수행해야하는 작업을 정확히 확인
  • MS 코드를 효과적으로 포크합니다. 즉, 버그가 발견되었는지 정기적으로 확인해야합니다.
  • Microsoft 코드는 어셈블리의 내부 멤버를 사용하므로 깔끔하게 이식되지 않습니다.

마이크로 소프트가 DecompressionHandler공개 한다면 많은 도움이 될 것입니다.하지만 그것은 제가 필요로하는 것보다 더 긴 시간이 걸릴 것입니다.

내가 찾고있는 것은 가능한 경우 대체 접근 방식입니다. 압축 해제 전에 콘텐츠를 볼 수 있도록 놓친 것입니다. 나는 재발 명하고 싶지 않습니다. HttpClient예를 들어 응답은 종종 덩어리가되며, 그런면에 들어가고 싶지 않습니다. 제가 찾고있는 매우 구체적인 차단 지점입니다.


@Michael이 한 일을 보면 내가 놓친 힌트를 얻었습니다. 압축 된 콘텐츠를 가져온 후 , 및 사용 CryptoStream하고 필요 이상으로 메모리에로드하지 않고 응답을 읽을 수 있습니다 . 압축을 풀고 읽을 때 압축 된 콘텐츠를 해시합니다. 를 교체 로모그래퍼 당신은 최소한의 메모리 사용량과 파일에 데이터를 기록 할 수 있습니다 :GZipStreamStreamReaderCryptoStreamStreamReaderFileStream

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;
    }
}

나는 헤더 해시를 다음과 같이 수정했습니다.

  • HttpClientHandler를 상속하는 사용자 지정 처리기 만들기
  • 재정의 SendAsync
  • 다음을 사용하여 응답을 바이트로 읽습니다. base.SendAsync
  • GZipStream을 사용하여 압축
  • Gzip Md5를 base64로 해싱 (코드 사용)

이 문제는 "감압 전"이라고 말했듯이 여기서는 실제로 존중되지 않습니다.

아이디어는 https://github.com/dotnet/corefx/blob/master/src/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseParser.cs#L80if 과 같이 작동하도록하는 것입니다. -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;
    }
}

What about disabling automatic decompression, manually adding the Accept-Encoding header(s) and then decompressing after hash verification?

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}");
    }
}

참고URL : https://stackoverflow.com/questions/47324282/is-it-possible-to-access-the-compressed-data-before-decompression-in-httpclient

반응형