74 votes

Est-il possible d'accéder aux données compressées avant la décompression dans HttpClient?

Je suis en train de travailler sur la Google Cloud Storage .NET client de la bibliothèque. Il y a trois fonctions (entre les deux .NET, mon client la bibliothèque et le service de Stockage) qui sont en combinant en un manière désagréable:

  • Lors du téléchargement des fichiers (des objets dans Google Cloud Storage la terminologie), le serveur comprend une table de hachage de données stockées. Mon code client valide ensuite que de hachage contre les données qu'il est téléchargé.

  • Une fonction séparée de Google Cloud Storage est que l'utilisateur peut définir l'Encodage de Contenu de l'objet, et qui est inclus comme un l'en-tête lors du téléchargement, lorsque la demande contient une correspondance Accept-Encoding. (Pour le moment, nous allons ignorer le comportement lors de l' la demande ne comprend pas que...)

  • HttpClientHandler pouvez décompresser gzip (ou deflate) contenu automatiquement et de manière transparente.

Quand tous les trois sont combinées, nous avons des ennuis. Voici un courte mais complète du programme, ce qui démontre que, mais sans l'aide de mon de la bibliothèque du client (et de frapper un dossier accessible au public):

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 de Base du fichier de projet:

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

Sortie:

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

Comme vous pouvez le voir, le MD5 du contenu n'est pas le même que le MD5 une partie de l' X-Goog-Hash - tête. (Dans ma bibliothèque cliente, je suis en utilisant le crc32c de hachage, mais qui montre le même comportement.)

Ce n'est pas un bug en HttpClientHandler - c'est prévu, mais une douleur quand je veux valider le code de hachage. En gros, j'ai besoin d'au contenu avant et après la décompression. Et je ne trouve pas de toute façon de le faire.

Pour préciser ma exigences un peu, je sais comment éviter la décompression en HttpClient et au lieu de décompresser après, lors de la lecture de ce cours d'eau, mais j'ai besoin d'être en mesure de le faire sans changer tout le code qui utilise l'résultant HttpResponseMessage de la HttpClient. (Il y a beaucoup de code qui traite les réponses, et je veux seulement faire le changement dans un endroit central.)

J'ai un plan que j'ai conçu et qui travaille autant que je l'ai trouvé à ce jour, mais il est un peu moche. Elle implique la création d'un trois-couche gestionnaire:

  • HttpClientHandler avec décompression automatique désactivé.
  • Un nouveau gestionnaire qui remplace les flux de contenu avec un nouveau Stream sous-classe les délégués de l'origine et du contenu de flux, mais les hachages les données comme il est lu.
  • Une décompression seule gestionnaire, basée sur la Microsoft DecompressionHandler code.

Bien que cela fonctionne, il a les inconvénients de l':

  • La licence Open source: vérifier exactement ce que je dois faire dans l'ordre pour créer un nouveau fichier dans mon repo basé sur le MIT sous licence Microsoft code
  • Effectivement la fourche du MME code, ce qui signifie que je devrais probablement faire un contrôle régulier pour voir si les bugs ont été retrouvés
  • Le code Microsoft utilise en interne des membres de l'assemblée, de sorte qu'il ne pas port le plus proprement qu'il le pourrait.

Si Microsoft fait DecompressionHandler du public, ça aiderait beaucoup - mais qui est susceptible d'être dans un délai plus long que j'ai besoin.

Ce que je recherche est une approche alternative si possible - quelque chose que j'ai raté qui me permet d'obtenir le contenu avant la décompression. Je ne veux pas réinventer HttpClient - la réponse est souvent chunked par exemple, et je ne veux pas entrer dans cet aspect des choses. C'est une jolie interception spécifique au point que Je suis à la recherche pour.

14voto

shmuelie Points 414

En regardant ce que @Michael ne m'a donné l'astuce qui me manquait. Après l'obtention de la compression de contenu que vous pouvez utiliser CryptoStream, et GZipStream, et StreamReader de lire la réponse sans le charger en mémoire plus que nécessaire. CryptoStream sera hachage du contenu compressé comme il est décompressé et à lire. Remplacer l' StreamReader avec un FileStream et vous pouvez écrire les données dans un fichier avec un minimum d'utilisation de la mémoire :)

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

Sortie:

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

V2 de Réponse

Après la lecture de Jon réponse et une mise à jour de réponse que j'ai la version suivante. À peu près la même idée, mais j'ai déménagé à la diffusion en continu dans une HttpContent que j'ai injecter. Pas forcément joli, mais l'idée est là.

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

4voto

Alexandre Hgs Points 51

J'ai réussi à obtenir le headerhash corriger par:

  • création d'un gestionnaire personnalisé qui hérite HttpClientHandler
  • primordial SendAsync
  • lire aussi octets de la réponse à l'aide de base.SendAsync
  • Le comprimer à l'aide de GZipStream
  • Le hachage de l'Gzip Md5 base64 (à l'aide de votre code)

cette question est, comme vous l'avez dit "avant de décompression" n'est pas vraiment respecté ici

L'idée est d'obtenir cette if de travail comme vous le souhaitez https://github.com/dotnet/corefx/blob/master/src/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseParser.cs#L80-L91

elle correspond à

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

4voto

Michael Points 1459

Pourquoi ne pas désactiver la décompression automatique, ajouter manuellement les en-têtes Accept-Encoding , puis les décompresser après la vérification du hachage?

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X