c# - una - método web desconocido nombre del parámetro methodname
¿Es posible acceder a los datos comprimidos antes de la descompresión en HttpClient? (3)
¿Qué hay de deshabilitar la descompresión automática, agregar manualmente los encabezados de
Accept-Encoding
y luego descomprimir después de la verificación hash?
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}");
}
}
Estoy trabajando en la biblioteca del cliente .NET de Google Cloud Storage . Hay tres características (entre .NET, mi biblioteca de cliente y el servicio de almacenamiento) que se combinan de una manera desagradable:
-
Al descargar archivos (objetos en la terminología de Google Cloud Storage), el servidor incluye un hash de los datos almacenados. Mi código de cliente luego valida ese hash contra los datos que se descargan.
-
Una característica separada de Google Cloud Storage es que el usuario puede establecer la codificación de contenido del objeto, y eso se incluye como encabezado al descargar, cuando la solicitud contiene una codificación de aceptación correspondiente. (Por el momento, ignoremos el comportamiento cuando la solicitud no incluye eso ...)
-
HttpClientHandler
puede descomprimir el contenido de gzip (o desinflar) de forma automática y transparente.
Cuando los tres se combinan, nos metemos en problemas. Aquí hay un programa corto pero completo que lo demuestra, pero sin usar mi biblioteca cliente (y presionar un archivo de acceso público):
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}");
}
}
}
Archivo de proyecto de .NET Core:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<LangVersion>7.1</LangVersion>
</PropertyGroup>
</Project>
Salida:
Content: hello world
Hash header: crc32c=T1s5RQ==,md5=xhF4M6pNFRDQnvaRRNVnkA==
MD5 of content: XrY7u+Ae7tCTyyK7j1rNww==
Como puede ver, el MD5 del contenido no es el mismo que la parte MD5 del encabezado
X-Goog-Hash
.
(En mi biblioteca de cliente estoy usando el hash crc32c, pero eso muestra el mismo comportamiento).
Esto no es un error en
HttpClientHandler
, se espera, pero es un dolor cuando quiero validar el hash.
Básicamente, necesito el contenido antes
y
después de la descompresión.
Y no puedo encontrar ninguna manera de hacerlo.
Para aclarar un poco mis requisitos, sé cómo evitar la descompresión en
HttpClient
y, en su lugar, descomprimir después cuando leo desde el flujo, pero necesito poder hacer esto sin cambiar el código que utiliza el
HttpResponseMessage
resultante del
HttpClient
.
(Hay un montón de código que trata con las respuestas, y solo quiero hacer el cambio en un lugar central).
Tengo un plan, que he prototipado y que funciona hasta donde he encontrado hasta ahora, pero es un poco feo. Implica crear un controlador de tres capas:
-
HttpClientHandler
con descompresión automática deshabilitada. -
Un nuevo controlador que reemplaza la secuencia de contenido con una nueva subclase de
Stream
que delega en la secuencia de contenido original, pero agrega los datos a medida que se leen. -
Un controlador de solo descompresión, basado en el código de Microsoft
DecompressionHandler
.
Si bien esto funciona, tiene desventajas de:
- Licencias de código abierto: verificar exactamente lo que necesito hacer para crear un nuevo archivo en mi repositorio basado en el código de Microsoft con licencia MIT
- Bifurcando efectivamente el código MS, lo que significa que probablemente debería hacer una verificación regular para ver si se han encontrado errores.
- El código de Microsoft utiliza miembros internos del ensamblado, por lo que no se transfiere de la forma más limpia posible.
Si Microsoft hiciera público
DecompressionHandler
, eso ayudaría mucho, pero es probable que sea en un plazo más largo de lo que necesito.
Lo que estoy buscando es un enfoque alternativo si es posible, algo que me he perdido y que me permite acceder al contenido antes de la descompresión.
No quiero reinventar
HttpClient
: la respuesta a menudo se fragmenta, por ejemplo, y no quiero tener que entrar en ese lado de las cosas.
Es un punto de intercepción bastante específico que estoy buscando.
Logré obtener el encabezado correcto mediante:
- crear un controlador personalizado que herede HttpClientHandler
-
anulando
SendAsync
-
leer como byte la respuesta usando
base.SendAsync
- Comprimirlo usando GZipStream
- Hashing el Gzip Md5 a base64 (usando su código)
este problema es, como dijiste "antes de la descompresión" no se respeta realmente aquí
La idea es obtener esto
if
funciona como quisiera
https://github.com/dotnet/corefx/blob/master/src/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseParser.cs#L80-L91
concuerda
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;
}
}
Ver lo que hizo @Michael me dio la pista que me faltaba.
Después de obtener el contenido comprimido, puede usar
CryptoStream
y
GZipStream
y
StreamReader
para leer la respuesta sin cargarla en la memoria más de lo necesario.
CryptoStream
el contenido comprimido a medida que se descomprime y lee.
Reemplace
StreamReader
con un
FileStream
y puede escribir los datos en un archivo con un uso mínimo de memoria :)
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}");
}
}
}
}
Salida:
Hash header: crc32c=T1s5RQ==,md5=xhF4M6pNFRDQnvaRRNVnkA==
Content: hello world
MD5 of content: xhF4M6pNFRDQnvaRRNVnkA==
V2 de respuesta
Después de leer la respuesta de Jon y una respuesta actualizada, tengo la siguiente versión.
Prácticamente la misma idea, pero trasladé la transmisión a un
HttpContent
especial que inyecté.
No es exactamente bonito, pero la idea está ahí.
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;
}
}