using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Common.Kernel; using MediaBrowser.Common.Net; using MediaBrowser.Model.Logging; using ServiceStack.Common; using ServiceStack.Common.Web; using ServiceStack.ServiceHost; using ServiceStack.ServiceInterface; using System; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; using MimeTypes = MediaBrowser.Common.Net.MimeTypes; namespace MediaBrowser.Common.Implementations.HttpServer { /// /// Class BaseRestService /// public class BaseRestService : Service, IRestfulService { /// /// Gets or sets the kernel. /// /// The kernel. public IKernel Kernel { get; set; } /// /// Gets or sets the logger. /// /// The logger. public ILogger Logger { get; set; } /// /// Gets a value indicating whether this instance is range request. /// /// true if this instance is range request; otherwise, false. protected bool IsRangeRequest { get { return Request.Headers.AllKeys.Contains("Range"); } } /// /// To the optimized result. /// /// /// The result. /// System.Object. /// result protected object ToOptimizedResult(T result) where T : class { if (result == null) { throw new ArgumentNullException("result"); } Response.AddHeader("Vary", "Accept-Encoding"); return RequestContext.ToOptimizedResult(result); } /// /// To the optimized result using cache. /// /// /// The cache key. /// The last date modified. /// Duration of the cache. /// The factory fn. /// System.Object. /// cacheKey protected object ToOptimizedResultUsingCache(Guid cacheKey, DateTime lastDateModified, TimeSpan? cacheDuration, Func factoryFn) where T : class { if (cacheKey == Guid.Empty) { throw new ArgumentNullException("cacheKey"); } if (factoryFn == null) { throw new ArgumentNullException("factoryFn"); } var key = cacheKey.ToString("N"); var result = PreProcessCachedResult(cacheKey, key, lastDateModified, cacheDuration, string.Empty); if (result != null) { return result; } return ToOptimizedResult(factoryFn()); } /// /// To the cached result. /// /// /// The cache key. /// The last date modified. /// Duration of the cache. /// The factory fn. /// Type of the content. /// System.Object. /// cacheKey protected object ToCachedResult(Guid cacheKey, DateTime lastDateModified, TimeSpan? cacheDuration, Func factoryFn, string contentType) where T : class { if (cacheKey == Guid.Empty) { throw new ArgumentNullException("cacheKey"); } if (factoryFn == null) { throw new ArgumentNullException("factoryFn"); } var key = cacheKey.ToString("N"); var result = PreProcessCachedResult(cacheKey, key, lastDateModified, cacheDuration, contentType); if (result != null) { return result; } return factoryFn(); } /// /// To the static file result. /// /// The path. /// System.Object. /// path protected object ToStaticFileResult(string path) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); } var dateModified = File.GetLastWriteTimeUtc(path); var cacheKey = path + dateModified.Ticks; return ToStaticResult(cacheKey.GetMD5(), dateModified, null, MimeTypes.GetMimeType(path), () => Task.FromResult(GetFileStream(path))); } /// /// Gets the file stream. /// /// The path. /// Stream. private Stream GetFileStream(string path) { return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous); } /// /// To the static result. /// /// The cache key. /// The last date modified. /// Duration of the cache. /// Type of the content. /// The factory fn. /// System.Object. /// cacheKey protected object ToStaticResult(Guid cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration, string contentType, Func> factoryFn) { if (cacheKey == Guid.Empty) { throw new ArgumentNullException("cacheKey"); } if (factoryFn == null) { throw new ArgumentNullException("factoryFn"); } var key = cacheKey.ToString("N"); var result = PreProcessCachedResult(cacheKey, key, lastDateModified, cacheDuration, contentType); if (result != null) { return result; } var compress = ShouldCompressResponse(contentType); if (compress) { Response.AddHeader("Vary", "Accept-Encoding"); } return ToStaticResult(contentType, factoryFn, compress).Result; } /// /// Shoulds the compress response. /// /// Type of the content. /// true if XXXX, false otherwise private bool ShouldCompressResponse(string contentType) { // It will take some work to support compression with byte range requests if (IsRangeRequest) { return false; } // Don't compress media if (contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase)) { return false; } // Don't compress images if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) { return false; } if (contentType.StartsWith("font/", StringComparison.OrdinalIgnoreCase)) { return false; } if (contentType.StartsWith("application/", StringComparison.OrdinalIgnoreCase)) { return false; } return true; } /// /// To the static result. /// /// Type of the content. /// The factory fn. /// if set to true [compress]. /// System.Object. private async Task ToStaticResult(string contentType, Func> factoryFn, bool compress) { if (!compress || string.IsNullOrEmpty(RequestContext.CompressionType)) { Response.ContentType = contentType; var stream = await factoryFn().ConfigureAwait(false); return new StreamWriter(stream); } string content; using (var stream = await factoryFn().ConfigureAwait(false)) { using (var reader = new StreamReader(stream)) { content = await reader.ReadToEndAsync().ConfigureAwait(false); } } var contents = content.Compress(RequestContext.CompressionType); return new CompressedResult(contents, RequestContext.CompressionType, contentType); } /// /// Pres the process optimized result. /// /// The cache key. /// The cache key string. /// The last date modified. /// Duration of the cache. /// Type of the content. /// System.Object. private object PreProcessCachedResult(Guid cacheKey, string cacheKeyString, DateTime? lastDateModified, TimeSpan? cacheDuration, string contentType) { Response.AddHeader("ETag", cacheKeyString); if (IsNotModified(cacheKey, lastDateModified, cacheDuration)) { AddAgeHeader(lastDateModified); AddExpiresHeader(cacheKeyString, cacheDuration); //ctx.Response.SendChunked = false; if (!string.IsNullOrEmpty(contentType)) { Response.ContentType = contentType; } return new HttpResult(new byte[] { }, HttpStatusCode.NotModified); } SetCachingHeaders(cacheKeyString, lastDateModified, cacheDuration); return null; } /// /// Determines whether [is not modified] [the specified cache key]. /// /// The cache key. /// The last date modified. /// Duration of the cache. /// true if [is not modified] [the specified cache key]; otherwise, false. private bool IsNotModified(Guid? cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration) { var isNotModified = true; if (Request.Headers.AllKeys.Contains("If-Modified-Since")) { DateTime ifModifiedSince; if (DateTime.TryParse(Request.Headers["If-Modified-Since"], out ifModifiedSince)) { isNotModified = IsNotModified(ifModifiedSince.ToUniversalTime(), cacheDuration, lastDateModified); } } // Validate If-None-Match if (isNotModified && (cacheKey.HasValue || !string.IsNullOrEmpty(Request.Headers["If-None-Match"]))) { Guid ifNoneMatch; if (Guid.TryParse(Request.Headers["If-None-Match"] ?? string.Empty, out ifNoneMatch)) { if (cacheKey.HasValue && cacheKey.Value == ifNoneMatch) { return true; } } } return false; } /// /// Determines whether [is not modified] [the specified if modified since]. /// /// If modified since. /// Duration of the cache. /// The date modified. /// true if [is not modified] [the specified if modified since]; otherwise, false. private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified) { if (dateModified.HasValue) { var lastModified = NormalizeDateForComparison(dateModified.Value); ifModifiedSince = NormalizeDateForComparison(ifModifiedSince); return lastModified <= ifModifiedSince; } if (cacheDuration.HasValue) { var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value); if (DateTime.UtcNow < cacheExpirationDate) { return true; } } return false; } /// /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that /// /// The date. /// DateTime. private DateTime NormalizeDateForComparison(DateTime date) { return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind); } /// /// Sets the caching headers. /// /// The cache key. /// The last date modified. /// Duration of the cache. private void SetCachingHeaders(string cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration) { // Don't specify both last modified and Etag, unless caching unconditionally. They are redundant // https://developers.google.com/speed/docs/best-practices/caching#LeverageBrowserCaching if (lastDateModified.HasValue && (string.IsNullOrEmpty(cacheKey) || cacheDuration.HasValue)) { AddAgeHeader(lastDateModified); Response.AddHeader("LastModified", lastDateModified.Value.ToString("r")); } if (cacheDuration.HasValue) { Response.AddHeader("Cache-Control", "public, max-age=" + Convert.ToInt32(cacheDuration.Value.TotalSeconds)); } else if (!string.IsNullOrEmpty(cacheKey)) { Response.AddHeader("Cache-Control", "public"); } else { Response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate"); Response.AddHeader("pragma", "no-cache, no-store, must-revalidate"); } AddExpiresHeader(cacheKey, cacheDuration); } /// /// Adds the expires header. /// /// The cache key. /// Duration of the cache. private void AddExpiresHeader(string cacheKey, TimeSpan? cacheDuration) { if (cacheDuration.HasValue) { Response.AddHeader("Expires", DateTime.UtcNow.Add(cacheDuration.Value).ToString("r")); } else if (string.IsNullOrEmpty(cacheKey)) { Response.AddHeader("Expires", "-1"); } } /// /// Adds the age header. /// /// The last date modified. private void AddAgeHeader(DateTime? lastDateModified) { if (lastDateModified.HasValue) { Response.AddHeader("Age", Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture)); } } } }