From 2d06095447b972c8c7239277428e2c67c8b7ca86 Mon Sep 17 00:00:00 2001 From: LukePulverenti Date: Mon, 25 Feb 2013 22:43:04 -0500 Subject: plugin security fixes and other abstractions --- .../HttpServer/BaseRestService.cs | 453 +++++++++++++++++++++ MediaBrowser.Networking/HttpServer/HttpServer.cs | 43 +- .../HttpServer/ServerFactory.cs | 5 +- 3 files changed, 475 insertions(+), 26 deletions(-) create mode 100644 MediaBrowser.Networking/HttpServer/BaseRestService.cs (limited to 'MediaBrowser.Networking/HttpServer') diff --git a/MediaBrowser.Networking/HttpServer/BaseRestService.cs b/MediaBrowser.Networking/HttpServer/BaseRestService.cs new file mode 100644 index 000000000..d499f0781 --- /dev/null +++ b/MediaBrowser.Networking/HttpServer/BaseRestService.cs @@ -0,0 +1,453 @@ +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 ServiceStack.WebHost.Endpoints; +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using MimeTypes = MediaBrowser.Common.Net.MimeTypes; +using StreamWriter = MediaBrowser.Common.Net.StreamWriter; + +namespace MediaBrowser.Networking.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)); + } + } + } +} diff --git a/MediaBrowser.Networking/HttpServer/HttpServer.cs b/MediaBrowser.Networking/HttpServer/HttpServer.cs index b6250527d..08a6b3561 100644 --- a/MediaBrowser.Networking/HttpServer/HttpServer.cs +++ b/MediaBrowser.Networking/HttpServer/HttpServer.cs @@ -1,4 +1,3 @@ -using System.Net.WebSockets; using Funq; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Kernel; @@ -16,10 +15,12 @@ using ServiceStack.WebHost.Endpoints; using ServiceStack.WebHost.Endpoints.Extensions; using ServiceStack.WebHost.Endpoints.Support; using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.WebSockets; using System.Reactive.Linq; using System.Reflection; using System.Text; @@ -44,10 +45,9 @@ namespace MediaBrowser.Networking.HttpServer public string UrlPrefix { get; private set; } /// - /// Gets or sets the kernel. + /// The _rest services /// - /// The kernel. - private IKernel Kernel { get; set; } + private readonly List _restServices = new List(); /// /// Gets or sets the application host. @@ -88,19 +88,14 @@ namespace MediaBrowser.Networking.HttpServer /// Initializes a new instance of the class. /// /// The application host. - /// The kernel. /// The protobuf serializer. /// The logger. /// Name of the server. /// The default redirectpath. /// urlPrefix - public HttpServer(IApplicationHost applicationHost, IKernel kernel, IProtobufSerializer protobufSerializer, ILogger logger, string serverName, string defaultRedirectpath) + public HttpServer(IApplicationHost applicationHost, IProtobufSerializer protobufSerializer, ILogger logger, string serverName, string defaultRedirectpath) : base() { - if (kernel == null) - { - throw new ArgumentNullException("kernel"); - } if (protobufSerializer == null) { throw new ArgumentNullException("protobufSerializer"); @@ -130,13 +125,6 @@ namespace MediaBrowser.Networking.HttpServer EndpointHostConfig.Instance.ServiceStackHandlerFactoryPath = null; EndpointHostConfig.Instance.MetadataRedirectPath = "metadata"; - - Kernel = kernel; - - EndpointHost.ConfigureHost(this, ServerName, CreateServiceManager()); - ContentTypeFilters.Register(ContentType.ProtoBuf, (reqCtx, res, stream) => ProtobufSerializer.SerializeToStream(res, stream), (type, stream) => ProtobufSerializer.DeserializeFromStream(stream, type)); - - Init(); } /// @@ -161,11 +149,6 @@ namespace MediaBrowser.Networking.HttpServer container.Adapter = new ContainerAdapter(ApplicationHost); - foreach (var service in Kernel.RestServices) - { - service.Configure(this); - } - Plugins.Add(new SwaggerFeature()); Plugins.Add(new CorsFeature()); @@ -450,7 +433,7 @@ namespace MediaBrowser.Networking.HttpServer /// ServiceManager. protected override ServiceManager CreateServiceManager(params Assembly[] assembliesWithServices) { - var types = Kernel.RestServices.Select(r => r.GetType()).ToArray(); + var types = _restServices.Select(r => r.GetType()).ToArray(); return new ServiceManager(new Container(), new ServiceController(() => types)); } @@ -511,6 +494,20 @@ namespace MediaBrowser.Networking.HttpServer /// /// true if [enable HTTP request logging]; otherwise, false. public bool EnableHttpRequestLogging { get; set; } + + /// + /// Adds the rest handlers. + /// + /// The services. + public void Init(IEnumerable services) + { + _restServices.AddRange(services); + + EndpointHost.ConfigureHost(this, ServerName, CreateServiceManager()); + ContentTypeFilters.Register(ContentType.ProtoBuf, (reqCtx, res, stream) => ProtobufSerializer.SerializeToStream(res, stream), (type, stream) => ProtobufSerializer.DeserializeFromStream(stream, type)); + + Init(); + } } /// diff --git a/MediaBrowser.Networking/HttpServer/ServerFactory.cs b/MediaBrowser.Networking/HttpServer/ServerFactory.cs index e853a6ec2..716fd450a 100644 --- a/MediaBrowser.Networking/HttpServer/ServerFactory.cs +++ b/MediaBrowser.Networking/HttpServer/ServerFactory.cs @@ -14,15 +14,14 @@ namespace MediaBrowser.Networking.HttpServer /// Creates the server. /// /// The application host. - /// The kernel. /// The protobuf serializer. /// The logger. /// Name of the server. /// The default redirectpath. /// IHttpServer. - public static IHttpServer CreateServer(IApplicationHost applicationHost, IKernel kernel, IProtobufSerializer protobufSerializer, ILogger logger, string serverName, string defaultRedirectpath) + public static IHttpServer CreateServer(IApplicationHost applicationHost, IProtobufSerializer protobufSerializer, ILogger logger, string serverName, string defaultRedirectpath) { - return new HttpServer(applicationHost, kernel, protobufSerializer, logger, serverName, defaultRedirectpath); + return new HttpServer(applicationHost, protobufSerializer, logger, serverName, defaultRedirectpath); } } } -- cgit v1.2.3