2012-09-10 9 views
13

Tengo una aplicación de API Web .NET MVC 4 estándar bastante pantanosa.¿Cómo habilito una API web .Net para aceptar publicaciones g-ziped?

public class LogsController : ApiController 
{ 

    public HttpResponseMessage PostLog(List<LogDto> logs) 
    { 
     if (logs != null && logs.Any()) 
     { 
      var goodLogs = new List<Log>(); 
      var badLogs = new List<LogBad>(); 

      foreach (var logDto in logs) 
      { 
       if (logDto.IsValid()) 
       { 
        goodLogs.Add(logDto.ToLog()); 
       } 
       else 
       { 
        badLogs.Add(logDto.ToLogBad()); 
       } 
      } 

      if (goodLogs.Any()) 
      { 
       _logsRepo.Save(goodLogs); 
      } 

      if(badLogs.Any()) 
      { 
       _logsBadRepo.Save(badLogs); 
      } 


     } 
     return new HttpResponseMessage(HttpStatusCode.OK); 
    } 
} 

Todo funciona bien, tengo dispositivos que pueden enviarme sus registros y funciona bien. Sin embargo, ahora estamos empezando a preocuparnos por el tamaño de los datos que se transfieren, y queremos echar un vistazo a la aceptación de publicaciones que se han comprimido con GZIP.

¿Cómo podría hacer esto? ¿Está configurando IIS o podría usar filtros de acción?

EDIT 1

seguimiento de Filip de responder a mi pensamiento es que necesito para interceptar el procesamiento de la solicitud antes de que llegue al controlador. Si puedo capturar la solicitud antes de que el marco API web intente analizar el cuerpo de la solicitud en mi objeto comercial, que falla porque el cuerpo de la solicitud todavía está comprimido. Luego puedo descomprimir el cuerpo de la solicitud y luego volver a pasar la solicitud a la cadena de procesamiento, y con suerte el marco web API podrá analizar el cuerpo (descomprimido) en mis objetos comerciales.

Parece que usar DelagatingHandler es el camino a seguir. Me permite acceder a la solicitud durante el procesamiento, pero antes de mi controlador. ¿Entonces intenté lo siguiente?

public class gZipHandler : DelegatingHandler 
{ 

    protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) 
    { 
     string encodingType = request.Headers.AcceptEncoding.First().Value; 

     request.Content = new DeCompressedContent(request.Content, encodingType); 

     return base.SendAsync(request, cancellationToken); 
    } 
} 

public class DeCompressedContent : HttpContent 
{ 
    private HttpContent originalContent; 
    private string encodingType; 

    public DeCompressedContent(HttpContent content, string encodType) 
    { 
     originalContent = content; 
     encodingType = encodType; 
    } 

    protected override bool TryComputeLength(out long length) 
    { 
     length = -1; 

     return false; 
    } 


    protected override Task<Stream> CreateContentReadStreamAsync() 
    { 
     return base.CreateContentReadStreamAsync(); 
    } 

    protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) 
    { 
     Stream compressedStream = null; 

     if (encodingType == "gzip") 
     { 
      compressedStream = new GZipStream(stream, CompressionMode.Decompress, leaveOpen: true); 
     } 

     return originalContent.CopyToAsync(compressedStream).ContinueWith(tsk => 
     { 
      if (compressedStream != null) 
      { 
       compressedStream.Dispose(); 
      } 
     }); 
    } 



} 

}

Esto parece estar funcionando bien. Se llama al método SendAsync antes de que se llame a mi controlador y al constructor para DecompressedContent. Sin embargo, nunca se llama a SerializeToStreamAsync, así que agregué el CreateContentReadStreamAsync para ver si es allí donde debería estar la descompresión, pero tampoco se está llamando.

Me siento como si estuviese cerca de la solución, pero solo necesito un poco más para poder pasar la línea.

+0

¿Cómo comprimiste los datos JSON en el lado del cliente? Gracias. –

Respuesta

21

Tenía el mismo requisito para POSTAR datos comprimidos a un controlador de API web .NET. Se me ocurrió con esta solución:

