using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Threading.Tasks; using MediaBrowser.Common.Logging; namespace MediaBrowser.Common.Net.Handlers { public abstract class BaseHandler { private Stream CompressedStream { get; set; } public virtual bool? UseChunkedEncoding { get { return null; } } private bool _TotalContentLengthDiscovered = false; private long? _TotalContentLength = null; public long? TotalContentLength { get { if (!_TotalContentLengthDiscovered) { _TotalContentLength = GetTotalContentLength(); } return _TotalContentLength; } } protected virtual bool SupportsByteRangeRequests { get { return false; } } /// /// The original HttpListenerContext /// protected HttpListenerContext HttpListenerContext { get; set; } /// /// The original QueryString /// protected NameValueCollection QueryString { get { return HttpListenerContext.Request.QueryString; } } protected List> _RequestedRanges = null; protected IEnumerable> RequestedRanges { get { if (_RequestedRanges == null) { _RequestedRanges = new List>(); if (IsRangeRequest) { // Example: bytes=0-,32-63 string[] ranges = HttpListenerContext.Request.Headers["Range"].Split('=')[1].Split(','); foreach (string range in ranges) { string[] vals = range.Split('-'); long start = 0; long? end = null; if (!string.IsNullOrEmpty(vals[0])) { start = long.Parse(vals[0]); } if (!string.IsNullOrEmpty(vals[1])) { end = long.Parse(vals[1]); } _RequestedRanges.Add(new KeyValuePair(start, end)); } } } return _RequestedRanges; } } protected bool IsRangeRequest { get { return HttpListenerContext.Request.Headers.AllKeys.Contains("Range"); } } /// /// Gets the MIME type to include in the response headers /// public abstract Task GetContentType(); /// /// Gets the status code to include in the response headers /// protected int StatusCode { get; set; } /// /// Gets the cache duration to include in the response headers /// public virtual TimeSpan CacheDuration { get { return TimeSpan.FromTicks(0); } } public virtual bool ShouldCompressResponse(string contentType) { return true; } private bool ClientSupportsCompression { get { string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty; return enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1; } } private string CompressionMethod { get { string enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty; if (enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1) { return "deflate"; } if (enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1) { return "gzip"; } return null; } } public virtual async Task ProcessRequest(HttpListenerContext ctx) { HttpListenerContext = ctx; Logger.LogInfo("Http Server received request at: " + ctx.Request.Url.ToString()); Logger.LogInfo("Http Headers: " + string.Join(",", ctx.Request.Headers.AllKeys.Select(k => k + "=" + ctx.Request.Headers[k]))); ctx.Response.AddHeader("Access-Control-Allow-Origin", "*"); ctx.Response.KeepAlive = true; try { if (SupportsByteRangeRequests && IsRangeRequest) { ctx.Response.Headers["Accept-Ranges"] = "bytes"; } // Set the initial status code // When serving a range request, we need to return status code 206 to indicate a partial response body StatusCode = SupportsByteRangeRequests && IsRangeRequest ? 206 : 200; ctx.Response.ContentType = await GetContentType(); TimeSpan cacheDuration = CacheDuration; DateTime? lastDateModified = await GetLastDateModified(); if (ctx.Request.Headers.AllKeys.Contains("If-Modified-Since")) { DateTime ifModifiedSince; if (DateTime.TryParse(ctx.Request.Headers["If-Modified-Since"].Replace(" GMT", string.Empty), out ifModifiedSince)) { // If the cache hasn't expired yet just return a 304 if (IsCacheValid(ifModifiedSince, cacheDuration, lastDateModified)) { StatusCode = 304; } } } await PrepareResponse(); if (IsResponseValid) { bool compressResponse = ShouldCompressResponse(ctx.Response.ContentType) && ClientSupportsCompression; await ProcessUncachedRequest(ctx, compressResponse, cacheDuration, lastDateModified); } else { ctx.Response.StatusCode = StatusCode; ctx.Response.SendChunked = false; } } catch (Exception ex) { // It might be too late if some response data has already been transmitted, but try to set this ctx.Response.StatusCode = 500; Logger.LogException(ex); } finally { DisposeResponseStream(); } } private async Task ProcessUncachedRequest(HttpListenerContext ctx, bool compressResponse, TimeSpan cacheDuration, DateTime? lastDateModified) { long? totalContentLength = TotalContentLength; // By default, use chunked encoding if we don't know the content length bool useChunkedEncoding = UseChunkedEncoding == null ? (totalContentLength == null) : UseChunkedEncoding.Value; // Don't force this to true. HttpListener will default it to true if supported by the client. if (!useChunkedEncoding) { ctx.Response.SendChunked = false; } // Set the content length, if we know it if (totalContentLength.HasValue) { ctx.Response.ContentLength64 = totalContentLength.Value; } // Add the compression header if (compressResponse) { ctx.Response.AddHeader("Content-Encoding", CompressionMethod); } // Add caching headers if (cacheDuration.Ticks > 0) { CacheResponse(ctx.Response, cacheDuration, lastDateModified); } // Set the status code ctx.Response.StatusCode = StatusCode; if (IsResponseValid) { // Finally, write the response data Stream outputStream = ctx.Response.OutputStream; if (compressResponse) { if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase)) { CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, false); } else { CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, false); } outputStream = CompressedStream; } await WriteResponseToOutputStream(outputStream); } else { ctx.Response.SendChunked = false; } } private void CacheResponse(HttpListenerResponse response, TimeSpan duration, DateTime? dateModified) { DateTime lastModified = dateModified ?? DateTime.Now; response.Headers[HttpResponseHeader.CacheControl] = "public, max-age=" + Convert.ToInt32(duration.TotalSeconds); response.Headers[HttpResponseHeader.Expires] = DateTime.Now.Add(duration).ToString("r"); response.Headers[HttpResponseHeader.LastModified] = lastModified.ToString("r"); } /// /// Gives subclasses a chance to do any prep work, and also to validate data and set an error status code, if needed /// protected virtual Task PrepareResponse() { return Task.Run(() => { }); } protected abstract Task WriteResponseToOutputStream(Stream stream); protected virtual void DisposeResponseStream() { if (CompressedStream != null) { CompressedStream.Dispose(); } HttpListenerContext.Response.OutputStream.Dispose(); } private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified) { if (dateModified.HasValue) { DateTime lastModified = NormalizeDateForComparison(dateModified.Value); ifModifiedSince = NormalizeDateForComparison(ifModifiedSince); return lastModified <= ifModifiedSince; } DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration); if (DateTime.Now < cacheExpirationDate) { return true; } return false; } /// /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that /// private DateTime NormalizeDateForComparison(DateTime date) { return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second); } protected virtual long? GetTotalContentLength() { return null; } protected virtual Task GetLastDateModified() { DateTime? value = null; return Task.Run(() => { return value; }); } private bool IsResponseValid { get { return StatusCode == 200 || StatusCode == 206; } } } }