Возможно ли получить доступ к сжатым данным до декомпрессии в HttpClient?
Я работаю над клиентом Google Cloud Storage.NET
библиотека. Существует три функции (между .NET, моим клиентом
библиотека и служба хранения), которые объединяются в
неприятный способ:
-
При загрузке файлов (объектов в облачном хранилище Google
терминология), сервер включает хэш сохраненных данных. мой
клиентский код затем проверяет этот хэш на данные, которые он
скачали.
-
Отдельной функцией облачного хранилища Google является то, что пользователь может
установите Content-Encoding объекта и включите его как
заголовок при загрузке, когда запрос содержит совпадение
Accept-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
и вместо этого распаковать после этого при чтении из потока, но мне нужно сделать это, не изменяя код, который использует результирующий HttpResponseMessage
из HttpClient
. (Там много кода, который обрабатывает ответы, и я хочу только внести изменения в одно центральное место.)
У меня есть план, который я прототипировал и который работает, насколько я знаю
найденный до сих пор, но немного уродлив. Это предполагает создание трехслойного
обработчик:
-
HttpClientHandler
с отключенной автоматической декомпрессией.
- Новый обработчик, который заменяет поток контента новым подклассом
Stream
который делегирует исходный поток контента, но хэширует данные по мере их чтения.
- Обработчик декомпрессии, основанный на коде Microsoft
DecompressionHandler
.
Пока это работает, у него есть недостатки:
- Лицензирование с открытым исходным кодом: проверка того, что мне нужно сделать, чтобы
создать новый файл в моем репо на основе лицензии MIT
Код Microsoft
- Эффективное форматирование кода MS, что означает, что я должен, вероятно,
регулярно проверяйте, обнаружены ли в нем какие-либо ошибки.
- В коде Microsoft используются внутренние элементы сборки, поэтому
не переносится как можно чище.
Если Microsoft сделала DecompressionHandler
общедоступной, это поможет
лот - но это, вероятно, будет в более длительный срок, чем мне нужно.
Я ищу альтернативный подход, если это возможно -
то, что я пропустил, позволяет мне добираться до контента до
декомпрессия. Я не хочу изобретать HttpClient
- ответ
часто зацикливается, например, и я не хочу, чтобы
эта сторона вещей. Это довольно конкретная точка перехвата, которая
Я ищу.
Ответы
Ответ 1
Глядя на то, что @Michael дал мне подсказку, которую мне не хватало. После получения сжатого содержимого вы можете использовать CryptoStream
и GZipStream
и StreamReader
, чтобы прочитать ответ, не загружая его в память больше, чем необходимо. 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 ответа
После прочтения ответа Джона и обновленного ответа у меня есть следующая версия. В значительной степени та же идея, но я переместил поток в специальную 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;
}
}
Ответ 2
Мне удалось получить правильность заголовка:
- создание настраиваемого обработчика, который наследует 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;
}
}
Ответ 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}");
}
}