diff options
Diffstat (limited to 'MediaBrowser.Common/Net')
| -rw-r--r-- | MediaBrowser.Common/Net/Handlers/BaseHandler.cs | 808 | ||||
| -rw-r--r-- | MediaBrowser.Common/Net/Handlers/IHttpServerHandler.cs | 32 | ||||
| -rw-r--r-- | MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs | 264 |
3 files changed, 0 insertions, 1104 deletions
diff --git a/MediaBrowser.Common/Net/Handlers/BaseHandler.cs b/MediaBrowser.Common/Net/Handlers/BaseHandler.cs deleted file mode 100644 index 5d26c7e92..000000000 --- a/MediaBrowser.Common/Net/Handlers/BaseHandler.cs +++ /dev/null @@ -1,808 +0,0 @@ -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Kernel; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; - -namespace MediaBrowser.Common.Net.Handlers -{ - /// <summary> - /// Class BaseHandler - /// </summary> - public abstract class BaseHandler<TKernelType> : IHttpServerHandler - where TKernelType : IKernel - { - /// <summary> - /// Initializes the specified kernel. - /// </summary> - /// <param name="kernel">The kernel.</param> - public void Initialize(IKernel kernel) - { - Kernel = (TKernelType)kernel; - } - - /// <summary> - /// Gets or sets the kernel. - /// </summary> - /// <value>The kernel.</value> - protected TKernelType Kernel { get; private set; } - - /// <summary> - /// Gets the URL suffix used to determine if this handler can process a request. - /// </summary> - /// <value>The URL suffix.</value> - protected virtual string UrlSuffix - { - get - { - var name = GetType().Name; - - const string srch = "Handler"; - - if (name.EndsWith(srch, StringComparison.OrdinalIgnoreCase)) - { - name = name.Substring(0, name.Length - srch.Length); - } - - return "api/" + name; - } - } - - /// <summary> - /// Handleses the request. - /// </summary> - /// <param name="request">The request.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - public virtual bool HandlesRequest(HttpListenerRequest request) - { - var name = '/' + UrlSuffix.TrimStart('/'); - - var url = Kernel.WebApplicationName + name; - - return request.Url.LocalPath.EndsWith(url, StringComparison.OrdinalIgnoreCase); - } - - /// <summary> - /// Gets or sets the compressed stream. - /// </summary> - /// <value>The compressed stream.</value> - private Stream CompressedStream { get; set; } - - /// <summary> - /// Gets a value indicating whether [use chunked encoding]. - /// </summary> - /// <value><c>null</c> if [use chunked encoding] contains no value, <c>true</c> if [use chunked encoding]; otherwise, <c>false</c>.</value> - public virtual bool? UseChunkedEncoding - { - get - { - return null; - } - } - - /// <summary> - /// The original HttpListenerContext - /// </summary> - /// <value>The HTTP listener context.</value> - protected HttpListenerContext HttpListenerContext { get; set; } - - /// <summary> - /// The _query string - /// </summary> - private NameValueCollection _queryString; - /// <summary> - /// The original QueryString - /// </summary> - /// <value>The query string.</value> - public NameValueCollection QueryString - { - get - { - // HttpListenerContext.Request.QueryString is not decoded properly - return _queryString; - } - } - - /// <summary> - /// The _requested ranges - /// </summary> - private List<KeyValuePair<long, long?>> _requestedRanges; - /// <summary> - /// Gets the requested ranges. - /// </summary> - /// <value>The requested ranges.</value> - protected IEnumerable<KeyValuePair<long, long?>> RequestedRanges - { - get - { - if (_requestedRanges == null) - { - _requestedRanges = new List<KeyValuePair<long, long?>>(); - - if (IsRangeRequest) - { - // Example: bytes=0-,32-63 - var ranges = HttpListenerContext.Request.Headers["Range"].Split('=')[1].Split(','); - - foreach (var range in ranges) - { - var 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; - } - } - - /// <summary> - /// Gets a value indicating whether this instance is range request. - /// </summary> - /// <value><c>true</c> if this instance is range request; otherwise, <c>false</c>.</value> - protected bool IsRangeRequest - { - get - { - return HttpListenerContext.Request.Headers.AllKeys.Contains("Range"); - } - } - - /// <summary> - /// Gets a value indicating whether [client supports compression]. - /// </summary> - /// <value><c>true</c> if [client supports compression]; otherwise, <c>false</c>.</value> - protected bool ClientSupportsCompression - { - get - { - var enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty; - - return enc.Equals("*", StringComparison.OrdinalIgnoreCase) || - enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || - enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1; - } - } - - /// <summary> - /// Gets the compression method. - /// </summary> - /// <value>The compression method.</value> - private string CompressionMethod - { - get - { - var enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty; - - if (enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.Equals("*", StringComparison.OrdinalIgnoreCase)) - { - return "deflate"; - } - if (enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1) - { - return "gzip"; - } - - return null; - } - } - - /// <summary> - /// Processes the request. - /// </summary> - /// <param name="ctx">The CTX.</param> - /// <returns>Task.</returns> - public virtual async Task ProcessRequest(HttpListenerContext ctx) - { - HttpListenerContext = ctx; - - ctx.Response.AddHeader("Access-Control-Allow-Origin", "*"); - - ctx.Response.KeepAlive = true; - - try - { - await ProcessRequestInternal(ctx).ConfigureAwait(false); - } - catch (InvalidOperationException ex) - { - HandleException(ctx.Response, ex, 422); - - throw; - } - catch (ResourceNotFoundException ex) - { - HandleException(ctx.Response, ex, 404); - - throw; - } - catch (FileNotFoundException ex) - { - HandleException(ctx.Response, ex, 404); - - throw; - } - catch (DirectoryNotFoundException ex) - { - HandleException(ctx.Response, ex, 404); - - throw; - } - catch (UnauthorizedAccessException ex) - { - HandleException(ctx.Response, ex, 401); - - throw; - } - catch (ArgumentException ex) - { - HandleException(ctx.Response, ex, 400); - - throw; - } - catch (Exception ex) - { - HandleException(ctx.Response, ex, 500); - - throw; - } - finally - { - DisposeResponseStream(); - } - } - - /// <summary> - /// Appends the error message. - /// </summary> - /// <param name="response">The response.</param> - /// <param name="ex">The ex.</param> - /// <param name="statusCode">The status code.</param> - private void HandleException(HttpListenerResponse response, Exception ex, int statusCode) - { - response.StatusCode = statusCode; - - response.Headers.Add("Status", statusCode.ToString(new CultureInfo("en-US"))); - - response.Headers.Remove("Age"); - response.Headers.Remove("Expires"); - response.Headers.Remove("Cache-Control"); - response.Headers.Remove("Etag"); - response.Headers.Remove("Last-Modified"); - - response.ContentType = "text/plain"; - - //Logger.ErrorException("Error processing request", ex); - - if (!string.IsNullOrEmpty(ex.Message)) - { - response.AddHeader("X-Application-Error-Code", ex.Message); - } - - var bytes = Encoding.UTF8.GetBytes(ex.Message); - - var stream = CompressedStream ?? response.OutputStream; - - // This could fail, but try to add the stack trace as the body content - try - { - stream.Write(bytes, 0, bytes.Length); - } - catch (Exception ex1) - { - //Logger.ErrorException("Error dumping stack trace", ex1); - } - } - - /// <summary> - /// Processes the request internal. - /// </summary> - /// <param name="ctx">The CTX.</param> - /// <returns>Task.</returns> - private async Task ProcessRequestInternal(HttpListenerContext ctx) - { - var responseInfo = await GetResponseInfo().ConfigureAwait(false); - - // Let the client know if byte range requests are supported or not - if (responseInfo.SupportsByteRangeRequests) - { - ctx.Response.Headers["Accept-Ranges"] = "bytes"; - } - else if (!responseInfo.SupportsByteRangeRequests) - { - ctx.Response.Headers["Accept-Ranges"] = "none"; - } - - if (responseInfo.IsResponseValid && responseInfo.SupportsByteRangeRequests && IsRangeRequest) - { - // Set the initial status code - // When serving a range request, we need to return status code 206 to indicate a partial response body - responseInfo.StatusCode = 206; - } - - ctx.Response.ContentType = responseInfo.ContentType; - - if (responseInfo.Etag.HasValue) - { - ctx.Response.Headers["ETag"] = responseInfo.Etag.Value.ToString("N"); - } - - var isCacheValid = true; - - // Validate If-Modified-Since - if (ctx.Request.Headers.AllKeys.Contains("If-Modified-Since")) - { - DateTime ifModifiedSince; - - if (DateTime.TryParse(ctx.Request.Headers["If-Modified-Since"], out ifModifiedSince)) - { - isCacheValid = IsCacheValid(ifModifiedSince.ToUniversalTime(), responseInfo.CacheDuration, - responseInfo.DateLastModified); - } - } - - // Validate If-None-Match - if (isCacheValid && - (responseInfo.Etag.HasValue || !string.IsNullOrEmpty(ctx.Request.Headers["If-None-Match"]))) - { - Guid ifNoneMatch; - - if (Guid.TryParse(ctx.Request.Headers["If-None-Match"] ?? string.Empty, out ifNoneMatch)) - { - if (responseInfo.Etag.HasValue && responseInfo.Etag.Value == ifNoneMatch) - { - responseInfo.StatusCode = 304; - } - } - } - - LogResponse(ctx, responseInfo); - - if (responseInfo.IsResponseValid) - { - await OnProcessingRequest(responseInfo).ConfigureAwait(false); - } - - if (responseInfo.IsResponseValid) - { - await ProcessUncachedRequest(ctx, responseInfo).ConfigureAwait(false); - } - else - { - if (responseInfo.StatusCode == 304) - { - AddAgeHeader(ctx.Response, responseInfo); - AddExpiresHeader(ctx.Response, responseInfo); - } - - ctx.Response.StatusCode = responseInfo.StatusCode; - ctx.Response.SendChunked = false; - } - } - - /// <summary> - /// The _null task result - /// </summary> - private readonly Task<bool> _nullTaskResult = Task.FromResult(true); - - /// <summary> - /// Called when [processing request]. - /// </summary> - /// <param name="responseInfo">The response info.</param> - /// <returns>Task.</returns> - protected virtual Task OnProcessingRequest(ResponseInfo responseInfo) - { - return _nullTaskResult; - } - - /// <summary> - /// Logs the response. - /// </summary> - /// <param name="ctx">The CTX.</param> - /// <param name="responseInfo">The response info.</param> - private void LogResponse(HttpListenerContext ctx, ResponseInfo responseInfo) - { - // Don't log normal 200's - if (responseInfo.StatusCode == 200) - { - return; - } - - var log = new StringBuilder(); - - log.AppendLine(string.Format("Url: {0}", ctx.Request.Url)); - - log.AppendLine("Headers: " + string.Join(",", ctx.Response.Headers.AllKeys.Select(k => k + "=" + ctx.Response.Headers[k]))); - - var msg = "Http Response Sent (" + responseInfo.StatusCode + ") to " + ctx.Request.RemoteEndPoint; - - if (Kernel.Configuration.EnableHttpLevelLogging) - { - //Logger.LogMultiline(msg, LogSeverity.Debug, log); - } - } - - /// <summary> - /// Processes the uncached request. - /// </summary> - /// <param name="ctx">The CTX.</param> - /// <param name="responseInfo">The response info.</param> - /// <returns>Task.</returns> - private async Task ProcessUncachedRequest(HttpListenerContext ctx, ResponseInfo responseInfo) - { - var totalContentLength = GetTotalContentLength(responseInfo); - - // By default, use chunked encoding if we don't know the content length - var 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; - } - - var compressResponse = responseInfo.CompressResponse && ClientSupportsCompression; - - // Add the compression header - if (compressResponse) - { - ctx.Response.AddHeader("Content-Encoding", CompressionMethod); - ctx.Response.AddHeader("Vary", "Accept-Encoding"); - } - - // 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 (responseInfo.DateLastModified.HasValue && (!responseInfo.Etag.HasValue || responseInfo.CacheDuration.Ticks > 0)) - { - ctx.Response.Headers[HttpResponseHeader.LastModified] = responseInfo.DateLastModified.Value.ToString("r"); - AddAgeHeader(ctx.Response, responseInfo); - } - - // Add caching headers - ConfigureCaching(ctx.Response, responseInfo); - - // Set the status code - ctx.Response.StatusCode = responseInfo.StatusCode; - - if (responseInfo.IsResponseValid) - { - // Finally, write the response data - var outputStream = ctx.Response.OutputStream; - - if (compressResponse) - { - if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase)) - { - CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, true); - } - else - { - CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, true); - } - - outputStream = CompressedStream; - } - - await WriteResponseToOutputStream(outputStream, responseInfo, totalContentLength).ConfigureAwait(false); - } - else - { - ctx.Response.SendChunked = false; - } - } - - /// <summary> - /// Configures the caching. - /// </summary> - /// <param name="response">The response.</param> - /// <param name="responseInfo">The response info.</param> - private void ConfigureCaching(HttpListenerResponse response, ResponseInfo responseInfo) - { - if (responseInfo.CacheDuration.Ticks > 0) - { - response.Headers[HttpResponseHeader.CacheControl] = "public, max-age=" + Convert.ToInt32(responseInfo.CacheDuration.TotalSeconds); - } - else if (responseInfo.Etag.HasValue) - { - response.Headers[HttpResponseHeader.CacheControl] = "public"; - } - else - { - response.Headers[HttpResponseHeader.CacheControl] = "no-cache, no-store, must-revalidate"; - response.Headers[HttpResponseHeader.Pragma] = "no-cache, no-store, must-revalidate"; - } - - AddExpiresHeader(response, responseInfo); - } - - /// <summary> - /// Adds the expires header. - /// </summary> - /// <param name="response">The response.</param> - /// <param name="responseInfo">The response info.</param> - private void AddExpiresHeader(HttpListenerResponse response, ResponseInfo responseInfo) - { - if (responseInfo.CacheDuration.Ticks > 0) - { - response.Headers[HttpResponseHeader.Expires] = DateTime.UtcNow.Add(responseInfo.CacheDuration).ToString("r"); - } - else if (!responseInfo.Etag.HasValue) - { - response.Headers[HttpResponseHeader.Expires] = "-1"; - } - } - - /// <summary> - /// Adds the age header. - /// </summary> - /// <param name="response">The response.</param> - /// <param name="responseInfo">The response info.</param> - private void AddAgeHeader(HttpListenerResponse response, ResponseInfo responseInfo) - { - if (responseInfo.DateLastModified.HasValue) - { - response.Headers[HttpResponseHeader.Age] = Convert.ToInt32((DateTime.UtcNow - responseInfo.DateLastModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture); - } - } - - /// <summary> - /// Writes the response to output stream. - /// </summary> - /// <param name="stream">The stream.</param> - /// <param name="responseInfo">The response info.</param> - /// <param name="contentLength">Length of the content.</param> - /// <returns>Task.</returns> - protected abstract Task WriteResponseToOutputStream(Stream stream, ResponseInfo responseInfo, long? contentLength); - - /// <summary> - /// Disposes the response stream. - /// </summary> - protected virtual void DisposeResponseStream() - { - if (CompressedStream != null) - { - try - { - CompressedStream.Dispose(); - } - catch (Exception ex) - { - //Logger.ErrorException("Error disposing compressed stream", ex); - } - } - - try - { - //HttpListenerContext.Response.OutputStream.Dispose(); - HttpListenerContext.Response.Close(); - } - catch (Exception ex) - { - //Logger.ErrorException("Error disposing response", ex); - } - } - - /// <summary> - /// Determines whether [is cache valid] [the specified if modified since]. - /// </summary> - /// <param name="ifModifiedSince">If modified since.</param> - /// <param name="cacheDuration">Duration of the cache.</param> - /// <param name="dateModified">The date modified.</param> - /// <returns><c>true</c> if [is cache valid] [the specified if modified since]; otherwise, <c>false</c>.</returns> - 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.UtcNow < 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> - /// <param name="date">The date.</param> - /// <returns>DateTime.</returns> - private DateTime NormalizeDateForComparison(DateTime date) - { - return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind); - } - - /// <summary> - /// Gets the total length of the content. - /// </summary> - /// <param name="responseInfo">The response info.</param> - /// <returns>System.Nullable{System.Int64}.</returns> - protected virtual long? GetTotalContentLength(ResponseInfo responseInfo) - { - return null; - } - - /// <summary> - /// Gets the response info. - /// </summary> - /// <returns>Task{ResponseInfo}.</returns> - protected abstract Task<ResponseInfo> GetResponseInfo(); - - /// <summary> - /// Gets a bool query string param. - /// </summary> - /// <param name="name">The name.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - protected bool GetBoolQueryStringParam(string name) - { - var val = QueryString[name] ?? string.Empty; - - return val.Equals("1", StringComparison.OrdinalIgnoreCase) || val.Equals("true", StringComparison.OrdinalIgnoreCase); - } - - /// <summary> - /// The _form values - /// </summary> - private Hashtable _formValues; - - /// <summary> - /// Gets a value from form POST data - /// </summary> - /// <param name="name">The name.</param> - /// <returns>Task{System.String}.</returns> - protected async Task<string> GetFormValue(string name) - { - if (_formValues == null) - { - _formValues = await GetFormValues(HttpListenerContext.Request).ConfigureAwait(false); - } - - if (_formValues.ContainsKey(name)) - { - return _formValues[name].ToString(); - } - - return null; - } - - /// <summary> - /// Extracts form POST data from a request - /// </summary> - /// <param name="request">The request.</param> - /// <returns>Task{Hashtable}.</returns> - private async Task<Hashtable> GetFormValues(HttpListenerRequest request) - { - var formVars = new Hashtable(); - - if (request.HasEntityBody) - { - if (request.ContentType.IndexOf("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) != -1) - { - using (var requestBody = request.InputStream) - { - using (var reader = new StreamReader(requestBody, request.ContentEncoding)) - { - var s = await reader.ReadToEndAsync().ConfigureAwait(false); - - var pairs = s.Split('&'); - - foreach (var pair in pairs) - { - var index = pair.IndexOf('='); - - if (index != -1) - { - var name = pair.Substring(0, index); - var value = pair.Substring(index + 1); - formVars.Add(name, value); - } - } - } - } - } - } - - return formVars; - } - } - - /// <summary> - /// Class ResponseInfo - /// </summary> - public class ResponseInfo - { - /// <summary> - /// Gets or sets the type of the content. - /// </summary> - /// <value>The type of the content.</value> - public string ContentType { get; set; } - /// <summary> - /// Gets or sets the etag. - /// </summary> - /// <value>The etag.</value> - public Guid? Etag { get; set; } - /// <summary> - /// Gets or sets the date last modified. - /// </summary> - /// <value>The date last modified.</value> - public DateTime? DateLastModified { get; set; } - /// <summary> - /// Gets or sets the duration of the cache. - /// </summary> - /// <value>The duration of the cache.</value> - public TimeSpan CacheDuration { get; set; } - /// <summary> - /// Gets or sets a value indicating whether [compress response]. - /// </summary> - /// <value><c>true</c> if [compress response]; otherwise, <c>false</c>.</value> - public bool CompressResponse { get; set; } - /// <summary> - /// Gets or sets the status code. - /// </summary> - /// <value>The status code.</value> - public int StatusCode { get; set; } - /// <summary> - /// Gets or sets a value indicating whether [supports byte range requests]. - /// </summary> - /// <value><c>true</c> if [supports byte range requests]; otherwise, <c>false</c>.</value> - public bool SupportsByteRangeRequests { get; set; } - - /// <summary> - /// Initializes a new instance of the <see cref="ResponseInfo" /> class. - /// </summary> - public ResponseInfo() - { - CacheDuration = TimeSpan.FromTicks(0); - - CompressResponse = true; - - StatusCode = 200; - } - - /// <summary> - /// Gets a value indicating whether this instance is response valid. - /// </summary> - /// <value><c>true</c> if this instance is response valid; otherwise, <c>false</c>.</value> - public bool IsResponseValid - { - get - { - return StatusCode >= 200 && StatusCode < 300; - } - } - } -}
\ No newline at end of file diff --git a/MediaBrowser.Common/Net/Handlers/IHttpServerHandler.cs b/MediaBrowser.Common/Net/Handlers/IHttpServerHandler.cs deleted file mode 100644 index dadd61473..000000000 --- a/MediaBrowser.Common/Net/Handlers/IHttpServerHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using MediaBrowser.Common.Kernel; -using System.Net; -using System.Threading.Tasks; - -namespace MediaBrowser.Common.Net.Handlers -{ - /// <summary> - /// Interface IHttpServerHandler - /// </summary> - public interface IHttpServerHandler - { - /// <summary> - /// Initializes the specified kernel. - /// </summary> - /// <param name="kernel">The kernel.</param> - void Initialize(IKernel kernel); - - /// <summary> - /// Handleses the request. - /// </summary> - /// <param name="request">The request.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - bool HandlesRequest(HttpListenerRequest request); - - /// <summary> - /// Processes the request. - /// </summary> - /// <param name="ctx">The CTX.</param> - /// <returns>Task.</returns> - Task ProcessRequest(HttpListenerContext ctx); - } -} diff --git a/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs b/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs deleted file mode 100644 index 3967d15c3..000000000 --- a/MediaBrowser.Common/Net/Handlers/StaticFileHandler.cs +++ /dev/null @@ -1,264 +0,0 @@ -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.IO; -using MediaBrowser.Common.Kernel; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -namespace MediaBrowser.Common.Net.Handlers -{ - /// <summary> - /// Represents an http handler that serves static content - /// </summary> - public class StaticFileHandler : BaseHandler<IKernel> - { - /// <summary> - /// Initializes a new instance of the <see cref="StaticFileHandler" /> class. - /// </summary> - /// <param name="kernel">The kernel.</param> - public StaticFileHandler(IKernel kernel) - { - Initialize(kernel); - } - - /// <summary> - /// The _path - /// </summary> - private string _path; - /// <summary> - /// Gets or sets the path to the static resource - /// </summary> - /// <value>The path.</value> - public string Path - { - get - { - if (!string.IsNullOrWhiteSpace(_path)) - { - return _path; - } - - return QueryString["path"]; - } - set - { - _path = value; - } - } - - /// <summary> - /// Gets or sets the last date modified of the resource - /// </summary> - /// <value>The last date modified.</value> - public DateTime? LastDateModified { get; set; } - - /// <summary> - /// Gets or sets the content type of the resource - /// </summary> - /// <value>The type of the content.</value> - public string ContentType { get; set; } - - /// <summary> - /// Gets or sets the content type of the resource - /// </summary> - /// <value>The etag.</value> - public Guid Etag { get; set; } - - /// <summary> - /// Gets or sets the source stream of the resource - /// </summary> - /// <value>The source stream.</value> - public Stream SourceStream { get; set; } - - /// <summary> - /// Shoulds the compress response. - /// </summary> - /// <param name="contentType">Type of the content.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> - 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; - } - - return true; - } - - /// <summary> - /// Gets or sets the duration of the cache. - /// </summary> - /// <value>The duration of the cache.</value> - public TimeSpan? CacheDuration { get; set; } - - /// <summary> - /// Gets the total length of the content. - /// </summary> - /// <param name="responseInfo">The response info.</param> - /// <returns>System.Nullable{System.Int64}.</returns> - protected override long? GetTotalContentLength(ResponseInfo responseInfo) - { - // If we're compressing the response, content length must be the compressed length, which we don't know - if (responseInfo.CompressResponse && ClientSupportsCompression) - { - return null; - } - - return SourceStream.Length; - } - - /// <summary> - /// Gets the response info. - /// </summary> - /// <returns>Task{ResponseInfo}.</returns> - protected override Task<ResponseInfo> GetResponseInfo() - { - var info = new ResponseInfo - { - ContentType = ContentType ?? MimeTypes.GetMimeType(Path), - Etag = Etag, - DateLastModified = LastDateModified - }; - - if (SourceStream == null && !string.IsNullOrEmpty(Path)) - { - // FileShare must be ReadWrite in case someone else is currently writing to it. - SourceStream = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous); - } - - info.CompressResponse = ShouldCompressResponse(info.ContentType); - - info.SupportsByteRangeRequests = !info.CompressResponse || !ClientSupportsCompression; - - if (!info.DateLastModified.HasValue && !string.IsNullOrWhiteSpace(Path)) - { - info.DateLastModified = File.GetLastWriteTimeUtc(Path); - } - - if (CacheDuration.HasValue) - { - info.CacheDuration = CacheDuration.Value; - } - - if (SourceStream == null && string.IsNullOrEmpty(Path)) - { - throw new ResourceNotFoundException(); - } - - return Task.FromResult(info); - } - - /// <summary> - /// Writes the response to output stream. - /// </summary> - /// <param name="stream">The stream.</param> - /// <param name="responseInfo">The response info.</param> - /// <param name="totalContentLength">Total length of the content.</param> - /// <returns>Task.</returns> - protected override Task WriteResponseToOutputStream(Stream stream, ResponseInfo responseInfo, long? totalContentLength) - { - if (IsRangeRequest && totalContentLength.HasValue) - { - var requestedRange = RequestedRanges.First(); - - // If the requested range is "0-", we can optimize by just doing a stream copy - if (!requestedRange.Value.HasValue) - { - return ServeCompleteRangeRequest(requestedRange, stream, totalContentLength.Value); - } - - // This will have to buffer a portion of the content into memory - return ServePartialRangeRequest(requestedRange.Key, requestedRange.Value.Value, stream, totalContentLength.Value); - } - - return SourceStream.CopyToAsync(stream); - } - - /// <summary> - /// Disposes the response stream. - /// </summary> - protected override void DisposeResponseStream() - { - if (SourceStream != null) - { - SourceStream.Dispose(); - } - - base.DisposeResponseStream(); - } - - /// <summary> - /// Handles a range request of "bytes=0-" - /// This will serve the complete content and add the content-range header - /// </summary> - /// <param name="requestedRange">The requested range.</param> - /// <param name="responseStream">The response stream.</param> - /// <param name="totalContentLength">Total length of the content.</param> - /// <returns>Task.</returns> - private Task ServeCompleteRangeRequest(KeyValuePair<long, long?> requestedRange, Stream responseStream, long totalContentLength) - { - var rangeStart = requestedRange.Key; - var rangeEnd = totalContentLength - 1; - var rangeLength = 1 + rangeEnd - rangeStart; - - // Content-Length is the length of what we're serving, not the original content - HttpListenerContext.Response.ContentLength64 = rangeLength; - HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength); - - if (rangeStart > 0) - { - SourceStream.Position = rangeStart; - } - - return SourceStream.CopyToAsync(responseStream); - } - - /// <summary> - /// Serves a partial range request - /// </summary> - /// <param name="rangeStart">The range start.</param> - /// <param name="rangeEnd">The range end.</param> - /// <param name="responseStream">The response stream.</param> - /// <param name="totalContentLength">Total length of the content.</param> - /// <returns>Task.</returns> - private async Task ServePartialRangeRequest(long rangeStart, long rangeEnd, Stream responseStream, long totalContentLength) - { - var rangeLength = 1 + rangeEnd - rangeStart; - - // Content-Length is the length of what we're serving, not the original content - HttpListenerContext.Response.ContentLength64 = rangeLength; - HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength); - - SourceStream.Position = rangeStart; - - // Fast track to just copy the stream to the end - if (rangeEnd == totalContentLength - 1) - { - await SourceStream.CopyToAsync(responseStream).ConfigureAwait(false); - } - else - { - // Read the bytes we need - var buffer = new byte[Convert.ToInt32(rangeLength)]; - await SourceStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); - - await responseStream.WriteAsync(buffer, 0, Convert.ToInt32(rangeLength)).ConfigureAwait(false); - } - } - } -} |