public class GZipToJsonHandler : DelegatingHandler 
{ 
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, 
                  CancellationToken cancellationToken) 
    { 
     // Handle only if content type is 'application/gzip' 
     if (request.Content.Headers.ContentType == null || 
      request.Content.Headers.ContentType.MediaType != "application/gzip") 
     { 
      return base.SendAsync(request, cancellationToken); 
     } 

     // Read in the input stream, then decompress in to the outputstream. 
     // Doing this asynronously, but not really required at this point 
     // since we end up waiting on it right after this. 
     Stream outputStream = new MemoryStream(); 
     Task task = request.Content.ReadAsStreamAsync().ContinueWith(t => 
      { 
       Stream inputStream = t.Result; 
       var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress); 

       gzipStream.CopyTo(outputStream); 
       gzipStream.Dispose(); 

       outputStream.Seek(0, SeekOrigin.Begin); 
      }); 

     // Wait for inputstream and decompression to complete. Would be nice 
     // to not block here and work async when ready instead, but I couldn't 
     // figure out how to do it in context of a DelegatingHandler. 
     task.Wait(); 

     // This next section is the key... 

     // Save the original content 
     HttpContent origContent = request.Content; 

     // Replace request content with the newly decompressed stream 
     request.Content = new StreamContent(outputStream); 

     // Copy all headers from original content in to new one 
     foreach (var header in origContent.Headers) 
     { 
      request.Content.Headers.Add(header.Key, header.Value); 
     } 

     // Replace the original content-type with content type 
     // of decompressed data. In our case, we can assume application/json. A 
     // more generic and reuseable handler would need some other 
     // way to differentiate the decompressed content type. 
     request.Content.Headers.Remove("Content-Type"); 
     request.Content.Headers.Add("Content-Type", "application/json"); 

     return base.SendAsync(request, cancellationToken); 
    } 
} 

Con este enfoque, el controlador existente, que normalmente funciona con la unión JSON modelo de contenido y automático, continuó trabajando sin ningún cambio.

No estoy seguro de por qué se aceptó la otra respuesta. Proporciona una solución para manejar las respuestas (que es común), pero no las solicitudes (que es poco común). El encabezado Accept-Encoding se usa para especificar codificaciones de respuesta aceptables, y no está relacionado con las codificaciones de solicitud.

+0

Se recomienda llamar a Dispose en outputStream? En caso afirmativo, ¿cuándo? Gracias –

+0

Dado que outputStream es un MemoryStream, no es necesario desechar o cerrar el flujo de forma explícita. Para obtener detalles, consulte esta respuesta: http://stackoverflow.com/questions/4274590/memorystream-close-or-memorystream-dispose – kaliatech

+4

Debe usar Content-Encoding para decidir si es necesario descomprimir, no una aplicación/gzip personalizada. El tipo de contenido no debe tocarse. – Softlion

6

Mientras API Web no admite Accept-Encoding cabecera de la caja, pero Kiran tiene un blog excelente sobre cómo hacerlo - http://blogs.msdn.com/b/kiranchalla/archive/2012/09/04/handling-compression-accept-encoding-sample.aspx - usando una costumbre MessageHandler

Si implementa su solución, todo lo que necesita hacer es emitir una solicitud con el encabezado Accept-Encoding: gzip o Accept-Encoding: deflate y la respuesta de la API web se comprimirá en el manejador de mensajes por usted.

+2

Genial, gracias por eso. Fue útil. Mi escenario es un poco diferente. Todavía no me preocupa comprimir la respuesta de mi API web, sino aceptar la solicitud (publicación) que ya se ha comprimido. Necesito descomprimir el cuerpo de estas publicaciones y luego manejar los datos en ellas. Ver edición en cuestión para mis próximos pasos. Gracias –

+0

Mientras tanto, en realidad hay una biblioteca de código abierto creada a partir de esa publicación que mencionas y otras publicaciones de blog sobre el tema: https://github.com/azzlack/Microsoft.AspNet.WebApi.MessageHandlers.Compression – fretje

20

Creo que la respuesta correcta es la de Kaliatech, y hubiera dejado esto como un comentario y hubiese votado su opinión si tuviera suficientes puntos de reputación, ya que creo que la suya es básicamente correcta.

Sin embargo, mi situación requería la necesidad de mirar el tipo de tipo de codificación en lugar del tipo de contenido. Al usar este enfoque, el sistema de llamadas puede especificar que el tipo de contenido es json/xml/etc en el tipo de contenido, pero especifique que los datos están codificados usando gzip o potencialmente otro mecanismo de codificación/compresión. Esto me impidió tener que cambiar el tipo de contenido después de decodificar la entrada y permite que fluya toda la información del tipo de contenido en su estado original.

Aquí está el código. De nuevo, el 99% de esta es la respuesta de Kaliatech, incluidos los comentarios, así que vote su publicación si es útil.

