123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 |
- using System;
- using System.Collections.Generic;
- using System.Collections.Specialized;
- using System.IO;
- using System.IO.Compression;
- using System.Linq;
- using System.Net;
- 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;
- }
- }
- /// <summary>
- /// Returns true or false indicating if the handler writes to the stream asynchronously.
- /// If so the subclass will be responsible for disposing the stream when complete.
- /// </summary>
- protected virtual bool IsAsyncHandler
- {
- get
- {
- return false;
- }
- }
- protected virtual bool SupportsByteRangeRequests
- {
- get
- {
- return false;
- }
- }
- /// <summary>
- /// The original HttpListenerContext
- /// </summary>
- protected HttpListenerContext HttpListenerContext { get; set; }
- /// <summary>
- /// The original QueryString
- /// </summary>
- protected NameValueCollection QueryString
- {
- get
- {
- return HttpListenerContext.Request.QueryString;
- }
- }
- protected List<KeyValuePair<long, long?>> _RequestedRanges = null;
- protected IEnumerable<KeyValuePair<long, long?>> RequestedRanges
- {
- get
- {
- if (_RequestedRanges == null)
- {
- _RequestedRanges = new List<KeyValuePair<long, long?>>();
- 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<long, long?>(start, end));
- }
- }
- }
- return _RequestedRanges;
- }
- }
- protected bool IsRangeRequest
- {
- get
- {
- return HttpListenerContext.Request.Headers.AllKeys.Contains("Range");
- }
- }
- /// <summary>
- /// Gets the MIME type to include in the response headers
- /// </summary>
- public abstract string ContentType { get; }
- /// <summary>
- /// Gets the status code to include in the response headers
- /// </summary>
- protected int StatusCode { get; set; }
- /// <summary>
- /// Gets the cache duration to include in the response headers
- /// </summary>
- public virtual TimeSpan CacheDuration
- {
- get
- {
- return TimeSpan.FromTicks(0);
- }
- }
- private bool _LastDateModifiedDiscovered = false;
- private DateTime? _LastDateModified = null;
- /// <summary>
- /// Gets the last date modified of the content being returned, if this can be determined.
- /// This will be used to invalidate the cache, so it's not needed if CacheDuration is 0.
- /// </summary>
- public DateTime? LastDateModified
- {
- get
- {
- if (!_LastDateModifiedDiscovered)
- {
- _LastDateModified = GetLastDateModified();
- }
- return _LastDateModified;
- }
- }
-
- public virtual bool CompressResponse
- {
- get
- {
- 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 void 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;
- 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 = ContentType;
- TimeSpan cacheDuration = CacheDuration;
- 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;
- }
- }
- }
- if (StatusCode == 200 || StatusCode == 206)
- {
- ProcessUncachedResponse(ctx, cacheDuration);
- }
- else
- {
- ctx.Response.StatusCode = StatusCode;
- ctx.Response.SendChunked = false;
- DisposeResponseStream();
- }
- }
- private void ProcessUncachedResponse(HttpListenerContext ctx, TimeSpan cacheDuration)
- {
- 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 && ClientSupportsCompression)
- {
- ctx.Response.AddHeader("Content-Encoding", CompressionMethod);
- }
- // Add caching headers
- if (cacheDuration.Ticks > 0)
- {
- CacheResponse(ctx.Response, cacheDuration, LastDateModified);
- }
- PrepareUncachedResponse(ctx, cacheDuration);
- // Set the status code
- ctx.Response.StatusCode = StatusCode;
- if (StatusCode == 200 || StatusCode == 206)
- {
- // Finally, write the response data
- Stream outputStream = ctx.Response.OutputStream;
- if (CompressResponse && ClientSupportsCompression)
- {
- if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase))
- {
- CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, false);
- }
- else
- {
- CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, false);
- }
- outputStream = CompressedStream;
- }
- WriteResponseToOutputStream(outputStream);
- if (!IsAsyncHandler)
- {
- DisposeResponseStream();
- }
- }
- else
- {
- ctx.Response.SendChunked = false;
- DisposeResponseStream();
- }
- }
- protected virtual void PrepareUncachedResponse(HttpListenerContext ctx, TimeSpan cacheDuration)
- {
- }
- 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");
- }
- protected abstract void WriteResponseToOutputStream(Stream stream);
- protected 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;
- }
- /// <summary>
- /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
- /// </summary>
- 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 DateTime? GetLastDateModified()
- {
- return null;
- }
- }
- }
|