using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Emby.Server.Implementations.Services;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using IRequest = MediaBrowser.Model.Services.IRequest;
using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
namespace Emby.Server.Implementations.HttpServer
{
///
/// Class HttpResultFactory
///
public class HttpResultFactory : IHttpResultFactory
{
///
/// The _logger
///
private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer;
private IBrotliCompressor _brotliCompressor;
///
/// Initializes a new instance of the class.
///
public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IBrotliCompressor brotliCompressor)
{
_fileSystem = fileSystem;
_jsonSerializer = jsonSerializer;
_brotliCompressor = brotliCompressor;
_logger = loggerfactory.CreateLogger("HttpResultFactory");
}
///
/// Gets the result.
///
/// The content.
/// Type of the content.
/// The response headers.
/// System.Object.
public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary responseHeaders = null)
{
return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
}
public object GetResult(string content, string contentType, IDictionary responseHeaders = null)
{
return GetHttpResult(null, content, contentType, true, responseHeaders);
}
public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary responseHeaders = null)
{
return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
}
public object GetResult(IRequest requestContext, string content, string contentType, IDictionary responseHeaders = null)
{
return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
}
public object GetRedirectResult(string url)
{
var responseHeaders = new Dictionary();
responseHeaders["Location"] = url;
var result = new HttpResult(Array.Empty(), "text/plain", HttpStatusCode.Redirect);
AddResponseHeaders(result, responseHeaders);
return result;
}
///
/// Gets the HTTP result.
///
private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary responseHeaders = null)
{
var result = new StreamWriter(content, contentType);
if (responseHeaders == null)
{
responseHeaders = new Dictionary();
}
if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string expires))
{
responseHeaders["Expires"] = "-1";
}
AddResponseHeaders(result, responseHeaders);
return result;
}
///
/// Gets the HTTP result.
///
private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary responseHeaders = null)
{
string compressionType = null;
bool isHeadRequest = false;
if (requestContext != null)
{
compressionType = GetCompressionType(requestContext, content, contentType);
isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
}
IHasHeaders result;
if (string.IsNullOrEmpty(compressionType))
{
var contentLength = content.Length;
if (isHeadRequest)
{
content = Array.Empty();
}
result = new StreamWriter(content, contentType, contentLength);
}
else
{
result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType);
}
if (responseHeaders == null)
{
responseHeaders = new Dictionary();
}
if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string _))
{
responseHeaders["Expires"] = "-1";
}
AddResponseHeaders(result, responseHeaders);
return result;
}
///
/// Gets the HTTP result.
///
private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary responseHeaders = null)
{
IHasHeaders result;
var bytes = Encoding.UTF8.GetBytes(content);
var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType);
var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
if (string.IsNullOrEmpty(compressionType))
{
var contentLength = bytes.Length;
if (isHeadRequest)
{
bytes = Array.Empty();
}
result = new StreamWriter(bytes, contentType, contentLength);
}
else
{
result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType);
}
if (responseHeaders == null)
{
responseHeaders = new Dictionary();
}
if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out string _))
{
responseHeaders["Expires"] = "-1";
}
AddResponseHeaders(result, responseHeaders);
return result;
}
///
/// Gets the optimized result.
///
///
public object GetResult(IRequest requestContext, T result, IDictionary responseHeaders = null)
where T : class
{
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
if (responseHeaders == null)
{
responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase);
}
responseHeaders["Expires"] = "-1";
return ToOptimizedResultInternal(requestContext, result, responseHeaders);
}
private string GetCompressionType(IRequest request, byte[] content, string responseContentType)
{
if (responseContentType == null)
{
return null;
}
// Per apple docs, hls manifests must be compressed
if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) &&
responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 &&
responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 &&
responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 &&
responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1)
{
return null;
}
if (content.Length < 1024)
{
return null;
}
return GetCompressionType(request);
}
private static string GetCompressionType(IRequest request)
{
var acceptEncoding = request.Headers["Accept-Encoding"];
if (acceptEncoding != null)
{
//if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
// return "br";
if (acceptEncoding.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
return "deflate";
if (acceptEncoding.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
return "gzip";
}
return null;
}
///
/// Returns the optimized result for the IRequestContext.
/// Does not use or store results in any cache.
///
///
///
///
public object ToOptimizedResult(IRequest request, T dto)
{
return ToOptimizedResultInternal(request, dto);
}
private object ToOptimizedResultInternal(IRequest request, T dto, IDictionary responseHeaders = null)
{
// TODO: @bond use Span and .Equals
var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant();
switch (contentType)
{
case "application/xml":
case "text/xml":
case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders);
case "application/json":
case "text/json":
return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders);
default:
break;
}
var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase);
var ms = new MemoryStream();
var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
writerFn(dto, ms);
ms.Position = 0;
if (isHeadRequest)
{
using (ms)
{
return GetHttpResult(request, Array.Empty(), contentType, true, responseHeaders);
}
}
return GetHttpResult(request, ms, contentType, true, responseHeaders);
}
private IHasHeaders GetCompressedResult(byte[] content,
string requestedCompressionType,
IDictionary responseHeaders,
bool isHeadRequest,
string contentType)
{
if (responseHeaders == null)
{
responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase);
}
content = Compress(content, requestedCompressionType);
responseHeaders["Content-Encoding"] = requestedCompressionType;
responseHeaders["Vary"] = "Accept-Encoding";
var contentLength = content.Length;
if (isHeadRequest)
{
var result = new StreamWriter(Array.Empty(), contentType, contentLength);
AddResponseHeaders(result, responseHeaders);
return result;
}
else
{
var result = new StreamWriter(content, contentType, contentLength);
AddResponseHeaders(result, responseHeaders);
return result;
}
}
private byte[] Compress(byte[] bytes, string compressionType)
{
if (string.Equals(compressionType, "br", StringComparison.OrdinalIgnoreCase))
{
return CompressBrotli(bytes);
}
if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
{
return Deflate(bytes);
}
if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase))
{
return GZip(bytes);
}
throw new NotSupportedException(compressionType);
}
private byte[] CompressBrotli(byte[] bytes)
{
return _brotliCompressor.Compress(bytes);
}
private static byte[] Deflate(byte[] bytes)
{
// In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream
// Which means we must use MemoryStream since you have to use ToArray() on a closed Stream
using (var ms = new MemoryStream())
using (var zipStream = new DeflateStream(ms, CompressionMode.Compress))
{
zipStream.Write(bytes, 0, bytes.Length);
zipStream.Dispose();
return ms.ToArray();
}
}
private static byte[] GZip(byte[] buffer)
{
using (var ms = new MemoryStream())
using (var zipStream = new GZipStream(ms, CompressionMode.Compress))
{
zipStream.Write(buffer, 0, buffer.Length);
zipStream.Dispose();
return ms.ToArray();
}
}
private static string SerializeToXmlString(object from)
{
using (var ms = new MemoryStream())
{
var xwSettings = new XmlWriterSettings();
xwSettings.Encoding = new UTF8Encoding(false);
xwSettings.OmitXmlDeclaration = false;
using (var xw = XmlWriter.Create(ms, xwSettings))
{
var serializer = new DataContractSerializer(from.GetType());
serializer.WriteObject(xw, from);
xw.Flush();
ms.Seek(0, SeekOrigin.Begin);
using (var reader = new StreamReader(ms))
{
return reader.ReadToEnd();
}
}
}
}
///
/// Pres the process optimized result.
///
private object GetCachedResult(IRequest requestContext, IDictionary responseHeaders, StaticResultOptions options)
{
bool noCache = (requestContext.Headers.Get("Cache-Control") ?? string.Empty).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
if (!noCache)
{
DateTime.TryParse(requestContext.Headers.Get("If-Modified-Since"), out var ifModifiedSinceHeader);
if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
{
AddAgeHeader(responseHeaders, options.DateLastModified);
var result = new HttpResult(Array.Empty(), options.ContentType ?? "text/html", HttpStatusCode.NotModified);
AddResponseHeaders(result, responseHeaders);
return result;
}
}
return null;
}
public Task