public class CompressedRequestHandler : DelegatingHandler 
{ 
    protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) 
    { 
     if (IsRequetCompressed(request)) 
     { 
      request.Content = DecompressRequestContent(request); 
     } 

     return base.SendAsync(request, cancellationToken); 
    } 

    private bool IsRequetCompressed(HttpRequestMessage request) 
    { 
     if (request.Content.Headers.ContentEncoding != null && 
      request.Content.Headers.ContentEncoding.Contains("gzip")) 
     { 
      return true; 
     } 

     return false; 
    } 

    private HttpContent DecompressRequestContent(HttpRequestMessage request) 
    { 
     // Read in the input stream, then decompress in to the outputstream. 
     // Doing this asynronously, but not really required at this point 
     // since we end up waiting on it right after this. 
     Stream outputStream = new MemoryStream(); 
     Task task = request.Content.ReadAsStreamAsync().ContinueWith(t => 
      { 
       Stream inputStream = t.Result; 
       var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress); 

       gzipStream.CopyTo(outputStream); 
       gzipStream.Dispose(); 

       outputStream.Seek(0, SeekOrigin.Begin); 
      }); 

     // Wait for inputstream and decompression to complete. Would be nice 
     // to not block here and work async when ready instead, but I couldn't 
     // figure out how to do it in context of a DelegatingHandler. 
     task.Wait(); 

     // Save the original content 
     HttpContent origContent = request.Content; 

     // Replace request content with the newly decompressed stream 
     HttpContent newContent = new StreamContent(outputStream); 

     // Copy all headers from original content in to new one 
     foreach (var header in origContent.Headers) 
     { 
      newContent.Headers.Add(header.Key, header.Value); 
     } 

     return newContent; 
    } 

entonces registrado este controlador a nivel mundial, lo que podría ser una propuesta peligrosa si es vulnerable a ataques de denegación de servicio, pero nuestro servicio está bloqueado, por lo que funciona para nosotros

GlobalConfiguration.Configuration.MessageHandlers.Add(new CompressedRequestHandler()); 
+0

Estaría muy interesado si explica por qué esto expondría una vulnerabilidad de Ataque DoS, ¿es porque cualquier solicitud generará un uso considerable de CPU? –

0

tratamos este

public class DeCompressedContent : HttpContent 
{ 
    private HttpContent originalContent; 
    private string encodingType; 

    /// <summary> 
    /// 
    /// </summary> 
    /// <param name="content"></param> 
    /// <param name="encodingType"></param> 
    public DeCompressedContent(HttpContent content, string encodingType) 
    { 

     if (content == null) throw new ArgumentNullException("content"); 
     if (string.IsNullOrWhiteSpace(encodingType)) throw new ArgumentNullException("encodingType"); 

     this.originalContent = content; 
     this.encodingType = encodingType.ToLowerInvariant(); 

     if (!this.encodingType.Equals("gzip", StringComparison.CurrentCultureIgnoreCase) && !this.encodingType.Equals("deflate", StringComparison.CurrentCultureIgnoreCase)) 
     { 
      throw new InvalidOperationException(string.Format("Encoding {0} is not supported. Only supports gzip or deflate encoding", this.encodingType)); 
     } 

     foreach (KeyValuePair<string, IEnumerable<string>> header in originalContent.Headers) 
     { 
      this.Headers.TryAddWithoutValidation(header.Key, header.Value); 
     } 

     this.Headers.ContentEncoding.Add(this.encodingType); 
    } 

    /// <summary> 
    /// 
    /// </summary> 
    /// <param name="stream"></param> 
    /// <param name="context"></param> 
    /// <returns></returns> 
    protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) 
    { 
     var output = new MemoryStream(); 

     return this.originalContent 
      .CopyToAsync(output).ContinueWith(task => 
      { 
       // go to start 
       output.Seek(0, SeekOrigin.Begin); 

       if (this.encodingType.Equals("gzip", StringComparison.CurrentCultureIgnoreCase)) 
       { 
        using (var dec = new GZipStream(output, CompressionMode.Decompress)) 
        { 
         dec.CopyTo(stream); 
        } 
       } 
       else 
       { 
        using (var def = new DeflateStream(output, CompressionMode.Decompress)) 
        { 
         def.CopyTo(stream); 
        } 
       } 

       if (output != null) 
        output.Dispose(); 
      }); 


    } 

    /// <summary> 
    /// 
    /// </summary> 
    /// <param name="length"></param> 
    /// <returns></returns> 
    protected override bool TryComputeLength(out long length) 
    { 
     length = -1; 

     return (false); 
    } 
} 
Cuestiones relacionadas