From 46efa464d851d3f78b74ac02d061388115cf6d66 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 3 Nov 2016 21:18:51 -0400 Subject: move classes --- .../HttpServer/IHttpListener.cs | 46 ++++ .../HttpServer/Security/AuthService.cs | 246 +++++++++++++++++++++ .../HttpServer/Security/AuthorizationContext.cs | 195 ++++++++++++++++ .../HttpServer/Security/SessionContext.cs | 67 ++++++ .../HttpServer/StreamWriter.cs | 127 +++++++++++ 5 files changed, 681 insertions(+) create mode 100644 Emby.Server.Implementations/HttpServer/IHttpListener.cs create mode 100644 Emby.Server.Implementations/HttpServer/Security/AuthService.cs create mode 100644 Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs create mode 100644 Emby.Server.Implementations/HttpServer/Security/SessionContext.cs create mode 100644 Emby.Server.Implementations/HttpServer/StreamWriter.cs (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs new file mode 100644 index 000000000..9f96a8e49 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/IHttpListener.cs @@ -0,0 +1,46 @@ +using MediaBrowser.Controller.Net; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.HttpServer +{ + public interface IHttpListener : IDisposable + { + /// + /// Gets or sets the error handler. + /// + /// The error handler. + Action ErrorHandler { get; set; } + + /// + /// Gets or sets the request handler. + /// + /// The request handler. + Func RequestHandler { get; set; } + + /// + /// Gets or sets the web socket handler. + /// + /// The web socket handler. + Action WebSocketConnected { get; set; } + + /// + /// Gets or sets the web socket connecting. + /// + /// The web socket connecting. + Action WebSocketConnecting { get; set; } + + /// + /// Starts this instance. + /// + /// The URL prefixes. + void Start(IEnumerable urlPrefixes); + + /// + /// Stops this instance. + /// + void Stop(); + } +} diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs new file mode 100644 index 000000000..4d00c9b19 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -0,0 +1,246 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Connect; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Emby.Server.Implementations.HttpServer.Security +{ + public class AuthService : IAuthService + { + private readonly IServerConfigurationManager _config; + + public AuthService(IUserManager userManager, IAuthorizationContext authorizationContext, IServerConfigurationManager config, IConnectManager connectManager, ISessionManager sessionManager, IDeviceManager deviceManager) + { + AuthorizationContext = authorizationContext; + _config = config; + DeviceManager = deviceManager; + SessionManager = sessionManager; + ConnectManager = connectManager; + UserManager = userManager; + } + + public IUserManager UserManager { get; private set; } + public IAuthorizationContext AuthorizationContext { get; private set; } + public IConnectManager ConnectManager { get; private set; } + public ISessionManager SessionManager { get; private set; } + public IDeviceManager DeviceManager { get; private set; } + + /// + /// Redirect the client to a specific URL if authentication failed. + /// If this property is null, simply `401 Unauthorized` is returned. + /// + public string HtmlRedirect { get; set; } + + public void Authenticate(IServiceRequest request, + IAuthenticationAttributes authAttribtues) + { + ValidateUser(request, authAttribtues); + } + + private void ValidateUser(IServiceRequest request, + IAuthenticationAttributes authAttribtues) + { + // This code is executed before the service + var auth = AuthorizationContext.GetAuthorizationInfo(request); + + if (!IsExemptFromAuthenticationToken(auth, authAttribtues)) + { + var valid = IsValidConnectKey(auth.Token); + + if (!valid) + { + ValidateSecurityToken(request, auth.Token); + } + } + + var user = string.IsNullOrWhiteSpace(auth.UserId) + ? null + : UserManager.GetUserById(auth.UserId); + + if (user == null & !string.IsNullOrWhiteSpace(auth.UserId)) + { + throw new SecurityException("User with Id " + auth.UserId + " not found"); + } + + if (user != null) + { + ValidateUserAccess(user, request, authAttribtues, auth); + } + + var info = GetTokenInfo(request); + + if (!IsExemptFromRoles(auth, authAttribtues, info)) + { + var roles = authAttribtues.GetRoles().ToList(); + + ValidateRoles(roles, user); + } + + if (!string.IsNullOrWhiteSpace(auth.DeviceId) && + !string.IsNullOrWhiteSpace(auth.Client) && + !string.IsNullOrWhiteSpace(auth.Device)) + { + SessionManager.LogSessionActivity(auth.Client, + auth.Version, + auth.DeviceId, + auth.Device, + request.RemoteIp, + user); + } + } + + private void ValidateUserAccess(User user, IServiceRequest request, + IAuthenticationAttributes authAttribtues, + AuthorizationInfo auth) + { + if (user.Policy.IsDisabled) + { + throw new SecurityException("User account has been disabled.") + { + SecurityExceptionType = SecurityExceptionType.Unauthenticated + }; + } + + if (!user.Policy.IsAdministrator && + !authAttribtues.EscapeParentalControl && + !user.IsParentalScheduleAllowed()) + { + request.AddResponseHeader("X-Application-Error-Code", "ParentalControl"); + + throw new SecurityException("This user account is not allowed access at this time.") + { + SecurityExceptionType = SecurityExceptionType.ParentalControl + }; + } + + if (!string.IsNullOrWhiteSpace(auth.DeviceId)) + { + if (!DeviceManager.CanAccessDevice(user.Id.ToString("N"), auth.DeviceId)) + { + throw new SecurityException("User is not allowed access from this device.") + { + SecurityExceptionType = SecurityExceptionType.ParentalControl + }; + } + } + } + + private bool IsExemptFromAuthenticationToken(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues) + { + if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard) + { + return true; + } + + return false; + } + + private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, AuthenticationInfo tokenInfo) + { + if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard) + { + return true; + } + + if (string.IsNullOrWhiteSpace(auth.Token)) + { + return true; + } + + if (tokenInfo != null && string.IsNullOrWhiteSpace(tokenInfo.UserId)) + { + return true; + } + + return false; + } + + private void ValidateRoles(List roles, User user) + { + if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase)) + { + if (user == null || !user.Policy.IsAdministrator) + { + throw new SecurityException("User does not have admin access.") + { + SecurityExceptionType = SecurityExceptionType.Unauthenticated + }; + } + } + if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase)) + { + if (user == null || !user.Policy.EnableContentDeletion) + { + throw new SecurityException("User does not have delete access.") + { + SecurityExceptionType = SecurityExceptionType.Unauthenticated + }; + } + } + if (roles.Contains("download", StringComparer.OrdinalIgnoreCase)) + { + if (user == null || !user.Policy.EnableContentDownloading) + { + throw new SecurityException("User does not have download access.") + { + SecurityExceptionType = SecurityExceptionType.Unauthenticated + }; + } + } + } + + private AuthenticationInfo GetTokenInfo(IServiceRequest request) + { + object info; + request.Items.TryGetValue("OriginalAuthenticationInfo", out info); + return info as AuthenticationInfo; + } + + private bool IsValidConnectKey(string token) + { + if (string.IsNullOrEmpty(token)) + { + return false; + } + + return ConnectManager.IsAuthorizationTokenValid(token); + } + + private void ValidateSecurityToken(IServiceRequest request, string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + throw new SecurityException("Access token is required."); + } + + var info = GetTokenInfo(request); + + if (info == null) + { + throw new SecurityException("Access token is invalid or expired."); + } + + if (!info.IsActive) + { + throw new SecurityException("Access token has expired."); + } + + //if (!string.IsNullOrWhiteSpace(info.UserId)) + //{ + // var user = _userManager.GetUserById(info.UserId); + + // if (user == null || user.Configuration.IsDisabled) + // { + // throw new SecurityException("User account has been disabled."); + // } + //} + } + } +} diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs new file mode 100644 index 000000000..ec3dfeb60 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -0,0 +1,195 @@ +using MediaBrowser.Controller.Connect; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Security; +using System; +using System.Collections.Generic; +using System.Linq; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.HttpServer.Security +{ + public class AuthorizationContext : IAuthorizationContext + { + private readonly IAuthenticationRepository _authRepo; + private readonly IConnectManager _connectManager; + + public AuthorizationContext(IAuthenticationRepository authRepo, IConnectManager connectManager) + { + _authRepo = authRepo; + _connectManager = connectManager; + } + + public AuthorizationInfo GetAuthorizationInfo(object requestContext) + { + var req = new ServiceRequest((IRequest)requestContext); + return GetAuthorizationInfo(req); + } + + public AuthorizationInfo GetAuthorizationInfo(IServiceRequest requestContext) + { + object cached; + if (requestContext.Items.TryGetValue("AuthorizationInfo", out cached)) + { + return (AuthorizationInfo)cached; + } + + return GetAuthorization(requestContext); + } + + /// + /// Gets the authorization. + /// + /// The HTTP req. + /// Dictionary{System.StringSystem.String}. + private AuthorizationInfo GetAuthorization(IServiceRequest httpReq) + { + var auth = GetAuthorizationDictionary(httpReq); + + string deviceId = null; + string device = null; + string client = null; + string version = null; + + if (auth != null) + { + auth.TryGetValue("DeviceId", out deviceId); + auth.TryGetValue("Device", out device); + auth.TryGetValue("Client", out client); + auth.TryGetValue("Version", out version); + } + + var token = httpReq.Headers["X-Emby-Token"]; + + if (string.IsNullOrWhiteSpace(token)) + { + token = httpReq.Headers["X-MediaBrowser-Token"]; + } + if (string.IsNullOrWhiteSpace(token)) + { + token = httpReq.QueryString["api_key"]; + } + + var info = new AuthorizationInfo + { + Client = client, + Device = device, + DeviceId = deviceId, + Version = version, + Token = token + }; + + if (!string.IsNullOrWhiteSpace(token)) + { + var result = _authRepo.Get(new AuthenticationInfoQuery + { + AccessToken = token + }); + + var tokenInfo = result.Items.FirstOrDefault(); + + if (tokenInfo != null) + { + info.UserId = tokenInfo.UserId; + + // TODO: Remove these checks for IsNullOrWhiteSpace + if (string.IsNullOrWhiteSpace(info.Client)) + { + info.Client = tokenInfo.AppName; + } + if (string.IsNullOrWhiteSpace(info.Device)) + { + info.Device = tokenInfo.DeviceName; + } + if (string.IsNullOrWhiteSpace(info.DeviceId)) + { + info.DeviceId = tokenInfo.DeviceId; + } + if (string.IsNullOrWhiteSpace(info.Version)) + { + info.Version = tokenInfo.AppVersion; + } + } + else + { + var user = _connectManager.GetUserFromExchangeToken(token); + if (user != null) + { + info.UserId = user.Id.ToString("N"); + } + } + httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo; + } + + httpReq.Items["AuthorizationInfo"] = info; + + return info; + } + + /// + /// Gets the auth. + /// + /// The HTTP req. + /// Dictionary{System.StringSystem.String}. + private Dictionary GetAuthorizationDictionary(IServiceRequest httpReq) + { + var auth = httpReq.Headers["X-Emby-Authorization"]; + + if (string.IsNullOrWhiteSpace(auth)) + { + auth = httpReq.Headers["Authorization"]; + } + + return GetAuthorization(auth); + } + + /// + /// Gets the authorization. + /// + /// The authorization header. + /// Dictionary{System.StringSystem.String}. + private Dictionary GetAuthorization(string authorizationHeader) + { + if (authorizationHeader == null) return null; + + var parts = authorizationHeader.Split(new[] { ' ' }, 2); + + // There should be at least to parts + if (parts.Length != 2) return null; + + // It has to be a digest request + if (!string.Equals(parts[0], "MediaBrowser", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // Remove uptil the first space + authorizationHeader = parts[1]; + parts = authorizationHeader.Split(','); + + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var item in parts) + { + var param = item.Trim().Split(new[] { '=' }, 2); + + if (param.Length == 2) + { + var value = NormalizeValue (param[1].Trim(new[] { '"' })); + result.Add(param[0], value); + } + } + + return result; + } + + private string NormalizeValue(string value) + { + if (string.IsNullOrWhiteSpace (value)) + { + return value; + } + + return System.Net.WebUtility.HtmlEncode(value); + } + } +} diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs new file mode 100644 index 000000000..33dd4e2d7 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -0,0 +1,67 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.HttpServer.Security +{ + public class SessionContext : ISessionContext + { + private readonly IUserManager _userManager; + private readonly ISessionManager _sessionManager; + private readonly IAuthorizationContext _authContext; + + public SessionContext(IUserManager userManager, IAuthorizationContext authContext, ISessionManager sessionManager) + { + _userManager = userManager; + _authContext = authContext; + _sessionManager = sessionManager; + } + + public Task GetSession(IServiceRequest requestContext) + { + var authorization = _authContext.GetAuthorizationInfo(requestContext); + + //if (!string.IsNullOrWhiteSpace(authorization.Token)) + //{ + // var auth = GetTokenInfo(requestContext); + // if (auth != null) + // { + // return _sessionManager.GetSessionByAuthenticationToken(auth, authorization.DeviceId, requestContext.RemoteIp, authorization.Version); + // } + //} + + var user = string.IsNullOrWhiteSpace(authorization.UserId) ? null : _userManager.GetUserById(authorization.UserId); + return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user); + } + + private AuthenticationInfo GetTokenInfo(IServiceRequest request) + { + object info; + request.Items.TryGetValue("OriginalAuthenticationInfo", out info); + return info as AuthenticationInfo; + } + + public Task GetSession(object requestContext) + { + var req = new ServiceRequest((IRequest)requestContext); + return GetSession(req); + } + + public async Task GetUser(IServiceRequest requestContext) + { + var session = await GetSession(requestContext).ConfigureAwait(false); + + return session == null || !session.UserId.HasValue ? null : _userManager.GetUserById(session.UserId.Value); + } + + public Task GetUser(object requestContext) + { + var req = new ServiceRequest((IRequest)requestContext); + return GetUser(req); + } + } +} diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs new file mode 100644 index 000000000..15488abaa --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/StreamWriter.cs @@ -0,0 +1,127 @@ +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Model.Services; + +namespace Emby.Server.Implementations.HttpServer +{ + /// + /// Class StreamWriter + /// + public class StreamWriter : IAsyncStreamWriter, IHasHeaders + { + private ILogger Logger { get; set; } + + private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + /// + /// Gets or sets the source stream. + /// + /// The source stream. + private Stream SourceStream { get; set; } + + /// + /// The _options + /// + private readonly IDictionary _options = new Dictionary(); + /// + /// Gets the options. + /// + /// The options. + public IDictionary Headers + { + get { return _options; } + } + + public Action OnComplete { get; set; } + public Action OnError { get; set; } + private readonly byte[] _bytes; + + /// + /// Initializes a new instance of the class. + /// + /// The source. + /// Type of the content. + /// The logger. + public StreamWriter(Stream source, string contentType, ILogger logger) + { + if (string.IsNullOrEmpty(contentType)) + { + throw new ArgumentNullException("contentType"); + } + + SourceStream = source; + Logger = logger; + + Headers["Content-Type"] = contentType; + + if (source.CanSeek) + { + Headers["Content-Length"] = source.Length.ToString(UsCulture); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The source. + /// Type of the content. + /// The logger. + public StreamWriter(byte[] source, string contentType, ILogger logger) + : this(new MemoryStream(source), contentType, logger) + { + if (string.IsNullOrEmpty(contentType)) + { + throw new ArgumentNullException("contentType"); + } + + _bytes = source; + Logger = logger; + + Headers["Content-Type"] = contentType; + + Headers["Content-Length"] = source.Length.ToString(UsCulture); + } + + public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) + { + try + { + if (_bytes != null) + { + await responseStream.WriteAsync(_bytes, 0, _bytes.Length); + } + else + { + using (var src = SourceStream) + { + await src.CopyToAsync(responseStream).ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + Logger.ErrorException("Error streaming data", ex); + + if (OnError != null) + { + OnError(); + } + + throw; + } + finally + { + if (OnComplete != null) + { + OnComplete(); + } + } + } + } +} -- cgit v1.2.3 From 3c1447804b5de9a7d840c7158c3cb4e0a27f76e1 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 4 Nov 2016 22:17:18 -0400 Subject: move localization classes --- Emby.Dlna/Didl/DidlBuilder.cs | 8 +- .../Emby.Server.Implementations.csproj | 246 +++++- .../HttpServer/GetSwaggerResource.cs | 17 + .../HttpServer/SocketSharp/HttpUtility.cs | 922 ++++++++++++++++++++ .../HttpServer/SwaggerService.cs | 46 + .../Localization/Core/ar.json | 178 ++++ .../Localization/Core/bg-BG.json | 178 ++++ .../Localization/Core/ca.json | 178 ++++ .../Localization/Core/core.json | 179 ++++ .../Localization/Core/cs.json | 178 ++++ .../Localization/Core/da.json | 178 ++++ .../Localization/Core/de.json | 178 ++++ .../Localization/Core/el.json | 178 ++++ .../Localization/Core/en-GB.json | 178 ++++ .../Localization/Core/en-US.json | 178 ++++ .../Localization/Core/es-AR.json | 178 ++++ .../Localization/Core/es-MX.json | 178 ++++ .../Localization/Core/es.json | 178 ++++ .../Localization/Core/fi.json | 178 ++++ .../Localization/Core/fr-CA.json | 178 ++++ .../Localization/Core/fr.json | 178 ++++ .../Localization/Core/gsw.json | 178 ++++ .../Localization/Core/he.json | 178 ++++ .../Localization/Core/hr.json | 178 ++++ .../Localization/Core/hu.json | 178 ++++ .../Localization/Core/id.json | 178 ++++ .../Localization/Core/it.json | 178 ++++ .../Localization/Core/kk.json | 178 ++++ .../Localization/Core/ko.json | 178 ++++ .../Localization/Core/ms.json | 178 ++++ .../Localization/Core/nb.json | 178 ++++ .../Localization/Core/nl.json | 178 ++++ .../Localization/Core/pl.json | 178 ++++ .../Localization/Core/pt-BR.json | 178 ++++ .../Localization/Core/pt-PT.json | 178 ++++ .../Localization/Core/ro.json | 178 ++++ .../Localization/Core/ru.json | 178 ++++ .../Localization/Core/sl-SI.json | 178 ++++ .../Localization/Core/sv.json | 178 ++++ .../Localization/Core/tr.json | 178 ++++ .../Localization/Core/uk.json | 178 ++++ .../Localization/Core/vi.json | 178 ++++ .../Localization/Core/zh-CN.json | 178 ++++ .../Localization/Core/zh-HK.json | 178 ++++ .../Localization/Core/zh-TW.json | 178 ++++ .../Localization/LocalizationManager.cs | 433 ++++++++++ .../Localization/Ratings/au.txt | 8 + .../Localization/Ratings/be.txt | 6 + .../Localization/Ratings/br.txt | 6 + .../Localization/Ratings/ca.txt | 6 + .../Localization/Ratings/co.txt | 8 + .../Localization/Ratings/de.txt | 10 + .../Localization/Ratings/dk.txt | 4 + .../Localization/Ratings/fr.txt | 5 + .../Localization/Ratings/gb.txt | 7 + .../Localization/Ratings/ie.txt | 6 + .../Localization/Ratings/jp.txt | 4 + .../Localization/Ratings/kz.txt | 6 + .../Localization/Ratings/mx.txt | 6 + .../Localization/Ratings/nl.txt | 6 + .../Localization/Ratings/nz.txt | 10 + .../Localization/Ratings/ru.txt | 5 + .../Localization/Ratings/us.txt | 22 + .../Localization/countries.json | 1 + .../Localization/iso6392.txt | 487 +++++++++++ .../HttpServer/GetSwaggerResource.cs | 17 - .../HttpServer/HttpListenerHost.cs | 1 + .../HttpServer/SocketSharp/HttpUtility.cs | 942 --------------------- .../SocketSharp/WebSocketSharpRequest.cs | 1 + .../HttpServer/SwaggerService.cs | 43 - .../Localization/Core/ar.json | 178 ---- .../Localization/Core/bg-BG.json | 178 ---- .../Localization/Core/ca.json | 178 ---- .../Localization/Core/core.json | 179 ---- .../Localization/Core/cs.json | 178 ---- .../Localization/Core/da.json | 178 ---- .../Localization/Core/de.json | 178 ---- .../Localization/Core/el.json | 178 ---- .../Localization/Core/en-GB.json | 178 ---- .../Localization/Core/en-US.json | 178 ---- .../Localization/Core/es-AR.json | 178 ---- .../Localization/Core/es-MX.json | 178 ---- .../Localization/Core/es.json | 178 ---- .../Localization/Core/fi.json | 178 ---- .../Localization/Core/fr-CA.json | 178 ---- .../Localization/Core/fr.json | 178 ---- .../Localization/Core/gsw.json | 178 ---- .../Localization/Core/he.json | 178 ---- .../Localization/Core/hr.json | 178 ---- .../Localization/Core/hu.json | 178 ---- .../Localization/Core/id.json | 178 ---- .../Localization/Core/it.json | 178 ---- .../Localization/Core/kk.json | 178 ---- .../Localization/Core/ko.json | 178 ---- .../Localization/Core/ms.json | 178 ---- .../Localization/Core/nb.json | 178 ---- .../Localization/Core/nl.json | 178 ---- .../Localization/Core/pl.json | 178 ---- .../Localization/Core/pt-BR.json | 178 ---- .../Localization/Core/pt-PT.json | 178 ---- .../Localization/Core/ro.json | 178 ---- .../Localization/Core/ru.json | 178 ---- .../Localization/Core/sl-SI.json | 178 ---- .../Localization/Core/sv.json | 178 ---- .../Localization/Core/tr.json | 178 ---- .../Localization/Core/uk.json | 178 ---- .../Localization/Core/vi.json | 178 ---- .../Localization/Core/zh-CN.json | 178 ---- .../Localization/Core/zh-HK.json | 178 ---- .../Localization/Core/zh-TW.json | 178 ---- .../Localization/LocalizationManager.cs | 422 --------- .../Localization/Ratings/au.txt | 8 - .../Localization/Ratings/be.txt | 6 - .../Localization/Ratings/br.txt | 6 - .../Localization/Ratings/ca.txt | 6 - .../Localization/Ratings/co.txt | 8 - .../Localization/Ratings/de.txt | 10 - .../Localization/Ratings/dk.txt | 4 - .../Localization/Ratings/fr.txt | 5 - .../Localization/Ratings/gb.txt | 7 - .../Localization/Ratings/ie.txt | 6 - .../Localization/Ratings/jp.txt | 4 - .../Localization/Ratings/kz.txt | 6 - .../Localization/Ratings/mx.txt | 6 - .../Localization/Ratings/nl.txt | 6 - .../Localization/Ratings/nz.txt | 10 - .../Localization/Ratings/ru.txt | 5 - .../Localization/Ratings/us.txt | 22 - .../Localization/countries.json | 1 - .../Localization/iso6392.txt | 487 ----------- .../MediaBrowser.Server.Implementations.csproj | 209 ----- .../ApplicationHost.cs | 9 +- .../MediaBrowser.Server.Startup.Common.csproj | 1 + .../TextLocalizer.cs | 25 + 134 files changed, 9434 insertions(+), 9376 deletions(-) create mode 100644 Emby.Server.Implementations/HttpServer/GetSwaggerResource.cs create mode 100644 Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs create mode 100644 Emby.Server.Implementations/HttpServer/SwaggerService.cs create mode 100644 Emby.Server.Implementations/Localization/Core/ar.json create mode 100644 Emby.Server.Implementations/Localization/Core/bg-BG.json create mode 100644 Emby.Server.Implementations/Localization/Core/ca.json create mode 100644 Emby.Server.Implementations/Localization/Core/core.json create mode 100644 Emby.Server.Implementations/Localization/Core/cs.json create mode 100644 Emby.Server.Implementations/Localization/Core/da.json create mode 100644 Emby.Server.Implementations/Localization/Core/de.json create mode 100644 Emby.Server.Implementations/Localization/Core/el.json create mode 100644 Emby.Server.Implementations/Localization/Core/en-GB.json create mode 100644 Emby.Server.Implementations/Localization/Core/en-US.json create mode 100644 Emby.Server.Implementations/Localization/Core/es-AR.json create mode 100644 Emby.Server.Implementations/Localization/Core/es-MX.json create mode 100644 Emby.Server.Implementations/Localization/Core/es.json create mode 100644 Emby.Server.Implementations/Localization/Core/fi.json create mode 100644 Emby.Server.Implementations/Localization/Core/fr-CA.json create mode 100644 Emby.Server.Implementations/Localization/Core/fr.json create mode 100644 Emby.Server.Implementations/Localization/Core/gsw.json create mode 100644 Emby.Server.Implementations/Localization/Core/he.json create mode 100644 Emby.Server.Implementations/Localization/Core/hr.json create mode 100644 Emby.Server.Implementations/Localization/Core/hu.json create mode 100644 Emby.Server.Implementations/Localization/Core/id.json create mode 100644 Emby.Server.Implementations/Localization/Core/it.json create mode 100644 Emby.Server.Implementations/Localization/Core/kk.json create mode 100644 Emby.Server.Implementations/Localization/Core/ko.json create mode 100644 Emby.Server.Implementations/Localization/Core/ms.json create mode 100644 Emby.Server.Implementations/Localization/Core/nb.json create mode 100644 Emby.Server.Implementations/Localization/Core/nl.json create mode 100644 Emby.Server.Implementations/Localization/Core/pl.json create mode 100644 Emby.Server.Implementations/Localization/Core/pt-BR.json create mode 100644 Emby.Server.Implementations/Localization/Core/pt-PT.json create mode 100644 Emby.Server.Implementations/Localization/Core/ro.json create mode 100644 Emby.Server.Implementations/Localization/Core/ru.json create mode 100644 Emby.Server.Implementations/Localization/Core/sl-SI.json create mode 100644 Emby.Server.Implementations/Localization/Core/sv.json create mode 100644 Emby.Server.Implementations/Localization/Core/tr.json create mode 100644 Emby.Server.Implementations/Localization/Core/uk.json create mode 100644 Emby.Server.Implementations/Localization/Core/vi.json create mode 100644 Emby.Server.Implementations/Localization/Core/zh-CN.json create mode 100644 Emby.Server.Implementations/Localization/Core/zh-HK.json create mode 100644 Emby.Server.Implementations/Localization/Core/zh-TW.json create mode 100644 Emby.Server.Implementations/Localization/LocalizationManager.cs create mode 100644 Emby.Server.Implementations/Localization/Ratings/au.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/be.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/br.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/ca.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/co.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/de.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/dk.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/fr.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/gb.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/ie.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/jp.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/kz.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/mx.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/nl.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/nz.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/ru.txt create mode 100644 Emby.Server.Implementations/Localization/Ratings/us.txt create mode 100644 Emby.Server.Implementations/Localization/countries.json create mode 100644 Emby.Server.Implementations/Localization/iso6392.txt delete mode 100644 MediaBrowser.Server.Implementations/HttpServer/GetSwaggerResource.cs delete mode 100644 MediaBrowser.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs delete mode 100644 MediaBrowser.Server.Implementations/HttpServer/SwaggerService.cs delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/ar.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/bg-BG.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/ca.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/core.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/cs.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/da.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/de.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/el.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/en-GB.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/en-US.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/es-AR.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/es-MX.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/es.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/fi.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/fr-CA.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/fr.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/gsw.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/he.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/hr.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/hu.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/id.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/it.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/kk.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/ko.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/ms.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/nb.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/nl.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/pl.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/pt-BR.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/pt-PT.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/ro.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/ru.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/sl-SI.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/sv.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/tr.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/uk.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/vi.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/zh-CN.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/zh-HK.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/Core/zh-TW.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/LocalizationManager.cs delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/au.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/be.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/br.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/ca.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/co.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/de.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/dk.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/fr.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/gb.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/ie.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/jp.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/kz.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/mx.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/nl.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/nz.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/ru.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/Ratings/us.txt delete mode 100644 MediaBrowser.Server.Implementations/Localization/countries.json delete mode 100644 MediaBrowser.Server.Implementations/Localization/iso6392.txt create mode 100644 MediaBrowser.Server.Startup.Common/TextLocalizer.cs (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 50668f555..ee5c8fecd 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -104,6 +104,8 @@ namespace Emby.Dlna.Didl writer.WriteStartElement(string.Empty, "item", NS_DIDL); + AddGeneralProperties(item, null, context, writer, filter); + writer.WriteAttributeString("restricted", "1"); writer.WriteAttributeString("id", clientId); @@ -122,8 +124,6 @@ namespace Emby.Dlna.Didl //AddBookmarkInfo(item, user, element); - AddGeneralProperties(item, null, context, writer, filter); - // refID? // storeAttribute(itemNode, object, ClassProperties.REF_ID, false); @@ -501,6 +501,8 @@ namespace Emby.Dlna.Didl { writer.WriteStartElement(string.Empty, "container", NS_DIDL); + AddGeneralProperties(folder, stubType, context, writer, filter); + writer.WriteAttributeString("restricted", "0"); writer.WriteAttributeString("searchable", "1"); writer.WriteAttributeString("childCount", childCount.ToString(_usCulture)); @@ -534,8 +536,6 @@ namespace Emby.Dlna.Didl } } - AddCommonFields(folder, stubType, null, writer, filter); - AddCover(folder, context, stubType, writer); writer.WriteEndElement(); diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 8d13d206a..33f29d64d 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -67,11 +67,14 @@ + + + @@ -144,6 +147,7 @@ + @@ -230,7 +234,9 @@ - + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} @@ -266,8 +272,246 @@ + + swagger-ui\fonts\droid-sans-v6-latin-700.svg + PreserveNewest + + + swagger-ui\fonts\droid-sans-v6-latin-regular.svg + PreserveNewest + + + swagger-ui\images\explorer_icons.png + PreserveNewest + + + swagger-ui\images\logo_small.png + PreserveNewest + + + swagger-ui\images\pet_store_api.png + PreserveNewest + + + swagger-ui\images\throbber.gif + PreserveNewest + + + swagger-ui\images\wordnik_api.png + PreserveNewest + + + swagger-ui\index.html + PreserveNewest + + + swagger-ui\lib\backbone-min.js + PreserveNewest + + + swagger-ui\lib\handlebars-2.0.0.js + PreserveNewest + + + swagger-ui\lib\highlight.7.3.pack.js + PreserveNewest + + + swagger-ui\lib\jquery-1.8.0.min.js + PreserveNewest + + + swagger-ui\lib\jquery.ba-bbq.min.js + PreserveNewest + + + swagger-ui\lib\jquery.slideto.min.js + PreserveNewest + + + swagger-ui\lib\jquery.wiggle.min.js + PreserveNewest + + + swagger-ui\lib\marked.js + PreserveNewest + + + swagger-ui\lib\shred.bundle.js + PreserveNewest + + + swagger-ui\lib\shred\content.js + PreserveNewest + + + swagger-ui\lib\swagger-client.js + PreserveNewest + + + swagger-ui\lib\swagger-oauth.js + PreserveNewest + + + swagger-ui\lib\underscore-min.js + PreserveNewest + + + swagger-ui\o2c.html + PreserveNewest + + + swagger-ui\patch.js + PreserveNewest + + + swagger-ui\swagger-ui.js + PreserveNewest + + + swagger-ui\swagger-ui.min.js + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + swagger-ui\fonts\droid-sans-v6-latin-700.eot + PreserveNewest + + + swagger-ui\fonts\droid-sans-v6-latin-700.ttf + PreserveNewest + + + swagger-ui\fonts\droid-sans-v6-latin-700.woff + PreserveNewest + + + swagger-ui\fonts\droid-sans-v6-latin-700.woff2 + PreserveNewest + + + swagger-ui\fonts\droid-sans-v6-latin-regular.eot + PreserveNewest + + + swagger-ui\fonts\droid-sans-v6-latin-regular.ttf + PreserveNewest + + + swagger-ui\fonts\droid-sans-v6-latin-regular.woff + PreserveNewest + + + swagger-ui\fonts\droid-sans-v6-latin-regular.woff2 + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + swagger-ui\css\reset.css + PreserveNewest + + + swagger-ui\css\screen.css + PreserveNewest + + + swagger-ui\css\typography.css + PreserveNewest + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + \ No newline at end of file diff --git a/ServiceStack/ServiceStack.nuget.targets b/ServiceStack/ServiceStack.nuget.targets new file mode 100644 index 000000000..e69ce0e64 --- /dev/null +++ b/ServiceStack/ServiceStack.nuget.targets @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ServiceStack/ServiceStack.xproj b/ServiceStack/ServiceStack.xproj new file mode 100644 index 000000000..ba8f8b8f2 --- /dev/null +++ b/ServiceStack/ServiceStack.xproj @@ -0,0 +1,19 @@ + + + + 14.0.25420 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + b2d733ab-620e-4c53-88a4-4b6638ab6a7a + ServiceStack + .\obj + .\bin\ + + + + 2.0 + + + \ No newline at end of file diff --git a/ServiceStack/ServiceStackHost.Runtime.cs b/ServiceStack/ServiceStackHost.Runtime.cs new file mode 100644 index 000000000..1a1656a0e --- /dev/null +++ b/ServiceStack/ServiceStackHost.Runtime.cs @@ -0,0 +1,74 @@ +// Copyright (c) Service Stack LLC. All Rights Reserved. +// License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt + + +using MediaBrowser.Model.Services; +using ServiceStack.Support.WebHost; + +namespace ServiceStack +{ + public abstract partial class ServiceStackHost + { + /// + /// Applies the request filters. Returns whether or not the request has been handled + /// and no more processing should be done. + /// + /// + public virtual bool ApplyRequestFilters(IRequest req, IResponse res, object requestDto) + { + if (res.IsClosed) return res.IsClosed; + + //Exec all RequestFilter attributes with Priority < 0 + var attributes = FilterAttributeCache.GetRequestFilterAttributes(requestDto.GetType()); + var i = 0; + for (; i < attributes.Length && attributes[i].Priority < 0; i++) + { + var attribute = attributes[i]; + attribute.RequestFilter(req, res, requestDto); + if (res.IsClosed) return res.IsClosed; + } + + if (res.IsClosed) return res.IsClosed; + + //Exec global filters + foreach (var requestFilter in GlobalRequestFilters) + { + requestFilter(req, res, requestDto); + if (res.IsClosed) return res.IsClosed; + } + + //Exec remaining RequestFilter attributes with Priority >= 0 + for (; i < attributes.Length && attributes[i].Priority >= 0; i++) + { + var attribute = attributes[i]; + attribute.RequestFilter(req, res, requestDto); + if (res.IsClosed) return res.IsClosed; + } + + return res.IsClosed; + } + + /// + /// Applies the response filters. Returns whether or not the request has been handled + /// and no more processing should be done. + /// + /// + public virtual bool ApplyResponseFilters(IRequest req, IResponse res, object response) + { + if (response != null) + { + if (res.IsClosed) return res.IsClosed; + } + + //Exec global filters + foreach (var responseFilter in GlobalResponseFilters) + { + responseFilter(req, res, response); + if (res.IsClosed) return res.IsClosed; + } + + return res.IsClosed; + } + } + +} \ No newline at end of file diff --git a/ServiceStack/ServiceStackHost.cs b/ServiceStack/ServiceStackHost.cs new file mode 100644 index 000000000..8a1db25e4 --- /dev/null +++ b/ServiceStack/ServiceStackHost.cs @@ -0,0 +1,104 @@ +// Copyright (c) Service Stack LLC. All Rights Reserved. +// License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt + + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Services; +using ServiceStack.Host; + +namespace ServiceStack +{ + public abstract partial class ServiceStackHost : IDisposable + { + public static ServiceStackHost Instance { get; protected set; } + + protected ServiceStackHost(string serviceName) + { + ServiceName = serviceName; + ServiceController = CreateServiceController(); + + RestPaths = new List(); + Metadata = new ServiceMetadata(); + GlobalRequestFilters = new List>(); + GlobalResponseFilters = new List>(); + } + + public abstract void Configure(); + + public abstract object CreateInstance(Type type); + + protected abstract ServiceController CreateServiceController(); + + public virtual ServiceStackHost Init() + { + Instance = this; + + ServiceController.Init(); + Configure(); + + ServiceController.AfterInit(); + + return this; + } + + public virtual ServiceStackHost Start(string urlBase) + { + throw new NotImplementedException("Start(listeningAtUrlBase) is not supported by this AppHost"); + } + + public string ServiceName { get; set; } + + public ServiceMetadata Metadata { get; set; } + + public ServiceController ServiceController { get; set; } + + public List RestPaths = new List(); + + public List> GlobalRequestFilters { get; set; } + + public List> GlobalResponseFilters { get; set; } + + public abstract T TryResolve(); + public abstract T Resolve(); + + public virtual MediaBrowser.Model.Services.RouteAttribute[] GetRouteAttributes(Type requestType) + { + return requestType.AllAttributes(); + } + + public abstract object GetTaskResult(Task task, string requestName); + + public abstract Func GetParseFn(Type propertyType); + + public abstract void SerializeToJson(object o, Stream stream); + public abstract void SerializeToXml(object o, Stream stream); + public abstract object DeserializeXml(Type type, Stream stream); + public abstract object DeserializeJson(Type type, Stream stream); + + public virtual void Dispose() + { + //JsConfig.Reset(); //Clears Runtime Attributes + + Instance = null; + } + + protected abstract ILogger Logger + { + get; + } + + public void OnLogError(Type type, string message) + { + Logger.Error(message); + } + + public void OnLogError(Type type, string message, Exception ex) + { + Logger.ErrorException(message, ex); + } + } +} diff --git a/ServiceStack/StringMapTypeDeserializer.cs b/ServiceStack/StringMapTypeDeserializer.cs new file mode 100644 index 000000000..762e8aaff --- /dev/null +++ b/ServiceStack/StringMapTypeDeserializer.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Linq; +using System.Reflection; + +namespace ServiceStack.Serialization +{ + /// + /// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls) + /// + public class StringMapTypeDeserializer + { + internal class PropertySerializerEntry + { + public PropertySerializerEntry(Action propertySetFn, Func propertyParseStringFn) + { + PropertySetFn = propertySetFn; + PropertyParseStringFn = propertyParseStringFn; + } + + public Action PropertySetFn; + public Func PropertyParseStringFn; + public Type PropertyType; + } + + private readonly Type type; + private readonly Dictionary propertySetterMap + = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public Func GetParseFn(Type propertyType) + { + //Don't JSV-decode string values for string properties + if (propertyType == typeof(string)) + return s => s; + + return ServiceStackHost.Instance.GetParseFn(propertyType); + } + + public StringMapTypeDeserializer(Type type) + { + this.type = type; + + foreach (var propertyInfo in type.GetSerializableProperties()) + { + var propertySetFn = TypeAccessor.GetSetPropertyMethod(type, propertyInfo); + var propertyType = propertyInfo.PropertyType; + var propertyParseStringFn = GetParseFn(propertyType); + var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn) { PropertyType = propertyType }; + + var attr = propertyInfo.AllAttributes().FirstOrDefault(); + if (attr != null && attr.Name != null) + { + propertySetterMap[attr.Name] = propertySerializer; + } + propertySetterMap[propertyInfo.Name] = propertySerializer; + } + } + + public object PopulateFromMap(object instance, IDictionary keyValuePairs) + { + string propertyName = null; + string propertyTextValue = null; + PropertySerializerEntry propertySerializerEntry = null; + + if (instance == null) + instance = ServiceStackHost.Instance.CreateInstance(type); + + foreach (var pair in keyValuePairs.Where(x => !string.IsNullOrEmpty(x.Value))) + { + propertyName = pair.Key; + propertyTextValue = pair.Value; + + if (!propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry)) + { + if (propertyName == "v") + { + continue; + } + + continue; + } + + if (propertySerializerEntry.PropertySetFn == null) + { + continue; + } + + if (propertySerializerEntry.PropertyType == typeof(bool)) + { + //InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value + propertyTextValue = LeftPart(propertyTextValue, ','); + } + + var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue); + if (value == null) + { + continue; + } + propertySerializerEntry.PropertySetFn(instance, value); + } + + return instance; + } + + public static string LeftPart(string strVal, char needle) + { + if (strVal == null) return null; + var pos = strVal.IndexOf(needle); + return pos == -1 + ? strVal + : strVal.Substring(0, pos); + } + } + + internal class TypeAccessor + { + public static Action GetSetPropertyMethod(Type type, PropertyInfo propertyInfo) + { + if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Any()) return null; + + var setMethodInfo = propertyInfo.SetMethod; + return (instance, value) => setMethodInfo.Invoke(instance, new[] { value }); + } + } +} diff --git a/ServiceStack/UrlExtensions.cs b/ServiceStack/UrlExtensions.cs new file mode 100644 index 000000000..7b5a50ef1 --- /dev/null +++ b/ServiceStack/UrlExtensions.cs @@ -0,0 +1,33 @@ +using System; + +namespace ServiceStack +{ + /// + /// Donated by Ivan Korneliuk from his post: + /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html + /// + /// Modified to only allow using routes matching the supplied HTTP Verb + /// + public static class UrlExtensions + { + public static string GetOperationName(this Type type) + { + var typeName = type.FullName != null //can be null, e.g. generic types + ? LeftPart(type.FullName, "[[") //Generic Fullname + .Replace(type.Namespace + ".", "") //Trim Namespaces + .Replace("+", ".") //Convert nested into normal type + : type.Name; + + return type.IsGenericParameter ? "'" + typeName : typeName; + } + + public static string LeftPart(string strVal, string needle) + { + if (strVal == null) return null; + var pos = strVal.IndexOf(needle, StringComparison.OrdinalIgnoreCase); + return pos == -1 + ? strVal + : strVal.Substring(0, pos); + } + } +} \ No newline at end of file diff --git a/ServiceStack/packages.config b/ServiceStack/packages.config new file mode 100644 index 000000000..6b8deb9c9 --- /dev/null +++ b/ServiceStack/packages.config @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ServiceStack/project.json b/ServiceStack/project.json new file mode 100644 index 000000000..fbbe9eaf3 --- /dev/null +++ b/ServiceStack/project.json @@ -0,0 +1,17 @@ +{ + "frameworks":{ + "netstandard1.6":{ + "dependencies":{ + "NETStandard.Library":"1.6.0", + } + }, + ".NETPortable,Version=v4.5,Profile=Profile7":{ + "buildOptions": { + "define": [ ] + }, + "frameworkAssemblies":{ + + } + } + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/ByteOrder.cs b/SocketHttpListener.Portable/ByteOrder.cs new file mode 100644 index 000000000..f5db52fd7 --- /dev/null +++ b/SocketHttpListener.Portable/ByteOrder.cs @@ -0,0 +1,17 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values that indicate whether the byte order is a Little-endian or Big-endian. + /// + public enum ByteOrder : byte + { + /// + /// Indicates a Little-endian. + /// + Little, + /// + /// Indicates a Big-endian. + /// + Big + } +} diff --git a/SocketHttpListener.Portable/CloseEventArgs.cs b/SocketHttpListener.Portable/CloseEventArgs.cs new file mode 100644 index 000000000..b1bb4b196 --- /dev/null +++ b/SocketHttpListener.Portable/CloseEventArgs.cs @@ -0,0 +1,90 @@ +using System; +using System.Text; + +namespace SocketHttpListener +{ + /// + /// Contains the event data associated with a event. + /// + /// + /// A event occurs when the WebSocket connection has been closed. + /// If you would like to get the reason for the close, you should access the or + /// property. + /// + public class CloseEventArgs : EventArgs + { + #region Private Fields + + private bool _clean; + private ushort _code; + private string _reason; + + #endregion + + #region Internal Constructors + + internal CloseEventArgs (PayloadData payload) + { + var data = payload.ApplicationData; + var len = data.Length; + _code = len > 1 + ? data.SubArray (0, 2).ToUInt16 (ByteOrder.Big) + : (ushort) CloseStatusCode.NoStatusCode; + + _reason = len > 2 + ? GetUtf8String(data.SubArray (2, len - 2)) + : String.Empty; + } + + private string GetUtf8String(byte[] bytes) + { + return Encoding.UTF8.GetString(bytes, 0, bytes.Length); + } + + #endregion + + #region Public Properties + + /// + /// Gets the status code for the close. + /// + /// + /// A that represents the status code for the close if any. + /// + public ushort Code { + get { + return _code; + } + } + + /// + /// Gets the reason for the close. + /// + /// + /// A that represents the reason for the close if any. + /// + public string Reason { + get { + return _reason; + } + } + + /// + /// Gets a value indicating whether the WebSocket connection has been closed cleanly. + /// + /// + /// true if the WebSocket connection has been closed cleanly; otherwise, false. + /// + public bool WasClean { + get { + return _clean; + } + + internal set { + _clean = value; + } + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/CloseStatusCode.cs b/SocketHttpListener.Portable/CloseStatusCode.cs new file mode 100644 index 000000000..62a268bce --- /dev/null +++ b/SocketHttpListener.Portable/CloseStatusCode.cs @@ -0,0 +1,94 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values of the status code for the WebSocket connection close. + /// + /// + /// + /// The values of the status code are defined in + /// Section 7.4 + /// of RFC 6455. + /// + /// + /// "Reserved value" must not be set as a status code in a close control frame + /// by an endpoint. It's designated for use in applications expecting a status + /// code to indicate that the connection was closed due to the system grounds. + /// + /// + public enum CloseStatusCode : ushort + { + /// + /// Equivalent to close status 1000. + /// Indicates a normal close. + /// + Normal = 1000, + /// + /// Equivalent to close status 1001. + /// Indicates that an endpoint is going away. + /// + Away = 1001, + /// + /// Equivalent to close status 1002. + /// Indicates that an endpoint is terminating the connection due to a protocol error. + /// + ProtocolError = 1002, + /// + /// Equivalent to close status 1003. + /// Indicates that an endpoint is terminating the connection because it has received + /// an unacceptable type message. + /// + IncorrectData = 1003, + /// + /// Equivalent to close status 1004. + /// Still undefined. Reserved value. + /// + Undefined = 1004, + /// + /// Equivalent to close status 1005. + /// Indicates that no status code was actually present. Reserved value. + /// + NoStatusCode = 1005, + /// + /// Equivalent to close status 1006. + /// Indicates that the connection was closed abnormally. Reserved value. + /// + Abnormal = 1006, + /// + /// Equivalent to close status 1007. + /// Indicates that an endpoint is terminating the connection because it has received + /// a message that contains a data that isn't consistent with the type of the message. + /// + InconsistentData = 1007, + /// + /// Equivalent to close status 1008. + /// Indicates that an endpoint is terminating the connection because it has received + /// a message that violates its policy. + /// + PolicyViolation = 1008, + /// + /// Equivalent to close status 1009. + /// Indicates that an endpoint is terminating the connection because it has received + /// a message that is too big to process. + /// + TooBig = 1009, + /// + /// Equivalent to close status 1010. + /// Indicates that the client is terminating the connection because it has expected + /// the server to negotiate one or more extension, but the server didn't return them + /// in the handshake response. + /// + IgnoreExtension = 1010, + /// + /// Equivalent to close status 1011. + /// Indicates that the server is terminating the connection because it has encountered + /// an unexpected condition that prevented it from fulfilling the request. + /// + ServerError = 1011, + /// + /// Equivalent to close status 1015. + /// Indicates that the connection was closed due to a failure to perform a TLS handshake. + /// Reserved value. + /// + TlsHandshakeFailure = 1015 + } +} diff --git a/SocketHttpListener.Portable/CompressionMethod.cs b/SocketHttpListener.Portable/CompressionMethod.cs new file mode 100644 index 000000000..36a48d94c --- /dev/null +++ b/SocketHttpListener.Portable/CompressionMethod.cs @@ -0,0 +1,23 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values of the compression method used to compress the message on the WebSocket + /// connection. + /// + /// + /// The values of the compression method are defined in + /// Compression + /// Extensions for WebSocket. + /// + public enum CompressionMethod : byte + { + /// + /// Indicates non compression. + /// + None, + /// + /// Indicates using DEFLATE. + /// + Deflate + } +} diff --git a/SocketHttpListener.Portable/ErrorEventArgs.cs b/SocketHttpListener.Portable/ErrorEventArgs.cs new file mode 100644 index 000000000..bf1d6fc95 --- /dev/null +++ b/SocketHttpListener.Portable/ErrorEventArgs.cs @@ -0,0 +1,46 @@ +using System; + +namespace SocketHttpListener +{ + /// + /// Contains the event data associated with a event. + /// + /// + /// A event occurs when the gets an error. + /// If you would like to get the error message, you should access the + /// property. + /// + public class ErrorEventArgs : EventArgs + { + #region Private Fields + + private string _message; + + #endregion + + #region Internal Constructors + + internal ErrorEventArgs (string message) + { + _message = message; + } + + #endregion + + #region Public Properties + + /// + /// Gets the error message. + /// + /// + /// A that represents the error message. + /// + public string Message { + get { + return _message; + } + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Ext.cs b/SocketHttpListener.Portable/Ext.cs new file mode 100644 index 000000000..303263d0b --- /dev/null +++ b/SocketHttpListener.Portable/Ext.cs @@ -0,0 +1,1089 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; +using SocketHttpListener.Net; +using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse; +using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; + +namespace SocketHttpListener +{ + /// + /// Provides a set of static methods for the websocket-sharp. + /// + public static class Ext + { + #region Private Const Fields + + private const string _tspecials = "()<>@,;:\\\"/[]?={} \t"; + + #endregion + + #region Private Methods + + private static MemoryStream compress(this Stream stream) + { + var output = new MemoryStream(); + if (stream.Length == 0) + return output; + + stream.Position = 0; + using (var ds = new DeflateStream(output, CompressionMode.Compress, true)) + { + stream.CopyTo(ds); + //ds.Close(); // "BFINAL" set to 1. + output.Position = 0; + + return output; + } + } + + private static byte[] decompress(this byte[] value) + { + if (value.Length == 0) + return value; + + using (var input = new MemoryStream(value)) + { + return input.decompressToArray(); + } + } + + private static MemoryStream decompress(this Stream stream) + { + var output = new MemoryStream(); + if (stream.Length == 0) + return output; + + stream.Position = 0; + using (var ds = new DeflateStream(stream, CompressionMode.Decompress, true)) + { + ds.CopyTo(output, true); + return output; + } + } + + private static byte[] decompressToArray(this Stream stream) + { + using (var decomp = stream.decompress()) + { + return decomp.ToArray(); + } + } + + private static byte[] readBytes(this Stream stream, byte[] buffer, int offset, int length) + { + var len = stream.Read(buffer, offset, length); + if (len < 1) + return buffer.SubArray(0, offset); + + var tmp = 0; + while (len < length) + { + tmp = stream.Read(buffer, offset + len, length - len); + if (tmp < 1) + break; + + len += tmp; + } + + return len < length + ? buffer.SubArray(0, offset + len) + : buffer; + } + + private static bool readBytes( + this Stream stream, byte[] buffer, int offset, int length, Stream dest) + { + var bytes = stream.readBytes(buffer, offset, length); + var len = bytes.Length; + dest.Write(bytes, 0, len); + + return len == offset + length; + } + + #endregion + + #region Internal Methods + + internal static byte[] Append(this ushort code, string reason) + { + using (var buffer = new MemoryStream()) + { + var tmp = code.ToByteArrayInternally(ByteOrder.Big); + buffer.Write(tmp, 0, 2); + if (reason != null && reason.Length > 0) + { + tmp = Encoding.UTF8.GetBytes(reason); + buffer.Write(tmp, 0, tmp.Length); + } + + return buffer.ToArray(); + } + } + + internal static string CheckIfClosable(this WebSocketState state) + { + return state == WebSocketState.Closing + ? "While closing the WebSocket connection." + : state == WebSocketState.Closed + ? "The WebSocket connection has already been closed." + : null; + } + + internal static string CheckIfOpen(this WebSocketState state) + { + return state == WebSocketState.Connecting + ? "A WebSocket connection isn't established." + : state == WebSocketState.Closing + ? "While closing the WebSocket connection." + : state == WebSocketState.Closed + ? "The WebSocket connection has already been closed." + : null; + } + + internal static string CheckIfValidControlData(this byte[] data, string paramName) + { + return data.Length > 125 + ? String.Format("'{0}' length must be less.", paramName) + : null; + } + + internal static string CheckIfValidSendData(this byte[] data) + { + return data == null + ? "'data' must not be null." + : null; + } + + internal static string CheckIfValidSendData(this string data) + { + return data == null + ? "'data' must not be null." + : null; + } + + internal static void Close(this HttpListenerResponse response, HttpStatusCode code) + { + response.StatusCode = (int)code; + response.OutputStream.Dispose(); + } + + internal static Stream Compress(this Stream stream, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? stream.compress() + : stream; + } + + internal static bool Contains(this IEnumerable source, Func condition) + { + foreach (T elm in source) + if (condition(elm)) + return true; + + return false; + } + + internal static void CopyTo(this Stream src, Stream dest, bool setDefaultPosition) + { + var readLen = 0; + var bufferLen = 256; + var buffer = new byte[bufferLen]; + while ((readLen = src.Read(buffer, 0, bufferLen)) > 0) + { + dest.Write(buffer, 0, readLen); + } + + if (setDefaultPosition) + dest.Position = 0; + } + + internal static byte[] Decompress(this byte[] value, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? value.decompress() + : value; + } + + internal static byte[] DecompressToArray(this Stream stream, CompressionMethod method) + { + return method == CompressionMethod.Deflate + ? stream.decompressToArray() + : stream.ToByteArray(); + } + + /// + /// Determines whether the specified equals the specified , + /// and invokes the specified Action<int> delegate at the same time. + /// + /// + /// true if equals ; + /// otherwise, false. + /// + /// + /// An to compare. + /// + /// + /// A to compare. + /// + /// + /// An Action<int> delegate that references the method(s) called at + /// the same time as comparing. An parameter to pass to + /// the method(s) is . + /// + /// + /// isn't between 0 and 255. + /// + internal static bool EqualsWith(this int value, char c, Action action) + { + if (value < 0 || value > 255) + throw new ArgumentOutOfRangeException("value"); + + action(value); + return value == c - 0; + } + + internal static string GetMessage(this CloseStatusCode code) + { + return code == CloseStatusCode.ProtocolError + ? "A WebSocket protocol error has occurred." + : code == CloseStatusCode.IncorrectData + ? "An incorrect data has been received." + : code == CloseStatusCode.Abnormal + ? "An exception has occurred." + : code == CloseStatusCode.InconsistentData + ? "An inconsistent data has been received." + : code == CloseStatusCode.PolicyViolation + ? "A policy violation has occurred." + : code == CloseStatusCode.TooBig + ? "A too big data has been received." + : code == CloseStatusCode.IgnoreExtension + ? "WebSocket client did not receive expected extension(s)." + : code == CloseStatusCode.ServerError + ? "WebSocket server got an internal error." + : code == CloseStatusCode.TlsHandshakeFailure + ? "An error has occurred while handshaking." + : String.Empty; + } + + internal static string GetNameInternal(this string nameAndValue, string separator) + { + var i = nameAndValue.IndexOf(separator); + return i > 0 + ? nameAndValue.Substring(0, i).Trim() + : null; + } + + internal static string GetValueInternal(this string nameAndValue, string separator) + { + var i = nameAndValue.IndexOf(separator); + return i >= 0 && i < nameAndValue.Length - 1 + ? nameAndValue.Substring(i + 1).Trim() + : null; + } + + internal static bool IsCompressionExtension(this string value, CompressionMethod method) + { + return value.StartsWith(method.ToExtensionString()); + } + + internal static bool IsPortNumber(this int value) + { + return value > 0 && value < 65536; + } + + internal static bool IsReserved(this ushort code) + { + return code == (ushort)CloseStatusCode.Undefined || + code == (ushort)CloseStatusCode.NoStatusCode || + code == (ushort)CloseStatusCode.Abnormal || + code == (ushort)CloseStatusCode.TlsHandshakeFailure; + } + + internal static bool IsReserved(this CloseStatusCode code) + { + return code == CloseStatusCode.Undefined || + code == CloseStatusCode.NoStatusCode || + code == CloseStatusCode.Abnormal || + code == CloseStatusCode.TlsHandshakeFailure; + } + + internal static bool IsText(this string value) + { + var len = value.Length; + for (var i = 0; i < len; i++) + { + char c = value[i]; + if (c < 0x20 && !"\r\n\t".Contains(c)) + return false; + + if (c == 0x7f) + return false; + + if (c == '\n' && ++i < len) + { + c = value[i]; + if (!" \t".Contains(c)) + return false; + } + } + + return true; + } + + internal static bool IsToken(this string value) + { + foreach (char c in value) + if (c < 0x20 || c >= 0x7f || _tspecials.Contains(c)) + return false; + + return true; + } + + internal static string Quote(this string value) + { + return value.IsToken() + ? value + : String.Format("\"{0}\"", value.Replace("\"", "\\\"")); + } + + internal static byte[] ReadBytes(this Stream stream, int length) + { + return stream.readBytes(new byte[length], 0, length); + } + + internal static byte[] ReadBytes(this Stream stream, long length, int bufferLength) + { + using (var result = new MemoryStream()) + { + var count = length / bufferLength; + var rem = (int)(length % bufferLength); + + var buffer = new byte[bufferLength]; + var end = false; + for (long i = 0; i < count; i++) + { + if (!stream.readBytes(buffer, 0, bufferLength, result)) + { + end = true; + break; + } + } + + if (!end && rem > 0) + stream.readBytes(new byte[rem], 0, rem, result); + + return result.ToArray(); + } + } + + internal static async Task ReadBytesAsync(this Stream stream, int length) + { + var buffer = new byte[length]; + + var len = await stream.ReadAsync(buffer, 0, length).ConfigureAwait(false); + var bytes = len < 1 + ? new byte[0] + : len < length + ? stream.readBytes(buffer, len, length - len) + : buffer; + + return bytes; + } + + internal static string RemovePrefix(this string value, params string[] prefixes) + { + var i = 0; + foreach (var prefix in prefixes) + { + if (value.StartsWith(prefix)) + { + i = prefix.Length; + break; + } + } + + return i > 0 + ? value.Substring(i) + : value; + } + + internal static T[] Reverse(this T[] array) + { + var len = array.Length; + T[] reverse = new T[len]; + + var end = len - 1; + for (var i = 0; i <= end; i++) + reverse[i] = array[end - i]; + + return reverse; + } + + internal static IEnumerable SplitHeaderValue( + this string value, params char[] separator) + { + var len = value.Length; + var separators = new string(separator); + + var buffer = new StringBuilder(32); + var quoted = false; + var escaped = false; + + char c; + for (var i = 0; i < len; i++) + { + c = value[i]; + if (c == '"') + { + if (escaped) + escaped = !escaped; + else + quoted = !quoted; + } + else if (c == '\\') + { + if (i < len - 1 && value[i + 1] == '"') + escaped = true; + } + else if (separators.Contains(c)) + { + if (!quoted) + { + yield return buffer.ToString(); + buffer.Length = 0; + + continue; + } + } + else { + } + + buffer.Append(c); + } + + if (buffer.Length > 0) + yield return buffer.ToString(); + } + + internal static byte[] ToByteArray(this Stream stream) + { + using (var output = new MemoryStream()) + { + stream.Position = 0; + stream.CopyTo(output); + + return output.ToArray(); + } + } + + internal static byte[] ToByteArrayInternally(this ushort value, ByteOrder order) + { + var bytes = BitConverter.GetBytes(value); + if (!order.IsHostOrder()) + Array.Reverse(bytes); + + return bytes; + } + + internal static byte[] ToByteArrayInternally(this ulong value, ByteOrder order) + { + var bytes = BitConverter.GetBytes(value); + if (!order.IsHostOrder()) + Array.Reverse(bytes); + + return bytes; + } + + internal static string ToExtensionString( + this CompressionMethod method, params string[] parameters) + { + if (method == CompressionMethod.None) + return String.Empty; + + var m = String.Format("permessage-{0}", method.ToString().ToLower()); + if (parameters == null || parameters.Length == 0) + return m; + + return String.Format("{0}; {1}", m, parameters.ToString("; ")); + } + + internal static List ToList(this IEnumerable source) + { + return new List(source); + } + + internal static ushort ToUInt16(this byte[] src, ByteOrder srcOrder) + { + return BitConverter.ToUInt16(src.ToHostOrder(srcOrder), 0); + } + + internal static ulong ToUInt64(this byte[] src, ByteOrder srcOrder) + { + return BitConverter.ToUInt64(src.ToHostOrder(srcOrder), 0); + } + + internal static string TrimEndSlash(this string value) + { + value = value.TrimEnd('/'); + return value.Length > 0 + ? value + : "/"; + } + + internal static string Unquote(this string value) + { + var start = value.IndexOf('\"'); + var end = value.LastIndexOf('\"'); + if (start < end) + value = value.Substring(start + 1, end - start - 1).Replace("\\\"", "\""); + + return value.Trim(); + } + + internal static void WriteBytes(this Stream stream, byte[] value) + { + using (var src = new MemoryStream(value)) + { + src.CopyTo(stream); + } + } + + #endregion + + #region Public Methods + + /// + /// Determines whether the specified contains any of characters + /// in the specified array of . + /// + /// + /// true if contains any of ; + /// otherwise, false. + /// + /// + /// A to test. + /// + /// + /// An array of that contains characters to find. + /// + public static bool Contains(this string value, params char[] chars) + { + return chars == null || chars.Length == 0 + ? true + : value == null || value.Length == 0 + ? false + : value.IndexOfAny(chars) != -1; + } + + /// + /// Determines whether the specified contains the entry + /// with the specified . + /// + /// + /// true if contains the entry + /// with ; otherwise, false. + /// + /// + /// A to test. + /// + /// + /// A that represents the key of the entry to find. + /// + public static bool Contains(this QueryParamCollection collection, string name) + { + return collection == null || collection.Count == 0 + ? false + : collection[name] != null; + } + + /// + /// Determines whether the specified contains the entry + /// with the specified both and . + /// + /// + /// true if contains the entry + /// with both and ; + /// otherwise, false. + /// + /// + /// A to test. + /// + /// + /// A that represents the key of the entry to find. + /// + /// + /// A that represents the value of the entry to find. + /// + public static bool Contains(this QueryParamCollection collection, string name, string value) + { + if (collection == null || collection.Count == 0) + return false; + + var values = collection[name]; + if (values == null) + return false; + + foreach (var v in values.Split(',')) + if (v.Trim().Equals(value, StringComparison.OrdinalIgnoreCase)) + return true; + + return false; + } + + /// + /// Emits the specified delegate if it isn't . + /// + /// + /// A to emit. + /// + /// + /// An from which emits this . + /// + /// + /// A that contains no event data. + /// + public static void Emit(this EventHandler eventHandler, object sender, EventArgs e) + { + if (eventHandler != null) + eventHandler(sender, e); + } + + /// + /// Emits the specified EventHandler<TEventArgs> delegate + /// if it isn't . + /// + /// + /// An EventHandler<TEventArgs> to emit. + /// + /// + /// An from which emits this . + /// + /// + /// A TEventArgs that represents the event data. + /// + /// + /// The type of the event data generated by the event. + /// + public static void Emit( + this EventHandler eventHandler, object sender, TEventArgs e) + where TEventArgs : EventArgs + { + if (eventHandler != null) + eventHandler(sender, e); + } + + /// + /// Gets the collection of the HTTP cookies from the specified HTTP . + /// + /// + /// A that receives a collection of the HTTP cookies. + /// + /// + /// A that contains a collection of the HTTP headers. + /// + /// + /// true if is a collection of the response headers; + /// otherwise, false. + /// + public static CookieCollection GetCookies(this QueryParamCollection headers, bool response) + { + var name = response ? "Set-Cookie" : "Cookie"; + return headers == null || !headers.Contains(name) + ? new CookieCollection() + : CookieHelper.Parse(headers[name], response); + } + + /// + /// Gets the description of the specified HTTP status . + /// + /// + /// A that represents the description of the HTTP status code. + /// + /// + /// One of enum values, indicates the HTTP status codes. + /// + public static string GetDescription(this HttpStatusCode code) + { + return ((int)code).GetStatusDescription(); + } + + /// + /// Gets the name from the specified that contains a pair of name and + /// value separated by a separator string. + /// + /// + /// A that represents the name if any; otherwise, null. + /// + /// + /// A that contains a pair of name and value separated by a separator + /// string. + /// + /// + /// A that represents a separator string. + /// + public static string GetName(this string nameAndValue, string separator) + { + return (nameAndValue != null && nameAndValue.Length > 0) && + (separator != null && separator.Length > 0) + ? nameAndValue.GetNameInternal(separator) + : null; + } + + /// + /// Gets the name and value from the specified that contains a pair of + /// name and value separated by a separator string. + /// + /// + /// A KeyValuePair<string, string> that represents the name and value if any. + /// + /// + /// A that contains a pair of name and value separated by a separator + /// string. + /// + /// + /// A that represents a separator string. + /// + public static KeyValuePair GetNameAndValue( + this string nameAndValue, string separator) + { + var name = nameAndValue.GetName(separator); + var value = nameAndValue.GetValue(separator); + return name != null + ? new KeyValuePair(name, value) + : new KeyValuePair(null, null); + } + + /// + /// Gets the description of the specified HTTP status . + /// + /// + /// A that represents the description of the HTTP status code. + /// + /// + /// An that represents the HTTP status code. + /// + public static string GetStatusDescription(this int code) + { + switch (code) + { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 307: return "Temporary Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-Uri Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 422: return "Unprocessable Entity"; + case 423: return "Locked"; + case 424: return "Failed Dependency"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "Http Version Not Supported"; + case 507: return "Insufficient Storage"; + } + + return String.Empty; + } + + /// + /// Gets the value from the specified that contains a pair of name and + /// value separated by a separator string. + /// + /// + /// A that represents the value if any; otherwise, null. + /// + /// + /// A that contains a pair of name and value separated by a separator + /// string. + /// + /// + /// A that represents a separator string. + /// + public static string GetValue(this string nameAndValue, string separator) + { + return (nameAndValue != null && nameAndValue.Length > 0) && + (separator != null && separator.Length > 0) + ? nameAndValue.GetValueInternal(separator) + : null; + } + + /// + /// Determines whether the specified is host + /// (this computer architecture) byte order. + /// + /// + /// true if is host byte order; + /// otherwise, false. + /// + /// + /// One of the enum values, to test. + /// + public static bool IsHostOrder(this ByteOrder order) + { + // true : !(true ^ true) or !(false ^ false) + // false: !(true ^ false) or !(false ^ true) + return !(BitConverter.IsLittleEndian ^ (order == ByteOrder.Little)); + } + + /// + /// Determines whether the specified is a predefined scheme. + /// + /// + /// true if is a predefined scheme; otherwise, false. + /// + /// + /// A to test. + /// + public static bool IsPredefinedScheme(this string value) + { + if (value == null || value.Length < 2) + return false; + + var c = value[0]; + if (c == 'h') + return value == "http" || value == "https"; + + if (c == 'w') + return value == "ws" || value == "wss"; + + if (c == 'f') + return value == "file" || value == "ftp"; + + if (c == 'n') + { + c = value[1]; + return c == 'e' + ? value == "news" || value == "net.pipe" || value == "net.tcp" + : value == "nntp"; + } + + return (c == 'g' && value == "gopher") || (c == 'm' && value == "mailto"); + } + + /// + /// Determines whether the specified is a URI string. + /// + /// + /// true if may be a URI string; otherwise, false. + /// + /// + /// A to test. + /// + public static bool MaybeUri(this string value) + { + if (value == null || value.Length == 0) + return false; + + var i = value.IndexOf(':'); + if (i == -1) + return false; + + if (i >= 10) + return false; + + return value.Substring(0, i).IsPredefinedScheme(); + } + + /// + /// Retrieves a sub-array from the specified . + /// A sub-array starts at the specified element position. + /// + /// + /// An array of T that receives a sub-array, or an empty array of T if any problems + /// with the parameters. + /// + /// + /// An array of T that contains the data to retrieve a sub-array. + /// + /// + /// An that contains the zero-based starting position of a sub-array + /// in . + /// + /// + /// An that contains the number of elements to retrieve a sub-array. + /// + /// + /// The type of elements in the . + /// + public static T[] SubArray(this T[] array, int startIndex, int length) + { + if (array == null || array.Length == 0) + return new T[0]; + + if (startIndex < 0 || length <= 0) + return new T[0]; + + if (startIndex + length > array.Length) + return new T[0]; + + if (startIndex == 0 && array.Length == length) + return array; + + T[] subArray = new T[length]; + Array.Copy(array, startIndex, subArray, 0, length); + + return subArray; + } + + /// + /// Converts the order of the specified array of to the host byte order. + /// + /// + /// An array of converted from . + /// + /// + /// An array of to convert. + /// + /// + /// One of the enum values, indicates the byte order of + /// . + /// + /// + /// is . + /// + public static byte[] ToHostOrder(this byte[] src, ByteOrder srcOrder) + { + if (src == null) + throw new ArgumentNullException("src"); + + return src.Length > 1 && !srcOrder.IsHostOrder() + ? src.Reverse() + : src; + } + + /// + /// Converts the specified to a that + /// concatenates the each element of across the specified + /// . + /// + /// + /// A converted from , + /// or if is empty. + /// + /// + /// An array of T to convert. + /// + /// + /// A that represents the separator string. + /// + /// + /// The type of elements in . + /// + /// + /// is . + /// + public static string ToString(this T[] array, string separator) + { + if (array == null) + throw new ArgumentNullException("array"); + + var len = array.Length; + if (len == 0) + return String.Empty; + + if (separator == null) + separator = String.Empty; + + var buff = new StringBuilder(64); + (len - 1).Times(i => buff.AppendFormat("{0}{1}", array[i].ToString(), separator)); + + buff.Append(array[len - 1].ToString()); + return buff.ToString(); + } + + /// + /// Executes the specified Action<int> delegate times. + /// + /// + /// An is the number of times to execute. + /// + /// + /// An Action<int> delegate that references the method(s) to execute. + /// An parameter to pass to the method(s) is the zero-based count of + /// iteration. + /// + public static void Times(this int n, Action action) + { + if (n > 0 && action != null) + for (int i = 0; i < n; i++) + action(i); + } + + /// + /// Converts the specified to a . + /// + /// + /// A converted from , or + /// if isn't successfully converted. + /// + /// + /// A to convert. + /// + public static Uri ToUri(this string uriString) + { + Uri res; + return Uri.TryCreate( + uriString, uriString.MaybeUri() ? UriKind.Absolute : UriKind.Relative, out res) + ? res + : null; + } + + /// + /// URL-decodes the specified . + /// + /// + /// A that receives the decoded string, or the + /// if it's or empty. + /// + /// + /// A to decode. + /// + public static string UrlDecode(this string value) + { + return value == null || value.Length == 0 + ? value + : WebUtility.UrlDecode(value); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/Fin.cs b/SocketHttpListener.Portable/Fin.cs new file mode 100644 index 000000000..f91401b99 --- /dev/null +++ b/SocketHttpListener.Portable/Fin.cs @@ -0,0 +1,8 @@ +namespace SocketHttpListener +{ + internal enum Fin : byte + { + More = 0x0, + Final = 0x1 + } +} diff --git a/SocketHttpListener.Portable/HttpBase.cs b/SocketHttpListener.Portable/HttpBase.cs new file mode 100644 index 000000000..5172ba497 --- /dev/null +++ b/SocketHttpListener.Portable/HttpBase.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Text; +using System.Threading; +using MediaBrowser.Model.Services; + +namespace SocketHttpListener +{ + internal abstract class HttpBase + { + #region Private Fields + + private QueryParamCollection _headers; + private Version _version; + + #endregion + + #region Internal Fields + + internal byte[] EntityBodyData; + + #endregion + + #region Protected Fields + + protected const string CrLf = "\r\n"; + + #endregion + + #region Protected Constructors + + protected HttpBase(Version version, QueryParamCollection headers) + { + _version = version; + _headers = headers; + } + + #endregion + + #region Public Properties + + public string EntityBody + { + get + { + var data = EntityBodyData; + + return data != null && data.Length > 0 + ? getEncoding(_headers["Content-Type"]).GetString(data, 0, data.Length) + : String.Empty; + } + } + + public QueryParamCollection Headers + { + get + { + return _headers; + } + } + + public Version ProtocolVersion + { + get + { + return _version; + } + } + + #endregion + + #region Private Methods + + private static Encoding getEncoding(string contentType) + { + if (contentType == null || contentType.Length == 0) + return Encoding.UTF8; + + var i = contentType.IndexOf("charset=", StringComparison.Ordinal); + if (i == -1) + return Encoding.UTF8; + + var charset = contentType.Substring(i + 8); + i = charset.IndexOf(';'); + if (i != -1) + charset = charset.Substring(0, i).TrimEnd(); + + return Encoding.GetEncoding(charset.Trim('"')); + } + + #endregion + + #region Public Methods + + public byte[] ToByteArray() + { + return Encoding.UTF8.GetBytes(ToString()); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/HttpResponse.cs b/SocketHttpListener.Portable/HttpResponse.cs new file mode 100644 index 000000000..5aca28c7c --- /dev/null +++ b/SocketHttpListener.Portable/HttpResponse.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Specialized; +using System.IO; +using System.Net; +using System.Text; +using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; +using HttpVersion = SocketHttpListener.Net.HttpVersion; +using System.Linq; +using MediaBrowser.Model.Services; + +namespace SocketHttpListener +{ + internal class HttpResponse : HttpBase + { + #region Private Fields + + private string _code; + private string _reason; + + #endregion + + #region Private Constructors + + private HttpResponse(string code, string reason, Version version, QueryParamCollection headers) + : base(version, headers) + { + _code = code; + _reason = reason; + } + + #endregion + + #region Internal Constructors + + internal HttpResponse(HttpStatusCode code) + : this(code, code.GetDescription()) + { + } + + internal HttpResponse(HttpStatusCode code, string reason) + : this(((int)code).ToString(), reason, HttpVersion.Version11, new QueryParamCollection()) + { + Headers["Server"] = "websocket-sharp/1.0"; + } + + #endregion + + #region Public Properties + + public CookieCollection Cookies + { + get + { + return Headers.GetCookies(true); + } + } + + public bool IsProxyAuthenticationRequired + { + get + { + return _code == "407"; + } + } + + public bool IsUnauthorized + { + get + { + return _code == "401"; + } + } + + public bool IsWebSocketResponse + { + get + { + var headers = Headers; + return ProtocolVersion > HttpVersion.Version10 && + _code == "101" && + headers.Contains("Upgrade", "websocket") && + headers.Contains("Connection", "Upgrade"); + } + } + + public string Reason + { + get + { + return _reason; + } + } + + public string StatusCode + { + get + { + return _code; + } + } + + #endregion + + #region Internal Methods + + internal static HttpResponse CreateCloseResponse(HttpStatusCode code) + { + var res = new HttpResponse(code); + res.Headers["Connection"] = "close"; + + return res; + } + + internal static HttpResponse CreateWebSocketResponse() + { + var res = new HttpResponse(HttpStatusCode.SwitchingProtocols); + + var headers = res.Headers; + headers["Upgrade"] = "websocket"; + headers["Connection"] = "Upgrade"; + + return res; + } + + #endregion + + #region Public Methods + + public void SetCookies(CookieCollection cookies) + { + if (cookies == null || cookies.Count == 0) + return; + + var headers = Headers; + var sorted = cookies.OfType().OrderBy(i => i.Name).ToList(); + + foreach (var cookie in sorted) + headers.Add("Set-Cookie", cookie.ToString()); + } + + public override string ToString() + { + var output = new StringBuilder(64); + output.AppendFormat("HTTP/{0} {1} {2}{3}", ProtocolVersion, _code, _reason, CrLf); + + var headers = Headers; + foreach (var key in headers.Keys) + output.AppendFormat("{0}: {1}{2}", key, headers[key], CrLf); + + output.Append(CrLf); + + var entity = EntityBody; + if (entity.Length > 0) + output.Append(entity); + + return output.ToString(); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/Mask.cs b/SocketHttpListener.Portable/Mask.cs new file mode 100644 index 000000000..adc2f098e --- /dev/null +++ b/SocketHttpListener.Portable/Mask.cs @@ -0,0 +1,8 @@ +namespace SocketHttpListener +{ + internal enum Mask : byte + { + Unmask = 0x0, + Mask = 0x1 + } +} diff --git a/SocketHttpListener.Portable/MessageEventArgs.cs b/SocketHttpListener.Portable/MessageEventArgs.cs new file mode 100644 index 000000000..9dbadb9ab --- /dev/null +++ b/SocketHttpListener.Portable/MessageEventArgs.cs @@ -0,0 +1,96 @@ +using System; +using System.Text; + +namespace SocketHttpListener +{ + /// + /// Contains the event data associated with a event. + /// + /// + /// A event occurs when the receives + /// a text or binary data frame. + /// If you want to get the received data, you access the or + /// property. + /// + public class MessageEventArgs : EventArgs + { + #region Private Fields + + private string _data; + private Opcode _opcode; + private byte[] _rawData; + + #endregion + + #region Internal Constructors + + internal MessageEventArgs (Opcode opcode, byte[] data) + { + _opcode = opcode; + _rawData = data; + _data = convertToString (opcode, data); + } + + internal MessageEventArgs (Opcode opcode, PayloadData payload) + { + _opcode = opcode; + _rawData = payload.ApplicationData; + _data = convertToString (opcode, _rawData); + } + + #endregion + + #region Public Properties + + /// + /// Gets the received data as a . + /// + /// + /// A that contains the received data. + /// + public string Data { + get { + return _data; + } + } + + /// + /// Gets the received data as an array of . + /// + /// + /// An array of that contains the received data. + /// + public byte [] RawData { + get { + return _rawData; + } + } + + /// + /// Gets the type of the received data. + /// + /// + /// One of the values, indicates the type of the received data. + /// + public Opcode Type { + get { + return _opcode; + } + } + + #endregion + + #region Private Methods + + private static string convertToString (Opcode opcode, byte [] data) + { + return data.Length == 0 + ? String.Empty + : opcode == Opcode.Text + ? Encoding.UTF8.GetString (data, 0, data.Length) + : opcode.ToString (); + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Net/AuthenticationSchemeSelector.cs b/SocketHttpListener.Portable/Net/AuthenticationSchemeSelector.cs new file mode 100644 index 000000000..c6e7e538e --- /dev/null +++ b/SocketHttpListener.Portable/Net/AuthenticationSchemeSelector.cs @@ -0,0 +1,6 @@ +using System.Net; + +namespace SocketHttpListener.Net +{ + public delegate AuthenticationSchemes AuthenticationSchemeSelector(HttpListenerRequest httpRequest); +} diff --git a/SocketHttpListener.Portable/Net/ChunkStream.cs b/SocketHttpListener.Portable/Net/ChunkStream.cs new file mode 100644 index 000000000..3f3b4a667 --- /dev/null +++ b/SocketHttpListener.Portable/Net/ChunkStream.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; + +namespace SocketHttpListener.Net +{ + class ChunkStream + { + enum State + { + None, + PartialSize, + Body, + BodyFinished, + Trailer + } + + class Chunk + { + public byte[] Bytes; + public int Offset; + + public Chunk(byte[] chunk) + { + this.Bytes = chunk; + } + + public int Read(byte[] buffer, int offset, int size) + { + int nread = (size > Bytes.Length - Offset) ? Bytes.Length - Offset : size; + Buffer.BlockCopy(Bytes, Offset, buffer, offset, nread); + Offset += nread; + return nread; + } + } + + internal WebHeaderCollection headers; + int chunkSize; + int chunkRead; + int totalWritten; + State state; + //byte [] waitBuffer; + StringBuilder saved; + bool sawCR; + bool gotit; + int trailerState; + List chunks; + + public ChunkStream(WebHeaderCollection headers) + { + this.headers = headers; + saved = new StringBuilder(); + chunks = new List(); + chunkSize = -1; + totalWritten = 0; + } + + public void ResetBuffer() + { + chunkSize = -1; + chunkRead = 0; + totalWritten = 0; + chunks.Clear(); + } + + public void WriteAndReadBack(byte[] buffer, int offset, int size, ref int read) + { + if (offset + read > 0) + Write(buffer, offset, offset + read); + read = Read(buffer, offset, size); + } + + public int Read(byte[] buffer, int offset, int size) + { + return ReadFromChunks(buffer, offset, size); + } + + int ReadFromChunks(byte[] buffer, int offset, int size) + { + int count = chunks.Count; + int nread = 0; + + var chunksForRemoving = new List(count); + for (int i = 0; i < count; i++) + { + Chunk chunk = (Chunk)chunks[i]; + + if (chunk.Offset == chunk.Bytes.Length) + { + chunksForRemoving.Add(chunk); + continue; + } + + nread += chunk.Read(buffer, offset + nread, size - nread); + if (nread == size) + break; + } + + foreach (var chunk in chunksForRemoving) + chunks.Remove(chunk); + + return nread; + } + + public void Write(byte[] buffer, int offset, int size) + { + if (offset < size) + InternalWrite(buffer, ref offset, size); + } + + void InternalWrite(byte[] buffer, ref int offset, int size) + { + if (state == State.None || state == State.PartialSize) + { + state = GetChunkSize(buffer, ref offset, size); + if (state == State.PartialSize) + return; + + saved.Length = 0; + sawCR = false; + gotit = false; + } + + if (state == State.Body && offset < size) + { + state = ReadBody(buffer, ref offset, size); + if (state == State.Body) + return; + } + + if (state == State.BodyFinished && offset < size) + { + state = ReadCRLF(buffer, ref offset, size); + if (state == State.BodyFinished) + return; + + sawCR = false; + } + + if (state == State.Trailer && offset < size) + { + state = ReadTrailer(buffer, ref offset, size); + if (state == State.Trailer) + return; + + saved.Length = 0; + sawCR = false; + gotit = false; + } + + if (offset < size) + InternalWrite(buffer, ref offset, size); + } + + public bool WantMore + { + get { return (chunkRead != chunkSize || chunkSize != 0 || state != State.None); } + } + + public bool DataAvailable + { + get + { + int count = chunks.Count; + for (int i = 0; i < count; i++) + { + Chunk ch = (Chunk)chunks[i]; + if (ch == null || ch.Bytes == null) + continue; + if (ch.Bytes.Length > 0 && ch.Offset < ch.Bytes.Length) + return (state != State.Body); + } + return false; + } + } + + public int TotalDataSize + { + get { return totalWritten; } + } + + public int ChunkLeft + { + get { return chunkSize - chunkRead; } + } + + State ReadBody(byte[] buffer, ref int offset, int size) + { + if (chunkSize == 0) + return State.BodyFinished; + + int diff = size - offset; + if (diff + chunkRead > chunkSize) + diff = chunkSize - chunkRead; + + byte[] chunk = new byte[diff]; + Buffer.BlockCopy(buffer, offset, chunk, 0, diff); + chunks.Add(new Chunk(chunk)); + offset += diff; + chunkRead += diff; + totalWritten += diff; + return (chunkRead == chunkSize) ? State.BodyFinished : State.Body; + + } + + State GetChunkSize(byte[] buffer, ref int offset, int size) + { + chunkRead = 0; + chunkSize = 0; + char c = '\0'; + while (offset < size) + { + c = (char)buffer[offset++]; + if (c == '\r') + { + if (sawCR) + ThrowProtocolViolation("2 CR found"); + + sawCR = true; + continue; + } + + if (sawCR && c == '\n') + break; + + if (c == ' ') + gotit = true; + + if (!gotit) + saved.Append(c); + + if (saved.Length > 20) + ThrowProtocolViolation("chunk size too long."); + } + + if (!sawCR || c != '\n') + { + if (offset < size) + ThrowProtocolViolation("Missing \\n"); + + try + { + if (saved.Length > 0) + { + chunkSize = Int32.Parse(RemoveChunkExtension(saved.ToString()), NumberStyles.HexNumber); + } + } + catch (Exception) + { + ThrowProtocolViolation("Cannot parse chunk size."); + } + + return State.PartialSize; + } + + chunkRead = 0; + try + { + chunkSize = Int32.Parse(RemoveChunkExtension(saved.ToString()), NumberStyles.HexNumber); + } + catch (Exception) + { + ThrowProtocolViolation("Cannot parse chunk size."); + } + + if (chunkSize == 0) + { + trailerState = 2; + return State.Trailer; + } + + return State.Body; + } + + static string RemoveChunkExtension(string input) + { + int idx = input.IndexOf(';'); + if (idx == -1) + return input; + return input.Substring(0, idx); + } + + State ReadCRLF(byte[] buffer, ref int offset, int size) + { + if (!sawCR) + { + if ((char)buffer[offset++] != '\r') + ThrowProtocolViolation("Expecting \\r"); + + sawCR = true; + if (offset == size) + return State.BodyFinished; + } + + if (sawCR && (char)buffer[offset++] != '\n') + ThrowProtocolViolation("Expecting \\n"); + + return State.None; + } + + State ReadTrailer(byte[] buffer, ref int offset, int size) + { + char c = '\0'; + + // short path + if (trailerState == 2 && (char)buffer[offset] == '\r' && saved.Length == 0) + { + offset++; + if (offset < size && (char)buffer[offset] == '\n') + { + offset++; + return State.None; + } + offset--; + } + + int st = trailerState; + string stString = "\r\n\r"; + while (offset < size && st < 4) + { + c = (char)buffer[offset++]; + if ((st == 0 || st == 2) && c == '\r') + { + st++; + continue; + } + + if ((st == 1 || st == 3) && c == '\n') + { + st++; + continue; + } + + if (st > 0) + { + saved.Append(stString.Substring(0, saved.Length == 0 ? st - 2 : st)); + st = 0; + if (saved.Length > 4196) + ThrowProtocolViolation("Error reading trailer (too long)."); + } + } + + if (st < 4) + { + trailerState = st; + if (offset < size) + ThrowProtocolViolation("Error reading trailer."); + + return State.Trailer; + } + + StringReader reader = new StringReader(saved.ToString()); + string line; + while ((line = reader.ReadLine()) != null && line != "") + headers.Add(line); + + return State.None; + } + + static void ThrowProtocolViolation(string message) + { + WebException we = new WebException(message, null, WebExceptionStatus.UnknownError, null); + //WebException we = new WebException(message, null, WebExceptionStatus.ServerProtocolViolation, null); + throw we; + } + } +} diff --git a/SocketHttpListener.Portable/Net/ChunkedInputStream.cs b/SocketHttpListener.Portable/Net/ChunkedInputStream.cs new file mode 100644 index 000000000..6dfd8d8a1 --- /dev/null +++ b/SocketHttpListener.Portable/Net/ChunkedInputStream.cs @@ -0,0 +1,160 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + class ChunkedInputStream : RequestStream + { + bool disposed; + ChunkStream decoder; + HttpListenerContext context; + bool no_more_data; + + //class ReadBufferState + //{ + // public byte[] Buffer; + // public int Offset; + // public int Count; + // public int InitialCount; + // public HttpStreamAsyncResult Ares; + // public ReadBufferState(byte[] buffer, int offset, int count, + // HttpStreamAsyncResult ares) + // { + // Buffer = buffer; + // Offset = offset; + // Count = count; + // InitialCount = count; + // Ares = ares; + // } + //} + + public ChunkedInputStream(HttpListenerContext context, Stream stream, + byte[] buffer, int offset, int length) + : base(stream, buffer, offset, length) + { + this.context = context; + WebHeaderCollection coll = (WebHeaderCollection)context.Request.Headers; + decoder = new ChunkStream(coll); + } + + //public ChunkStream Decoder + //{ + // get { return decoder; } + // set { decoder = value; } + //} + + //public override int Read([In, Out] byte[] buffer, int offset, int count) + //{ + // IAsyncResult ares = BeginRead(buffer, offset, count, null, null); + // return EndRead(ares); + //} + + //public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // if (disposed) + // throw new ObjectDisposedException(GetType().ToString()); + + // if (buffer == null) + // throw new ArgumentNullException("buffer"); + + // int len = buffer.Length; + // if (offset < 0 || offset > len) + // throw new ArgumentOutOfRangeException("offset exceeds the size of buffer"); + + // if (count < 0 || offset > len - count) + // throw new ArgumentOutOfRangeException("offset+size exceeds the size of buffer"); + + // HttpStreamAsyncResult ares = new HttpStreamAsyncResult(); + // ares.Callback = cback; + // ares.State = state; + // if (no_more_data) + // { + // ares.Complete(); + // return ares; + // } + // int nread = decoder.Read(buffer, offset, count); + // offset += nread; + // count -= nread; + // if (count == 0) + // { + // // got all we wanted, no need to bother the decoder yet + // ares.Count = nread; + // ares.Complete(); + // return ares; + // } + // if (!decoder.WantMore) + // { + // no_more_data = nread == 0; + // ares.Count = nread; + // ares.Complete(); + // return ares; + // } + // ares.Buffer = new byte[8192]; + // ares.Offset = 0; + // ares.Count = 8192; + // ReadBufferState rb = new ReadBufferState(buffer, offset, count, ares); + // rb.InitialCount += nread; + // base.BeginRead(ares.Buffer, ares.Offset, ares.Count, OnRead, rb); + // return ares; + //} + + //void OnRead(IAsyncResult base_ares) + //{ + // ReadBufferState rb = (ReadBufferState)base_ares.AsyncState; + // HttpStreamAsyncResult ares = rb.Ares; + // try + // { + // int nread = base.EndRead(base_ares); + // decoder.Write(ares.Buffer, ares.Offset, nread); + // nread = decoder.Read(rb.Buffer, rb.Offset, rb.Count); + // rb.Offset += nread; + // rb.Count -= nread; + // if (rb.Count == 0 || !decoder.WantMore || nread == 0) + // { + // no_more_data = !decoder.WantMore && nread == 0; + // ares.Count = rb.InitialCount - rb.Count; + // ares.Complete(); + // return; + // } + // ares.Offset = 0; + // ares.Count = Math.Min(8192, decoder.ChunkLeft + 6); + // base.BeginRead(ares.Buffer, ares.Offset, ares.Count, OnRead, rb); + // } + // catch (Exception e) + // { + // context.Connection.SendError(e.Message, 400); + // ares.Complete(e); + // } + //} + + //public override int EndRead(IAsyncResult ares) + //{ + // if (disposed) + // throw new ObjectDisposedException(GetType().ToString()); + + // HttpStreamAsyncResult my_ares = ares as HttpStreamAsyncResult; + // if (ares == null) + // throw new ArgumentException("Invalid IAsyncResult", "ares"); + + // if (!ares.IsCompleted) + // ares.AsyncWaitHandle.WaitOne(); + + // if (my_ares.Error != null) + // throw new HttpListenerException(400, "I/O operation aborted: " + my_ares.Error.Message); + + // return my_ares.Count; + //} + + //protected override void Dispose(bool disposing) + //{ + // if (!disposed) + // { + // disposed = true; + // base.Dispose(disposing); + // } + //} + } +} diff --git a/SocketHttpListener.Portable/Net/CookieHelper.cs b/SocketHttpListener.Portable/Net/CookieHelper.cs new file mode 100644 index 000000000..470507d6b --- /dev/null +++ b/SocketHttpListener.Portable/Net/CookieHelper.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace SocketHttpListener.Net +{ + public static class CookieHelper + { + internal static CookieCollection Parse(string value, bool response) + { + return response + ? parseResponse(value) + : null; + } + + private static string[] splitCookieHeaderValue(string value) + { + return new List(value.SplitHeaderValue(',', ';')).ToArray(); + } + + private static CookieCollection parseResponse(string value) + { + var cookies = new CookieCollection(); + + Cookie cookie = null; + var pairs = splitCookieHeaderValue(value); + for (int i = 0; i < pairs.Length; i++) + { + var pair = pairs[i].Trim(); + if (pair.Length == 0) + continue; + + if (pair.StartsWith("version", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Version = Int32.Parse(pair.GetValueInternal("=").Trim('"')); + } + else if (pair.StartsWith("expires", StringComparison.OrdinalIgnoreCase)) + { + var buffer = new StringBuilder(pair.GetValueInternal("="), 32); + if (i < pairs.Length - 1) + buffer.AppendFormat(", {0}", pairs[++i].Trim()); + + DateTime expires; + if (!DateTime.TryParseExact( + buffer.ToString(), + new[] { "ddd, dd'-'MMM'-'yyyy HH':'mm':'ss 'GMT'", "r" }, + new CultureInfo("en-US"), + DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, + out expires)) + expires = DateTime.Now; + + if (cookie != null && cookie.Expires == DateTime.MinValue) + cookie.Expires = expires.ToLocalTime(); + } + else if (pair.StartsWith("max-age", StringComparison.OrdinalIgnoreCase)) + { + var max = Int32.Parse(pair.GetValueInternal("=").Trim('"')); + var expires = DateTime.Now.AddSeconds((double)max); + if (cookie != null) + cookie.Expires = expires; + } + else if (pair.StartsWith("path", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Path = pair.GetValueInternal("="); + } + else if (pair.StartsWith("domain", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Domain = pair.GetValueInternal("="); + } + else if (pair.StartsWith("port", StringComparison.OrdinalIgnoreCase)) + { + var port = pair.Equals("port", StringComparison.OrdinalIgnoreCase) + ? "\"\"" + : pair.GetValueInternal("="); + + if (cookie != null) + cookie.Port = port; + } + else if (pair.StartsWith("comment", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Comment = pair.GetValueInternal("=").UrlDecode(); + } + else if (pair.StartsWith("commenturl", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.CommentUri = pair.GetValueInternal("=").Trim('"').ToUri(); + } + else if (pair.StartsWith("discard", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Discard = true; + } + else if (pair.StartsWith("secure", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.Secure = true; + } + else if (pair.StartsWith("httponly", StringComparison.OrdinalIgnoreCase)) + { + if (cookie != null) + cookie.HttpOnly = true; + } + else + { + if (cookie != null) + cookies.Add(cookie); + + string name; + string val = String.Empty; + + var pos = pair.IndexOf('='); + if (pos == -1) + { + name = pair; + } + else if (pos == pair.Length - 1) + { + name = pair.Substring(0, pos).TrimEnd(' '); + } + else + { + name = pair.Substring(0, pos).TrimEnd(' '); + val = pair.Substring(pos + 1).TrimStart(' '); + } + + cookie = new Cookie(name, val); + } + } + + if (cookie != null) + cookies.Add(cookie); + + return cookies; + } + } +} diff --git a/SocketHttpListener.Portable/Net/EndPointListener.cs b/SocketHttpListener.Portable/Net/EndPointListener.cs new file mode 100644 index 000000000..b50660ad0 --- /dev/null +++ b/SocketHttpListener.Portable/Net/EndPointListener.cs @@ -0,0 +1,368 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + sealed class EndPointListener + { + HttpListener listener; + IpEndPointInfo endpoint; + ISocket sock; + Dictionary prefixes; // Dictionary + List unhandled; // List unhandled; host = '*' + List all; // List all; host = '+' + ICertificate cert; + bool secure; + Dictionary unregistered; + private readonly ILogger _logger; + private bool _closed; + private readonly bool _enableDualMode; + private readonly ICryptoProvider _cryptoProvider; + private readonly IStreamFactory _streamFactory; + private readonly ISocketFactory _socketFactory; + private readonly ITextEncoding _textEncoding; + private readonly IMemoryStreamFactory _memoryStreamFactory; + + public EndPointListener(HttpListener listener, IpAddressInfo addr, int port, bool secure, ICertificate cert, ILogger logger, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + this.listener = listener; + _logger = logger; + _cryptoProvider = cryptoProvider; + _streamFactory = streamFactory; + _socketFactory = socketFactory; + _memoryStreamFactory = memoryStreamFactory; + _textEncoding = textEncoding; + + this.secure = secure; + this.cert = cert; + + _enableDualMode = addr.Equals(IpAddressInfo.IPv6Any); + endpoint = new IpEndPointInfo(addr, port); + + prefixes = new Dictionary(); + unregistered = new Dictionary(); + + CreateSocket(); + } + + internal HttpListener Listener + { + get + { + return listener; + } + } + + private void CreateSocket() + { + sock = _socketFactory.CreateSocket(endpoint.IpAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp, _enableDualMode); + + sock.Bind(endpoint); + + // This is the number TcpListener uses. + sock.Listen(2147483647); + + sock.StartAccept(ProcessAccept, () => _closed); + _closed = false; + } + + private async void ProcessAccept(ISocket accepted) + { + try + { + var listener = this; + + if (listener.secure && listener.cert == null) + { + accepted.Close(); + return; + } + + HttpConnection conn = await HttpConnection.Create(_logger, accepted, listener, listener.secure, listener.cert, _cryptoProvider, _streamFactory, _memoryStreamFactory, _textEncoding).ConfigureAwait(false); + + //_logger.Debug("Adding unregistered connection to {0}. Id: {1}", accepted.RemoteEndPoint, connectionId); + lock (listener.unregistered) + { + listener.unregistered[conn] = conn; + } + conn.BeginReadRequest(); + } + catch (Exception ex) + { + _logger.ErrorException("Error in ProcessAccept", ex); + } + } + + internal void RemoveConnection(HttpConnection conn) + { + lock (unregistered) + { + unregistered.Remove(conn); + } + } + + public bool BindContext(HttpListenerContext context) + { + HttpListenerRequest req = context.Request; + ListenerPrefix prefix; + HttpListener listener = SearchListener(req.Url, out prefix); + if (listener == null) + return false; + + context.Listener = listener; + context.Connection.Prefix = prefix; + return true; + } + + public void UnbindContext(HttpListenerContext context) + { + if (context == null || context.Request == null) + return; + + context.Listener.UnregisterContext(context); + } + + HttpListener SearchListener(Uri uri, out ListenerPrefix prefix) + { + prefix = null; + if (uri == null) + return null; + + string host = uri.Host; + int port = uri.Port; + string path = WebUtility.UrlDecode(uri.AbsolutePath); + string path_slash = path[path.Length - 1] == '/' ? path : path + "/"; + + HttpListener best_match = null; + int best_length = -1; + + if (host != null && host != "") + { + var p_ro = prefixes; + foreach (ListenerPrefix p in p_ro.Keys) + { + string ppath = p.Path; + if (ppath.Length < best_length) + continue; + + if (p.Host != host || p.Port != port) + continue; + + if (path.StartsWith(ppath) || path_slash.StartsWith(ppath)) + { + best_length = ppath.Length; + best_match = (HttpListener)p_ro[p]; + prefix = p; + } + } + if (best_length != -1) + return best_match; + } + + List list = unhandled; + best_match = MatchFromList(host, path, list, out prefix); + if (path != path_slash && best_match == null) + best_match = MatchFromList(host, path_slash, list, out prefix); + if (best_match != null) + return best_match; + + list = all; + best_match = MatchFromList(host, path, list, out prefix); + if (path != path_slash && best_match == null) + best_match = MatchFromList(host, path_slash, list, out prefix); + if (best_match != null) + return best_match; + + return null; + } + + HttpListener MatchFromList(string host, string path, List list, out ListenerPrefix prefix) + { + prefix = null; + if (list == null) + return null; + + HttpListener best_match = null; + int best_length = -1; + + foreach (ListenerPrefix p in list) + { + string ppath = p.Path; + if (ppath.Length < best_length) + continue; + + if (path.StartsWith(ppath)) + { + best_length = ppath.Length; + best_match = p.Listener; + prefix = p; + } + } + + return best_match; + } + + void AddSpecial(List coll, ListenerPrefix prefix) + { + if (coll == null) + return; + + foreach (ListenerPrefix p in coll) + { + if (p.Path == prefix.Path) //TODO: code + throw new HttpListenerException(400, "Prefix already in use."); + } + coll.Add(prefix); + } + + bool RemoveSpecial(List coll, ListenerPrefix prefix) + { + if (coll == null) + return false; + + int c = coll.Count; + for (int i = 0; i < c; i++) + { + ListenerPrefix p = (ListenerPrefix)coll[i]; + if (p.Path == prefix.Path) + { + coll.RemoveAt(i); + return true; + } + } + return false; + } + + void CheckIfRemove() + { + if (prefixes.Count > 0) + return; + + List list = unhandled; + if (list != null && list.Count > 0) + return; + + list = all; + if (list != null && list.Count > 0) + return; + + EndPointManager.RemoveEndPoint(this, endpoint); + } + + public void Close() + { + _closed = true; + sock.Close(); + lock (unregistered) + { + // + // Clone the list because RemoveConnection can be called from Close + // + var connections = new List(unregistered.Keys); + + foreach (HttpConnection c in connections) + c.Close(true); + unregistered.Clear(); + } + } + + public void AddPrefix(ListenerPrefix prefix, HttpListener listener) + { + List current; + List future; + if (prefix.Host == "*") + { + do + { + current = unhandled; + future = (current != null) ? current.ToList() : new List(); + prefix.Listener = listener; + AddSpecial(future, prefix); + } while (Interlocked.CompareExchange(ref unhandled, future, current) != current); + return; + } + + if (prefix.Host == "+") + { + do + { + current = all; + future = (current != null) ? current.ToList() : new List(); + prefix.Listener = listener; + AddSpecial(future, prefix); + } while (Interlocked.CompareExchange(ref all, future, current) != current); + return; + } + + Dictionary prefs; + Dictionary p2; + do + { + prefs = prefixes; + if (prefs.ContainsKey(prefix)) + { + HttpListener other = (HttpListener)prefs[prefix]; + if (other != listener) // TODO: code. + throw new HttpListenerException(400, "There's another listener for " + prefix); + return; + } + p2 = new Dictionary(prefs); + p2[prefix] = listener; + } while (Interlocked.CompareExchange(ref prefixes, p2, prefs) != prefs); + } + + public void RemovePrefix(ListenerPrefix prefix, HttpListener listener) + { + List current; + List future; + if (prefix.Host == "*") + { + do + { + current = unhandled; + future = (current != null) ? current.ToList() : new List(); + if (!RemoveSpecial(future, prefix)) + break; // Prefix not found + } while (Interlocked.CompareExchange(ref unhandled, future, current) != current); + CheckIfRemove(); + return; + } + + if (prefix.Host == "+") + { + do + { + current = all; + future = (current != null) ? current.ToList() : new List(); + if (!RemoveSpecial(future, prefix)) + break; // Prefix not found + } while (Interlocked.CompareExchange(ref all, future, current) != current); + CheckIfRemove(); + return; + } + + Dictionary prefs; + Dictionary p2; + do + { + prefs = prefixes; + if (!prefs.ContainsKey(prefix)) + break; + + p2 = new Dictionary(prefs); + p2.Remove(prefix); + } while (Interlocked.CompareExchange(ref prefixes, p2, prefs) != prefs); + CheckIfRemove(); + } + } +} diff --git a/SocketHttpListener.Portable/Net/EndPointManager.cs b/SocketHttpListener.Portable/Net/EndPointManager.cs new file mode 100644 index 000000000..797684b3e --- /dev/null +++ b/SocketHttpListener.Portable/Net/EndPointManager.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + sealed class EndPointManager + { + // Dictionary> + static Dictionary> ip_to_endpoints = new Dictionary>(); + + private EndPointManager() + { + } + + public static void AddListener(ILogger logger, HttpListener listener) + { + List added = new List(); + try + { + lock (ip_to_endpoints) + { + foreach (string prefix in listener.Prefixes) + { + AddPrefixInternal(logger, prefix, listener); + added.Add(prefix); + } + } + } + catch + { + foreach (string prefix in added) + { + RemovePrefix(logger, prefix, listener); + } + throw; + } + } + + public static void AddPrefix(ILogger logger, string prefix, HttpListener listener) + { + lock (ip_to_endpoints) + { + AddPrefixInternal(logger, prefix, listener); + } + } + + static void AddPrefixInternal(ILogger logger, string p, HttpListener listener) + { + ListenerPrefix lp = new ListenerPrefix(p); + if (lp.Path.IndexOf('%') != -1) + throw new HttpListenerException(400, "Invalid path."); + + if (lp.Path.IndexOf("//", StringComparison.Ordinal) != -1) // TODO: Code? + throw new HttpListenerException(400, "Invalid path."); + + // listens on all the interfaces if host name cannot be parsed by IPAddress. + EndPointListener epl = GetEPListener(logger, lp.Host, lp.Port, listener, lp.Secure).Result; + epl.AddPrefix(lp, listener); + } + + private static IpAddressInfo GetIpAnyAddress(HttpListener listener) + { + return listener.EnableDualMode ? IpAddressInfo.IPv6Any : IpAddressInfo.Any; + } + + static async Task GetEPListener(ILogger logger, string host, int port, HttpListener listener, bool secure) + { + var networkManager = listener.NetworkManager; + + IpAddressInfo addr; + if (host == "*" || host == "+") + addr = GetIpAnyAddress(listener); + else if (networkManager.TryParseIpAddress(host, out addr) == false) + { + try + { + addr = (await networkManager.GetHostAddressesAsync(host).ConfigureAwait(false)).FirstOrDefault() ?? + GetIpAnyAddress(listener); + } + catch + { + addr = GetIpAnyAddress(listener); + } + } + + Dictionary p = null; // Dictionary + if (!ip_to_endpoints.TryGetValue(addr.Address, out p)) + { + p = new Dictionary(); + ip_to_endpoints[addr.Address] = p; + } + + EndPointListener epl = null; + if (p.ContainsKey(port)) + { + epl = (EndPointListener)p[port]; + } + else + { + epl = new EndPointListener(listener, addr, port, secure, listener.Certificate, logger, listener.CryptoProvider, listener.StreamFactory, listener.SocketFactory, listener.MemoryStreamFactory, listener.TextEncoding); + p[port] = epl; + } + + return epl; + } + + public static void RemoveEndPoint(EndPointListener epl, IpEndPointInfo ep) + { + lock (ip_to_endpoints) + { + // Dictionary p + Dictionary p; + if (ip_to_endpoints.TryGetValue(ep.IpAddress.Address, out p)) + { + p.Remove(ep.Port); + if (p.Count == 0) + { + ip_to_endpoints.Remove(ep.IpAddress.Address); + } + } + epl.Close(); + } + } + + public static void RemoveListener(ILogger logger, HttpListener listener) + { + lock (ip_to_endpoints) + { + foreach (string prefix in listener.Prefixes) + { + RemovePrefixInternal(logger, prefix, listener); + } + } + } + + public static void RemovePrefix(ILogger logger, string prefix, HttpListener listener) + { + lock (ip_to_endpoints) + { + RemovePrefixInternal(logger, prefix, listener); + } + } + + static void RemovePrefixInternal(ILogger logger, string prefix, HttpListener listener) + { + ListenerPrefix lp = new ListenerPrefix(prefix); + if (lp.Path.IndexOf('%') != -1) + return; + + if (lp.Path.IndexOf("//", StringComparison.Ordinal) != -1) + return; + + EndPointListener epl = GetEPListener(logger, lp.Host, lp.Port, listener, lp.Secure).Result; + epl.RemovePrefix(lp, listener); + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpConnection.cs b/SocketHttpListener.Portable/Net/HttpConnection.cs new file mode 100644 index 000000000..d31da4132 --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpConnection.cs @@ -0,0 +1,550 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + sealed class HttpConnection + { + const int BufferSize = 8192; + ISocket sock; + Stream stream; + EndPointListener epl; + MemoryStream ms; + byte[] buffer; + HttpListenerContext context; + StringBuilder current_line; + ListenerPrefix prefix; + RequestStream i_stream; + ResponseStream o_stream; + bool chunked; + int reuses; + bool context_bound; + bool secure; + int s_timeout = 300000; // 90k ms for first request, 15k ms from then on + IpEndPointInfo local_ep; + HttpListener last_listener; + int[] client_cert_errors; + ICertificate cert; + Stream ssl_stream; + + private ILogger _logger; + private readonly ICryptoProvider _cryptoProvider; + private readonly IMemoryStreamFactory _memoryStreamFactory; + private readonly ITextEncoding _textEncoding; + private readonly IStreamFactory _streamFactory; + + private HttpConnection(ILogger logger, ISocket sock, EndPointListener epl, bool secure, ICertificate cert, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + _logger = logger; + this.sock = sock; + this.epl = epl; + this.secure = secure; + this.cert = cert; + _cryptoProvider = cryptoProvider; + _memoryStreamFactory = memoryStreamFactory; + _textEncoding = textEncoding; + _streamFactory = streamFactory; + } + + private async Task InitStream() + { + if (secure == false) + { + stream = _streamFactory.CreateNetworkStream(sock, false); + } + else + { + //ssl_stream = epl.Listener.CreateSslStream(new NetworkStream(sock, false), false, (t, c, ch, e) => + //{ + // if (c == null) + // return true; + // var c2 = c as X509Certificate2; + // if (c2 == null) + // c2 = new X509Certificate2(c.GetRawCertData()); + // client_cert = c2; + // client_cert_errors = new int[] { (int)e }; + // return true; + //}); + //stream = ssl_stream.AuthenticatedStream; + + ssl_stream = _streamFactory.CreateSslStream(_streamFactory.CreateNetworkStream(sock, false), false); + await _streamFactory.AuthenticateSslStreamAsServer(ssl_stream, cert).ConfigureAwait(false); + stream = ssl_stream; + } + Init(); + } + + public static async Task Create(ILogger logger, ISocket sock, EndPointListener epl, bool secure, ICertificate cert, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + var connection = new HttpConnection(logger, sock, epl, secure, cert, cryptoProvider, streamFactory, memoryStreamFactory, textEncoding); + + await connection.InitStream().ConfigureAwait(false); + + return connection; + } + + public Stream Stream + { + get + { + return stream; + } + } + + internal int[] ClientCertificateErrors + { + get { return client_cert_errors; } + } + + void Init() + { + if (ssl_stream != null) + { + //ssl_stream.AuthenticateAsServer(client_cert, true, (SslProtocols)ServicePointManager.SecurityProtocol, false); + //_streamFactory.AuthenticateSslStreamAsServer(ssl_stream, cert); + } + + context_bound = false; + i_stream = null; + o_stream = null; + prefix = null; + chunked = false; + ms = _memoryStreamFactory.CreateNew(); + position = 0; + input_state = InputState.RequestLine; + line_state = LineState.None; + context = new HttpListenerContext(this, _logger, _cryptoProvider, _memoryStreamFactory, _textEncoding); + } + + public bool IsClosed + { + get { return (sock == null); } + } + + public int Reuses + { + get { return reuses; } + } + + public IpEndPointInfo LocalEndPoint + { + get + { + if (local_ep != null) + return local_ep; + + local_ep = (IpEndPointInfo)sock.LocalEndPoint; + return local_ep; + } + } + + public IpEndPointInfo RemoteEndPoint + { + get { return (IpEndPointInfo)sock.RemoteEndPoint; } + } + + public bool IsSecure + { + get { return secure; } + } + + public ListenerPrefix Prefix + { + get { return prefix; } + set { prefix = value; } + } + + public async Task BeginReadRequest() + { + if (buffer == null) + buffer = new byte[BufferSize]; + + try + { + //if (reuses == 1) + // s_timeout = 15000; + var nRead = await stream.ReadAsync(buffer, 0, BufferSize).ConfigureAwait(false); + + OnReadInternal(nRead); + } + catch (Exception ex) + { + OnReadInternalException(ms, ex); + } + } + + public RequestStream GetRequestStream(bool chunked, long contentlength) + { + if (i_stream == null) + { + byte[] buffer; + _memoryStreamFactory.TryGetBuffer(ms, out buffer); + + int length = (int)ms.Length; + ms = null; + if (chunked) + { + this.chunked = true; + //context.Response.SendChunked = true; + i_stream = new ChunkedInputStream(context, stream, buffer, position, length - position); + } + else + { + i_stream = new RequestStream(stream, buffer, position, length - position, contentlength); + } + } + return i_stream; + } + + public ResponseStream GetResponseStream() + { + // TODO: can we get this stream before reading the input? + if (o_stream == null) + { + HttpListener listener = context.Listener; + + if (listener == null) + return new ResponseStream(stream, context.Response, true, _memoryStreamFactory, _textEncoding); + + o_stream = new ResponseStream(stream, context.Response, listener.IgnoreWriteExceptions, _memoryStreamFactory, _textEncoding); + } + return o_stream; + } + + void OnReadInternal(int nread) + { + ms.Write(buffer, 0, nread); + if (ms.Length > 32768) + { + SendError("Bad request", 400); + Close(true); + return; + } + + if (nread == 0) + { + //if (ms.Length > 0) + // SendError (); // Why bother? + CloseSocket(); + Unbind(); + return; + } + + if (ProcessInput(ms)) + { + if (!context.HaveError) + context.Request.FinishInitialization(); + + if (context.HaveError) + { + SendError(); + Close(true); + return; + } + + if (!epl.BindContext(context)) + { + SendError("Invalid host", 400); + Close(true); + return; + } + HttpListener listener = context.Listener; + if (last_listener != listener) + { + RemoveConnection(); + listener.AddConnection(this); + last_listener = listener; + } + + context_bound = true; + listener.RegisterContext(context); + return; + } + + BeginReadRequest(); + } + + private void OnReadInternalException(MemoryStream ms, Exception ex) + { + //_logger.ErrorException("Error in HttpConnection.OnReadInternal", ex); + + if (ms != null && ms.Length > 0) + SendError(); + if (sock != null) + { + CloseSocket(); + Unbind(); + } + } + + void RemoveConnection() + { + if (last_listener == null) + epl.RemoveConnection(this); + else + last_listener.RemoveConnection(this); + } + + enum InputState + { + RequestLine, + Headers + } + + enum LineState + { + None, + CR, + LF + } + + InputState input_state = InputState.RequestLine; + LineState line_state = LineState.None; + int position; + + // true -> done processing + // false -> need more input + bool ProcessInput(MemoryStream ms) + { + byte[] buffer; + _memoryStreamFactory.TryGetBuffer(ms, out buffer); + + int len = (int)ms.Length; + int used = 0; + string line; + + while (true) + { + if (context.HaveError) + return true; + + if (position >= len) + break; + + try + { + line = ReadLine(buffer, position, len - position, ref used); + position += used; + } + catch + { + context.ErrorMessage = "Bad request"; + context.ErrorStatus = 400; + return true; + } + + if (line == null) + break; + + if (line == "") + { + if (input_state == InputState.RequestLine) + continue; + current_line = null; + ms = null; + return true; + } + + if (input_state == InputState.RequestLine) + { + context.Request.SetRequestLine(line); + input_state = InputState.Headers; + } + else + { + try + { + context.Request.AddHeader(line); + } + catch (Exception e) + { + context.ErrorMessage = e.Message; + context.ErrorStatus = 400; + return true; + } + } + } + + if (used == len) + { + ms.SetLength(0); + position = 0; + } + return false; + } + + string ReadLine(byte[] buffer, int offset, int len, ref int used) + { + if (current_line == null) + current_line = new StringBuilder(128); + int last = offset + len; + used = 0; + + for (int i = offset; i < last && line_state != LineState.LF; i++) + { + used++; + byte b = buffer[i]; + if (b == 13) + { + line_state = LineState.CR; + } + else if (b == 10) + { + line_state = LineState.LF; + } + else + { + current_line.Append((char)b); + } + } + + string result = null; + if (line_state == LineState.LF) + { + line_state = LineState.None; + result = current_line.ToString(); + current_line.Length = 0; + } + + return result; + } + + public void SendError(string msg, int status) + { + try + { + HttpListenerResponse response = context.Response; + response.StatusCode = status; + response.ContentType = "text/html"; + string description = HttpListenerResponse.GetStatusDescription(status); + string str; + if (msg != null) + str = String.Format("

{0} ({1})

", description, msg); + else + str = String.Format("

{0}

", description); + + byte[] error = context.Response.ContentEncoding.GetBytes(str); + response.Close(error, false); + } + catch + { + // response was already closed + } + } + + public void SendError() + { + SendError(context.ErrorMessage, context.ErrorStatus); + } + + void Unbind() + { + if (context_bound) + { + epl.UnbindContext(context); + context_bound = false; + } + } + + public void Close() + { + Close(false); + } + + private void CloseSocket() + { + if (sock == null) + return; + + try + { + sock.Close(); + } + catch + { + } + finally + { + sock = null; + } + RemoveConnection(); + } + + internal void Close(bool force_close) + { + if (sock != null) + { + if (!context.Request.IsWebSocketRequest || force_close) + { + Stream st = GetResponseStream(); + if (st != null) + st.Dispose(); + + o_stream = null; + } + } + + if (sock != null) + { + force_close |= !context.Request.KeepAlive; + if (!force_close) + force_close = (context.Response.Headers["connection"] == "close"); + /* + if (!force_close) { +// bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 || +// status_code == 413 || status_code == 414 || status_code == 500 || +// status_code == 503); + force_close |= (context.Request.ProtocolVersion <= HttpVersion.Version10); + } + */ + + if (!force_close && context.Request.FlushInput()) + { + if (chunked && context.Response.ForceCloseChunked == false) + { + // Don't close. Keep working. + reuses++; + Unbind(); + Init(); + BeginReadRequest(); + return; + } + + reuses++; + Unbind(); + Init(); + BeginReadRequest(); + return; + } + + ISocket s = sock; + sock = null; + try + { + if (s != null) + s.Shutdown(true); + } + catch + { + } + finally + { + if (s != null) + s.Close(); + } + Unbind(); + RemoveConnection(); + return; + } + } + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/Net/HttpListener.cs b/SocketHttpListener.Portable/Net/HttpListener.cs new file mode 100644 index 000000000..83660100a --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListener.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Net; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + public sealed class HttpListener : IDisposable + { + internal ICryptoProvider CryptoProvider { get; private set; } + internal IStreamFactory StreamFactory { get; private set; } + internal ISocketFactory SocketFactory { get; private set; } + internal ITextEncoding TextEncoding { get; private set; } + internal IMemoryStreamFactory MemoryStreamFactory { get; private set; } + internal INetworkManager NetworkManager { get; private set; } + + public bool EnableDualMode { get; set; } + + AuthenticationSchemes auth_schemes; + HttpListenerPrefixCollection prefixes; + AuthenticationSchemeSelector auth_selector; + string realm; + bool ignore_write_exceptions; + bool unsafe_ntlm_auth; + bool listening; + bool disposed; + + Dictionary registry; // Dictionary + Dictionary connections; + private ILogger _logger; + private ICertificate _certificate; + + public Action OnContext { get; set; } + + public HttpListener(ILogger logger, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory) + { + _logger = logger; + CryptoProvider = cryptoProvider; + StreamFactory = streamFactory; + SocketFactory = socketFactory; + NetworkManager = networkManager; + TextEncoding = textEncoding; + MemoryStreamFactory = memoryStreamFactory; + prefixes = new HttpListenerPrefixCollection(logger, this); + registry = new Dictionary(); + connections = new Dictionary(); + auth_schemes = AuthenticationSchemes.Anonymous; + } + + public HttpListener(ICertificate certificate, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory) + :this(new NullLogger(), certificate, cryptoProvider, streamFactory, socketFactory, networkManager, textEncoding, memoryStreamFactory) + { + } + + public HttpListener(ILogger logger, ICertificate certificate, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, ISocketFactory socketFactory, INetworkManager networkManager, ITextEncoding textEncoding, IMemoryStreamFactory memoryStreamFactory) + : this(logger, cryptoProvider, streamFactory, socketFactory, networkManager, textEncoding, memoryStreamFactory) + { + _certificate = certificate; + } + + public void LoadCert(ICertificate cert) + { + _certificate = cert; + } + + // TODO: Digest, NTLM and Negotiate require ControlPrincipal + public AuthenticationSchemes AuthenticationSchemes + { + get { return auth_schemes; } + set + { + CheckDisposed(); + auth_schemes = value; + } + } + + public AuthenticationSchemeSelector AuthenticationSchemeSelectorDelegate + { + get { return auth_selector; } + set + { + CheckDisposed(); + auth_selector = value; + } + } + + public bool IgnoreWriteExceptions + { + get { return ignore_write_exceptions; } + set + { + CheckDisposed(); + ignore_write_exceptions = value; + } + } + + public bool IsListening + { + get { return listening; } + } + + public static bool IsSupported + { + get { return true; } + } + + public HttpListenerPrefixCollection Prefixes + { + get + { + CheckDisposed(); + return prefixes; + } + } + + // TODO: use this + public string Realm + { + get { return realm; } + set + { + CheckDisposed(); + realm = value; + } + } + + public bool UnsafeConnectionNtlmAuthentication + { + get { return unsafe_ntlm_auth; } + set + { + CheckDisposed(); + unsafe_ntlm_auth = value; + } + } + + //internal IMonoSslStream CreateSslStream(Stream innerStream, bool ownsStream, MSI.MonoRemoteCertificateValidationCallback callback) + //{ + // lock (registry) + // { + // if (tlsProvider == null) + // tlsProvider = MonoTlsProviderFactory.GetProviderInternal(); + // if (tlsSettings == null) + // tlsSettings = MSI.MonoTlsSettings.CopyDefaultSettings(); + // if (tlsSettings.RemoteCertificateValidationCallback == null) + // tlsSettings.RemoteCertificateValidationCallback = callback; + // return tlsProvider.CreateSslStream(innerStream, ownsStream, tlsSettings); + // } + //} + + internal ICertificate Certificate + { + get { return _certificate; } + } + + public void Abort() + { + if (disposed) + return; + + if (!listening) + { + return; + } + + Close(true); + } + + public void Close() + { + if (disposed) + return; + + if (!listening) + { + disposed = true; + return; + } + + Close(true); + disposed = true; + } + + void Close(bool force) + { + CheckDisposed(); + EndPointManager.RemoveListener(_logger, this); + Cleanup(force); + } + + void Cleanup(bool close_existing) + { + lock (registry) + { + if (close_existing) + { + // Need to copy this since closing will call UnregisterContext + ICollection keys = registry.Keys; + var all = new HttpListenerContext[keys.Count]; + keys.CopyTo(all, 0); + registry.Clear(); + for (int i = all.Length - 1; i >= 0; i--) + all[i].Connection.Close(true); + } + + lock (connections) + { + ICollection keys = connections.Keys; + var conns = new HttpConnection[keys.Count]; + keys.CopyTo(conns, 0); + connections.Clear(); + for (int i = conns.Length - 1; i >= 0; i--) + conns[i].Close(true); + } + } + } + + internal AuthenticationSchemes SelectAuthenticationScheme(HttpListenerContext context) + { + if (AuthenticationSchemeSelectorDelegate != null) + return AuthenticationSchemeSelectorDelegate(context.Request); + else + return auth_schemes; + } + + public void Start() + { + CheckDisposed(); + if (listening) + return; + + EndPointManager.AddListener(_logger, this); + listening = true; + } + + public void Stop() + { + CheckDisposed(); + listening = false; + Close(false); + } + + void IDisposable.Dispose() + { + if (disposed) + return; + + Close(true); //TODO: Should we force here or not? + disposed = true; + } + + internal void CheckDisposed() + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + } + + internal void RegisterContext(HttpListenerContext context) + { + if (OnContext != null && IsListening) + { + OnContext(context); + } + + lock (registry) + registry[context] = context; + } + + internal void UnregisterContext(HttpListenerContext context) + { + lock (registry) + registry.Remove(context); + } + + internal void AddConnection(HttpConnection cnc) + { + lock (connections) + { + connections[cnc] = cnc; + } + } + + internal void RemoveConnection(HttpConnection cnc) + { + lock (connections) + { + connections.Remove(cnc); + } + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerBasicIdentity.cs b/SocketHttpListener.Portable/Net/HttpListenerBasicIdentity.cs new file mode 100644 index 000000000..faa26693d --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerBasicIdentity.cs @@ -0,0 +1,70 @@ +using System.Security.Principal; + +namespace SocketHttpListener.Net +{ + public class HttpListenerBasicIdentity : GenericIdentity + { + string password; + + public HttpListenerBasicIdentity(string username, string password) + : base(username, "Basic") + { + this.password = password; + } + + public virtual string Password + { + get { return password; } + } + } + + public class GenericIdentity : IIdentity + { + private string m_name; + private string m_type; + + public GenericIdentity(string name) + { + if (name == null) + throw new System.ArgumentNullException("name"); + + m_name = name; + m_type = ""; + } + + public GenericIdentity(string name, string type) + { + if (name == null) + throw new System.ArgumentNullException("name"); + if (type == null) + throw new System.ArgumentNullException("type"); + + m_name = name; + m_type = type; + } + + public virtual string Name + { + get + { + return m_name; + } + } + + public virtual string AuthenticationType + { + get + { + return m_type; + } + } + + public virtual bool IsAuthenticated + { + get + { + return !m_name.Equals(""); + } + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerContext.cs b/SocketHttpListener.Portable/Net/HttpListenerContext.cs new file mode 100644 index 000000000..84c6a8c19 --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerContext.cs @@ -0,0 +1,201 @@ +using System; +using System.Net; +using System.Security.Principal; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Text; +using SocketHttpListener.Net.WebSockets; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + public sealed class HttpListenerContext + { + HttpListenerRequest request; + HttpListenerResponse response; + IPrincipal user; + HttpConnection cnc; + string error; + int err_status = 400; + internal HttpListener Listener; + private readonly ILogger _logger; + private readonly ICryptoProvider _cryptoProvider; + private readonly IMemoryStreamFactory _memoryStreamFactory; + private readonly ITextEncoding _textEncoding; + + internal HttpListenerContext(HttpConnection cnc, ILogger logger, ICryptoProvider cryptoProvider, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + this.cnc = cnc; + _logger = logger; + _cryptoProvider = cryptoProvider; + _memoryStreamFactory = memoryStreamFactory; + _textEncoding = textEncoding; + request = new HttpListenerRequest(this, _textEncoding); + response = new HttpListenerResponse(this, _logger, _textEncoding); + } + + internal int ErrorStatus + { + get { return err_status; } + set { err_status = value; } + } + + internal string ErrorMessage + { + get { return error; } + set { error = value; } + } + + internal bool HaveError + { + get { return (error != null); } + } + + internal HttpConnection Connection + { + get { return cnc; } + } + + public HttpListenerRequest Request + { + get { return request; } + } + + public HttpListenerResponse Response + { + get { return response; } + } + + public IPrincipal User + { + get { return user; } + } + + internal void ParseAuthentication(AuthenticationSchemes expectedSchemes) + { + if (expectedSchemes == AuthenticationSchemes.Anonymous) + return; + + // TODO: Handle NTLM/Digest modes + string header = request.Headers["Authorization"]; + if (header == null || header.Length < 2) + return; + + string[] authenticationData = header.Split(new char[] { ' ' }, 2); + if (string.Equals(authenticationData[0], "basic", StringComparison.OrdinalIgnoreCase)) + { + user = ParseBasicAuthentication(authenticationData[1]); + } + // TODO: throw if malformed -> 400 bad request + } + + internal IPrincipal ParseBasicAuthentication(string authData) + { + try + { + // Basic AUTH Data is a formatted Base64 String + //string domain = null; + string user = null; + string password = null; + int pos = -1; + var authDataBytes = Convert.FromBase64String(authData); + string authString = _textEncoding.GetDefaultEncoding().GetString(authDataBytes, 0, authDataBytes.Length); + + // The format is DOMAIN\username:password + // Domain is optional + + pos = authString.IndexOf(':'); + + // parse the password off the end + password = authString.Substring(pos + 1); + + // discard the password + authString = authString.Substring(0, pos); + + // check if there is a domain + pos = authString.IndexOf('\\'); + + if (pos > 0) + { + //domain = authString.Substring (0, pos); + user = authString.Substring(pos); + } + else + { + user = authString; + } + + HttpListenerBasicIdentity identity = new HttpListenerBasicIdentity(user, password); + // TODO: What are the roles MS sets + return new GenericPrincipal(identity, new string[0]); + } + catch (Exception) + { + // Invalid auth data is swallowed silently + return null; + } + } + + public HttpListenerWebSocketContext AcceptWebSocket(string protocol) + { + if (protocol != null) + { + if (protocol.Length == 0) + throw new ArgumentException("An empty string.", "protocol"); + + if (!protocol.IsToken()) + throw new ArgumentException("Contains an invalid character.", "protocol"); + } + + return new HttpListenerWebSocketContext(this, protocol, _cryptoProvider, _memoryStreamFactory); + } + } + + public class GenericPrincipal : IPrincipal + { + private IIdentity m_identity; + private string[] m_roles; + + public GenericPrincipal(IIdentity identity, string[] roles) + { + if (identity == null) + throw new ArgumentNullException("identity"); + + m_identity = identity; + if (roles != null) + { + m_roles = new string[roles.Length]; + for (int i = 0; i < roles.Length; ++i) + { + m_roles[i] = roles[i]; + } + } + else + { + m_roles = null; + } + } + + public virtual IIdentity Identity + { + get + { + return m_identity; + } + } + + public virtual bool IsInRole(string role) + { + if (role == null || m_roles == null) + return false; + + for (int i = 0; i < m_roles.Length; ++i) + { + if (m_roles[i] != null && String.Compare(m_roles[i], role, StringComparison.OrdinalIgnoreCase) == 0) + return true; + } + return false; + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerPrefixCollection.cs b/SocketHttpListener.Portable/Net/HttpListenerPrefixCollection.cs new file mode 100644 index 000000000..0b05539ee --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerPrefixCollection.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using MediaBrowser.Model.Logging; + +namespace SocketHttpListener.Net +{ + public class HttpListenerPrefixCollection : ICollection, IEnumerable, IEnumerable + { + List prefixes = new List(); + HttpListener listener; + + private ILogger _logger; + + internal HttpListenerPrefixCollection(ILogger logger, HttpListener listener) + { + _logger = logger; + this.listener = listener; + } + + public int Count + { + get { return prefixes.Count; } + } + + public bool IsReadOnly + { + get { return false; } + } + + public bool IsSynchronized + { + get { return false; } + } + + public void Add(string uriPrefix) + { + listener.CheckDisposed(); + ListenerPrefix.CheckUri(uriPrefix); + if (prefixes.Contains(uriPrefix)) + return; + + prefixes.Add(uriPrefix); + if (listener.IsListening) + EndPointManager.AddPrefix(_logger, uriPrefix, listener); + } + + public void Clear() + { + listener.CheckDisposed(); + prefixes.Clear(); + if (listener.IsListening) + EndPointManager.RemoveListener(_logger, listener); + } + + public bool Contains(string uriPrefix) + { + listener.CheckDisposed(); + return prefixes.Contains(uriPrefix); + } + + public void CopyTo(string[] array, int offset) + { + listener.CheckDisposed(); + prefixes.CopyTo(array, offset); + } + + public void CopyTo(Array array, int offset) + { + listener.CheckDisposed(); + ((ICollection)prefixes).CopyTo(array, offset); + } + + public IEnumerator GetEnumerator() + { + return prefixes.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return prefixes.GetEnumerator(); + } + + public bool Remove(string uriPrefix) + { + listener.CheckDisposed(); + if (uriPrefix == null) + throw new ArgumentNullException("uriPrefix"); + + bool result = prefixes.Remove(uriPrefix); + if (result && listener.IsListening) + EndPointManager.RemovePrefix(_logger, uriPrefix, listener); + + return result; + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerRequest.cs b/SocketHttpListener.Portable/Net/HttpListenerRequest.cs new file mode 100644 index 000000000..63d5e510d --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerRequest.cs @@ -0,0 +1,654 @@ +using System; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + public sealed class HttpListenerRequest + { + string[] accept_types; + Encoding content_encoding; + long content_length; + bool cl_set; + CookieCollection cookies; + WebHeaderCollection headers; + string method; + Stream input_stream; + Version version; + QueryParamCollection query_string; // check if null is ok, check if read-only, check case-sensitiveness + string raw_url; + Uri url; + Uri referrer; + string[] user_languages; + HttpListenerContext context; + bool is_chunked; + bool ka_set; + bool keep_alive; + + private readonly ITextEncoding _textEncoding; + + internal HttpListenerRequest(HttpListenerContext context, ITextEncoding textEncoding) + { + this.context = context; + _textEncoding = textEncoding; + headers = new WebHeaderCollection(); + version = HttpVersion.Version10; + } + + static char[] separators = new char[] { ' ' }; + + internal void SetRequestLine(string req) + { + string[] parts = req.Split(separators, 3); + if (parts.Length != 3) + { + context.ErrorMessage = "Invalid request line (parts)."; + return; + } + + method = parts[0]; + foreach (char c in method) + { + int ic = (int)c; + + if ((ic >= 'A' && ic <= 'Z') || + (ic > 32 && c < 127 && c != '(' && c != ')' && c != '<' && + c != '<' && c != '>' && c != '@' && c != ',' && c != ';' && + c != ':' && c != '\\' && c != '"' && c != '/' && c != '[' && + c != ']' && c != '?' && c != '=' && c != '{' && c != '}')) + continue; + + context.ErrorMessage = "(Invalid verb)"; + return; + } + + raw_url = parts[1]; + if (parts[2].Length != 8 || !parts[2].StartsWith("HTTP/")) + { + context.ErrorMessage = "Invalid request line (version)."; + return; + } + + try + { + version = new Version(parts[2].Substring(5)); + if (version.Major < 1) + throw new Exception(); + } + catch + { + context.ErrorMessage = "Invalid request line (version)."; + return; + } + } + + void CreateQueryString(string query) + { + if (query == null || query.Length == 0) + { + query_string = new QueryParamCollection(); + return; + } + + query_string = new QueryParamCollection(); + if (query[0] == '?') + query = query.Substring(1); + string[] components = query.Split('&'); + foreach (string kv in components) + { + int pos = kv.IndexOf('='); + if (pos == -1) + { + query_string.Add(null, WebUtility.UrlDecode(kv)); + } + else + { + string key = WebUtility.UrlDecode(kv.Substring(0, pos)); + string val = WebUtility.UrlDecode(kv.Substring(pos + 1)); + + query_string.Add(key, val); + } + } + } + + internal void FinishInitialization() + { + string host = UserHostName; + if (version > HttpVersion.Version10 && (host == null || host.Length == 0)) + { + context.ErrorMessage = "Invalid host name"; + return; + } + + string path; + Uri raw_uri = null; + if (MaybeUri(raw_url.ToLowerInvariant()) && Uri.TryCreate(raw_url, UriKind.Absolute, out raw_uri)) + path = raw_uri.PathAndQuery; + else + path = raw_url; + + if ((host == null || host.Length == 0)) + host = UserHostAddress; + + if (raw_uri != null) + host = raw_uri.Host; + + int colon = host.LastIndexOf(':'); + if (colon >= 0) + host = host.Substring(0, colon); + + string base_uri = String.Format("{0}://{1}:{2}", + (IsSecureConnection) ? (IsWebSocketRequest ? "wss" : "https") : (IsWebSocketRequest ? "ws" : "http"), + host, LocalEndPoint.Port); + + if (!Uri.TryCreate(base_uri + path, UriKind.Absolute, out url)) + { + context.ErrorMessage = WebUtility.HtmlEncode("Invalid url: " + base_uri + path); + return; return; + } + + CreateQueryString(url.Query); + + if (version >= HttpVersion.Version11) + { + string t_encoding = Headers["Transfer-Encoding"]; + is_chunked = (t_encoding != null && String.Compare(t_encoding, "chunked", StringComparison.OrdinalIgnoreCase) == 0); + // 'identity' is not valid! + if (t_encoding != null && !is_chunked) + { + context.Connection.SendError(null, 501); + return; + } + } + + if (!is_chunked && !cl_set) + { + if (String.Compare(method, "POST", StringComparison.OrdinalIgnoreCase) == 0 || + String.Compare(method, "PUT", StringComparison.OrdinalIgnoreCase) == 0) + { + context.Connection.SendError(null, 411); + return; + } + } + + if (String.Compare(Headers["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0) + { + ResponseStream output = context.Connection.GetResponseStream(); + + var _100continue = _textEncoding.GetASCIIEncoding().GetBytes("HTTP/1.1 100 Continue\r\n\r\n"); + + output.InternalWrite(_100continue, 0, _100continue.Length); + } + } + + static bool MaybeUri(string s) + { + int p = s.IndexOf(':'); + if (p == -1) + return false; + + if (p >= 10) + return false; + + return IsPredefinedScheme(s.Substring(0, p)); + } + + // + // Using a simple block of if's is twice as slow as the compiler generated + // switch statement. But using this tuned code is faster than the + // compiler generated code, with a million loops on x86-64: + // + // With "http": .10 vs .51 (first check) + // with "https": .16 vs .51 (second check) + // with "foo": .22 vs .31 (never found) + // with "mailto": .12 vs .51 (last check) + // + // + static bool IsPredefinedScheme(string scheme) + { + if (scheme == null || scheme.Length < 3) + return false; + + char c = scheme[0]; + if (c == 'h') + return (scheme == "http" || scheme == "https"); + if (c == 'f') + return (scheme == "file" || scheme == "ftp"); + + if (c == 'n') + { + c = scheme[1]; + if (c == 'e') + return (scheme == "news" || scheme == "net.pipe" || scheme == "net.tcp"); + if (scheme == "nntp") + return true; + return false; + } + if ((c == 'g' && scheme == "gopher") || (c == 'm' && scheme == "mailto")) + return true; + + return false; + } + + internal static string Unquote(String str) + { + int start = str.IndexOf('\"'); + int end = str.LastIndexOf('\"'); + if (start >= 0 && end >= 0) + str = str.Substring(start + 1, end - 1); + return str.Trim(); + } + + internal void AddHeader(string header) + { + int colon = header.IndexOf(':'); + if (colon == -1 || colon == 0) + { + context.ErrorMessage = "Bad Request"; + context.ErrorStatus = 400; + return; + } + + string name = header.Substring(0, colon).Trim(); + string val = header.Substring(colon + 1).Trim(); + string lower = name.ToLowerInvariant(); + headers.SetInternal(name, val); + switch (lower) + { + case "accept-language": + user_languages = val.Split(','); // yes, only split with a ',' + break; + case "accept": + accept_types = val.Split(','); // yes, only split with a ',' + break; + case "content-length": + try + { + //TODO: max. content_length? + content_length = Int64.Parse(val.Trim()); + if (content_length < 0) + context.ErrorMessage = "Invalid Content-Length."; + cl_set = true; + } + catch + { + context.ErrorMessage = "Invalid Content-Length."; + } + + break; + case "content-type": + { + var contents = val.Split(';'); + foreach (var content in contents) + { + var tmp = content.Trim(); + if (tmp.StartsWith("charset")) + { + var charset = tmp.GetValue("="); + if (charset != null && charset.Length > 0) + { + try + { + + // Support upnp/dlna devices - CONTENT-TYPE: text/xml ; charset="utf-8"\r\n + charset = charset.Trim('"'); + var index = charset.IndexOf('"'); + if (index != -1) charset = charset.Substring(0, index); + + content_encoding = Encoding.GetEncoding(charset); + } + catch + { + context.ErrorMessage = "Invalid Content-Type header: " + charset; + } + } + + break; + } + } + } + break; + case "referer": + try + { + referrer = new Uri(val); + } + catch + { + referrer = new Uri("http://someone.is.screwing.with.the.headers.com/"); + } + break; + case "cookie": + if (cookies == null) + cookies = new CookieCollection(); + + string[] cookieStrings = val.Split(new char[] { ',', ';' }); + Cookie current = null; + int version = 0; + foreach (string cookieString in cookieStrings) + { + string str = cookieString.Trim(); + if (str.Length == 0) + continue; + if (str.StartsWith("$Version")) + { + version = Int32.Parse(Unquote(str.Substring(str.IndexOf('=') + 1))); + } + else if (str.StartsWith("$Path")) + { + if (current != null) + current.Path = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else if (str.StartsWith("$Domain")) + { + if (current != null) + current.Domain = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else if (str.StartsWith("$Port")) + { + if (current != null) + current.Port = str.Substring(str.IndexOf('=') + 1).Trim(); + } + else + { + if (current != null) + { + cookies.Add(current); + } + current = new Cookie(); + int idx = str.IndexOf('='); + if (idx > 0) + { + current.Name = str.Substring(0, idx).Trim(); + current.Value = str.Substring(idx + 1).Trim(); + } + else + { + current.Name = str.Trim(); + current.Value = String.Empty; + } + current.Version = version; + } + } + if (current != null) + { + cookies.Add(current); + } + break; + } + } + + // returns true is the stream could be reused. + internal bool FlushInput() + { + if (!HasEntityBody) + return true; + + int length = 2048; + if (content_length > 0) + length = (int)Math.Min(content_length, (long)length); + + byte[] bytes = new byte[length]; + while (true) + { + // TODO: test if MS has a timeout when doing this + try + { + var task = InputStream.ReadAsync(bytes, 0, length); + var result = Task.WaitAll(new [] { task }, 1000); + if (!result) + { + return false; + } + if (task.Result <= 0) + { + return true; + } + } + catch (ObjectDisposedException e) + { + input_stream = null; + return true; + } + catch + { + return false; + } + } + } + + public string[] AcceptTypes + { + get { return accept_types; } + } + + public int ClientCertificateError + { + get + { + HttpConnection cnc = context.Connection; + //if (cnc.ClientCertificate == null) + // throw new InvalidOperationException("No client certificate"); + //int[] errors = cnc.ClientCertificateErrors; + //if (errors != null && errors.Length > 0) + // return errors[0]; + return 0; + } + } + + public Encoding ContentEncoding + { + get + { + if (content_encoding == null) + content_encoding = _textEncoding.GetDefaultEncoding(); + return content_encoding; + } + } + + public long ContentLength64 + { + get { return content_length; } + } + + public string ContentType + { + get { return headers["content-type"]; } + } + + public CookieCollection Cookies + { + get + { + // TODO: check if the collection is read-only + if (cookies == null) + cookies = new CookieCollection(); + return cookies; + } + } + + public bool HasEntityBody + { + get { return (content_length > 0 || is_chunked); } + } + + public QueryParamCollection Headers + { + get { return headers; } + } + + public string HttpMethod + { + get { return method; } + } + + public Stream InputStream + { + get + { + if (input_stream == null) + { + if (is_chunked || content_length > 0) + input_stream = context.Connection.GetRequestStream(is_chunked, content_length); + else + input_stream = Stream.Null; + } + + return input_stream; + } + } + + public bool IsAuthenticated + { + get { return false; } + } + + public bool IsLocal + { + get { return RemoteEndPoint.IpAddress.Equals(IpAddressInfo.Loopback) || RemoteEndPoint.IpAddress.Equals(IpAddressInfo.IPv6Loopback) || LocalEndPoint.IpAddress.Equals(RemoteEndPoint.IpAddress); } + } + + public bool IsSecureConnection + { + get { return context.Connection.IsSecure; } + } + + public bool KeepAlive + { + get + { + if (ka_set) + return keep_alive; + + ka_set = true; + // 1. Connection header + // 2. Protocol (1.1 == keep-alive by default) + // 3. Keep-Alive header + string cnc = headers["Connection"]; + if (!String.IsNullOrEmpty(cnc)) + { + keep_alive = (0 == String.Compare(cnc, "keep-alive", StringComparison.OrdinalIgnoreCase)); + } + else if (version == HttpVersion.Version11) + { + keep_alive = true; + } + else + { + cnc = headers["keep-alive"]; + if (!String.IsNullOrEmpty(cnc)) + keep_alive = (0 != String.Compare(cnc, "closed", StringComparison.OrdinalIgnoreCase)); + } + return keep_alive; + } + } + + public IpEndPointInfo LocalEndPoint + { + get { return context.Connection.LocalEndPoint; } + } + + public Version ProtocolVersion + { + get { return version; } + } + + public QueryParamCollection QueryString + { + get { return query_string; } + } + + public string RawUrl + { + get { return raw_url; } + } + + public IpEndPointInfo RemoteEndPoint + { + get { return context.Connection.RemoteEndPoint; } + } + + public Guid RequestTraceIdentifier + { + get { return Guid.Empty; } + } + + public Uri Url + { + get { return url; } + } + + public Uri UrlReferrer + { + get { return referrer; } + } + + public string UserAgent + { + get { return headers["user-agent"]; } + } + + public string UserHostAddress + { + get { return LocalEndPoint.ToString(); } + } + + public string UserHostName + { + get { return headers["host"]; } + } + + public string[] UserLanguages + { + get { return user_languages; } + } + + public string ServiceName + { + get + { + return null; + } + } + + private bool _websocketRequestWasSet; + private bool _websocketRequest; + + /// + /// Gets a value indicating whether the request is a WebSocket connection request. + /// + /// + /// true if the request is a WebSocket connection request; otherwise, false. + /// + public bool IsWebSocketRequest + { + get + { + if (!_websocketRequestWasSet) + { + _websocketRequest = method == "GET" && + version > HttpVersion.Version10 && + headers.Contains("Upgrade", "websocket") && + headers.Contains("Connection", "Upgrade"); + + _websocketRequestWasSet = true; + } + + return _websocketRequest; + } + } + + public Task GetClientCertificateAsync() + { + return Task.FromResult(null); + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpListenerResponse.cs b/SocketHttpListener.Portable/Net/HttpListenerResponse.cs new file mode 100644 index 000000000..0bc827b5a --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpListenerResponse.cs @@ -0,0 +1,517 @@ +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + public sealed class HttpListenerResponse : IDisposable + { + bool disposed; + Encoding content_encoding; + long content_length; + bool cl_set; + string content_type; + CookieCollection cookies; + WebHeaderCollection headers = new WebHeaderCollection(); + bool keep_alive = true; + ResponseStream output_stream; + Version version = HttpVersion.Version11; + string location; + int status_code = 200; + string status_description = "OK"; + bool chunked; + HttpListenerContext context; + + internal bool HeadersSent; + internal object headers_lock = new object(); + + bool force_close_chunked; + + private readonly ILogger _logger; + private readonly ITextEncoding _textEncoding; + + internal HttpListenerResponse(HttpListenerContext context, ILogger logger, ITextEncoding textEncoding) + { + this.context = context; + _logger = logger; + _textEncoding = textEncoding; + } + + internal bool CloseConnection + { + get + { + return headers["Connection"] == "close"; + } + } + + internal bool ForceCloseChunked + { + get { return force_close_chunked; } + } + + public Encoding ContentEncoding + { + get + { + if (content_encoding == null) + content_encoding = _textEncoding.GetDefaultEncoding(); + return content_encoding; + } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + content_encoding = value; + } + } + + public long ContentLength64 + { + get { return content_length; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + if (HeadersSent) + throw new InvalidOperationException("Cannot be changed after headers are sent."); + + if (value < 0) + throw new ArgumentOutOfRangeException("Must be >= 0", "value"); + + cl_set = true; + content_length = value; + } + } + + public string ContentType + { + get { return content_type; } + set + { + // TODO: is null ok? + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + content_type = value; + } + } + + // RFC 2109, 2965 + the netscape specification at http://wp.netscape.com/newsref/std/cookie_spec.html + public CookieCollection Cookies + { + get + { + if (cookies == null) + cookies = new CookieCollection(); + return cookies; + } + set { cookies = value; } // null allowed? + } + + public WebHeaderCollection Headers + { + get { return headers; } + set + { + /** + * "If you attempt to set a Content-Length, Keep-Alive, Transfer-Encoding, or + * WWW-Authenticate header using the Headers property, an exception will be + * thrown. Use the KeepAlive or ContentLength64 properties to set these headers. + * You cannot set the Transfer-Encoding or WWW-Authenticate headers manually." + */ + // TODO: check if this is marked readonly after headers are sent. + headers = value; + } + } + + public bool KeepAlive + { + get { return keep_alive; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + keep_alive = value; + } + } + + public Stream OutputStream + { + get + { + if (output_stream == null) + output_stream = context.Connection.GetResponseStream(); + return output_stream; + } + } + + public Version ProtocolVersion + { + get { return version; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + if (value == null) + throw new ArgumentNullException("value"); + + if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1)) + throw new ArgumentException("Must be 1.0 or 1.1", "value"); + + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + version = value; + } + } + + public string RedirectLocation + { + get { return location; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + location = value; + } + } + + public bool SendChunked + { + get { return chunked; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + chunked = value; + } + } + + public int StatusCode + { + get { return status_code; } + set + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + if (value < 100 || value > 999) + throw new ProtocolViolationException("StatusCode must be between 100 and 999."); + status_code = value; + status_description = GetStatusDescription(value); + } + } + + internal static string GetStatusDescription(int code) + { + switch (code) + { + case 100: return "Continue"; + case 101: return "Switching Protocols"; + case 102: return "Processing"; + case 200: return "OK"; + case 201: return "Created"; + case 202: return "Accepted"; + case 203: return "Non-Authoritative Information"; + case 204: return "No Content"; + case 205: return "Reset Content"; + case 206: return "Partial Content"; + case 207: return "Multi-Status"; + case 300: return "Multiple Choices"; + case 301: return "Moved Permanently"; + case 302: return "Found"; + case 303: return "See Other"; + case 304: return "Not Modified"; + case 305: return "Use Proxy"; + case 307: return "Temporary Redirect"; + case 400: return "Bad Request"; + case 401: return "Unauthorized"; + case 402: return "Payment Required"; + case 403: return "Forbidden"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 406: return "Not Acceptable"; + case 407: return "Proxy Authentication Required"; + case 408: return "Request Timeout"; + case 409: return "Conflict"; + case 410: return "Gone"; + case 411: return "Length Required"; + case 412: return "Precondition Failed"; + case 413: return "Request Entity Too Large"; + case 414: return "Request-Uri Too Long"; + case 415: return "Unsupported Media Type"; + case 416: return "Requested Range Not Satisfiable"; + case 417: return "Expectation Failed"; + case 422: return "Unprocessable Entity"; + case 423: return "Locked"; + case 424: return "Failed Dependency"; + case 500: return "Internal Server Error"; + case 501: return "Not Implemented"; + case 502: return "Bad Gateway"; + case 503: return "Service Unavailable"; + case 504: return "Gateway Timeout"; + case 505: return "Http Version Not Supported"; + case 507: return "Insufficient Storage"; + } + return ""; + } + + public string StatusDescription + { + get { return status_description; } + set + { + status_description = value; + } + } + + void IDisposable.Dispose() + { + Close(true); //TODO: Abort or Close? + } + + public void Abort() + { + if (disposed) + return; + + Close(true); + } + + public void AddHeader(string name, string value) + { + if (name == null) + throw new ArgumentNullException("name"); + + if (name == "") + throw new ArgumentException("'name' cannot be empty", "name"); + + //TODO: check for forbidden headers and invalid characters + if (value.Length > 65535) + throw new ArgumentOutOfRangeException("value"); + + headers.Set(name, value); + } + + public void AppendCookie(Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException("cookie"); + + Cookies.Add(cookie); + } + + public void AppendHeader(string name, string value) + { + if (name == null) + throw new ArgumentNullException("name"); + + if (name == "") + throw new ArgumentException("'name' cannot be empty", "name"); + + if (value.Length > 65535) + throw new ArgumentOutOfRangeException("value"); + + headers.Add(name, value); + } + + void Close(bool force) + { + if (force) + { + _logger.Debug("HttpListenerResponse force closing HttpConnection"); + } + disposed = true; + context.Connection.Close(force); + } + + public void Close() + { + if (disposed) + return; + + Close(false); + } + + public void Close(byte[] responseEntity, bool willBlock) + { + if (disposed) + return; + + if (responseEntity == null) + throw new ArgumentNullException("responseEntity"); + + //TODO: if willBlock -> BeginWrite + Close ? + ContentLength64 = responseEntity.Length; + OutputStream.Write(responseEntity, 0, (int)content_length); + Close(false); + } + + public void Redirect(string url) + { + StatusCode = 302; // Found + location = url; + } + + bool FindCookie(Cookie cookie) + { + string name = cookie.Name; + string domain = cookie.Domain; + string path = cookie.Path; + foreach (Cookie c in cookies) + { + if (name != c.Name) + continue; + if (domain != c.Domain) + continue; + if (path == c.Path) + return true; + } + + return false; + } + + internal void SendHeaders(bool closing, MemoryStream ms) + { + Encoding encoding = content_encoding; + if (encoding == null) + encoding = _textEncoding.GetDefaultEncoding(); + + if (content_type != null) + { + if (content_encoding != null && content_type.IndexOf("charset=", StringComparison.Ordinal) == -1) + { + string enc_name = content_encoding.WebName; + headers.SetInternal("Content-Type", content_type + "; charset=" + enc_name); + } + else + { + headers.SetInternal("Content-Type", content_type); + } + } + + if (headers["Server"] == null) + headers.SetInternal("Server", "Mono-HTTPAPI/1.0"); + + CultureInfo inv = CultureInfo.InvariantCulture; + if (headers["Date"] == null) + headers.SetInternal("Date", DateTime.UtcNow.ToString("r", inv)); + + if (!chunked) + { + if (!cl_set && closing) + { + cl_set = true; + content_length = 0; + } + + if (cl_set) + headers.SetInternal("Content-Length", content_length.ToString(inv)); + } + + Version v = context.Request.ProtocolVersion; + if (!cl_set && !chunked && v >= HttpVersion.Version11) + chunked = true; + + /* Apache forces closing the connection for these status codes: + * HttpStatusCode.BadRequest 400 + * HttpStatusCode.RequestTimeout 408 + * HttpStatusCode.LengthRequired 411 + * HttpStatusCode.RequestEntityTooLarge 413 + * HttpStatusCode.RequestUriTooLong 414 + * HttpStatusCode.InternalServerError 500 + * HttpStatusCode.ServiceUnavailable 503 + */ + bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 || + status_code == 413 || status_code == 414 || status_code == 500 || + status_code == 503); + + if (conn_close == false) + conn_close = !context.Request.KeepAlive; + + // They sent both KeepAlive: true and Connection: close!? + if (!keep_alive || conn_close) + { + headers.SetInternal("Connection", "close"); + conn_close = true; + } + + if (chunked) + headers.SetInternal("Transfer-Encoding", "chunked"); + + //int reuses = context.Connection.Reuses; + //if (reuses >= 100) + //{ + // _logger.Debug("HttpListenerResponse - keep alive has exceeded 100 uses and will be closed."); + + // force_close_chunked = true; + // if (!conn_close) + // { + // headers.SetInternal("Connection", "close"); + // conn_close = true; + // } + //} + + if (!conn_close) + { + if (context.Request.ProtocolVersion <= HttpVersion.Version10) + headers.SetInternal("Connection", "keep-alive"); + } + + if (location != null) + headers.SetInternal("Location", location); + + if (cookies != null) + { + foreach (Cookie cookie in cookies) + headers.SetInternal("Set-Cookie", cookie.ToString()); + } + + using (StreamWriter writer = new StreamWriter(ms, encoding, 256, true)) + { + writer.Write("HTTP/{0} {1} {2}\r\n", version, status_code, status_description); + string headers_str = headers.ToStringMultiValue(); + writer.Write(headers_str); + writer.Flush(); + } + + int preamble = encoding.GetPreamble().Length; + if (output_stream == null) + output_stream = context.Connection.GetResponseStream(); + + /* Assumes that the ms was at position 0 */ + ms.Position = preamble; + HeadersSent = true; + } + + public void SetCookie(Cookie cookie) + { + if (cookie == null) + throw new ArgumentNullException("cookie"); + + if (cookies != null) + { + if (FindCookie(cookie)) + throw new ArgumentException("The cookie already exists."); + } + else + { + cookies = new CookieCollection(); + } + + cookies.Add(cookie); + } + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/Net/HttpStatusCode.cs b/SocketHttpListener.Portable/Net/HttpStatusCode.cs new file mode 100644 index 000000000..93da82ba0 --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpStatusCode.cs @@ -0,0 +1,321 @@ +namespace SocketHttpListener.Net +{ + /// + /// Contains the values of the HTTP status codes. + /// + /// + /// The HttpStatusCode enumeration contains the values of the HTTP status codes defined in + /// RFC 2616 for HTTP 1.1. + /// + public enum HttpStatusCode + { + /// + /// Equivalent to status code 100. + /// Indicates that the client should continue with its request. + /// + Continue = 100, + /// + /// Equivalent to status code 101. + /// Indicates that the server is switching the HTTP version or protocol on the connection. + /// + SwitchingProtocols = 101, + /// + /// Equivalent to status code 200. + /// Indicates that the client's request has succeeded. + /// + OK = 200, + /// + /// Equivalent to status code 201. + /// Indicates that the client's request has been fulfilled and resulted in a new resource being + /// created. + /// + Created = 201, + /// + /// Equivalent to status code 202. + /// Indicates that the client's request has been accepted for processing, but the processing + /// hasn't been completed. + /// + Accepted = 202, + /// + /// Equivalent to status code 203. + /// Indicates that the returned metainformation is from a local or a third-party copy instead of + /// the origin server. + /// + NonAuthoritativeInformation = 203, + /// + /// Equivalent to status code 204. + /// Indicates that the server has fulfilled the client's request but doesn't need to return + /// an entity-body. + /// + NoContent = 204, + /// + /// Equivalent to status code 205. + /// Indicates that the server has fulfilled the client's request, and the user agent should + /// reset the document view which caused the request to be sent. + /// + ResetContent = 205, + /// + /// Equivalent to status code 206. + /// Indicates that the server has fulfilled the partial GET request for the resource. + /// + PartialContent = 206, + /// + /// + /// Equivalent to status code 300. + /// Indicates that the requested resource corresponds to any of multiple representations. + /// + /// + /// MultipleChoices is a synonym for Ambiguous. + /// + /// + MultipleChoices = 300, + /// + /// + /// Equivalent to status code 300. + /// Indicates that the requested resource corresponds to any of multiple representations. + /// + /// + /// Ambiguous is a synonym for MultipleChoices. + /// + /// + Ambiguous = 300, + /// + /// + /// Equivalent to status code 301. + /// Indicates that the requested resource has been assigned a new permanent URI and + /// any future references to this resource should use one of the returned URIs. + /// + /// + /// MovedPermanently is a synonym for Moved. + /// + /// + MovedPermanently = 301, + /// + /// + /// Equivalent to status code 301. + /// Indicates that the requested resource has been assigned a new permanent URI and + /// any future references to this resource should use one of the returned URIs. + /// + /// + /// Moved is a synonym for MovedPermanently. + /// + /// + Moved = 301, + /// + /// + /// Equivalent to status code 302. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// Found is a synonym for Redirect. + /// + /// + Found = 302, + /// + /// + /// Equivalent to status code 302. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// Redirect is a synonym for Found. + /// + /// + Redirect = 302, + /// + /// + /// Equivalent to status code 303. + /// Indicates that the response to the request can be found under a different URI and + /// should be retrieved using a GET method on that resource. + /// + /// + /// SeeOther is a synonym for RedirectMethod. + /// + /// + SeeOther = 303, + /// + /// + /// Equivalent to status code 303. + /// Indicates that the response to the request can be found under a different URI and + /// should be retrieved using a GET method on that resource. + /// + /// + /// RedirectMethod is a synonym for SeeOther. + /// + /// + RedirectMethod = 303, + /// + /// Equivalent to status code 304. + /// Indicates that the client has performed a conditional GET request and access is allowed, + /// but the document hasn't been modified. + /// + NotModified = 304, + /// + /// Equivalent to status code 305. + /// Indicates that the requested resource must be accessed through the proxy given by + /// the Location field. + /// + UseProxy = 305, + /// + /// Equivalent to status code 306. + /// This status code was used in a previous version of the specification, is no longer used, + /// and is reserved for future use. + /// + Unused = 306, + /// + /// + /// Equivalent to status code 307. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// TemporaryRedirect is a synonym for RedirectKeepVerb. + /// + /// + TemporaryRedirect = 307, + /// + /// + /// Equivalent to status code 307. + /// Indicates that the requested resource is located temporarily under a different URI. + /// + /// + /// RedirectKeepVerb is a synonym for TemporaryRedirect. + /// + /// + RedirectKeepVerb = 307, + /// + /// Equivalent to status code 400. + /// Indicates that the client's request couldn't be understood by the server due to + /// malformed syntax. + /// + BadRequest = 400, + /// + /// Equivalent to status code 401. + /// Indicates that the client's request requires user authentication. + /// + Unauthorized = 401, + /// + /// Equivalent to status code 402. + /// This status code is reserved for future use. + /// + PaymentRequired = 402, + /// + /// Equivalent to status code 403. + /// Indicates that the server understood the client's request but is refusing to fulfill it. + /// + Forbidden = 403, + /// + /// Equivalent to status code 404. + /// Indicates that the server hasn't found anything matching the request URI. + /// + NotFound = 404, + /// + /// Equivalent to status code 405. + /// Indicates that the method specified in the request line isn't allowed for the resource + /// identified by the request URI. + /// + MethodNotAllowed = 405, + /// + /// Equivalent to status code 406. + /// Indicates that the server doesn't have the appropriate resource to respond to the Accept + /// headers in the client's request. + /// + NotAcceptable = 406, + /// + /// Equivalent to status code 407. + /// Indicates that the client must first authenticate itself with the proxy. + /// + ProxyAuthenticationRequired = 407, + /// + /// Equivalent to status code 408. + /// Indicates that the client didn't produce a request within the time that the server was + /// prepared to wait. + /// + RequestTimeout = 408, + /// + /// Equivalent to status code 409. + /// Indicates that the client's request couldn't be completed due to a conflict on the server. + /// + Conflict = 409, + /// + /// Equivalent to status code 410. + /// Indicates that the requested resource is no longer available at the server and + /// no forwarding address is known. + /// + Gone = 410, + /// + /// Equivalent to status code 411. + /// Indicates that the server refuses to accept the client's request without a defined + /// Content-Length. + /// + LengthRequired = 411, + /// + /// Equivalent to status code 412. + /// Indicates that the precondition given in one or more of the request headers evaluated to + /// false when it was tested on the server. + /// + PreconditionFailed = 412, + /// + /// Equivalent to status code 413. + /// Indicates that the entity of the client's request is larger than the server is willing or + /// able to process. + /// + RequestEntityTooLarge = 413, + /// + /// Equivalent to status code 414. + /// Indicates that the request URI is longer than the server is willing to interpret. + /// + RequestUriTooLong = 414, + /// + /// Equivalent to status code 415. + /// Indicates that the entity of the client's request is in a format not supported by + /// the requested resource for the requested method. + /// + UnsupportedMediaType = 415, + /// + /// Equivalent to status code 416. + /// Indicates that none of the range specifier values in a Range request header overlap + /// the current extent of the selected resource. + /// + RequestedRangeNotSatisfiable = 416, + /// + /// Equivalent to status code 417. + /// Indicates that the expectation given in an Expect request header couldn't be met by + /// the server. + /// + ExpectationFailed = 417, + /// + /// Equivalent to status code 500. + /// Indicates that the server encountered an unexpected condition which prevented it from + /// fulfilling the client's request. + /// + InternalServerError = 500, + /// + /// Equivalent to status code 501. + /// Indicates that the server doesn't support the functionality required to fulfill the client's + /// request. + /// + NotImplemented = 501, + /// + /// Equivalent to status code 502. + /// Indicates that a gateway or proxy server received an invalid response from the upstream + /// server. + /// + BadGateway = 502, + /// + /// Equivalent to status code 503. + /// Indicates that the server is currently unable to handle the client's request due to + /// a temporary overloading or maintenance of the server. + /// + ServiceUnavailable = 503, + /// + /// Equivalent to status code 504. + /// Indicates that a gateway or proxy server didn't receive a timely response from the upstream + /// server or some other auxiliary server. + /// + GatewayTimeout = 504, + /// + /// Equivalent to status code 505. + /// Indicates that the server doesn't support the HTTP version used in the client's request. + /// + HttpVersionNotSupported = 505, + } +} diff --git a/SocketHttpListener.Portable/Net/HttpStreamAsyncResult.cs b/SocketHttpListener.Portable/Net/HttpStreamAsyncResult.cs new file mode 100644 index 000000000..518c45acb --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpStreamAsyncResult.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading; + +namespace SocketHttpListener.Net +{ + class HttpStreamAsyncResult : IAsyncResult + { + object locker = new object(); + ManualResetEvent handle; + bool completed; + + internal byte[] Buffer; + internal int Offset; + internal int Count; + internal AsyncCallback Callback; + internal object State; + internal int SynchRead; + internal Exception Error; + + public void Complete(Exception e) + { + Error = e; + Complete(); + } + + public void Complete() + { + lock (locker) + { + if (completed) + return; + + completed = true; + if (handle != null) + handle.Set(); + + if (Callback != null) + Callback.BeginInvoke(this, null, null); + } + } + + public object AsyncState + { + get { return State; } + } + + public WaitHandle AsyncWaitHandle + { + get + { + lock (locker) + { + if (handle == null) + handle = new ManualResetEvent(completed); + } + + return handle; + } + } + + public bool CompletedSynchronously + { + get { return (SynchRead == Count); } + } + + public bool IsCompleted + { + get + { + lock (locker) + { + return completed; + } + } + } + } +} diff --git a/SocketHttpListener.Portable/Net/HttpVersion.cs b/SocketHttpListener.Portable/Net/HttpVersion.cs new file mode 100644 index 000000000..c0839b46d --- /dev/null +++ b/SocketHttpListener.Portable/Net/HttpVersion.cs @@ -0,0 +1,16 @@ +using System; + +namespace SocketHttpListener.Net +{ + // + // + public class HttpVersion + { + + public static readonly Version Version10 = new Version(1, 0); + public static readonly Version Version11 = new Version(1, 1); + + // pretty useless.. + public HttpVersion() { } + } +} diff --git a/SocketHttpListener.Portable/Net/ListenerPrefix.cs b/SocketHttpListener.Portable/Net/ListenerPrefix.cs new file mode 100644 index 000000000..2c314da50 --- /dev/null +++ b/SocketHttpListener.Portable/Net/ListenerPrefix.cs @@ -0,0 +1,148 @@ +using System; +using System.Net; +using MediaBrowser.Model.Net; + +namespace SocketHttpListener.Net +{ + sealed class ListenerPrefix + { + string original; + string host; + ushort port; + string path; + bool secure; + IpAddressInfo[] addresses; + public HttpListener Listener; + + public ListenerPrefix(string prefix) + { + this.original = prefix; + Parse(prefix); + } + + public override string ToString() + { + return original; + } + + public IpAddressInfo[] Addresses + { + get { return addresses; } + set { addresses = value; } + } + public bool Secure + { + get { return secure; } + } + + public string Host + { + get { return host; } + } + + public int Port + { + get { return (int)port; } + } + + public string Path + { + get { return path; } + } + + // Equals and GetHashCode are required to detect duplicates in HttpListenerPrefixCollection. + public override bool Equals(object o) + { + ListenerPrefix other = o as ListenerPrefix; + if (other == null) + return false; + + return (original == other.original); + } + + public override int GetHashCode() + { + return original.GetHashCode(); + } + + void Parse(string uri) + { + ushort default_port = 80; + if (uri.StartsWith("https://")) + { + default_port = 443; + secure = true; + } + + int length = uri.Length; + int start_host = uri.IndexOf(':') + 3; + if (start_host >= length) + throw new ArgumentException("No host specified."); + + int colon = uri.IndexOf(':', start_host, length - start_host); + int root; + if (colon > 0) + { + host = uri.Substring(start_host, colon - start_host); + root = uri.IndexOf('/', colon, length - colon); + port = (ushort)Int32.Parse(uri.Substring(colon + 1, root - colon - 1)); + path = uri.Substring(root); + } + else + { + root = uri.IndexOf('/', start_host, length - start_host); + host = uri.Substring(start_host, root - start_host); + port = default_port; + path = uri.Substring(root); + } + if (path.Length != 1) + path = path.Substring(0, path.Length - 1); + } + + public static void CheckUri(string uri) + { + if (uri == null) + throw new ArgumentNullException("uriPrefix"); + + if (!uri.StartsWith("http://") && !uri.StartsWith("https://")) + throw new ArgumentException("Only 'http' and 'https' schemes are supported."); + + int length = uri.Length; + int start_host = uri.IndexOf(':') + 3; + if (start_host >= length) + throw new ArgumentException("No host specified."); + + int colon = uri.IndexOf(':', start_host, length - start_host); + if (start_host == colon) + throw new ArgumentException("No host specified."); + + int root; + if (colon > 0) + { + root = uri.IndexOf('/', colon, length - colon); + if (root == -1) + throw new ArgumentException("No path specified."); + + try + { + int p = Int32.Parse(uri.Substring(colon + 1, root - colon - 1)); + if (p <= 0 || p >= 65536) + throw new Exception(); + } + catch + { + throw new ArgumentException("Invalid port."); + } + } + else + { + root = uri.IndexOf('/', start_host, length - start_host); + if (root == -1) + throw new ArgumentException("No path specified."); + } + + if (uri[uri.Length - 1] != '/') + throw new ArgumentException("The prefix must end with '/'"); + } + } +} diff --git a/SocketHttpListener.Portable/Net/RequestStream.cs b/SocketHttpListener.Portable/Net/RequestStream.cs new file mode 100644 index 000000000..58030500d --- /dev/null +++ b/SocketHttpListener.Portable/Net/RequestStream.cs @@ -0,0 +1,231 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace SocketHttpListener.Net +{ + class RequestStream : Stream + { + byte[] buffer; + int offset; + int length; + long remaining_body; + bool disposed; + Stream stream; + + internal RequestStream(Stream stream, byte[] buffer, int offset, int length) + : this(stream, buffer, offset, length, -1) + { + } + + internal RequestStream(Stream stream, byte[] buffer, int offset, int length, long contentlength) + { + this.stream = stream; + this.buffer = buffer; + this.offset = offset; + this.length = length; + this.remaining_body = contentlength; + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + + protected override void Dispose(bool disposing) + { + disposed = true; + } + + public override void Flush() + { + } + + + // Returns 0 if we can keep reading from the base stream, + // > 0 if we read something from the buffer. + // -1 if we had a content length set and we finished reading that many bytes. + int FillFromBuffer(byte[] buffer, int off, int count) + { + if (buffer == null) + throw new ArgumentNullException("buffer"); + if (off < 0) + throw new ArgumentOutOfRangeException("offset", "< 0"); + if (count < 0) + throw new ArgumentOutOfRangeException("count", "< 0"); + int len = buffer.Length; + if (off > len) + throw new ArgumentException("destination offset is beyond array size"); + if (off > len - count) + throw new ArgumentException("Reading would overrun buffer"); + + if (this.remaining_body == 0) + return -1; + + if (this.length == 0) + return 0; + + int size = Math.Min(this.length, count); + if (this.remaining_body > 0) + size = (int)Math.Min(size, this.remaining_body); + + if (this.offset > this.buffer.Length - size) + { + size = Math.Min(size, this.buffer.Length - this.offset); + } + if (size == 0) + return 0; + + Buffer.BlockCopy(this.buffer, this.offset, buffer, off, size); + this.offset += size; + this.length -= size; + if (this.remaining_body > 0) + remaining_body -= size; + return size; + } + + public override int Read([In, Out] byte[] buffer, int offset, int count) + { + if (disposed) + throw new ObjectDisposedException(typeof(RequestStream).ToString()); + + // Call FillFromBuffer to check for buffer boundaries even when remaining_body is 0 + int nread = FillFromBuffer(buffer, offset, count); + if (nread == -1) + { // No more bytes available (Content-Length) + return 0; + } + else if (nread > 0) + { + return nread; + } + + nread = stream.Read(buffer, offset, count); + if (nread > 0 && remaining_body > 0) + remaining_body -= nread; + return nread; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (disposed) + throw new ObjectDisposedException(typeof(RequestStream).ToString()); + + int nread = FillFromBuffer(buffer, offset, count); + if (nread > 0 || nread == -1) + { + return Math.Max(0, nread); + } + + // Avoid reading past the end of the request to allow + // for HTTP pipelining + if (remaining_body >= 0 && count > remaining_body) + count = (int)Math.Min(Int32.MaxValue, remaining_body); + + nread = await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + if (remaining_body > 0 && nread > 0) + remaining_body -= nread; + return nread; + } + + //public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // if (disposed) + // throw new ObjectDisposedException(typeof(RequestStream).ToString()); + + // int nread = FillFromBuffer(buffer, offset, count); + // if (nread > 0 || nread == -1) + // { + // HttpStreamAsyncResult ares = new HttpStreamAsyncResult(); + // ares.Buffer = buffer; + // ares.Offset = offset; + // ares.Count = count; + // ares.Callback = cback; + // ares.State = state; + // ares.SynchRead = Math.Max(0, nread); + // ares.Complete(); + // return ares; + // } + + // // Avoid reading past the end of the request to allow + // // for HTTP pipelining + // if (remaining_body >= 0 && count > remaining_body) + // count = (int)Math.Min(Int32.MaxValue, remaining_body); + // return stream.BeginRead(buffer, offset, count, cback, state); + //} + + //public override int EndRead(IAsyncResult ares) + //{ + // if (disposed) + // throw new ObjectDisposedException(typeof(RequestStream).ToString()); + + // if (ares == null) + // throw new ArgumentNullException("async_result"); + + // if (ares is HttpStreamAsyncResult) + // { + // HttpStreamAsyncResult r = (HttpStreamAsyncResult)ares; + // if (!ares.IsCompleted) + // ares.AsyncWaitHandle.WaitOne(); + // return r.SynchRead; + // } + + // // Close on exception? + // int nread = stream.EndRead(ares); + // if (remaining_body > 0 && nread > 0) + // remaining_body -= nread; + // return nread; + //} + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + //public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // throw new NotSupportedException(); + //} + + //public override void EndWrite(IAsyncResult async_result) + //{ + // throw new NotSupportedException(); + //} + } +} diff --git a/SocketHttpListener.Portable/Net/ResponseStream.cs b/SocketHttpListener.Portable/Net/ResponseStream.cs new file mode 100644 index 000000000..6ecbf9742 --- /dev/null +++ b/SocketHttpListener.Portable/Net/ResponseStream.cs @@ -0,0 +1,316 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net +{ + // FIXME: Does this buffer the response until Close? + // Update: we send a single packet for the first non-chunked Write + // What happens when we set content-length to X and write X-1 bytes then close? + // what if we don't set content-length at all? + class ResponseStream : Stream + { + HttpListenerResponse response; + bool ignore_errors; + bool disposed; + bool trailer_sent; + Stream stream; + private readonly IMemoryStreamFactory _memoryStreamFactory; + private readonly ITextEncoding _textEncoding; + + internal ResponseStream(Stream stream, HttpListenerResponse response, bool ignore_errors, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + { + this.response = response; + this.ignore_errors = ignore_errors; + _memoryStreamFactory = memoryStreamFactory; + _textEncoding = textEncoding; + this.stream = stream; + } + + public override bool CanRead + { + get { return false; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return true; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + + protected override void Dispose(bool disposing) + { + if (disposed == false) + { + disposed = true; + byte[] bytes = null; + MemoryStream ms = GetHeaders(true); + bool chunked = response.SendChunked; + if (stream.CanWrite) + { + try + { + if (ms != null) + { + long start = ms.Position; + if (chunked && !trailer_sent) + { + bytes = GetChunkSizeBytes(0, true); + ms.Position = ms.Length; + ms.Write(bytes, 0, bytes.Length); + } + byte[] msBuffer; + _memoryStreamFactory.TryGetBuffer(ms, out msBuffer); + InternalWrite(msBuffer, (int)start, (int)(ms.Length - start)); + trailer_sent = true; + } + else if (chunked && !trailer_sent) + { + bytes = GetChunkSizeBytes(0, true); + InternalWrite(bytes, 0, bytes.Length); + trailer_sent = true; + } + } + catch (IOException ex) + { + // Ignore error due to connection reset by peer + } + } + response.Close(); + } + + base.Dispose(disposing); + } + + MemoryStream GetHeaders(bool closing) + { + // SendHeaders works on shared headers + lock (response.headers_lock) + { + if (response.HeadersSent) + return null; + MemoryStream ms = _memoryStreamFactory.CreateNew(); + response.SendHeaders(closing, ms); + return ms; + } + } + + public override void Flush() + { + } + + static byte[] crlf = new byte[] { 13, 10 }; + byte[] GetChunkSizeBytes(int size, bool final) + { + string str = String.Format("{0:x}\r\n{1}", size, final ? "\r\n" : ""); + return _textEncoding.GetASCIIEncoding().GetBytes(str); + } + + internal void InternalWrite(byte[] buffer, int offset, int count) + { + if (ignore_errors) + { + try + { + stream.Write(buffer, offset, count); + } + catch { } + } + else + { + stream.Write(buffer, offset, count); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + byte[] bytes = null; + MemoryStream ms = GetHeaders(false); + bool chunked = response.SendChunked; + if (ms != null) + { + long start = ms.Position; // After the possible preamble for the encoding + ms.Position = ms.Length; + if (chunked) + { + bytes = GetChunkSizeBytes(count, false); + ms.Write(bytes, 0, bytes.Length); + } + + int new_count = Math.Min(count, 16384 - (int)ms.Position + (int)start); + ms.Write(buffer, offset, new_count); + count -= new_count; + offset += new_count; + byte[] msBuffer; + _memoryStreamFactory.TryGetBuffer(ms, out msBuffer); + InternalWrite(msBuffer, (int)start, (int)(ms.Length - start)); + ms.SetLength(0); + ms.Capacity = 0; // 'dispose' the buffer in ms. + } + else if (chunked) + { + bytes = GetChunkSizeBytes(count, false); + InternalWrite(bytes, 0, bytes.Length); + } + + if (count > 0) + InternalWrite(buffer, offset, count); + if (chunked) + InternalWrite(crlf, 0, 2); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (disposed) + throw new ObjectDisposedException(GetType().ToString()); + + byte[] bytes = null; + MemoryStream ms = GetHeaders(false); + bool chunked = response.SendChunked; + if (ms != null) + { + long start = ms.Position; + ms.Position = ms.Length; + if (chunked) + { + bytes = GetChunkSizeBytes(count, false); + ms.Write(bytes, 0, bytes.Length); + } + ms.Write(buffer, offset, count); + byte[] msBuffer; + _memoryStreamFactory.TryGetBuffer(ms, out msBuffer); + buffer = msBuffer; + offset = (int)start; + count = (int)(ms.Position - start); + } + else if (chunked) + { + bytes = GetChunkSizeBytes(count, false); + InternalWrite(bytes, 0, bytes.Length); + } + + try + { + if (count > 0) + { + await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + } + + if (response.SendChunked) + stream.Write(crlf, 0, 2); + } + catch + { + if (!ignore_errors) + { + throw; + } + } + } + + //public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // if (disposed) + // throw new ObjectDisposedException(GetType().ToString()); + + // byte[] bytes = null; + // MemoryStream ms = GetHeaders(false); + // bool chunked = response.SendChunked; + // if (ms != null) + // { + // long start = ms.Position; + // ms.Position = ms.Length; + // if (chunked) + // { + // bytes = GetChunkSizeBytes(count, false); + // ms.Write(bytes, 0, bytes.Length); + // } + // ms.Write(buffer, offset, count); + // buffer = ms.ToArray(); + // offset = (int)start; + // count = (int)(ms.Position - start); + // } + // else if (chunked) + // { + // bytes = GetChunkSizeBytes(count, false); + // InternalWrite(bytes, 0, bytes.Length); + // } + + // return stream.BeginWrite(buffer, offset, count, cback, state); + //} + + //public override void EndWrite(IAsyncResult ares) + //{ + // if (disposed) + // throw new ObjectDisposedException(GetType().ToString()); + + // if (ignore_errors) + // { + // try + // { + // stream.EndWrite(ares); + // if (response.SendChunked) + // stream.Write(crlf, 0, 2); + // } + // catch { } + // } + // else { + // stream.EndWrite(ares); + // if (response.SendChunked) + // stream.Write(crlf, 0, 2); + // } + //} + + public override int Read([In, Out] byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + //public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, + // AsyncCallback cback, object state) + //{ + // throw new NotSupportedException(); + //} + + //public override int EndRead(IAsyncResult ares) + //{ + // throw new NotSupportedException(); + //} + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + } +} diff --git a/SocketHttpListener.Portable/Net/WebHeaderCollection.cs b/SocketHttpListener.Portable/Net/WebHeaderCollection.cs new file mode 100644 index 000000000..d20f99b9b --- /dev/null +++ b/SocketHttpListener.Portable/Net/WebHeaderCollection.cs @@ -0,0 +1,391 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Text; +using MediaBrowser.Model.Services; + +namespace SocketHttpListener.Net +{ + [ComVisible(true)] + public class WebHeaderCollection : QueryParamCollection + { + [Flags] + internal enum HeaderInfo + { + Request = 1, + Response = 1 << 1, + MultiValue = 1 << 10 + } + + static readonly bool[] allowed_chars = { + false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, false, false, false, false, false, false, false, false, false, + false, false, false, false, false, true, false, true, true, true, true, false, false, false, true, + true, false, true, true, false, true, true, true, true, true, true, true, true, true, true, false, + false, false, false, false, false, false, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + false, false, false, true, true, true, true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, + false, true, false + }; + + static readonly Dictionary headers; + HeaderInfo? headerRestriction; + HeaderInfo? headerConsistency; + + static WebHeaderCollection() + { + headers = new Dictionary(StringComparer.OrdinalIgnoreCase) { + { "Allow", HeaderInfo.MultiValue }, + { "Accept", HeaderInfo.Request | HeaderInfo.MultiValue }, + { "Accept-Charset", HeaderInfo.MultiValue }, + { "Accept-Encoding", HeaderInfo.MultiValue }, + { "Accept-Language", HeaderInfo.MultiValue }, + { "Accept-Ranges", HeaderInfo.MultiValue }, + { "Age", HeaderInfo.Response }, + { "Authorization", HeaderInfo.MultiValue }, + { "Cache-Control", HeaderInfo.MultiValue }, + { "Cookie", HeaderInfo.MultiValue }, + { "Connection", HeaderInfo.Request | HeaderInfo.MultiValue }, + { "Content-Encoding", HeaderInfo.MultiValue }, + { "Content-Length", HeaderInfo.Request | HeaderInfo.Response }, + { "Content-Type", HeaderInfo.Request }, + { "Content-Language", HeaderInfo.MultiValue }, + { "Date", HeaderInfo.Request }, + { "Expect", HeaderInfo.Request | HeaderInfo.MultiValue}, + { "Host", HeaderInfo.Request }, + { "If-Match", HeaderInfo.MultiValue }, + { "If-Modified-Since", HeaderInfo.Request }, + { "If-None-Match", HeaderInfo.MultiValue }, + { "Keep-Alive", HeaderInfo.Response }, + { "Pragma", HeaderInfo.MultiValue }, + { "Proxy-Authenticate", HeaderInfo.MultiValue }, + { "Proxy-Authorization", HeaderInfo.MultiValue }, + { "Proxy-Connection", HeaderInfo.Request | HeaderInfo.MultiValue }, + { "Range", HeaderInfo.Request | HeaderInfo.MultiValue }, + { "Referer", HeaderInfo.Request }, + { "Set-Cookie", HeaderInfo.MultiValue }, + { "Set-Cookie2", HeaderInfo.MultiValue }, + { "Server", HeaderInfo.Response }, + { "TE", HeaderInfo.MultiValue }, + { "Trailer", HeaderInfo.MultiValue }, + { "Transfer-Encoding", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo.MultiValue }, + { "Translate", HeaderInfo.Request | HeaderInfo.Response }, + { "Upgrade", HeaderInfo.MultiValue }, + { "User-Agent", HeaderInfo.Request }, + { "Vary", HeaderInfo.MultiValue }, + { "Via", HeaderInfo.MultiValue }, + { "Warning", HeaderInfo.MultiValue }, + { "WWW-Authenticate", HeaderInfo.Response | HeaderInfo. MultiValue }, + { "SecWebSocketAccept", HeaderInfo.Response }, + { "SecWebSocketExtensions", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo. MultiValue }, + { "SecWebSocketKey", HeaderInfo.Request }, + { "Sec-WebSocket-Protocol", HeaderInfo.Request | HeaderInfo.Response | HeaderInfo. MultiValue }, + { "SecWebSocketVersion", HeaderInfo.Response | HeaderInfo. MultiValue } + }; + } + + // Methods + + public void Add(string header) + { + if (header == null) + throw new ArgumentNullException("header"); + int pos = header.IndexOf(':'); + if (pos == -1) + throw new ArgumentException("no colon found", "header"); + + this.Add(header.Substring(0, pos), header.Substring(pos + 1)); + } + + public override void Add(string name, string value) + { + if (name == null) + throw new ArgumentNullException("name"); + + ThrowIfRestricted(name); + this.AddWithoutValidate(name, value); + } + + protected void AddWithoutValidate(string headerName, string headerValue) + { + if (!IsHeaderName(headerName)) + throw new ArgumentException("invalid header name: " + headerName, "headerName"); + if (headerValue == null) + headerValue = String.Empty; + else + headerValue = headerValue.Trim(); + if (!IsHeaderValue(headerValue)) + throw new ArgumentException("invalid header value: " + headerValue, "headerValue"); + + AddValue(headerName, headerValue); + } + + internal void AddValue(string headerName, string headerValue) + { + base.Add(headerName, headerValue); + } + + internal string[] GetValues_internal(string header, bool split) + { + if (header == null) + throw new ArgumentNullException("header"); + + string[] values = base.GetValues(header); + if (values == null || values.Length == 0) + return null; + + if (split && IsMultiValue(header)) + { + List separated = null; + foreach (var value in values) + { + if (value.IndexOf(',') < 0) + { + if (separated != null) + separated.Add(value); + + continue; + } + + if (separated == null) + { + separated = new List(values.Length + 1); + foreach (var v in values) + { + if (v == value) + break; + + separated.Add(v); + } + } + + var slices = value.Split(','); + var slices_length = slices.Length; + if (value[value.Length - 1] == ',') + --slices_length; + + for (int i = 0; i < slices_length; ++i) + { + separated.Add(slices[i].Trim()); + } + } + + if (separated != null) + return separated.ToArray(); + } + + return values; + } + + public override string[] GetValues(string header) + { + return GetValues_internal(header, true); + } + + public override string[] GetValues(int index) + { + string[] values = base.GetValues(index); + + if (values == null || values.Length == 0) + { + return null; + } + + return values; + } + + public static bool IsRestricted(string headerName) + { + return IsRestricted(headerName, false); + } + + public static bool IsRestricted(string headerName, bool response) + { + if (headerName == null) + throw new ArgumentNullException("headerName"); + + if (headerName.Length == 0) + throw new ArgumentException("empty string", "headerName"); + + if (!IsHeaderName(headerName)) + throw new ArgumentException("Invalid character in header"); + + HeaderInfo info; + if (!headers.TryGetValue(headerName, out info)) + return false; + + var flag = response ? HeaderInfo.Response : HeaderInfo.Request; + return (info & flag) != 0; + } + + public override void Set(string name, string value) + { + if (name == null) + throw new ArgumentNullException("name"); + if (!IsHeaderName(name)) + throw new ArgumentException("invalid header name"); + if (value == null) + value = String.Empty; + else + value = value.Trim(); + if (!IsHeaderValue(value)) + throw new ArgumentException("invalid header value"); + + ThrowIfRestricted(name); + base.Set(name, value); + } + + internal string ToStringMultiValue() + { + StringBuilder sb = new StringBuilder(); + + int count = base.Count; + for (int i = 0; i < count; i++) + { + string key = GetKey(i); + if (IsMultiValue(key)) + { + foreach (string v in GetValues(i)) + { + sb.Append(key) + .Append(": ") + .Append(v) + .Append("\r\n"); + } + } + else + { + sb.Append(key) + .Append(": ") + .Append(Get(i)) + .Append("\r\n"); + } + } + return sb.Append("\r\n").ToString(); + } + + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + + int count = base.Count; + for (int i = 0; i < count; i++) + sb.Append(GetKey(i)) + .Append(": ") + .Append(Get(i)) + .Append("\r\n"); + + return sb.Append("\r\n").ToString(); + } + + + // Internal Methods + + // With this we don't check for invalid characters in header. See bug #55994. + internal void SetInternal(string header) + { + int pos = header.IndexOf(':'); + if (pos == -1) + throw new ArgumentException("no colon found", "header"); + + SetInternal(header.Substring(0, pos), header.Substring(pos + 1)); + } + + internal void SetInternal(string name, string value) + { + if (value == null) + value = String.Empty; + else + value = value.Trim(); + if (!IsHeaderValue(value)) + throw new ArgumentException("invalid header value"); + + if (IsMultiValue(name)) + { + base.Add(name, value); + } + else + { + base.Remove(name); + base.Set(name, value); + } + } + + // Private Methods + + public override int Remove(string name) + { + ThrowIfRestricted(name); + return base.Remove(name); + } + + protected void ThrowIfRestricted(string headerName) + { + if (!headerRestriction.HasValue) + return; + + HeaderInfo info; + if (!headers.TryGetValue(headerName, out info)) + return; + + if ((info & headerRestriction.Value) != 0) + throw new ArgumentException("This header must be modified with the appropriate property."); + } + + internal static bool IsMultiValue(string headerName) + { + if (headerName == null) + return false; + + HeaderInfo info; + return headers.TryGetValue(headerName, out info) && (info & HeaderInfo.MultiValue) != 0; + } + + internal static bool IsHeaderValue(string value) + { + // TEXT any 8 bit value except CTL's (0-31 and 127) + // but including \r\n space and \t + // after a newline at least one space or \t must follow + // certain header fields allow comments () + + int len = value.Length; + for (int i = 0; i < len; i++) + { + char c = value[i]; + if (c == 127) + return false; + if (c < 0x20 && (c != '\r' && c != '\n' && c != '\t')) + return false; + if (c == '\n' && ++i < len) + { + c = value[i]; + if (c != ' ' && c != '\t') + return false; + } + } + + return true; + } + + internal static bool IsHeaderName(string name) + { + if (name == null || name.Length == 0) + return false; + + int len = name.Length; + for (int i = 0; i < len; i++) + { + char c = name[i]; + if (c > 126 || !allowed_chars[c]) + return false; + } + + return true; + } + } +} diff --git a/SocketHttpListener.Portable/Net/WebSockets/HttpListenerWebSocketContext.cs b/SocketHttpListener.Portable/Net/WebSockets/HttpListenerWebSocketContext.cs new file mode 100644 index 000000000..426e15ecd --- /dev/null +++ b/SocketHttpListener.Portable/Net/WebSockets/HttpListenerWebSocketContext.cs @@ -0,0 +1,347 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Net; +using System.Security.Principal; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Services; +using SocketHttpListener.Primitives; + +namespace SocketHttpListener.Net.WebSockets +{ + /// + /// Provides the properties used to access the information in a WebSocket connection request + /// received by the . + /// + /// + /// + public class HttpListenerWebSocketContext : WebSocketContext + { + #region Private Fields + + private HttpListenerContext _context; + private WebSocket _websocket; + + #endregion + + #region Internal Constructors + + internal HttpListenerWebSocketContext( + HttpListenerContext context, string protocol, ICryptoProvider cryptoProvider, IMemoryStreamFactory memoryStreamFactory) + { + _context = context; + _websocket = new WebSocket(this, protocol, cryptoProvider, memoryStreamFactory); + } + + #endregion + + #region Internal Properties + + internal Stream Stream + { + get + { + return _context.Connection.Stream; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the HTTP cookies included in the request. + /// + /// + /// A that contains the cookies. + /// + public override CookieCollection CookieCollection + { + get + { + return _context.Request.Cookies; + } + } + + /// + /// Gets the HTTP headers included in the request. + /// + /// + /// A that contains the headers. + /// + public override QueryParamCollection Headers + { + get + { + return _context.Request.Headers; + } + } + + /// + /// Gets the value of the Host header included in the request. + /// + /// + /// A that represents the value of the Host header. + /// + public override string Host + { + get + { + return _context.Request.Headers["Host"]; + } + } + + /// + /// Gets a value indicating whether the client is authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public override bool IsAuthenticated + { + get + { + return _context.Request.IsAuthenticated; + } + } + + /// + /// Gets a value indicating whether the client connected from the local computer. + /// + /// + /// true if the client connected from the local computer; otherwise, false. + /// + public override bool IsLocal + { + get + { + return _context.Request.IsLocal; + } + } + + /// + /// Gets a value indicating whether the WebSocket connection is secured. + /// + /// + /// true if the connection is secured; otherwise, false. + /// + public override bool IsSecureConnection + { + get + { + return _context.Connection.IsSecure; + } + } + + /// + /// Gets a value indicating whether the request is a WebSocket connection request. + /// + /// + /// true if the request is a WebSocket connection request; otherwise, false. + /// + public override bool IsWebSocketRequest + { + get + { + return _context.Request.IsWebSocketRequest; + } + } + + /// + /// Gets the value of the Origin header included in the request. + /// + /// + /// A that represents the value of the Origin header. + /// + public override string Origin + { + get + { + return _context.Request.Headers["Origin"]; + } + } + + /// + /// Gets the query string included in the request. + /// + /// + /// A that contains the query string parameters. + /// + public override QueryParamCollection QueryString + { + get + { + return _context.Request.QueryString; + } + } + + /// + /// Gets the URI requested by the client. + /// + /// + /// A that represents the requested URI. + /// + public override Uri RequestUri + { + get + { + return _context.Request.Url; + } + } + + /// + /// Gets the value of the Sec-WebSocket-Key header included in the request. + /// + /// + /// This property provides a part of the information used by the server to prove that it + /// received a valid WebSocket connection request. + /// + /// + /// A that represents the value of the Sec-WebSocket-Key header. + /// + public override string SecWebSocketKey + { + get + { + return _context.Request.Headers["Sec-WebSocket-Key"]; + } + } + + /// + /// Gets the values of the Sec-WebSocket-Protocol header included in the request. + /// + /// + /// This property represents the subprotocols requested by the client. + /// + /// + /// An instance that provides + /// an enumerator which supports the iteration over the values of the Sec-WebSocket-Protocol + /// header. + /// + public override IEnumerable SecWebSocketProtocols + { + get + { + var protocols = _context.Request.Headers["Sec-WebSocket-Protocol"]; + if (protocols != null) + foreach (var protocol in protocols.Split(',')) + yield return protocol.Trim(); + } + } + + /// + /// Gets the value of the Sec-WebSocket-Version header included in the request. + /// + /// + /// This property represents the WebSocket protocol version. + /// + /// + /// A that represents the value of the Sec-WebSocket-Version header. + /// + public override string SecWebSocketVersion + { + get + { + return _context.Request.Headers["Sec-WebSocket-Version"]; + } + } + + /// + /// Gets the server endpoint as an IP address and a port number. + /// + /// + /// + public override IpEndPointInfo ServerEndPoint + { + get + { + return _context.Connection.LocalEndPoint; + } + } + + /// + /// Gets the client information (identity, authentication, and security roles). + /// + /// + /// A that represents the client information. + /// + public override IPrincipal User + { + get + { + return _context.User; + } + } + + /// + /// Gets the client endpoint as an IP address and a port number. + /// + /// + /// + public override IpEndPointInfo UserEndPoint + { + get + { + return _context.Connection.RemoteEndPoint; + } + } + + /// + /// Gets the instance used for two-way communication + /// between client and server. + /// + /// + /// A . + /// + public override WebSocket WebSocket + { + get + { + return _websocket; + } + } + + #endregion + + #region Internal Methods + + internal void Close() + { + try + { + _context.Connection.Close(true); + } + catch + { + // catch errors sending the closing handshake + } + } + + internal void Close(HttpStatusCode code) + { + _context.Response.Close(code); + } + + #endregion + + #region Public Methods + + /// + /// Returns a that represents the current + /// . + /// + /// + /// A that represents the current + /// . + /// + public override string ToString() + { + return _context.Request.ToString(); + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Net/WebSockets/WebSocketContext.cs b/SocketHttpListener.Portable/Net/WebSockets/WebSocketContext.cs new file mode 100644 index 000000000..3ffa6e639 --- /dev/null +++ b/SocketHttpListener.Portable/Net/WebSockets/WebSocketContext.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using System.Security.Principal; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Services; + +namespace SocketHttpListener.Net.WebSockets +{ + /// + /// Exposes the properties used to access the information in a WebSocket connection request. + /// + /// + /// The WebSocketContext class is an abstract class. + /// + public abstract class WebSocketContext + { + #region Protected Constructors + + /// + /// Initializes a new instance of the class. + /// + protected WebSocketContext() + { + } + + #endregion + + #region Public Properties + + /// + /// Gets the HTTP cookies included in the request. + /// + /// + /// A that contains the cookies. + /// + public abstract CookieCollection CookieCollection { get; } + + /// + /// Gets the HTTP headers included in the request. + /// + /// + /// A that contains the headers. + /// + public abstract QueryParamCollection Headers { get; } + + /// + /// Gets the value of the Host header included in the request. + /// + /// + /// A that represents the value of the Host header. + /// + public abstract string Host { get; } + + /// + /// Gets a value indicating whether the client is authenticated. + /// + /// + /// true if the client is authenticated; otherwise, false. + /// + public abstract bool IsAuthenticated { get; } + + /// + /// Gets a value indicating whether the client connected from the local computer. + /// + /// + /// true if the client connected from the local computer; otherwise, false. + /// + public abstract bool IsLocal { get; } + + /// + /// Gets a value indicating whether the WebSocket connection is secured. + /// + /// + /// true if the connection is secured; otherwise, false. + /// + public abstract bool IsSecureConnection { get; } + + /// + /// Gets a value indicating whether the request is a WebSocket connection request. + /// + /// + /// true if the request is a WebSocket connection request; otherwise, false. + /// + public abstract bool IsWebSocketRequest { get; } + + /// + /// Gets the value of the Origin header included in the request. + /// + /// + /// A that represents the value of the Origin header. + /// + public abstract string Origin { get; } + + /// + /// Gets the query string included in the request. + /// + /// + /// A that contains the query string parameters. + /// + public abstract QueryParamCollection QueryString { get; } + + /// + /// Gets the URI requested by the client. + /// + /// + /// A that represents the requested URI. + /// + public abstract Uri RequestUri { get; } + + /// + /// Gets the value of the Sec-WebSocket-Key header included in the request. + /// + /// + /// This property provides a part of the information used by the server to prove that it + /// received a valid WebSocket connection request. + /// + /// + /// A that represents the value of the Sec-WebSocket-Key header. + /// + public abstract string SecWebSocketKey { get; } + + /// + /// Gets the values of the Sec-WebSocket-Protocol header included in the request. + /// + /// + /// This property represents the subprotocols requested by the client. + /// + /// + /// An instance that provides + /// an enumerator which supports the iteration over the values of the Sec-WebSocket-Protocol + /// header. + /// + public abstract IEnumerable SecWebSocketProtocols { get; } + + /// + /// Gets the value of the Sec-WebSocket-Version header included in the request. + /// + /// + /// This property represents the WebSocket protocol version. + /// + /// + /// A that represents the value of the Sec-WebSocket-Version header. + /// + public abstract string SecWebSocketVersion { get; } + + /// + /// Gets the server endpoint as an IP address and a port number. + /// + /// + /// A that represents the server endpoint. + /// + public abstract IpEndPointInfo ServerEndPoint { get; } + + /// + /// Gets the client information (identity, authentication, and security roles). + /// + /// + /// A that represents the client information. + /// + public abstract IPrincipal User { get; } + + /// + /// Gets the client endpoint as an IP address and a port number. + /// + /// + /// A that represents the client endpoint. + /// + public abstract IpEndPointInfo UserEndPoint { get; } + + /// + /// Gets the instance used for two-way communication + /// between client and server. + /// + /// + /// A . + /// + public abstract WebSocket WebSocket { get; } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Opcode.cs b/SocketHttpListener.Portable/Opcode.cs new file mode 100644 index 000000000..62b7d8585 --- /dev/null +++ b/SocketHttpListener.Portable/Opcode.cs @@ -0,0 +1,43 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values of the opcode that indicates the type of a WebSocket frame. + /// + /// + /// The values of the opcode are defined in + /// Section 5.2 of RFC 6455. + /// + public enum Opcode : byte + { + /// + /// Equivalent to numeric value 0. + /// Indicates a continuation frame. + /// + Cont = 0x0, + /// + /// Equivalent to numeric value 1. + /// Indicates a text frame. + /// + Text = 0x1, + /// + /// Equivalent to numeric value 2. + /// Indicates a binary frame. + /// + Binary = 0x2, + /// + /// Equivalent to numeric value 8. + /// Indicates a connection close frame. + /// + Close = 0x8, + /// + /// Equivalent to numeric value 9. + /// Indicates a ping frame. + /// + Ping = 0x9, + /// + /// Equivalent to numeric value 10. + /// Indicates a pong frame. + /// + Pong = 0xa + } +} diff --git a/SocketHttpListener.Portable/PayloadData.cs b/SocketHttpListener.Portable/PayloadData.cs new file mode 100644 index 000000000..a6318da2b --- /dev/null +++ b/SocketHttpListener.Portable/PayloadData.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace SocketHttpListener +{ + internal class PayloadData : IEnumerable + { + #region Private Fields + + private byte [] _applicationData; + private byte [] _extensionData; + private bool _masked; + + #endregion + + #region Public Const Fields + + public const ulong MaxLength = long.MaxValue; + + #endregion + + #region Public Constructors + + public PayloadData () + : this (new byte [0], new byte [0], false) + { + } + + public PayloadData (byte [] applicationData) + : this (new byte [0], applicationData, false) + { + } + + public PayloadData (string applicationData) + : this (new byte [0], Encoding.UTF8.GetBytes (applicationData), false) + { + } + + public PayloadData (byte [] applicationData, bool masked) + : this (new byte [0], applicationData, masked) + { + } + + public PayloadData (byte [] extensionData, byte [] applicationData, bool masked) + { + _extensionData = extensionData; + _applicationData = applicationData; + _masked = masked; + } + + #endregion + + #region Internal Properties + + internal bool ContainsReservedCloseStatusCode { + get { + return _applicationData.Length > 1 && + _applicationData.SubArray (0, 2).ToUInt16 (ByteOrder.Big).IsReserved (); + } + } + + #endregion + + #region Public Properties + + public byte [] ApplicationData { + get { + return _applicationData; + } + } + + public byte [] ExtensionData { + get { + return _extensionData; + } + } + + public bool IsMasked { + get { + return _masked; + } + } + + public ulong Length { + get { + return (ulong) (_extensionData.Length + _applicationData.Length); + } + } + + #endregion + + #region Private Methods + + private static void mask (byte [] src, byte [] key) + { + for (long i = 0; i < src.Length; i++) + src [i] = (byte) (src [i] ^ key [i % 4]); + } + + #endregion + + #region Public Methods + + public IEnumerator GetEnumerator () + { + foreach (byte b in _extensionData) + yield return b; + + foreach (byte b in _applicationData) + yield return b; + } + + public void Mask (byte [] maskingKey) + { + if (_extensionData.Length > 0) + mask (_extensionData, maskingKey); + + if (_applicationData.Length > 0) + mask (_applicationData, maskingKey); + + _masked = !_masked; + } + + public byte [] ToByteArray () + { + return _extensionData.Length > 0 + ? new List (this).ToArray () + : _applicationData; + } + + public override string ToString () + { + return BitConverter.ToString (ToByteArray ()); + } + + #endregion + + #region Explicitly Implemented Interface Members + + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/Primitives/HttpListenerException.cs b/SocketHttpListener.Portable/Primitives/HttpListenerException.cs new file mode 100644 index 000000000..7b383fd23 --- /dev/null +++ b/SocketHttpListener.Portable/Primitives/HttpListenerException.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SocketHttpListener.Primitives +{ + public class HttpListenerException : Exception + { + public HttpListenerException(int statusCode, string message) + : base(message) + { + + } + } +} diff --git a/SocketHttpListener.Portable/Primitives/ICertificate.cs b/SocketHttpListener.Portable/Primitives/ICertificate.cs new file mode 100644 index 000000000..1289da13d --- /dev/null +++ b/SocketHttpListener.Portable/Primitives/ICertificate.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SocketHttpListener.Primitives +{ + public interface ICertificate + { + } +} diff --git a/SocketHttpListener.Portable/Primitives/IStreamFactory.cs b/SocketHttpListener.Portable/Primitives/IStreamFactory.cs new file mode 100644 index 000000000..f189b95b4 --- /dev/null +++ b/SocketHttpListener.Portable/Primitives/IStreamFactory.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Net; + +namespace SocketHttpListener.Primitives +{ + public interface IStreamFactory + { + Stream CreateNetworkStream(ISocket socket, bool ownsSocket); + Stream CreateSslStream(Stream innerStream, bool leaveInnerStreamOpen); + + Task AuthenticateSslStreamAsServer(Stream stream, ICertificate certificate); + } +} diff --git a/SocketHttpListener.Portable/Primitives/ITextEncoding.cs b/SocketHttpListener.Portable/Primitives/ITextEncoding.cs new file mode 100644 index 000000000..b10145687 --- /dev/null +++ b/SocketHttpListener.Portable/Primitives/ITextEncoding.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Text; + +namespace SocketHttpListener.Primitives +{ + public static class TextEncodingExtensions + { + public static Encoding GetDefaultEncoding(this ITextEncoding encoding) + { + return Encoding.UTF8; + } + } +} diff --git a/SocketHttpListener.Portable/Properties/AssemblyInfo.cs b/SocketHttpListener.Portable/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..870426460 --- /dev/null +++ b/SocketHttpListener.Portable/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Resources; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("SocketHttpListener.Portable")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SocketHttpListener.Portable")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SocketHttpListener.Portable/Rsv.cs b/SocketHttpListener.Portable/Rsv.cs new file mode 100644 index 000000000..668059b8a --- /dev/null +++ b/SocketHttpListener.Portable/Rsv.cs @@ -0,0 +1,8 @@ +namespace SocketHttpListener +{ + internal enum Rsv : byte + { + Off = 0x0, + On = 0x1 + } +} diff --git a/SocketHttpListener.Portable/SocketHttpListener.Portable.csproj b/SocketHttpListener.Portable/SocketHttpListener.Portable.csproj new file mode 100644 index 000000000..f7b3a643c --- /dev/null +++ b/SocketHttpListener.Portable/SocketHttpListener.Portable.csproj @@ -0,0 +1,109 @@ + + + + + 11.0 + Debug + AnyCPU + {4F26D5D8-A7B0-42B3-BA42-7CB7D245934E} + Library + Properties + SocketHttpListener.Portable + SocketHttpListener.Portable + en-US + 512 + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Profile7 + v4.5 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + if $(ConfigurationName) == Release ( +xcopy "$(TargetPath)" "$(SolutionDir)\Nuget\dlls\" /y /d /r /i +) + + + \ No newline at end of file diff --git a/SocketHttpListener.Portable/SocketHttpListener.Portable.nuget.targets b/SocketHttpListener.Portable/SocketHttpListener.Portable.nuget.targets new file mode 100644 index 000000000..e69ce0e64 --- /dev/null +++ b/SocketHttpListener.Portable/SocketHttpListener.Portable.nuget.targets @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/SocketHttpListener.Portable/WebSocket.cs b/SocketHttpListener.Portable/WebSocket.cs new file mode 100644 index 000000000..889880387 --- /dev/null +++ b/SocketHttpListener.Portable/WebSocket.cs @@ -0,0 +1,898 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using SocketHttpListener.Net.WebSockets; +using SocketHttpListener.Primitives; +using HttpStatusCode = SocketHttpListener.Net.HttpStatusCode; + +namespace SocketHttpListener +{ + /// + /// Implements the WebSocket interface. + /// + /// + /// The WebSocket class provides a set of methods and properties for two-way communication using + /// the WebSocket protocol (RFC 6455). + /// + public class WebSocket : IDisposable + { + #region Private Fields + + private string _base64Key; + private Action _closeContext; + private CompressionMethod _compression; + private WebSocketContext _context; + private CookieCollection _cookies; + private string _extensions; + private AutoResetEvent _exitReceiving; + private object _forConn; + private object _forEvent; + private object _forMessageEventQueue; + private object _forSend; + private const string _guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + private Func + _handshakeRequestChecker; + private Queue _messageEventQueue; + private uint _nonceCount; + private string _origin; + private bool _preAuth; + private string _protocol; + private string[] _protocols; + private Uri _proxyUri; + private volatile WebSocketState _readyState; + private AutoResetEvent _receivePong; + private bool _secure; + private Stream _stream; + private Uri _uri; + private const string _version = "13"; + private readonly IMemoryStreamFactory _memoryStreamFactory; + + private readonly ICryptoProvider _cryptoProvider; + + #endregion + + #region Internal Fields + + internal const int FragmentLength = 1016; // Max value is int.MaxValue - 14. + + #endregion + + #region Internal Constructors + + // As server + internal WebSocket(HttpListenerWebSocketContext context, string protocol, ICryptoProvider cryptoProvider, IMemoryStreamFactory memoryStreamFactory) + { + _context = context; + _protocol = protocol; + _cryptoProvider = cryptoProvider; + _memoryStreamFactory = memoryStreamFactory; + + _closeContext = context.Close; + _secure = context.IsSecureConnection; + _stream = context.Stream; + + init(); + } + + #endregion + + // As server + internal Func CustomHandshakeRequestChecker + { + get + { + return _handshakeRequestChecker ?? (context => null); + } + + set + { + _handshakeRequestChecker = value; + } + } + + internal bool IsConnected + { + get + { + return _readyState == WebSocketState.Open || _readyState == WebSocketState.Closing; + } + } + + /// + /// Gets the state of the WebSocket connection. + /// + /// + /// One of the enum values, indicates the state of the WebSocket + /// connection. The default value is . + /// + public WebSocketState ReadyState + { + get + { + return _readyState; + } + } + + #region Public Events + + /// + /// Occurs when the WebSocket connection has been closed. + /// + public event EventHandler OnClose; + + /// + /// Occurs when the gets an error. + /// + public event EventHandler OnError; + + /// + /// Occurs when the receives a message. + /// + public event EventHandler OnMessage; + + /// + /// Occurs when the WebSocket connection has been established. + /// + public event EventHandler OnOpen; + + #endregion + + #region Private Methods + + // As server + private bool acceptHandshake() + { + var msg = checkIfValidHandshakeRequest(_context); + if (msg != null) + { + error("An error has occurred while connecting: " + msg); + Close(HttpStatusCode.BadRequest); + + return false; + } + + if (_protocol != null && + !_context.SecWebSocketProtocols.Contains(protocol => protocol == _protocol)) + _protocol = null; + + ////var extensions = _context.Headers["Sec-WebSocket-Extensions"]; + ////if (extensions != null && extensions.Length > 0) + //// processSecWebSocketExtensionsHeader(extensions); + + return sendHttpResponse(createHandshakeResponse()); + } + + // As server + private string checkIfValidHandshakeRequest(WebSocketContext context) + { + var headers = context.Headers; + return context.RequestUri == null + ? "Invalid request url." + : !context.IsWebSocketRequest + ? "Not WebSocket connection request." + : !validateSecWebSocketKeyHeader(headers["Sec-WebSocket-Key"]) + ? "Invalid Sec-WebSocket-Key header." + : !validateSecWebSocketVersionClientHeader(headers["Sec-WebSocket-Version"]) + ? "Invalid Sec-WebSocket-Version header." + : CustomHandshakeRequestChecker(context); + } + + private void close(CloseStatusCode code, string reason, bool wait) + { + close(new PayloadData(((ushort)code).Append(reason)), !code.IsReserved(), wait); + } + + private void close(PayloadData payload, bool send, bool wait) + { + lock (_forConn) + { + if (_readyState == WebSocketState.Closing || _readyState == WebSocketState.Closed) + { + return; + } + + _readyState = WebSocketState.Closing; + } + + var e = new CloseEventArgs(payload); + e.WasClean = + closeHandshake( + send ? WebSocketFrame.CreateCloseFrame(Mask.Unmask, payload).ToByteArray() : null, + wait ? 1000 : 0, + closeServerResources); + + _readyState = WebSocketState.Closed; + try + { + OnClose.Emit(this, e); + } + catch (Exception ex) + { + error("An exception has occurred while OnClose.", ex); + } + } + + private bool closeHandshake(byte[] frameAsBytes, int millisecondsTimeout, Action release) + { + var sent = frameAsBytes != null && writeBytes(frameAsBytes); + var received = + millisecondsTimeout == 0 || + (sent && _exitReceiving != null && _exitReceiving.WaitOne(millisecondsTimeout)); + + release(); + if (_receivePong != null) + { + _receivePong.Dispose(); + _receivePong = null; + } + + if (_exitReceiving != null) + { + _exitReceiving.Dispose(); + _exitReceiving = null; + } + + var result = sent && received; + + return result; + } + + // As server + private void closeServerResources() + { + if (_closeContext == null) + return; + + _closeContext(); + _closeContext = null; + _stream = null; + _context = null; + } + + private bool concatenateFragmentsInto(Stream dest) + { + while (true) + { + var frame = WebSocketFrame.Read(_stream, true); + if (frame.IsFinal) + { + /* FINAL */ + + // CONT + if (frame.IsContinuation) + { + dest.WriteBytes(frame.PayloadData.ApplicationData); + break; + } + + // PING + if (frame.IsPing) + { + processPingFrame(frame); + continue; + } + + // PONG + if (frame.IsPong) + { + processPongFrame(frame); + continue; + } + + // CLOSE + if (frame.IsClose) + return processCloseFrame(frame); + } + else + { + /* MORE */ + + // CONT + if (frame.IsContinuation) + { + dest.WriteBytes(frame.PayloadData.ApplicationData); + continue; + } + } + + // ? + return processUnsupportedFrame( + frame, + CloseStatusCode.IncorrectData, + "An incorrect data has been received while receiving fragmented data."); + } + + return true; + } + + // As server + private HttpResponse createHandshakeCloseResponse(HttpStatusCode code) + { + var res = HttpResponse.CreateCloseResponse(code); + res.Headers["Sec-WebSocket-Version"] = _version; + + return res; + } + + // As server + private HttpResponse createHandshakeResponse() + { + var res = HttpResponse.CreateWebSocketResponse(); + + var headers = res.Headers; + headers["Sec-WebSocket-Accept"] = CreateResponseKey(_base64Key); + + if (_protocol != null) + headers["Sec-WebSocket-Protocol"] = _protocol; + + if (_extensions != null) + headers["Sec-WebSocket-Extensions"] = _extensions; + + if (_cookies.Count > 0) + res.SetCookies(_cookies); + + return res; + } + + private MessageEventArgs dequeueFromMessageEventQueue() + { + lock (_forMessageEventQueue) + return _messageEventQueue.Count > 0 + ? _messageEventQueue.Dequeue() + : null; + } + + private void enqueueToMessageEventQueue(MessageEventArgs e) + { + lock (_forMessageEventQueue) + _messageEventQueue.Enqueue(e); + } + + private void error(string message, Exception exception) + { + try + { + if (exception != null) + { + message += ". Exception.Message: " + exception.Message; + } + OnError.Emit(this, new ErrorEventArgs(message)); + } + catch (Exception ex) + { + } + } + + private void error(string message) + { + try + { + OnError.Emit(this, new ErrorEventArgs(message)); + } + catch (Exception ex) + { + } + } + + private void init() + { + _compression = CompressionMethod.None; + _cookies = new CookieCollection(); + _forConn = new object(); + _forEvent = new object(); + _forSend = new object(); + _messageEventQueue = new Queue(); + _forMessageEventQueue = ((ICollection)_messageEventQueue).SyncRoot; + _readyState = WebSocketState.Connecting; + } + + private void open() + { + try + { + startReceiving(); + + lock (_forEvent) + { + try + { + OnOpen.Emit(this, EventArgs.Empty); + } + catch (Exception ex) + { + processException(ex, "An exception has occurred while OnOpen."); + } + } + } + catch (Exception ex) + { + processException(ex, "An exception has occurred while opening."); + } + } + + private bool processCloseFrame(WebSocketFrame frame) + { + var payload = frame.PayloadData; + close(payload, !payload.ContainsReservedCloseStatusCode, false); + + return false; + } + + private bool processDataFrame(WebSocketFrame frame) + { + var e = frame.IsCompressed + ? new MessageEventArgs( + frame.Opcode, frame.PayloadData.ApplicationData.Decompress(_compression)) + : new MessageEventArgs(frame.Opcode, frame.PayloadData); + + enqueueToMessageEventQueue(e); + return true; + } + + private void processException(Exception exception, string message) + { + var code = CloseStatusCode.Abnormal; + var reason = message; + if (exception is WebSocketException) + { + var wsex = (WebSocketException)exception; + code = wsex.Code; + reason = wsex.Message; + } + + error(message ?? code.GetMessage(), exception); + if (_readyState == WebSocketState.Connecting) + Close(HttpStatusCode.BadRequest); + else + close(code, reason ?? code.GetMessage(), false); + } + + private bool processFragmentedFrame(WebSocketFrame frame) + { + return frame.IsContinuation // Not first fragment + ? true + : processFragments(frame); + } + + private bool processFragments(WebSocketFrame first) + { + using (var buff = _memoryStreamFactory.CreateNew()) + { + buff.WriteBytes(first.PayloadData.ApplicationData); + if (!concatenateFragmentsInto(buff)) + return false; + + byte[] data; + if (_compression != CompressionMethod.None) + { + data = buff.DecompressToArray(_compression); + } + else + { + data = buff.ToArray(); + } + + enqueueToMessageEventQueue(new MessageEventArgs(first.Opcode, data)); + return true; + } + } + + private bool processPingFrame(WebSocketFrame frame) + { + var mask = Mask.Unmask; + + return true; + } + + private bool processPongFrame(WebSocketFrame frame) + { + _receivePong.Set(); + + return true; + } + + private bool processUnsupportedFrame(WebSocketFrame frame, CloseStatusCode code, string reason) + { + processException(new WebSocketException(code, reason), null); + + return false; + } + + private bool processWebSocketFrame(WebSocketFrame frame) + { + return frame.IsCompressed && _compression == CompressionMethod.None + ? processUnsupportedFrame( + frame, + CloseStatusCode.IncorrectData, + "A compressed data has been received without available decompression method.") + : frame.IsFragmented + ? processFragmentedFrame(frame) + : frame.IsData + ? processDataFrame(frame) + : frame.IsPing + ? processPingFrame(frame) + : frame.IsPong + ? processPongFrame(frame) + : frame.IsClose + ? processCloseFrame(frame) + : processUnsupportedFrame(frame, CloseStatusCode.PolicyViolation, null); + } + + private bool send(Opcode opcode, Stream stream) + { + lock (_forSend) + { + var src = stream; + var compressed = false; + var sent = false; + try + { + if (_compression != CompressionMethod.None) + { + stream = stream.Compress(_compression); + compressed = true; + } + + sent = send(opcode, Mask.Unmask, stream, compressed); + if (!sent) + error("Sending a data has been interrupted."); + } + catch (Exception ex) + { + error("An exception has occurred while sending a data.", ex); + } + finally + { + if (compressed) + stream.Dispose(); + + src.Dispose(); + } + + return sent; + } + } + + private bool send(Opcode opcode, Mask mask, Stream stream, bool compressed) + { + var len = stream.Length; + + /* Not fragmented */ + + if (len == 0) + return send(Fin.Final, opcode, mask, new byte[0], compressed); + + var quo = len / FragmentLength; + var rem = (int)(len % FragmentLength); + + byte[] buff = null; + if (quo == 0) + { + buff = new byte[rem]; + return stream.Read(buff, 0, rem) == rem && + send(Fin.Final, opcode, mask, buff, compressed); + } + + buff = new byte[FragmentLength]; + if (quo == 1 && rem == 0) + return stream.Read(buff, 0, FragmentLength) == FragmentLength && + send(Fin.Final, opcode, mask, buff, compressed); + + /* Send fragmented */ + + // Begin + if (stream.Read(buff, 0, FragmentLength) != FragmentLength || + !send(Fin.More, opcode, mask, buff, compressed)) + return false; + + var n = rem == 0 ? quo - 2 : quo - 1; + for (long i = 0; i < n; i++) + if (stream.Read(buff, 0, FragmentLength) != FragmentLength || + !send(Fin.More, Opcode.Cont, mask, buff, compressed)) + return false; + + // End + if (rem == 0) + rem = FragmentLength; + else + buff = new byte[rem]; + + return stream.Read(buff, 0, rem) == rem && + send(Fin.Final, Opcode.Cont, mask, buff, compressed); + } + + private bool send(Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed) + { + lock (_forConn) + { + if (_readyState != WebSocketState.Open) + { + return false; + } + + return writeBytes( + WebSocketFrame.CreateWebSocketFrame(fin, opcode, mask, data, compressed).ToByteArray()); + } + } + + private void sendAsync(Opcode opcode, Stream stream, Action completed) + { + Func sender = send; + sender.BeginInvoke( + opcode, + stream, + ar => + { + try + { + var sent = sender.EndInvoke(ar); + if (completed != null) + completed(sent); + } + catch (Exception ex) + { + error("An exception has occurred while callback.", ex); + } + }, + null); + } + + // As server + private bool sendHttpResponse(HttpResponse response) + { + return writeBytes(response.ToByteArray()); + } + + private void startReceiving() + { + if (_messageEventQueue.Count > 0) + _messageEventQueue.Clear(); + + _exitReceiving = new AutoResetEvent(false); + _receivePong = new AutoResetEvent(false); + + Action receive = null; + receive = () => WebSocketFrame.ReadAsync( + _stream, + true, + frame => + { + if (processWebSocketFrame(frame) && _readyState != WebSocketState.Closed) + { + receive(); + + if (!frame.IsData) + return; + + lock (_forEvent) + { + try + { + var e = dequeueFromMessageEventQueue(); + if (e != null && _readyState == WebSocketState.Open) + OnMessage.Emit(this, e); + } + catch (Exception ex) + { + processException(ex, "An exception has occurred while OnMessage."); + } + } + } + else if (_exitReceiving != null) + { + _exitReceiving.Set(); + } + }, + ex => processException(ex, "An exception has occurred while receiving a message.")); + + receive(); + } + + // As server + private bool validateSecWebSocketKeyHeader(string value) + { + if (value == null || value.Length == 0) + return false; + + _base64Key = value; + return true; + } + + // As server + private bool validateSecWebSocketVersionClientHeader(string value) + { + return true; + //return value != null && value == _version; + } + + private bool writeBytes(byte[] data) + { + try + { + _stream.Write(data, 0, data.Length); + return true; + } + catch (Exception ex) + { + return false; + } + } + + #endregion + + #region Internal Methods + + // As server + internal void Close(HttpResponse response) + { + _readyState = WebSocketState.Closing; + + sendHttpResponse(response); + closeServerResources(); + + _readyState = WebSocketState.Closed; + } + + // As server + internal void Close(HttpStatusCode code) + { + Close(createHandshakeCloseResponse(code)); + } + + // As server + public void ConnectAsServer() + { + try + { + if (acceptHandshake()) + { + _readyState = WebSocketState.Open; + open(); + } + } + catch (Exception ex) + { + processException(ex, "An exception has occurred while connecting."); + } + } + + private string CreateResponseKey(string base64Key) + { + var buff = new StringBuilder(base64Key, 64); + buff.Append(_guid); + var src = _cryptoProvider.ComputeSHA1(Encoding.UTF8.GetBytes(buff.ToString())); + + return Convert.ToBase64String(src); + } + + #endregion + + #region Public Methods + + /// + /// Closes the WebSocket connection, and releases all associated resources. + /// + public void Close() + { + var msg = _readyState.CheckIfClosable(); + if (msg != null) + { + error(msg); + + return; + } + + var send = _readyState == WebSocketState.Open; + close(new PayloadData(), send, send); + } + + /// + /// Closes the WebSocket connection with the specified + /// and , and releases all associated resources. + /// + /// + /// This method emits a event if the size + /// of is greater than 123 bytes. + /// + /// + /// One of the enum values, represents the status code + /// indicating the reason for the close. + /// + /// + /// A that represents the reason for the close. + /// + public void Close(CloseStatusCode code, string reason) + { + byte[] data = null; + var msg = _readyState.CheckIfClosable() ?? + (data = ((ushort)code).Append(reason)).CheckIfValidControlData("reason"); + + if (msg != null) + { + error(msg); + + return; + } + + var send = _readyState == WebSocketState.Open && !code.IsReserved(); + close(new PayloadData(data), send, send); + } + + /// + /// Sends a binary asynchronously using the WebSocket connection. + /// + /// + /// This method doesn't wait for the send to be complete. + /// + /// + /// An array of that represents the binary data to send. + /// + /// + /// An Action<bool> delegate that references the method(s) called when the send is + /// complete. A passed to this delegate is true if the send is + /// complete successfully; otherwise, false. + /// + public void SendAsync(byte[] data, Action completed) + { + var msg = _readyState.CheckIfOpen() ?? data.CheckIfValidSendData(); + if (msg != null) + { + error(msg); + + return; + } + + sendAsync(Opcode.Binary, _memoryStreamFactory.CreateNew(data), completed); + } + + /// + /// Sends a text asynchronously using the WebSocket connection. + /// + /// + /// This method doesn't wait for the send to be complete. + /// + /// + /// A that represents the text data to send. + /// + /// + /// An Action<bool> delegate that references the method(s) called when the send is + /// complete. A passed to this delegate is true if the send is + /// complete successfully; otherwise, false. + /// + public void SendAsync(string data, Action completed) + { + var msg = _readyState.CheckIfOpen() ?? data.CheckIfValidSendData(); + if (msg != null) + { + error(msg); + + return; + } + + sendAsync(Opcode.Text, _memoryStreamFactory.CreateNew(Encoding.UTF8.GetBytes(data)), completed); + } + + #endregion + + #region Explicit Interface Implementation + + /// + /// Closes the WebSocket connection, and releases all associated resources. + /// + /// + /// This method closes the WebSocket connection with . + /// + void IDisposable.Dispose() + { + Close(CloseStatusCode.Away, null); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/WebSocketException.cs b/SocketHttpListener.Portable/WebSocketException.cs new file mode 100644 index 000000000..260721317 --- /dev/null +++ b/SocketHttpListener.Portable/WebSocketException.cs @@ -0,0 +1,60 @@ +using System; + +namespace SocketHttpListener +{ + /// + /// The exception that is thrown when a gets a fatal error. + /// + public class WebSocketException : Exception + { + #region Internal Constructors + + internal WebSocketException () + : this (CloseStatusCode.Abnormal, null, null) + { + } + + internal WebSocketException (string message) + : this (CloseStatusCode.Abnormal, message, null) + { + } + + internal WebSocketException (CloseStatusCode code) + : this (code, null, null) + { + } + + internal WebSocketException (string message, Exception innerException) + : this (CloseStatusCode.Abnormal, message, innerException) + { + } + + internal WebSocketException (CloseStatusCode code, string message) + : this (code, message, null) + { + } + + internal WebSocketException (CloseStatusCode code, string message, Exception innerException) + : base (message ?? code.GetMessage (), innerException) + { + Code = code; + } + + #endregion + + #region Public Properties + + /// + /// Gets the status code indicating the cause for the exception. + /// + /// + /// One of the enum values, represents the status code indicating + /// the cause for the exception. + /// + public CloseStatusCode Code { + get; private set; + } + + #endregion + } +} diff --git a/SocketHttpListener.Portable/WebSocketFrame.cs b/SocketHttpListener.Portable/WebSocketFrame.cs new file mode 100644 index 000000000..44fa4a5dc --- /dev/null +++ b/SocketHttpListener.Portable/WebSocketFrame.cs @@ -0,0 +1,578 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace SocketHttpListener +{ + internal class WebSocketFrame : IEnumerable + { + #region Private Fields + + private byte[] _extPayloadLength; + private Fin _fin; + private Mask _mask; + private byte[] _maskingKey; + private Opcode _opcode; + private PayloadData _payloadData; + private byte _payloadLength; + private Rsv _rsv1; + private Rsv _rsv2; + private Rsv _rsv3; + + #endregion + + #region Internal Fields + + internal static readonly byte[] EmptyUnmaskPingData; + + #endregion + + #region Static Constructor + + static WebSocketFrame() + { + EmptyUnmaskPingData = CreatePingFrame(Mask.Unmask).ToByteArray(); + } + + #endregion + + #region Private Constructors + + private WebSocketFrame() + { + } + + #endregion + + #region Internal Constructors + + internal WebSocketFrame(Opcode opcode, PayloadData payload) + : this(Fin.Final, opcode, Mask.Mask, payload, false) + { + } + + internal WebSocketFrame(Opcode opcode, Mask mask, PayloadData payload) + : this(Fin.Final, opcode, mask, payload, false) + { + } + + internal WebSocketFrame(Fin fin, Opcode opcode, Mask mask, PayloadData payload) + : this(fin, opcode, mask, payload, false) + { + } + + internal WebSocketFrame( + Fin fin, Opcode opcode, Mask mask, PayloadData payload, bool compressed) + { + _fin = fin; + _rsv1 = isData(opcode) && compressed ? Rsv.On : Rsv.Off; + _rsv2 = Rsv.Off; + _rsv3 = Rsv.Off; + _opcode = opcode; + _mask = mask; + + var len = payload.Length; + if (len < 126) + { + _payloadLength = (byte)len; + _extPayloadLength = new byte[0]; + } + else if (len < 0x010000) + { + _payloadLength = (byte)126; + _extPayloadLength = ((ushort)len).ToByteArrayInternally(ByteOrder.Big); + } + else + { + _payloadLength = (byte)127; + _extPayloadLength = len.ToByteArrayInternally(ByteOrder.Big); + } + + if (mask == Mask.Mask) + { + _maskingKey = createMaskingKey(); + payload.Mask(_maskingKey); + } + else + { + _maskingKey = new byte[0]; + } + + _payloadData = payload; + } + + #endregion + + #region Public Properties + + public byte[] ExtendedPayloadLength + { + get + { + return _extPayloadLength; + } + } + + public Fin Fin + { + get + { + return _fin; + } + } + + public bool IsBinary + { + get + { + return _opcode == Opcode.Binary; + } + } + + public bool IsClose + { + get + { + return _opcode == Opcode.Close; + } + } + + public bool IsCompressed + { + get + { + return _rsv1 == Rsv.On; + } + } + + public bool IsContinuation + { + get + { + return _opcode == Opcode.Cont; + } + } + + public bool IsControl + { + get + { + return _opcode == Opcode.Close || _opcode == Opcode.Ping || _opcode == Opcode.Pong; + } + } + + public bool IsData + { + get + { + return _opcode == Opcode.Binary || _opcode == Opcode.Text; + } + } + + public bool IsFinal + { + get + { + return _fin == Fin.Final; + } + } + + public bool IsFragmented + { + get + { + return _fin == Fin.More || _opcode == Opcode.Cont; + } + } + + public bool IsMasked + { + get + { + return _mask == Mask.Mask; + } + } + + public bool IsPerMessageCompressed + { + get + { + return (_opcode == Opcode.Binary || _opcode == Opcode.Text) && _rsv1 == Rsv.On; + } + } + + public bool IsPing + { + get + { + return _opcode == Opcode.Ping; + } + } + + public bool IsPong + { + get + { + return _opcode == Opcode.Pong; + } + } + + public bool IsText + { + get + { + return _opcode == Opcode.Text; + } + } + + public ulong Length + { + get + { + return 2 + (ulong)(_extPayloadLength.Length + _maskingKey.Length) + _payloadData.Length; + } + } + + public Mask Mask + { + get + { + return _mask; + } + } + + public byte[] MaskingKey + { + get + { + return _maskingKey; + } + } + + public Opcode Opcode + { + get + { + return _opcode; + } + } + + public PayloadData PayloadData + { + get + { + return _payloadData; + } + } + + public byte PayloadLength + { + get + { + return _payloadLength; + } + } + + public Rsv Rsv1 + { + get + { + return _rsv1; + } + } + + public Rsv Rsv2 + { + get + { + return _rsv2; + } + } + + public Rsv Rsv3 + { + get + { + return _rsv3; + } + } + + #endregion + + #region Private Methods + + private byte[] createMaskingKey() + { + var key = new byte[4]; + var rand = new Random(); + rand.NextBytes(key); + + return key; + } + + private static bool isControl(Opcode opcode) + { + return opcode == Opcode.Close || opcode == Opcode.Ping || opcode == Opcode.Pong; + } + + private static bool isData(Opcode opcode) + { + return opcode == Opcode.Text || opcode == Opcode.Binary; + } + + private static WebSocketFrame read(byte[] header, Stream stream, bool unmask) + { + /* Header */ + + // FIN + var fin = (header[0] & 0x80) == 0x80 ? Fin.Final : Fin.More; + // RSV1 + var rsv1 = (header[0] & 0x40) == 0x40 ? Rsv.On : Rsv.Off; + // RSV2 + var rsv2 = (header[0] & 0x20) == 0x20 ? Rsv.On : Rsv.Off; + // RSV3 + var rsv3 = (header[0] & 0x10) == 0x10 ? Rsv.On : Rsv.Off; + // Opcode + var opcode = (Opcode)(header[0] & 0x0f); + // MASK + var mask = (header[1] & 0x80) == 0x80 ? Mask.Mask : Mask.Unmask; + // Payload Length + var payloadLen = (byte)(header[1] & 0x7f); + + // Check if correct frame. + var incorrect = isControl(opcode) && fin == Fin.More + ? "A control frame is fragmented." + : !isData(opcode) && rsv1 == Rsv.On + ? "A non data frame is compressed." + : null; + + if (incorrect != null) + throw new WebSocketException(CloseStatusCode.IncorrectData, incorrect); + + // Check if consistent frame. + if (isControl(opcode) && payloadLen > 125) + throw new WebSocketException( + CloseStatusCode.InconsistentData, + "The length of payload data of a control frame is greater than 125 bytes."); + + var frame = new WebSocketFrame(); + frame._fin = fin; + frame._rsv1 = rsv1; + frame._rsv2 = rsv2; + frame._rsv3 = rsv3; + frame._opcode = opcode; + frame._mask = mask; + frame._payloadLength = payloadLen; + + /* Extended Payload Length */ + + var size = payloadLen < 126 + ? 0 + : payloadLen == 126 + ? 2 + : 8; + + var extPayloadLen = size > 0 ? stream.ReadBytes(size) : new byte[0]; + if (size > 0 && extPayloadLen.Length != size) + throw new WebSocketException( + "The 'Extended Payload Length' of a frame cannot be read from the data source."); + + frame._extPayloadLength = extPayloadLen; + + /* Masking Key */ + + var masked = mask == Mask.Mask; + var maskingKey = masked ? stream.ReadBytes(4) : new byte[0]; + if (masked && maskingKey.Length != 4) + throw new WebSocketException( + "The 'Masking Key' of a frame cannot be read from the data source."); + + frame._maskingKey = maskingKey; + + /* Payload Data */ + + ulong len = payloadLen < 126 + ? payloadLen + : payloadLen == 126 + ? extPayloadLen.ToUInt16(ByteOrder.Big) + : extPayloadLen.ToUInt64(ByteOrder.Big); + + byte[] data = null; + if (len > 0) + { + // Check if allowable payload data length. + if (payloadLen > 126 && len > PayloadData.MaxLength) + throw new WebSocketException( + CloseStatusCode.TooBig, + "The length of 'Payload Data' of a frame is greater than the allowable length."); + + data = payloadLen > 126 + ? stream.ReadBytes((long)len, 1024) + : stream.ReadBytes((int)len); + + //if (data.LongLength != (long)len) + // throw new WebSocketException( + // "The 'Payload Data' of a frame cannot be read from the data source."); + } + else + { + data = new byte[0]; + } + + var payload = new PayloadData(data, masked); + if (masked && unmask) + { + payload.Mask(maskingKey); + frame._mask = Mask.Unmask; + frame._maskingKey = new byte[0]; + } + + frame._payloadData = payload; + return frame; + } + + #endregion + + #region Internal Methods + + internal static WebSocketFrame CreateCloseFrame(Mask mask, byte[] data) + { + return new WebSocketFrame(Opcode.Close, mask, new PayloadData(data)); + } + + internal static WebSocketFrame CreateCloseFrame(Mask mask, PayloadData payload) + { + return new WebSocketFrame(Opcode.Close, mask, payload); + } + + internal static WebSocketFrame CreateCloseFrame(Mask mask, CloseStatusCode code, string reason) + { + return new WebSocketFrame( + Opcode.Close, mask, new PayloadData(((ushort)code).Append(reason))); + } + + internal static WebSocketFrame CreatePingFrame(Mask mask) + { + return new WebSocketFrame(Opcode.Ping, mask, new PayloadData()); + } + + internal static WebSocketFrame CreatePingFrame(Mask mask, byte[] data) + { + return new WebSocketFrame(Opcode.Ping, mask, new PayloadData(data)); + } + + internal static WebSocketFrame CreatePongFrame(Mask mask, PayloadData payload) + { + return new WebSocketFrame(Opcode.Pong, mask, payload); + } + + internal static WebSocketFrame CreateWebSocketFrame( + Fin fin, Opcode opcode, Mask mask, byte[] data, bool compressed) + { + return new WebSocketFrame(fin, opcode, mask, new PayloadData(data), compressed); + } + + internal static WebSocketFrame Read(Stream stream) + { + return Read(stream, true); + } + + internal static WebSocketFrame Read(Stream stream, bool unmask) + { + var header = stream.ReadBytes(2); + if (header.Length != 2) + throw new WebSocketException( + "The header part of a frame cannot be read from the data source."); + + return read(header, stream, unmask); + } + + internal static async void ReadAsync( + Stream stream, bool unmask, Action completed, Action error) + { + try + { + var header = await stream.ReadBytesAsync(2).ConfigureAwait(false); + if (header.Length != 2) + throw new WebSocketException( + "The header part of a frame cannot be read from the data source."); + + var frame = read(header, stream, unmask); + if (completed != null) + completed(frame); + } + catch (Exception ex) + { + if (error != null) + { + error(ex); + } + } + } + + #endregion + + #region Public Methods + + public IEnumerator GetEnumerator() + { + foreach (var b in ToByteArray()) + yield return b; + } + + public void Print(bool dumped) + { + //Console.WriteLine(dumped ? dump(this) : print(this)); + } + + public byte[] ToByteArray() + { + using (var buff = new MemoryStream()) + { + var header = (int)_fin; + header = (header << 1) + (int)_rsv1; + header = (header << 1) + (int)_rsv2; + header = (header << 1) + (int)_rsv3; + header = (header << 4) + (int)_opcode; + header = (header << 1) + (int)_mask; + header = (header << 7) + (int)_payloadLength; + buff.Write(((ushort)header).ToByteArrayInternally(ByteOrder.Big), 0, 2); + + if (_payloadLength > 125) + buff.Write(_extPayloadLength, 0, _extPayloadLength.Length); + + if (_mask == Mask.Mask) + buff.Write(_maskingKey, 0, _maskingKey.Length); + + if (_payloadLength > 0) + { + var payload = _payloadData.ToByteArray(); + if (_payloadLength < 127) + buff.Write(payload, 0, payload.Length); + else + buff.WriteBytes(payload); + } + + return buff.ToArray(); + } + } + + public override string ToString() + { + return BitConverter.ToString(ToByteArray()); + } + + #endregion + + #region Explicitly Implemented Interface Members + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} \ No newline at end of file diff --git a/SocketHttpListener.Portable/WebSocketState.cs b/SocketHttpListener.Portable/WebSocketState.cs new file mode 100644 index 000000000..73b3a49dd --- /dev/null +++ b/SocketHttpListener.Portable/WebSocketState.cs @@ -0,0 +1,35 @@ +namespace SocketHttpListener +{ + /// + /// Contains the values of the state of the WebSocket connection. + /// + /// + /// The values of the state are defined in + /// The WebSocket + /// API. + /// + public enum WebSocketState : ushort + { + /// + /// Equivalent to numeric value 0. + /// Indicates that the connection has not yet been established. + /// + Connecting = 0, + /// + /// Equivalent to numeric value 1. + /// Indicates that the connection is established and the communication is possible. + /// + Open = 1, + /// + /// Equivalent to numeric value 2. + /// Indicates that the connection is going through the closing handshake or + /// the WebSocket.Close method has been invoked. + /// + Closing = 2, + /// + /// Equivalent to numeric value 3. + /// Indicates that the connection has been closed or couldn't be opened. + /// + Closed = 3 + } +} diff --git a/SocketHttpListener.Portable/packages.config b/SocketHttpListener.Portable/packages.config new file mode 100644 index 000000000..2aae715b5 --- /dev/null +++ b/SocketHttpListener.Portable/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/SocketHttpListener.Portable/project.json b/SocketHttpListener.Portable/project.json new file mode 100644 index 000000000..fbbe9eaf3 --- /dev/null +++ b/SocketHttpListener.Portable/project.json @@ -0,0 +1,17 @@ +{ + "frameworks":{ + "netstandard1.6":{ + "dependencies":{ + "NETStandard.Library":"1.6.0", + } + }, + ".NETPortable,Version=v4.5,Profile=Profile7":{ + "buildOptions": { + "define": [ ] + }, + "frameworkAssemblies":{ + + } + } + } +} \ No newline at end of file -- cgit v1.2.3 From c035f2baa1f3537d298a6559d15bd7ca40188e5d Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sat, 12 Nov 2016 01:58:50 -0500 Subject: update responses --- Emby.Server.Core/ApplicationHost.cs | 8 +- .../HttpServer/HttpListenerHost.cs | 17 +- .../HttpServer/HttpResultFactory.cs | 91 ++++------ .../SocketSharp/WebSocketSharpResponse.cs | 7 - .../HttpServer/StreamWriter.cs | 12 +- MediaBrowser.Api/MediaBrowser.Api.csproj | 1 + .../Playback/StaticRemoteStreamWriter.cs | 12 +- MediaBrowser.Api/TestService.cs | 77 ++++++++ MediaBrowser.Model/Services/IHttpResult.cs | 5 - MediaBrowser.Model/Services/IRequest.cs | 16 -- ServiceStack/Host/ContentTypes.cs | 20 +-- ServiceStack/Host/HttpResponseStreamWrapper.cs | 95 ---------- ServiceStack/Host/ServiceController.cs | 3 - ServiceStack/HttpResponseExtensionsInternal.cs | 90 ++-------- ServiceStack/HttpResult.cs | 193 +-------------------- ServiceStack/ServiceStack.csproj | 1 - .../Net/EndPointListener.cs | 3 +- SocketHttpListener.Portable/Net/HttpConnection.cs | 10 +- SocketHttpListener.Portable/Net/HttpListener.cs | 11 -- .../Net/HttpListenerContext.cs | 1 - SocketHttpListener.Portable/Net/ResponseStream.cs | 37 +--- 21 files changed, 174 insertions(+), 536 deletions(-) create mode 100644 MediaBrowser.Api/TestService.cs delete mode 100644 ServiceStack/Host/HttpResponseStreamWrapper.cs (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Server.Core/ApplicationHost.cs b/Emby.Server.Core/ApplicationHost.cs index 9e0aee325..0c0ef894e 100644 --- a/Emby.Server.Core/ApplicationHost.cs +++ b/Emby.Server.Core/ApplicationHost.cs @@ -554,7 +554,7 @@ namespace Emby.Server.Core ZipClient = new ZipClient(FileSystemManager); RegisterSingleInstance(ZipClient); - RegisterSingleInstance(new HttpResultFactory(LogManager, FileSystemManager, JsonSerializer, XmlSerializer)); + RegisterSingleInstance(new HttpResultFactory(LogManager, FileSystemManager, JsonSerializer, MemoryStreamFactory)); RegisterSingleInstance(this); RegisterSingleInstance(ApplicationPaths); @@ -614,6 +614,9 @@ namespace Emby.Server.Core RegisterSingleInstance(() => new SearchEngine(LogManager, LibraryManager, UserManager)); + CertificatePath = GetCertificatePath(true); + Certificate = GetCertificate(CertificatePath); + HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamFactory, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate); HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); RegisterSingleInstance(HttpServer, false); @@ -995,9 +998,6 @@ namespace Emby.Server.Core /// private void StartServer() { - CertificatePath = GetCertificatePath(true); - Certificate = GetCertificate(CertificatePath); - try { ServerManager.Start(GetUrlPrefixes()); diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 64f498b12..49c664eec 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -86,9 +86,7 @@ namespace Emby.Server.Implementations.HttpServer public string GlobalResponse { get; set; } - public override void Configure() - { - var mapExceptionToStatusCode = new Dictionary + readonly Dictionary _mapExceptionToStatusCode = new Dictionary { {typeof (InvalidOperationException), 500}, {typeof (NotImplementedException), 500}, @@ -102,6 +100,8 @@ namespace Emby.Server.Implementations.HttpServer {typeof (NotSupportedException), 500} }; + public override void Configure() + { var requestFilters = _appHost.GetExports().ToList(); foreach (var filter in requestFilters) { @@ -240,7 +240,12 @@ namespace Emby.Server.Implementations.HttpServer return; } - httpRes.StatusCode = 500; + int statusCode; + if (!_mapExceptionToStatusCode.TryGetValue(ex.GetType(), out statusCode)) + { + statusCode = 500; + } + httpRes.StatusCode = statusCode; httpRes.ContentType = "text/html"; httpRes.Write(ex.Message); @@ -518,6 +523,10 @@ namespace Emby.Server.Implementations.HttpServer { await handler.ProcessRequestAsync(httpReq, httpRes, operationName).ConfigureAwait(false); } + else + { + ErrorHandler(new FileNotFoundException(), httpReq); + } } catch (Exception ex) { diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index bbd556661..f65331ec7 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -34,19 +34,16 @@ namespace Emby.Server.Implementations.HttpServer private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; - private readonly IXmlSerializer _xmlSerializer; + private readonly IMemoryStreamFactory _memoryStreamFactory; /// /// Initializes a new instance of the class. /// - /// The log manager. - /// The file system. - /// The json serializer. - public HttpResultFactory(ILogManager logManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer) + public HttpResultFactory(ILogManager logManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IMemoryStreamFactory memoryStreamFactory) { _fileSystem = fileSystem; _jsonSerializer = jsonSerializer; - _xmlSerializer = xmlSerializer; + _memoryStreamFactory = memoryStreamFactory; _logger = logManager.GetLogger("HttpResultFactory"); } @@ -59,17 +56,13 @@ namespace Emby.Server.Implementations.HttpServer /// System.Object. public object GetResult(object content, string contentType, IDictionary responseHeaders = null) { - return GetHttpResult(content, contentType, responseHeaders); + return GetHttpResult(content, contentType, true, responseHeaders); } /// /// Gets the HTTP result. /// - /// The content. - /// Type of the content. - /// The response headers. - /// IHasHeaders. - private IHasHeaders GetHttpResult(object content, string contentType, IDictionary responseHeaders = null) + private IHasHeaders GetHttpResult(object content, string contentType, bool addCachePrevention, IDictionary responseHeaders = null) { IHasHeaders result; @@ -98,7 +91,7 @@ namespace Emby.Server.Implementations.HttpServer } else { - result = new HttpResult(content, contentType); + result = new HttpResult(content, contentType, HttpStatusCode.OK); } } } @@ -107,7 +100,11 @@ namespace Emby.Server.Implementations.HttpServer responseHeaders = new Dictionary(); } - responseHeaders["Expires"] = "-1"; + if (addCachePrevention) + { + responseHeaders["Expires"] = "-1"; + } + AddResponseHeaders(result, responseHeaders); return result; @@ -184,8 +181,6 @@ namespace Emby.Server.Implementations.HttpServer /// public object ToOptimizedResult(IRequest request, T dto) { - request.Response.Dto = dto; - var compressionType = GetCompressionType(request); if (compressionType == null) { @@ -204,6 +199,7 @@ namespace Emby.Server.Implementations.HttpServer } } + // Do not use the memoryStreamFactory here, they don't place nice with compression using (var ms = new MemoryStream()) { using (var compressionStream = GetCompressionStream(ms, compressionType)) @@ -213,12 +209,9 @@ namespace Emby.Server.Implementations.HttpServer var compressedBytes = ms.ToArray(); - var httpResult = new HttpResult(compressedBytes, request.ResponseContentType) - { - Status = request.Response.StatusCode - }; + var httpResult = new StreamWriter(compressedBytes, request.ResponseContentType, _logger); - httpResult.Headers["Content-Length"] = compressedBytes.Length.ToString(UsCulture); + //httpResult.Headers["Content-Length"] = compressedBytes.Length.ToString(UsCulture); httpResult.Headers["Content-Encoding"] = compressionType; return httpResult; @@ -226,6 +219,16 @@ namespace Emby.Server.Implementations.HttpServer } } + private static Stream GetCompressionStream(Stream outputStream, string compressionType) + { + if (compressionType == "deflate") + return new DeflateStream(outputStream, CompressionMode.Compress, true); + if (compressionType == "gzip") + return new GZipStream(outputStream, CompressionMode.Compress, true); + + throw new NotSupportedException(compressionType); + } + public static string GetRealContentType(string contentType) { return contentType == null @@ -233,7 +236,7 @@ namespace Emby.Server.Implementations.HttpServer : contentType.Split(';')[0].ToLower().Trim(); } - public static string SerializeToXmlString(object from) + private string SerializeToXmlString(object from) { using (var ms = new MemoryStream()) { @@ -253,16 +256,6 @@ namespace Emby.Server.Implementations.HttpServer } } - private static Stream GetCompressionStream(Stream outputStream, string compressionType) - { - if (compressionType == "deflate") - return new DeflateStream(outputStream, CompressionMode.Compress); - if (compressionType == "gzip") - return new GZipStream(outputStream, CompressionMode.Compress); - - throw new NotSupportedException(compressionType); - } - /// /// Gets the optimized result using cache. /// @@ -358,23 +351,7 @@ namespace Emby.Server.Implementations.HttpServer return hasHeaders; } - IHasHeaders httpResult; - - var stream = result as Stream; - - if (stream != null) - { - httpResult = new StreamWriter(stream, contentType, _logger); - } - else - { - // Otherwise wrap into an HttpResult - httpResult = new HttpResult(result, contentType ?? "text/html", HttpStatusCode.NotModified); - } - - AddResponseHeaders(httpResult, responseHeaders); - - return httpResult; + return GetHttpResult(result, contentType, false, responseHeaders); } /// @@ -603,7 +580,7 @@ namespace Emby.Server.Implementations.HttpServer { stream.Dispose(); - return GetHttpResult(new byte[] { }, contentType); + return GetHttpResult(new byte[] { }, contentType, true); } return new StreamWriter(stream, contentType, _logger) @@ -630,13 +607,13 @@ namespace Emby.Server.Implementations.HttpServer if (isHeadRequest) { - return GetHttpResult(new byte[] { }, contentType); + return GetHttpResult(new byte[] { }, contentType, true); } - return GetHttpResult(contents, contentType, responseHeaders); + return GetHttpResult(contents, contentType, true, responseHeaders); } - public static byte[] Compress(string text, string compressionType) + private byte[] Compress(string text, string compressionType) { if (compressionType == "deflate") return Deflate(text); @@ -647,12 +624,12 @@ namespace Emby.Server.Implementations.HttpServer throw new NotSupportedException(compressionType); } - public static byte[] Deflate(string text) + private byte[] Deflate(string text) { return Deflate(Encoding.UTF8.GetBytes(text)); } - public static byte[] Deflate(byte[] bytes) + private byte[] Deflate(byte[] bytes) { // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream @@ -666,12 +643,12 @@ namespace Emby.Server.Implementations.HttpServer } } - public static byte[] GZip(string text) + private byte[] GZip(string text) { return GZip(Encoding.UTF8.GetBytes(text)); } - public static byte[] GZip(byte[] buffer) + private byte[] GZip(byte[] buffer) { using (var ms = new MemoryStream()) using (var zipStream = new GZipStream(ms, CompressionMode.Compress)) diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs index de0b33fe3..9de86e9cc 100644 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs @@ -77,8 +77,6 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp get { return _response.OutputStream; } } - public object Dto { get; set; } - public void Write(string text) { var bOutput = System.Text.Encoding.UTF8.GetBytes(text); @@ -120,11 +118,6 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp } } - public void End() - { - Close(); - } - public void Flush() { _response.OutputStream.Flush(); diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs index 15488abaa..33378949c 100644 --- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs +++ b/Emby.Server.Implementations/HttpServer/StreamWriter.cs @@ -25,6 +25,8 @@ namespace Emby.Server.Implementations.HttpServer /// The source stream. private Stream SourceStream { get; set; } + private byte[] SourceBytes { get; set; } + /// /// The _options /// @@ -40,7 +42,6 @@ namespace Emby.Server.Implementations.HttpServer public Action OnComplete { get; set; } public Action OnError { get; set; } - private readonly byte[] _bytes; /// /// Initializes a new instance of the class. @@ -73,14 +74,13 @@ namespace Emby.Server.Implementations.HttpServer /// Type of the content. /// The logger. public StreamWriter(byte[] source, string contentType, ILogger logger) - : this(new MemoryStream(source), contentType, logger) { if (string.IsNullOrEmpty(contentType)) { throw new ArgumentNullException("contentType"); } - _bytes = source; + SourceBytes = source; Logger = logger; Headers["Content-Type"] = contentType; @@ -92,9 +92,11 @@ namespace Emby.Server.Implementations.HttpServer { try { - if (_bytes != null) + var bytes = SourceBytes; + + if (bytes != null) { - await responseStream.WriteAsync(_bytes, 0, _bytes.Length); + await responseStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); } else { diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index df491ce85..c1a7347b5 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -142,6 +142,7 @@ + diff --git a/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs b/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs index c4a25a926..6bb3b6b80 100644 --- a/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs +++ b/MediaBrowser.Api/Playback/StaticRemoteStreamWriter.cs @@ -1,6 +1,8 @@ using MediaBrowser.Common.Net; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using MediaBrowser.Model.Services; namespace MediaBrowser.Api.Playback @@ -8,7 +10,7 @@ namespace MediaBrowser.Api.Playback /// /// Class StaticRemoteStreamWriter /// - public class StaticRemoteStreamWriter : IStreamWriter, IHasHeaders + public class StaticRemoteStreamWriter : IAsyncStreamWriter, IHasHeaders { /// /// The _input stream @@ -34,15 +36,11 @@ namespace MediaBrowser.Api.Playback get { return _options; } } - /// - /// Writes to. - /// - /// The response stream. - public void WriteTo(Stream responseStream) + public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) { using (_response) { - _response.Content.CopyTo(responseStream, 819200); + await _response.Content.CopyToAsync(responseStream, 81920, cancellationToken).ConfigureAwait(false); } } } diff --git a/MediaBrowser.Api/TestService.cs b/MediaBrowser.Api/TestService.cs new file mode 100644 index 000000000..5340b816c --- /dev/null +++ b/MediaBrowser.Api/TestService.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.Services; + +namespace MediaBrowser.Api +{ + [Route("/Test/String", "GET")] + public class GetString + { + } + + [Route("/Test/OptimizedString", "GET")] + public class GetOptimizedString + { + } + + [Route("/Test/Bytes", "GET")] + public class GetBytes + { + } + + [Route("/Test/OptimizedBytes", "GET")] + public class GetOptimizedBytes + { + } + + [Route("/Test/Stream", "GET")] + public class GetStream + { + } + + [Route("/Test/OptimizedStream", "GET")] + public class GetOptimizedStream + { + } + + [Route("/Test/BytesWithContentType", "GET")] + public class GetBytesWithContentType + { + } + + public class TestService : BaseApiService + { + public object Get(GetString request) + { + return "Welcome to Emby!"; + } + public object Get(GetOptimizedString request) + { + return ToOptimizedResult("Welcome to Emby!"); + } + public object Get(GetBytes request) + { + return Encoding.UTF8.GetBytes("Welcome to Emby!"); + } + public object Get(GetOptimizedBytes request) + { + return ToOptimizedResult(Encoding.UTF8.GetBytes("Welcome to Emby!")); + } + public object Get(GetBytesWithContentType request) + { + return ApiEntryPoint.Instance.ResultFactory.GetResult(Encoding.UTF8.GetBytes("Welcome to Emby!"), "text/html"); + } + public object Get(GetStream request) + { + return new MemoryStream(Encoding.UTF8.GetBytes("Welcome to Emby!")); + } + public object Get(GetOptimizedStream request) + { + return ToOptimizedResult(new MemoryStream(Encoding.UTF8.GetBytes("Welcome to Emby!"))); + } + } +} diff --git a/MediaBrowser.Model/Services/IHttpResult.cs b/MediaBrowser.Model/Services/IHttpResult.cs index 36ffeb284..fcb137c6b 100644 --- a/MediaBrowser.Model/Services/IHttpResult.cs +++ b/MediaBrowser.Model/Services/IHttpResult.cs @@ -19,11 +19,6 @@ namespace MediaBrowser.Model.Services /// HttpStatusCode StatusCode { get; set; } - /// - /// The HTTP Status Description - /// - string StatusDescription { get; set; } - /// /// The HTTP Response ContentType /// diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs index 5a4d24007..455a69d37 100644 --- a/MediaBrowser.Model/Services/IRequest.cs +++ b/MediaBrowser.Model/Services/IRequest.cs @@ -136,34 +136,18 @@ namespace MediaBrowser.Model.Services Stream OutputStream { get; } - /// - /// The Response DTO - /// - object Dto { get; set; } - /// /// Write once to the Response Stream then close it. /// /// void Write(string text); - /// - /// Buffer the Response OutputStream so it can be written in 1 batch - /// - bool UseBufferedStream { get; set; } - /// /// Signal that this response has been handled and no more processing should be done. /// When used in a request or response filter, no more filters or processing is done on this request. /// void Close(); - /// - /// Calls Response.End() on ASP.NET HttpResponse otherwise is an alias for Close(). - /// Useful when you want to prevent ASP.NET to provide it's own custom error page. - /// - void End(); - /// /// Response.Flush() and OutputStream.Flush() seem to have different behaviour in ASP.NET /// diff --git a/ServiceStack/Host/ContentTypes.cs b/ServiceStack/Host/ContentTypes.cs index 22fdc3e50..58ba29801 100644 --- a/ServiceStack/Host/ContentTypes.cs +++ b/ServiceStack/Host/ContentTypes.cs @@ -13,37 +13,31 @@ namespace ServiceStack.Host public void SerializeToStream(IRequest req, object response, Stream responseStream) { var contentType = req.ResponseContentType; - var serializer = GetResponseSerializer(contentType); - if (serializer == null) - throw new NotSupportedException("ContentType not supported: " + contentType); + var serializer = GetStreamSerializer(contentType); - var httpRes = new HttpResponseStreamWrapper(responseStream, req) - { - Dto = req.Response.Dto - }; - serializer(req, response, httpRes); + serializer(response, responseStream); } - public Action GetResponseSerializer(string contentType) + public Action GetResponseSerializer(string contentType) { var serializer = GetStreamSerializer(contentType); if (serializer == null) return null; - return (httpReq, dto, httpRes) => serializer(httpReq, dto, httpRes.OutputStream); + return (dto, httpRes) => serializer(dto, httpRes.OutputStream); } - public Action GetStreamSerializer(string contentType) + public Action GetStreamSerializer(string contentType) { switch (GetRealContentType(contentType)) { case "application/xml": case "text/xml": case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return (r, o, s) => ServiceStackHost.Instance.SerializeToXml(o, s); + return (o, s) => ServiceStackHost.Instance.SerializeToXml(o, s); case "application/json": case "text/json": - return (r, o, s) => ServiceStackHost.Instance.SerializeToJson(o, s); + return (o, s) => ServiceStackHost.Instance.SerializeToJson(o, s); } return null; diff --git a/ServiceStack/Host/HttpResponseStreamWrapper.cs b/ServiceStack/Host/HttpResponseStreamWrapper.cs deleted file mode 100644 index 33038da72..000000000 --- a/ServiceStack/Host/HttpResponseStreamWrapper.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Text; -using MediaBrowser.Model.Services; - -namespace ServiceStack.Host -{ - public class HttpResponseStreamWrapper : IHttpResponse - { - private static readonly UTF8Encoding UTF8EncodingWithoutBom = new UTF8Encoding(false); - - public HttpResponseStreamWrapper(Stream stream, IRequest request) - { - this.OutputStream = stream; - this.Request = request; - this.Headers = new Dictionary(); - this.Items = new Dictionary(); - } - - public Dictionary Headers { get; set; } - - public object OriginalResponse - { - get { return null; } - } - - public IRequest Request { get; private set; } - - public int StatusCode { set; get; } - public string StatusDescription { set; get; } - public string ContentType { get; set; } - - public void AddHeader(string name, string value) - { - this.Headers[name] = value; - } - - public string GetHeader(string name) - { - return this.Headers[name]; - } - - public void Redirect(string url) - { - this.Headers["Location"] = url; - } - - public Stream OutputStream { get; private set; } - - public object Dto { get; set; } - - public void Write(string text) - { - var bytes = UTF8EncodingWithoutBom.GetBytes(text); - OutputStream.Write(bytes, 0, bytes.Length); - } - - public bool UseBufferedStream { get; set; } - - public void Close() - { - if (IsClosed) return; - - OutputStream.Dispose(); - IsClosed = true; - } - - public void End() - { - Close(); - } - - public void Flush() - { - OutputStream.Flush(); - } - - public bool IsClosed { get; private set; } - - public void SetContentLength(long contentLength) {} - - public bool KeepAlive { get; set; } - - public Dictionary Items { get; private set; } - - public void SetCookie(Cookie cookie) - { - } - - public void ClearCookies() - { - } - } -} \ No newline at end of file diff --git a/ServiceStack/Host/ServiceController.cs b/ServiceStack/Host/ServiceController.cs index 703f06365..7eb1253b3 100644 --- a/ServiceStack/Host/ServiceController.cs +++ b/ServiceStack/Host/ServiceController.cs @@ -210,9 +210,6 @@ namespace ServiceStack.Host //Executes the service and returns the result var response = await ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetOperationName()).ConfigureAwait(false); - if (req.Response.Dto == null) - req.Response.Dto = response; - return response; } } diff --git a/ServiceStack/HttpResponseExtensionsInternal.cs b/ServiceStack/HttpResponseExtensionsInternal.cs index 1195f63ca..88b82bdf6 100644 --- a/ServiceStack/HttpResponseExtensionsInternal.cs +++ b/ServiceStack/HttpResponseExtensionsInternal.cs @@ -33,8 +33,11 @@ namespace ServiceStack var stream = result as Stream; if (stream != null) { - WriteTo(stream, response.OutputStream); - return true; + using (stream) + { + await stream.CopyToAsync(response.OutputStream).ConfigureAwait(false); + return true; + } } var bytes = result as byte[]; @@ -43,35 +46,13 @@ namespace ServiceStack response.ContentType = "application/octet-stream"; response.SetContentLength(bytes.Length); - response.OutputStream.Write(bytes, 0, bytes.Length); + await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); return true; } return false; } - public static long WriteTo(Stream inStream, Stream outStream) - { - var memoryStream = inStream as MemoryStream; - if (memoryStream != null) - { - memoryStream.WriteTo(outStream); - return memoryStream.Position; - } - - var data = new byte[4096]; - long total = 0; - int bytesRead; - - while ((bytesRead = inStream.Read(data, 0, data.Length)) > 0) - { - outStream.Write(data, 0, bytesRead); - total += bytesRead; - } - - return total; - } - /// /// End a ServiceStack Request with no content /// @@ -85,7 +66,7 @@ namespace ServiceStack httpRes.SetContentLength(0); } - public static Task WriteToResponse(this IResponse httpRes, MediaBrowser.Model.Services.IRequest httpReq, object result) + public static Task WriteToResponse(this IResponse httpRes, IRequest httpReq, object result) { if (result == null) { @@ -98,19 +79,10 @@ namespace ServiceStack { httpResult.RequestContext = httpReq; httpReq.ResponseContentType = httpResult.ContentType ?? httpReq.ResponseContentType; - var httpResSerializer = ContentTypes.Instance.GetResponseSerializer(httpReq.ResponseContentType); - return httpRes.WriteToResponse(httpResult, httpResSerializer, httpReq); + return httpRes.WriteToResponseInternal(httpResult, httpReq); } - var serializer = ContentTypes.Instance.GetResponseSerializer(httpReq.ResponseContentType); - return httpRes.WriteToResponse(result, serializer, httpReq); - } - - private static object GetDto(object response) - { - if (response == null) return null; - var httpResult = response as IHttpResult; - return httpResult != null ? httpResult.Response : response; + return httpRes.WriteToResponseInternal(result, httpReq); } /// @@ -119,17 +91,11 @@ namespace ServiceStack /// /// The response. /// Whether or not it was implicity handled by ServiceStack's built-in handlers. - /// The default action. /// The serialization context. /// - public static async Task WriteToResponse(this IResponse response, object result, Action defaultAction, MediaBrowser.Model.Services.IRequest request) + private static async Task WriteToResponseInternal(this IResponse response, object result, IRequest request) { var defaultContentType = request.ResponseContentType; - if (result == null) - { - response.EndRequestWithNoContent(); - return; - } var httpResult = result as IHttpResult; if (httpResult != null) @@ -137,10 +103,8 @@ namespace ServiceStack if (httpResult.RequestContext == null) httpResult.RequestContext = request; - response.Dto = response.Dto ?? GetDto(httpResult); - response.StatusCode = httpResult.Status; - response.StatusDescription = httpResult.StatusDescription ?? httpResult.StatusCode.ToString(); + response.StatusDescription = httpResult.StatusCode.ToString(); if (string.IsNullOrEmpty(httpResult.ContentType)) { httpResult.ContentType = defaultContentType; @@ -159,21 +123,12 @@ namespace ServiceStack } } } - else - { - response.Dto = result; - } - /* Mono Error: Exception: Method not found: 'System.Web.HttpResponse.get_Headers' */ var responseOptions = result as IHasHeaders; if (responseOptions != null) { - //Reserving options with keys in the format 'xx.xxx' (No Http headers contain a '.' so its a safe restriction) - const string reservedOptions = "."; - foreach (var responseHeaders in responseOptions.Headers) { - if (responseHeaders.Key.Contains(reservedOptions)) continue; if (responseHeaders.Key == "Content-Length") { response.SetContentLength(long.Parse(responseHeaders.Value)); @@ -196,42 +151,25 @@ namespace ServiceStack response.ContentType += "; charset=utf-8"; } - var disposableResult = result as IDisposable; var writeToOutputStreamResult = await WriteToOutputStream(response, result).ConfigureAwait(false); if (writeToOutputStreamResult) { response.Flush(); //required for Compression - if (disposableResult != null) disposableResult.Dispose(); return; } - if (httpResult != null) - result = httpResult.Response; - var responseText = result as string; if (responseText != null) { if (response.ContentType == null || response.ContentType == "text/html") response.ContentType = defaultContentType; - response.Write(responseText); + response.Write(responseText); return; } - if (defaultAction == null) - { - throw new ArgumentNullException("defaultAction", String.Format( - "As result '{0}' is not a supported responseType, a defaultAction must be supplied", - (result != null ? result.GetType().GetOperationName() : ""))); - } - - - if (result != null) - defaultAction(request, result, response); - - if (disposableResult != null) - disposableResult.Dispose(); + var serializer = ContentTypes.Instance.GetResponseSerializer(defaultContentType); + serializer(result, response); } - } } diff --git a/ServiceStack/HttpResult.cs b/ServiceStack/HttpResult.cs index 23a5cdffb..e25002b3e 100644 --- a/ServiceStack/HttpResult.cs +++ b/ServiceStack/HttpResult.cs @@ -13,31 +13,7 @@ namespace ServiceStack public class HttpResult : IHttpResult, IAsyncStreamWriter { - public HttpResult() - : this((object)null, null) - { - } - - public HttpResult(object response) - : this(response, null) - { - } - - public HttpResult(object response, string contentType) - : this(response, contentType, HttpStatusCode.OK) - { - } - - public HttpResult(HttpStatusCode statusCode, string statusDescription) - : this() - { - StatusCode = statusCode; - StatusDescription = statusDescription; - } - - public HttpResult(object response, HttpStatusCode statusCode) - : this(response, null, statusCode) - { } + public object Response { get; set; } public HttpResult(object response, string contentType, HttpStatusCode statusCode) { @@ -49,102 +25,12 @@ namespace ServiceStack this.StatusCode = statusCode; } - public HttpResult(Stream responseStream, string contentType) - : this(null, contentType, HttpStatusCode.OK) - { - this.ResponseStream = responseStream; - } - - public HttpResult(string responseText, string contentType) - : this(null, contentType, HttpStatusCode.OK) - { - this.ResponseText = responseText; - } - - public HttpResult(byte[] responseBytes, string contentType) - : this(null, contentType, HttpStatusCode.OK) - { - this.ResponseStream = new MemoryStream(responseBytes); - } - - public string ResponseText { get; private set; } - - public Stream ResponseStream { get; private set; } - public string ContentType { get; set; } public IDictionary Headers { get; private set; } public List Cookies { get; private set; } - public string ETag { get; set; } - - public TimeSpan? Age { get; set; } - - public TimeSpan? MaxAge { get; set; } - - public DateTime? Expires { get; set; } - - public DateTime? LastModified { get; set; } - - public Func ResultScope { get; set; } - - public string Location - { - set - { - if (StatusCode == HttpStatusCode.OK) - StatusCode = HttpStatusCode.Redirect; - - this.Headers["Location"] = value; - } - } - - public void SetPermanentCookie(string name, string value) - { - SetCookie(name, value, DateTime.UtcNow.AddYears(20), null); - } - - public void SetPermanentCookie(string name, string value, string path) - { - SetCookie(name, value, DateTime.UtcNow.AddYears(20), path); - } - - public void SetSessionCookie(string name, string value) - { - SetSessionCookie(name, value, null); - } - - public void SetSessionCookie(string name, string value, string path) - { - path = path ?? "/"; - this.Headers["Set-Cookie"] = string.Format("{0}={1};path=" + path, name, value); - } - - public void SetCookie(string name, string value, TimeSpan expiresIn, string path) - { - var expiresAt = DateTime.UtcNow.Add(expiresIn); - SetCookie(name, value, expiresAt, path); - } - - public void SetCookie(string name, string value, DateTime expiresAt, string path, bool secure = false, bool httpOnly = false) - { - path = path ?? "/"; - var cookie = string.Format("{0}={1};expires={2};path={3}", name, value, expiresAt.ToString("R"), path); - if (secure) - cookie += ";Secure"; - if (httpOnly) - cookie += ";HttpOnly"; - - this.Headers["Set-Cookie"] = cookie; - } - - public void DeleteCookie(string name) - { - var cookie = string.Format("{0}=;expires={1};path=/", name, DateTime.UtcNow.AddDays(-1).ToString("R")); - this.Headers["Set-Cookie"] = cookie; - } - public int Status { get; set; } public HttpStatusCode StatusCode @@ -153,75 +39,12 @@ namespace ServiceStack set { Status = (int)value; } } - public string StatusDescription { get; set; } - - public object Response { get; set; } - - public MediaBrowser.Model.Services.IRequest RequestContext { get; set; } - - public string View { get; set; } - - public string Template { get; set; } - - public int PaddingLength { get; set; } + public IRequest RequestContext { get; set; } public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) - { - try - { - await WriteToInternalAsync(responseStream, cancellationToken).ConfigureAwait(false); - responseStream.Flush(); - } - finally - { - DisposeStream(); - } - } - - public static Task WriteTo(Stream inStream, Stream outStream, CancellationToken cancellationToken) - { - var memoryStream = inStream as MemoryStream; - if (memoryStream != null) - { - memoryStream.WriteTo(outStream); - return Task.FromResult(true); - } - - return inStream.CopyToAsync(outStream, 81920, cancellationToken); - } - - public async Task WriteToInternalAsync(Stream responseStream, CancellationToken cancellationToken) { var response = RequestContext != null ? RequestContext.Response : null; - if (this.ResponseStream != null) - { - if (response != null) - { - var ms = ResponseStream as MemoryStream; - if (ms != null) - { - response.SetContentLength(ms.Length); - - await ms.CopyToAsync(responseStream, 81920, cancellationToken).ConfigureAwait(false); - return; - } - } - - await WriteTo(this.ResponseStream, responseStream, cancellationToken).ConfigureAwait(false); - return; - } - - if (this.ResponseText != null) - { - var bytes = Encoding.UTF8.GetBytes(this.ResponseText); - if (response != null) - response.SetContentLength(bytes.Length); - - await responseStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); - return; - } - var bytesResponse = this.Response as byte[]; if (bytesResponse != null) { @@ -234,17 +57,5 @@ namespace ServiceStack ContentTypes.Instance.SerializeToStream(this.RequestContext, this.Response, responseStream); } - - private void DisposeStream() - { - try - { - if (ResponseStream != null) - { - this.ResponseStream.Dispose(); - } - } - catch { /*ignore*/ } - } } } diff --git a/ServiceStack/ServiceStack.csproj b/ServiceStack/ServiceStack.csproj index 3402339a6..5413d4e55 100644 --- a/ServiceStack/ServiceStack.csproj +++ b/ServiceStack/ServiceStack.csproj @@ -73,7 +73,6 @@ - diff --git a/SocketHttpListener.Portable/Net/EndPointListener.cs b/SocketHttpListener.Portable/Net/EndPointListener.cs index b50660ad0..52385e2ba 100644 --- a/SocketHttpListener.Portable/Net/EndPointListener.cs +++ b/SocketHttpListener.Portable/Net/EndPointListener.cs @@ -119,7 +119,6 @@ namespace SocketHttpListener.Net if (listener == null) return false; - context.Listener = listener; context.Connection.Prefix = prefix; return true; } @@ -129,7 +128,7 @@ namespace SocketHttpListener.Net if (context == null || context.Request == null) return; - context.Listener.UnregisterContext(context); + listener.UnregisterContext(context); } HttpListener SearchListener(Uri uri, out ListenerPrefix prefix) diff --git a/SocketHttpListener.Portable/Net/HttpConnection.cs b/SocketHttpListener.Portable/Net/HttpConnection.cs index d31da4132..db34c4218 100644 --- a/SocketHttpListener.Portable/Net/HttpConnection.cs +++ b/SocketHttpListener.Portable/Net/HttpConnection.cs @@ -1,7 +1,6 @@ using System; using System.IO; using System.Text; -using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.IO; @@ -210,12 +209,7 @@ namespace SocketHttpListener.Net // TODO: can we get this stream before reading the input? if (o_stream == null) { - HttpListener listener = context.Listener; - - if (listener == null) - return new ResponseStream(stream, context.Response, true, _memoryStreamFactory, _textEncoding); - - o_stream = new ResponseStream(stream, context.Response, listener.IgnoreWriteExceptions, _memoryStreamFactory, _textEncoding); + o_stream = new ResponseStream(stream, context.Response, _memoryStreamFactory, _textEncoding); } return o_stream; } @@ -257,7 +251,7 @@ namespace SocketHttpListener.Net Close(true); return; } - HttpListener listener = context.Listener; + HttpListener listener = epl.Listener; if (last_listener != listener) { RemoveConnection(); diff --git a/SocketHttpListener.Portable/Net/HttpListener.cs b/SocketHttpListener.Portable/Net/HttpListener.cs index 83660100a..2b0f75d01 100644 --- a/SocketHttpListener.Portable/Net/HttpListener.cs +++ b/SocketHttpListener.Portable/Net/HttpListener.cs @@ -28,7 +28,6 @@ namespace SocketHttpListener.Net HttpListenerPrefixCollection prefixes; AuthenticationSchemeSelector auth_selector; string realm; - bool ignore_write_exceptions; bool unsafe_ntlm_auth; bool listening; bool disposed; @@ -92,16 +91,6 @@ namespace SocketHttpListener.Net } } - public bool IgnoreWriteExceptions - { - get { return ignore_write_exceptions; } - set - { - CheckDisposed(); - ignore_write_exceptions = value; - } - } - public bool IsListening { get { return listening; } diff --git a/SocketHttpListener.Portable/Net/HttpListenerContext.cs b/SocketHttpListener.Portable/Net/HttpListenerContext.cs index 84c6a8c19..182fd2d2a 100644 --- a/SocketHttpListener.Portable/Net/HttpListenerContext.cs +++ b/SocketHttpListener.Portable/Net/HttpListenerContext.cs @@ -18,7 +18,6 @@ namespace SocketHttpListener.Net HttpConnection cnc; string error; int err_status = 400; - internal HttpListener Listener; private readonly ILogger _logger; private readonly ICryptoProvider _cryptoProvider; private readonly IMemoryStreamFactory _memoryStreamFactory; diff --git a/SocketHttpListener.Portable/Net/ResponseStream.cs b/SocketHttpListener.Portable/Net/ResponseStream.cs index 6ecbf9742..07788ea41 100644 --- a/SocketHttpListener.Portable/Net/ResponseStream.cs +++ b/SocketHttpListener.Portable/Net/ResponseStream.cs @@ -17,17 +17,15 @@ namespace SocketHttpListener.Net class ResponseStream : Stream { HttpListenerResponse response; - bool ignore_errors; bool disposed; bool trailer_sent; Stream stream; private readonly IMemoryStreamFactory _memoryStreamFactory; private readonly ITextEncoding _textEncoding; - internal ResponseStream(Stream stream, HttpListenerResponse response, bool ignore_errors, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) + internal ResponseStream(Stream stream, HttpListenerResponse response, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding) { this.response = response; - this.ignore_errors = ignore_errors; _memoryStreamFactory = memoryStreamFactory; _textEncoding = textEncoding; this.stream = stream; @@ -130,18 +128,7 @@ namespace SocketHttpListener.Net internal void InternalWrite(byte[] buffer, int offset, int count) { - if (ignore_errors) - { - try - { - stream.Write(buffer, offset, count); - } - catch { } - } - else - { - stream.Write(buffer, offset, count); - } + stream.Write(buffer, offset, count); } public override void Write(byte[] buffer, int offset, int count) @@ -214,23 +201,13 @@ namespace SocketHttpListener.Net InternalWrite(bytes, 0, bytes.Length); } - try - { - if (count > 0) - { - await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); - } - - if (response.SendChunked) - stream.Write(crlf, 0, 2); - } - catch + if (count > 0) { - if (!ignore_errors) - { - throw; - } + await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); } + + if (response.SendChunked) + stream.Write(crlf, 0, 2); } //public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, -- cgit v1.2.3 From a855864207fe3ed0ac9b4d648617bb1cb39df3f3 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sat, 12 Nov 2016 02:14:04 -0500 Subject: limit access to response stream --- .../HttpServer/HttpListenerHost.cs | 22 +++++++++++++++------- .../SocketSharp/WebSocketSharpResponse.cs | 21 ++++----------------- MediaBrowser.Model/Services/IRequest.cs | 13 ------------- ServiceStack/Host/ContentTypes.cs | 10 +--------- ServiceStack/HttpResponseExtensionsInternal.cs | 22 ++++++++++++---------- 5 files changed, 32 insertions(+), 56 deletions(-) (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 49c664eec..41b7a4622 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Text; using System.Threading.Tasks; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer.SocketSharp; @@ -248,9 +249,7 @@ namespace Emby.Server.Implementations.HttpServer httpRes.StatusCode = statusCode; httpRes.ContentType = "text/html"; - httpRes.Write(ex.Message); - - httpRes.Close(); + Write(httpRes, ex.Message); } catch { @@ -404,7 +403,7 @@ namespace Emby.Server.Implementations.HttpServer { httpRes.StatusCode = 400; httpRes.ContentType = "text/plain"; - httpRes.Write("Invalid host"); + Write(httpRes, "Invalid host"); return; } @@ -458,7 +457,7 @@ namespace Emby.Server.Implementations.HttpServer if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase)) { - httpRes.Write( + Write(httpRes, "EmbyPlease update your Emby bookmark to " + newUrl + ""); return; @@ -475,7 +474,7 @@ namespace Emby.Server.Implementations.HttpServer if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase)) { - httpRes.Write( + Write(httpRes, "EmbyPlease update your Emby bookmark to " + newUrl + ""); return; @@ -513,7 +512,7 @@ namespace Emby.Server.Implementations.HttpServer { httpRes.StatusCode = 503; httpRes.ContentType = "text/html"; - httpRes.Write(GlobalResponse); + Write(httpRes, GlobalResponse); return; } @@ -547,6 +546,15 @@ namespace Emby.Server.Implementations.HttpServer } } + private void Write(IResponse response, string text) + { + var bOutput = Encoding.UTF8.GetBytes(text); + response.SetContentLength(bOutput.Length); + + var outputStream = response.OutputStream; + outputStream.Write(bOutput, 0, bOutput.Length); + } + public static void RedirectToUrl(IResponse httpRes, string url) { httpRes.StatusCode = 302; diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs index 9de86e9cc..a8b115056 100644 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs @@ -77,16 +77,6 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp get { return _response.OutputStream; } } - public void Write(string text) - { - var bOutput = System.Text.Encoding.UTF8.GetBytes(text); - _response.ContentLength64 = bOutput.Length; - - var outputStream = _response.OutputStream; - outputStream.Write(bOutput, 0, bOutput.Length); - Close(); - } - public void Close() { if (!this.IsClosed) @@ -108,8 +98,10 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp { try { - response.OutputStream.Flush(); - response.OutputStream.Dispose(); + var outputStream = response.OutputStream; + + outputStream.Flush(); + outputStream.Dispose(); response.Close(); } catch (Exception ex) @@ -118,11 +110,6 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp } } - public void Flush() - { - _response.OutputStream.Flush(); - } - public bool IsClosed { get; diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs index 455a69d37..e9a9f1c5b 100644 --- a/MediaBrowser.Model/Services/IRequest.cs +++ b/MediaBrowser.Model/Services/IRequest.cs @@ -136,23 +136,12 @@ namespace MediaBrowser.Model.Services Stream OutputStream { get; } - /// - /// Write once to the Response Stream then close it. - /// - /// - void Write(string text); - /// /// Signal that this response has been handled and no more processing should be done. /// When used in a request or response filter, no more filters or processing is done on this request. /// void Close(); - /// - /// Response.Flush() and OutputStream.Flush() seem to have different behaviour in ASP.NET - /// - void Flush(); - /// /// Gets a value indicating whether this instance is closed. /// @@ -160,8 +149,6 @@ namespace MediaBrowser.Model.Services void SetContentLength(long contentLength); - bool KeepAlive { get; set; } - //Add Metadata to Response Dictionary Items { get; } } diff --git a/ServiceStack/Host/ContentTypes.cs b/ServiceStack/Host/ContentTypes.cs index 58ba29801..8840e7c8b 100644 --- a/ServiceStack/Host/ContentTypes.cs +++ b/ServiceStack/Host/ContentTypes.cs @@ -18,15 +18,7 @@ namespace ServiceStack.Host serializer(response, responseStream); } - public Action GetResponseSerializer(string contentType) - { - var serializer = GetStreamSerializer(contentType); - if (serializer == null) return null; - - return (dto, httpRes) => serializer(dto, httpRes.OutputStream); - } - - public Action GetStreamSerializer(string contentType) + private Action GetStreamSerializer(string contentType) { switch (GetRealContentType(contentType)) { diff --git a/ServiceStack/HttpResponseExtensionsInternal.cs b/ServiceStack/HttpResponseExtensionsInternal.cs index 88b82bdf6..44b790f5f 100644 --- a/ServiceStack/HttpResponseExtensionsInternal.cs +++ b/ServiceStack/HttpResponseExtensionsInternal.cs @@ -6,6 +6,7 @@ using System.IO; using System.Net; using System.Threading.Tasks; using System.Collections.Generic; +using System.Text; using System.Threading; using MediaBrowser.Model.Services; using ServiceStack.Host; @@ -14,19 +15,19 @@ namespace ServiceStack { public static class HttpResponseExtensionsInternal { - public static async Task WriteToOutputStream(IResponse response, object result) + public static async Task WriteToOutputStream(IResponse response, Stream outputStream, object result) { var asyncStreamWriter = result as IAsyncStreamWriter; if (asyncStreamWriter != null) { - await asyncStreamWriter.WriteToAsync(response.OutputStream, CancellationToken.None).ConfigureAwait(false); + await asyncStreamWriter.WriteToAsync(outputStream, CancellationToken.None).ConfigureAwait(false); return true; } var streamWriter = result as IStreamWriter; if (streamWriter != null) { - streamWriter.WriteTo(response.OutputStream); + streamWriter.WriteTo(outputStream); return true; } @@ -35,7 +36,7 @@ namespace ServiceStack { using (stream) { - await stream.CopyToAsync(response.OutputStream).ConfigureAwait(false); + await stream.CopyToAsync(outputStream).ConfigureAwait(false); return true; } } @@ -46,7 +47,7 @@ namespace ServiceStack response.ContentType = "application/octet-stream"; response.SetContentLength(bytes.Length); - await response.OutputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); + await outputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); return true; } @@ -151,10 +152,11 @@ namespace ServiceStack response.ContentType += "; charset=utf-8"; } - var writeToOutputStreamResult = await WriteToOutputStream(response, result).ConfigureAwait(false); + var outputStream = response.OutputStream; + + var writeToOutputStreamResult = await WriteToOutputStream(response, outputStream, result).ConfigureAwait(false); if (writeToOutputStreamResult) { - response.Flush(); //required for Compression return; } @@ -164,12 +166,12 @@ namespace ServiceStack if (response.ContentType == null || response.ContentType == "text/html") response.ContentType = defaultContentType; - response.Write(responseText); + var bytes = Encoding.UTF8.GetBytes(responseText); + await outputStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); return; } - var serializer = ContentTypes.Instance.GetResponseSerializer(defaultContentType); - serializer(result, response); + ContentTypes.Instance.SerializeToStream(request, result, outputStream); } } } -- cgit v1.2.3 From 1714cb8764f2311fd255945d5a03d6b298f62071 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sat, 12 Nov 2016 03:02:46 -0500 Subject: update response stream handling --- Emby.Server.Core/ApplicationHost.cs | 3 --- .../HttpServer/SocketSharp/WebSocketSharpResponse.cs | 7 ++++++- MediaBrowser.ServerApplication/MainStartup.cs | 4 ++++ SocketHttpListener.Portable/Net/HttpConnection.cs | 17 ++++++++++++++--- SocketHttpListener.Portable/Net/HttpListenerRequest.cs | 4 ++-- SocketHttpListener.Portable/Net/HttpListenerResponse.cs | 2 +- SocketHttpListener.Portable/Net/ResponseStream.cs | 10 +++++----- 7 files changed, 32 insertions(+), 15 deletions(-) (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Server.Core/ApplicationHost.cs b/Emby.Server.Core/ApplicationHost.cs index 0c0ef894e..5c8aea7ed 100644 --- a/Emby.Server.Core/ApplicationHost.cs +++ b/Emby.Server.Core/ApplicationHost.cs @@ -1136,9 +1136,6 @@ namespace Emby.Server.Core { get { -#if DEBUG - return false; -#endif #pragma warning disable 162 return NativeApp.CanSelfUpdate; #pragma warning restore 162 diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs index a8b115056..dc049cbde 100644 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs @@ -100,7 +100,12 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp { var outputStream = response.OutputStream; - outputStream.Flush(); + // This is needed with compression + //if (!string.IsNullOrWhiteSpace(GetHeader("Content-Encoding"))) + { + outputStream.Flush(); + } + outputStream.Dispose(); response.Close(); } diff --git a/MediaBrowser.ServerApplication/MainStartup.cs b/MediaBrowser.ServerApplication/MainStartup.cs index 09c948a4a..fa8cccf34 100644 --- a/MediaBrowser.ServerApplication/MainStartup.cs +++ b/MediaBrowser.ServerApplication/MainStartup.cs @@ -291,6 +291,10 @@ namespace MediaBrowser.ServerApplication { get { +#if DEBUG + return false; +#endif + if (_isRunningAsService) { return _canRestartService; diff --git a/SocketHttpListener.Portable/Net/HttpConnection.cs b/SocketHttpListener.Portable/Net/HttpConnection.cs index db34c4218..8e472117e 100644 --- a/SocketHttpListener.Portable/Net/HttpConnection.cs +++ b/SocketHttpListener.Portable/Net/HttpConnection.cs @@ -23,7 +23,7 @@ namespace SocketHttpListener.Net StringBuilder current_line; ListenerPrefix prefix; RequestStream i_stream; - ResponseStream o_stream; + Stream o_stream; bool chunked; int reuses; bool context_bound; @@ -204,12 +204,23 @@ namespace SocketHttpListener.Net return i_stream; } - public ResponseStream GetResponseStream() + public Stream GetResponseStream() { // TODO: can we get this stream before reading the input? if (o_stream == null) { - o_stream = new ResponseStream(stream, context.Response, _memoryStreamFactory, _textEncoding); + if (context.Response.SendChunked) + { + o_stream = new ResponseStream(stream, context.Response, _memoryStreamFactory, _textEncoding); + } + else + { + o_stream = stream; + using (var headerStream = ResponseStream.GetHeaders(context.Response, _memoryStreamFactory, false)) + { + headerStream.CopyTo(o_stream); + } + } } return o_stream; } diff --git a/SocketHttpListener.Portable/Net/HttpListenerRequest.cs b/SocketHttpListener.Portable/Net/HttpListenerRequest.cs index 63d5e510d..5631fc0a1 100644 --- a/SocketHttpListener.Portable/Net/HttpListenerRequest.cs +++ b/SocketHttpListener.Portable/Net/HttpListenerRequest.cs @@ -181,11 +181,11 @@ namespace SocketHttpListener.Net if (String.Compare(Headers["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0) { - ResponseStream output = context.Connection.GetResponseStream(); + var output = context.Connection.GetResponseStream(); var _100continue = _textEncoding.GetASCIIEncoding().GetBytes("HTTP/1.1 100 Continue\r\n\r\n"); - output.InternalWrite(_100continue, 0, _100continue.Length); + //output.InternalWrite(_100continue, 0, _100continue.Length); } } diff --git a/SocketHttpListener.Portable/Net/HttpListenerResponse.cs b/SocketHttpListener.Portable/Net/HttpListenerResponse.cs index 0bc827b5a..93358cae4 100644 --- a/SocketHttpListener.Portable/Net/HttpListenerResponse.cs +++ b/SocketHttpListener.Portable/Net/HttpListenerResponse.cs @@ -19,7 +19,7 @@ namespace SocketHttpListener.Net CookieCollection cookies; WebHeaderCollection headers = new WebHeaderCollection(); bool keep_alive = true; - ResponseStream output_stream; + Stream output_stream; Version version = HttpVersion.Version11; string location; int status_code = 200; diff --git a/SocketHttpListener.Portable/Net/ResponseStream.cs b/SocketHttpListener.Portable/Net/ResponseStream.cs index 07788ea41..7a6425dea 100644 --- a/SocketHttpListener.Portable/Net/ResponseStream.cs +++ b/SocketHttpListener.Portable/Net/ResponseStream.cs @@ -64,7 +64,7 @@ namespace SocketHttpListener.Net { disposed = true; byte[] bytes = null; - MemoryStream ms = GetHeaders(true); + MemoryStream ms = GetHeaders(response, _memoryStreamFactory, false); bool chunked = response.SendChunked; if (stream.CanWrite) { @@ -102,14 +102,14 @@ namespace SocketHttpListener.Net base.Dispose(disposing); } - MemoryStream GetHeaders(bool closing) + internal static MemoryStream GetHeaders(HttpListenerResponse response, IMemoryStreamFactory memoryStreamFactory, bool closing) { // SendHeaders works on shared headers lock (response.headers_lock) { if (response.HeadersSent) return null; - MemoryStream ms = _memoryStreamFactory.CreateNew(); + MemoryStream ms = memoryStreamFactory.CreateNew(); response.SendHeaders(closing, ms); return ms; } @@ -137,7 +137,7 @@ namespace SocketHttpListener.Net throw new ObjectDisposedException(GetType().ToString()); byte[] bytes = null; - MemoryStream ms = GetHeaders(false); + MemoryStream ms = GetHeaders(response, _memoryStreamFactory, false); bool chunked = response.SendChunked; if (ms != null) { @@ -177,7 +177,7 @@ namespace SocketHttpListener.Net throw new ObjectDisposedException(GetType().ToString()); byte[] bytes = null; - MemoryStream ms = GetHeaders(false); + MemoryStream ms = GetHeaders(response, _memoryStreamFactory, false); bool chunked = response.SendChunked; if (ms != null) { -- cgit v1.2.3 From 0e9cd51f9c64d4cfad5cb5c7b0ddae6af8d18ac6 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 13 Nov 2016 16:04:21 -0500 Subject: update .net core startup --- Emby.Common.Implementations/BaseApplicationHost.cs | 4 +- .../BaseApplicationPaths.cs | 10 +- .../EnvironmentInfo/EnvironmentInfo.cs | 10 + .../IO/ManagedFileSystem.cs | 8 + Emby.Common.Implementations/Net/NetSocket.cs | 9 + Emby.Common.Implementations/Net/SocketAcceptor.cs | 24 ++- Emby.Common.Implementations/Net/SocketFactory.cs | 7 +- Emby.Server.Core/ApplicationHost.cs | 14 +- Emby.Server.Core/Data/DataExtensions.cs | 4 +- Emby.Server.Core/Data/SqliteItemRepository.cs | 19 +- .../Notifications/SqliteNotificationsRepository.cs | 2 +- Emby.Server.Core/ServerApplicationPaths.cs | 4 +- .../Channels/ChannelManager.cs | 16 +- .../HttpServer/HttpListenerHost.cs | 2 + .../LiveTv/EmbyTV/EmbyTV.cs | 90 +++++++-- .../LiveTv/EmbyTV/ItemDataProvider.cs | 3 +- .../LiveTv/LiveTvDtoService.cs | 20 +- .../LiveTv/LiveTvManager.cs | 67 +++++-- .../LiveTv/ProgramImageProvider.cs | 14 +- MediaBrowser.Api/ApiEntryPoint.cs | 6 +- .../Configuration/IApplicationPaths.cs | 6 - MediaBrowser.Controller/Entities/BaseItem.cs | 19 +- .../Encoder/EncoderValidator.cs | 16 +- MediaBrowser.Model/IO/IFileSystem.cs | 1 + MediaBrowser.Model/System/IEnvironmentInfo.cs | 2 + MediaBrowser.Server.Mono/MonoAppHost.cs | 2 +- MediaBrowser.Server.Mono/Program.cs | 10 +- .../Persistence/SqliteExtensions.cs | 6 +- MediaBrowser.ServerApplication/MainStartup.cs | 28 +-- .../Updates/ApplicationUpdater.cs | 2 +- MediaBrowser.ServerApplication/WindowsAppHost.cs | 9 +- MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs | 2 - Nuget/MediaBrowser.Common.nuspec | 2 +- Nuget/MediaBrowser.Server.Core.nuspec | 4 +- src/Emby.Server/ApplicationPathHelper.cs | 11 +- src/Emby.Server/CoreAppHost.cs | 2 +- src/Emby.Server/Program.cs | 213 ++++----------------- src/Emby.Server/project.json | 8 +- 38 files changed, 353 insertions(+), 323 deletions(-) (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Common.Implementations/BaseApplicationHost.cs b/Emby.Common.Implementations/BaseApplicationHost.cs index f0309511e..1f194968c 100644 --- a/Emby.Common.Implementations/BaseApplicationHost.cs +++ b/Emby.Common.Implementations/BaseApplicationHost.cs @@ -326,7 +326,7 @@ namespace Emby.Common.Implementations builder.AppendLine(string.Format("Processor count: {0}", Environment.ProcessorCount)); builder.AppendLine(string.Format("Program data path: {0}", appPaths.ProgramDataPath)); - builder.AppendLine(string.Format("Application Path: {0}", appPaths.ApplicationPath)); + builder.AppendLine(string.Format("Application directory: {0}", appPaths.ProgramSystemPath)); return builder; } @@ -548,7 +548,7 @@ return null; TimerFactory = new TimerFactory(); RegisterSingleInstance(TimerFactory); - SocketFactory = new SocketFactory(null); + SocketFactory = new SocketFactory(LogManager.GetLogger("SocketFactory")); RegisterSingleInstance(SocketFactory); RegisterSingleInstance(CryptographyProvider); diff --git a/Emby.Common.Implementations/BaseApplicationPaths.cs b/Emby.Common.Implementations/BaseApplicationPaths.cs index 628d62bd4..8792778ba 100644 --- a/Emby.Common.Implementations/BaseApplicationPaths.cs +++ b/Emby.Common.Implementations/BaseApplicationPaths.cs @@ -12,22 +12,18 @@ namespace Emby.Common.Implementations /// /// Initializes a new instance of the class. /// - protected BaseApplicationPaths(string programDataPath, string applicationPath) + protected BaseApplicationPaths(string programDataPath, string appFolderPath) { ProgramDataPath = programDataPath; - ApplicationPath = applicationPath; + ProgramSystemPath = appFolderPath; } - public string ApplicationPath { get; private set; } public string ProgramDataPath { get; private set; } /// /// Gets the path to the system folder /// - public string ProgramSystemPath - { - get { return Path.GetDirectoryName(ApplicationPath); } - } + public string ProgramSystemPath { get; private set; } /// /// The _data directory diff --git a/Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs b/Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs index 6a1b3ef74..c040e3931 100644 --- a/Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs +++ b/Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs @@ -95,5 +95,15 @@ namespace Emby.Common.Implementations.EnvironmentInfo return MediaBrowser.Model.System.Architecture.X64; } } + + public string GetEnvironmentVariable(string name) + { + return Environment.GetEnvironmentVariable(name); + } + + public virtual string GetUserId() + { + return null; + } } } diff --git a/Emby.Common.Implementations/IO/ManagedFileSystem.cs b/Emby.Common.Implementations/IO/ManagedFileSystem.cs index 1f0aa55fa..83bb50f94 100644 --- a/Emby.Common.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Common.Implementations/IO/ManagedFileSystem.cs @@ -57,6 +57,14 @@ namespace Emby.Common.Implementations.IO } } + public char PathSeparator + { + get + { + return Path.DirectorySeparatorChar; + } + } + public string GetFullPath(string path) { return Path.GetFullPath(path); diff --git a/Emby.Common.Implementations/Net/NetSocket.cs b/Emby.Common.Implementations/Net/NetSocket.cs index faa1a81e2..62ca3d6ac 100644 --- a/Emby.Common.Implementations/Net/NetSocket.cs +++ b/Emby.Common.Implementations/Net/NetSocket.cs @@ -15,6 +15,15 @@ namespace Emby.Common.Implementations.Net public NetSocket(Socket socket, ILogger logger) { + if (socket == null) + { + throw new ArgumentNullException("socket"); + } + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + Socket = socket; _logger = logger; } diff --git a/Emby.Common.Implementations/Net/SocketAcceptor.cs b/Emby.Common.Implementations/Net/SocketAcceptor.cs index fd65e9fbc..bddb7a079 100644 --- a/Emby.Common.Implementations/Net/SocketAcceptor.cs +++ b/Emby.Common.Implementations/Net/SocketAcceptor.cs @@ -14,6 +14,23 @@ namespace Emby.Common.Implementations.Net public SocketAcceptor(ILogger logger, Socket originalSocket, Action onAccept, Func isClosed) { + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + if (originalSocket == null) + { + throw new ArgumentNullException("originalSocket"); + } + if (onAccept == null) + { + throw new ArgumentNullException("onAccept"); + } + if (isClosed == null) + { + throw new ArgumentNullException("isClosed"); + } + _logger = logger; _originalSocket = originalSocket; _isClosed = isClosed; @@ -101,11 +118,8 @@ namespace Emby.Common.Implementations.Net _onAccept(new NetSocket(acceptSocket, _logger)); } - if (_originalSocket != null) - { - // Accept the next connection request - StartAccept(e, ref acceptSocket); - } + // Accept the next connection request + StartAccept(e, ref acceptSocket); } } } diff --git a/Emby.Common.Implementations/Net/SocketFactory.cs b/Emby.Common.Implementations/Net/SocketFactory.cs index 922b0f3cc..f26137683 100644 --- a/Emby.Common.Implementations/Net/SocketFactory.cs +++ b/Emby.Common.Implementations/Net/SocketFactory.cs @@ -23,10 +23,15 @@ namespace Emby.Common.Implementations.Net /// private IPAddress _LocalIP; - private ILogger _logger; + private readonly ILogger _logger; public SocketFactory(ILogger logger) { + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + _logger = logger; _LocalIP = IPAddress.Any; } diff --git a/Emby.Server.Core/ApplicationHost.cs b/Emby.Server.Core/ApplicationHost.cs index 7f795a68d..d3d292ca5 100644 --- a/Emby.Server.Core/ApplicationHost.cs +++ b/Emby.Server.Core/ApplicationHost.cs @@ -725,6 +725,11 @@ namespace Emby.Server.Core try { + if (!FileSystemManager.FileExists(certificateLocation)) + { + return null; + } + X509Certificate2 localCert = new X509Certificate2(certificateLocation); //localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA; if (!localCert.HasPrivateKey) @@ -1438,12 +1443,7 @@ namespace Emby.Server.Core try { - AuthorizeServer( - UdpServerEntryPoint.PortNumber, - ServerConfigurationManager.Configuration.HttpServerPortNumber, - ServerConfigurationManager.Configuration.HttpsPortNumber, - ConfigurationManager.CommonApplicationPaths.ApplicationPath, - ConfigurationManager.CommonApplicationPaths.TempDirectory); + AuthorizeServer(); } catch (Exception ex) { @@ -1451,7 +1451,7 @@ namespace Emby.Server.Core } } - protected abstract void AuthorizeServer(int udpPort, int httpServerPort, int httpsServerPort, string applicationPath, string tempDirectory); + protected abstract void AuthorizeServer(); protected abstract IDbConnector GetDbConnector(); public event EventHandler HasUpdateAvailableChanged; diff --git a/Emby.Server.Core/Data/DataExtensions.cs b/Emby.Server.Core/Data/DataExtensions.cs index b633d9217..631c1c500 100644 --- a/Emby.Server.Core/Data/DataExtensions.cs +++ b/Emby.Server.Core/Data/DataExtensions.cs @@ -40,7 +40,7 @@ namespace Emby.Server.Core.Data public static IDataParameter Add(this IDataParameterCollection paramCollection, IDbCommand cmd, string name) { var param = cmd.CreateParameter(); - + param.ParameterName = name; paramCollection.Add(param); @@ -173,7 +173,7 @@ namespace Emby.Server.Core.Data var builder = new StringBuilder(); builder.AppendLine("alter table " + table); - builder.AppendLine("add column " + columnName + " " + type); + builder.AppendLine("add column " + columnName + " " + type + " NULL"); connection.RunQueries(new[] { builder.ToString() }, logger); } diff --git a/Emby.Server.Core/Data/SqliteItemRepository.cs b/Emby.Server.Core/Data/SqliteItemRepository.cs index 2ca86c831..6ed409aa1 100644 --- a/Emby.Server.Core/Data/SqliteItemRepository.cs +++ b/Emby.Server.Core/Data/SqliteItemRepository.cs @@ -157,7 +157,7 @@ namespace Emby.Server.Core.Data string[] queries = { - "create table if not exists TypedBaseItems (guid GUID primary key, type TEXT, data BLOB, ParentId GUID, Path TEXT)", + "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)", "create table if not exists AncestorIds (ItemId GUID, AncestorId GUID, AncestorIdText TEXT, PRIMARY KEY (ItemId, AncestorId))", "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)", @@ -286,6 +286,7 @@ namespace Emby.Server.Core.Data _connection.AddColumn(Logger, "TypedBaseItems", "ExtraType", "Text"); _connection.AddColumn(Logger, "TypedBaseItems", "Artists", "Text"); _connection.AddColumn(Logger, "TypedBaseItems", "AlbumArtists", "Text"); + _connection.AddColumn(Logger, "TypedBaseItems", "ExternalId", "Text"); _connection.AddColumn(Logger, "ItemValues", "CleanValue", "Text"); @@ -440,7 +441,8 @@ namespace Emby.Server.Core.Data "TotalBitrate", "ExtraType", "Artists", - "AlbumArtists" + "AlbumArtists", + "ExternalId" }; private readonly string[] _mediaStreamSaveColumns = @@ -575,7 +577,8 @@ namespace Emby.Server.Core.Data "TotalBitrate", "ExtraType", "Artists", - "AlbumArtists" + "AlbumArtists", + "ExternalId" }; _saveItemCommand = _connection.CreateCommand(); _saveItemCommand.CommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns.ToArray()) + ") values ("; @@ -1084,6 +1087,10 @@ namespace Emby.Server.Core.Data } } + _saveItemCommand.GetParameter(index++).Value = item.ExternalId; + + //Logger.Debug(_saveItemCommand.CommandText); + _saveItemCommand.Transaction = transaction; _saveItemCommand.ExecuteNonQuery(); @@ -1967,6 +1974,12 @@ namespace Emby.Server.Core.Data } index++; + if (!reader.IsDBNull(index)) + { + item.ExternalId = reader.GetString(index); + } + index++; + if (string.IsNullOrWhiteSpace(item.Tagline)) { var movie = item as Movie; diff --git a/Emby.Server.Core/Notifications/SqliteNotificationsRepository.cs b/Emby.Server.Core/Notifications/SqliteNotificationsRepository.cs index 8a7fc9270..dee0d4cfd 100644 --- a/Emby.Server.Core/Notifications/SqliteNotificationsRepository.cs +++ b/Emby.Server.Core/Notifications/SqliteNotificationsRepository.cs @@ -30,7 +30,7 @@ namespace Emby.Server.Core.Notifications { string[] queries = { - "create table if not exists Notifications (Id GUID NOT NULL, UserId GUID NOT NULL, Date DATETIME NOT NULL, Name TEXT NOT NULL, Description TEXT, Url TEXT, Level TEXT NOT NULL, IsRead BOOLEAN NOT NULL, Category TEXT NOT NULL, RelatedId TEXT, PRIMARY KEY (Id, UserId))", + "create table if not exists Notifications (Id GUID NOT NULL, UserId GUID NOT NULL, Date DATETIME NOT NULL, Name TEXT NOT NULL, Description TEXT NULL, Url TEXT NULL, Level TEXT NOT NULL, IsRead BOOLEAN NOT NULL, Category TEXT NOT NULL, RelatedId TEXT NULL, PRIMARY KEY (Id, UserId))", "create index if not exists idx_Notifications1 on Notifications(Id)", "create index if not exists idx_Notifications2 on Notifications(UserId)" }; diff --git a/Emby.Server.Core/ServerApplicationPaths.cs b/Emby.Server.Core/ServerApplicationPaths.cs index d59dd89d9..dc80b773c 100644 --- a/Emby.Server.Core/ServerApplicationPaths.cs +++ b/Emby.Server.Core/ServerApplicationPaths.cs @@ -12,8 +12,8 @@ namespace Emby.Server.Core /// /// Initializes a new instance of the class. /// - public ServerApplicationPaths(string programDataPath, string applicationPath, string applicationResourcesPath) - : base(programDataPath, applicationPath) + public ServerApplicationPaths(string programDataPath, string appFolderPath, string applicationResourcesPath) + : base(programDataPath, appFolderPath) { ApplicationResourcesPath = applicationResourcesPath; } diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 2ce880c93..94ff7c342 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -326,7 +326,7 @@ namespace Emby.Server.Implementations.Channels if (requiresCallback != null) { - results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken) + results = await GetChannelItemMediaSourcesInternal(requiresCallback, GetItemExternalId(item), cancellationToken) .ConfigureAwait(false); } else @@ -1075,6 +1075,18 @@ namespace Emby.Server.Implementations.Channels return result; } + private string GetItemExternalId(BaseItem item) + { + var externalId = item.ExternalId; + + if (string.IsNullOrWhiteSpace(externalId)) + { + externalId = item.GetProviderId("ProviderExternalId"); + } + + return externalId; + } + private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); private async Task GetChannelItems(IChannel channel, User user, @@ -1145,7 +1157,7 @@ namespace Emby.Server.Implementations.Channels { var categoryItem = _libraryManager.GetItemById(new Guid(folderId)); - query.FolderId = categoryItem.ExternalId; + query.FolderId = GetItemExternalId(categoryItem); } var result = await channel.GetChannelItems(query, cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 41b7a4622..876d140ec 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -396,6 +396,8 @@ namespace Emby.Server.Implementations.HttpServer if (_disposed) { httpRes.StatusCode = 503; + httpRes.ContentType = "text/plain"; + Write(httpRes, "Server shutting down"); return; } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 63356a845..aaf74b5c6 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1551,13 +1551,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { try { + if (timer.IsSports) + { + AddGenre(timer.Genres, "Sports"); + } + if (timer.IsKids) + { + AddGenre(timer.Genres, "Kids"); + AddGenre(timer.Genres, "Children"); + } + if (timer.IsNews) + { + AddGenre(timer.Genres, "News"); + } + if (timer.IsProgramSeries) { SaveSeriesNfo(timer, recordingPath, seriesPath); + SaveVideoNfo(timer, recordingPath, false); } else if (!timer.IsMovie || timer.IsSports || timer.IsNews) { - SaveVideoNfo(timer, recordingPath); + SaveVideoNfo(timer, recordingPath, true); } } catch (Exception ex) @@ -1594,6 +1609,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV writer.WriteElementString("title", timer.Name); } + if (!string.IsNullOrEmpty(timer.OfficialRating)) + { + writer.WriteElementString("mpaa", timer.OfficialRating); + } + + foreach (var genre in timer.Genres) + { + writer.WriteElementString("genre", genre); + } + writer.WriteEndElement(); writer.WriteEndDocument(); } @@ -1601,7 +1626,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss"; - private void SaveVideoNfo(TimerInfo timer, string recordingPath) + private void SaveVideoNfo(TimerInfo timer, string recordingPath, bool lockData) { var nfoPath = Path.ChangeExtension(recordingPath, ".nfo"); @@ -1622,11 +1647,41 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV using (XmlWriter writer = XmlWriter.Create(stream, settings)) { writer.WriteStartDocument(true); - writer.WriteStartElement("movie"); - if (!string.IsNullOrWhiteSpace(timer.Name)) + if (timer.IsProgramSeries) { - writer.WriteElementString("title", timer.Name); + writer.WriteStartElement("episodedetails"); + + if (!string.IsNullOrWhiteSpace(timer.EpisodeTitle)) + { + writer.WriteElementString("title", timer.EpisodeTitle); + } + + if (timer.OriginalAirDate.HasValue) + { + var formatString = _config.GetNfoConfiguration().ReleaseDateFormat; + + writer.WriteElementString("aired", timer.OriginalAirDate.Value.ToLocalTime().ToString(formatString)); + } + + if (timer.EpisodeNumber.HasValue) + { + writer.WriteElementString("episode", timer.EpisodeNumber.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (timer.SeasonNumber.HasValue) + { + writer.WriteElementString("season", timer.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture)); + } + } + else + { + writer.WriteStartElement("movie"); + + if (!string.IsNullOrWhiteSpace(timer.Name)) + { + writer.WriteElementString("title", timer.Name); + } } writer.WriteElementString("dateadded", DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat)); @@ -1645,25 +1700,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV .Replace(""", "'"); writer.WriteElementString("plot", overview); - writer.WriteElementString("lockdata", true.ToString().ToLower()); - if (timer.CommunityRating.HasValue) + if (lockData) { - writer.WriteElementString("rating", timer.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString("lockdata", true.ToString().ToLower()); } - if (timer.IsSports) - { - AddGenre(timer.Genres, "Sports"); - } - if (timer.IsKids) - { - AddGenre(timer.Genres, "Kids"); - AddGenre(timer.Genres, "Children"); - } - if (timer.IsNews) + if (timer.CommunityRating.HasValue) { - AddGenre(timer.Genres, "News"); + writer.WriteElementString("rating", timer.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)); } foreach (var genre in timer.Genres) @@ -1968,4 +2013,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public CancellationTokenSource CancellationTokenSource { get; set; } } } + public static class ConfigurationExtension + { + public static XbmcMetadataOptions GetNfoConfiguration(this IConfigurationManager manager) + { + return manager.GetConfiguration("xbmcmetadata"); + } + } } \ No newline at end of file diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs index ded4f04c4..16ae26d45 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs @@ -54,9 +54,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV catch (FileNotFoundException) { } - catch (IOException ex) + catch (IOException) { - Logger.ErrorException("Error deserializing {0}", ex, jsonFile); } catch (Exception ex) { diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index 4e7161521..d3e30a46b 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -269,6 +269,18 @@ namespace Emby.Server.Implementations.LiveTv return _libraryManager.GetNewItemId(name.ToLower(), typeof(ILiveTvRecording)); } + private string GetItemExternalId(BaseItem item) + { + var externalId = item.ExternalId; + + if (string.IsNullOrWhiteSpace(externalId)) + { + externalId = item.GetProviderId("ProviderExternalId"); + } + + return externalId; + } + public async Task GetTimerInfo(TimerInfoDto dto, bool isNew, LiveTvManager liveTv, CancellationToken cancellationToken) { var info = new TimerInfo @@ -304,7 +316,7 @@ namespace Emby.Server.Implementations.LiveTv if (channel != null) { - info.ChannelId = channel.ExternalId; + info.ChannelId = GetItemExternalId(channel); } } @@ -314,7 +326,7 @@ namespace Emby.Server.Implementations.LiveTv if (program != null) { - info.ProgramId = program.ExternalId; + info.ProgramId = GetItemExternalId(program); } } @@ -370,7 +382,7 @@ namespace Emby.Server.Implementations.LiveTv if (channel != null) { - info.ChannelId = channel.ExternalId; + info.ChannelId = GetItemExternalId(channel); } } @@ -380,7 +392,7 @@ namespace Emby.Server.Implementations.LiveTv if (program != null) { - info.ProgramId = program.ExternalId; + info.ProgramId = GetItemExternalId(program); } } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index adec66858..3a6f23fe9 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -251,12 +251,24 @@ namespace Emby.Server.Implementations.LiveTv return await GetLiveStream(id, mediaSourceId, true, cancellationToken).ConfigureAwait(false); } + private string GetItemExternalId(BaseItem item) + { + var externalId = item.ExternalId; + + if (string.IsNullOrWhiteSpace(externalId)) + { + externalId = item.GetProviderId("ProviderExternalId"); + } + + return externalId; + } + public async Task> GetRecordingMediaSources(IHasMediaSources item, CancellationToken cancellationToken) { var baseItem = (BaseItem)item; var service = GetService(baseItem); - return await service.GetRecordingStreamMediaSources(baseItem.ExternalId, cancellationToken).ConfigureAwait(false); + return await service.GetRecordingStreamMediaSources(GetItemExternalId(baseItem), cancellationToken).ConfigureAwait(false); } public async Task> GetChannelMediaSources(IHasMediaSources item, CancellationToken cancellationToken) @@ -313,18 +325,18 @@ namespace Emby.Server.Implementations.LiveTv var channel = GetInternalChannel(id); isVideo = channel.ChannelType == ChannelType.TV; service = GetService(channel); - _logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId); + _logger.Info("Opening channel stream from {0}, external channel Id: {1}", service.Name, GetItemExternalId(channel)); var supportsManagedStream = service as ISupportsDirectStreamProvider; if (supportsManagedStream != null) { - var streamInfo = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false); + var streamInfo = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(GetItemExternalId(channel), mediaSourceId, cancellationToken).ConfigureAwait(false); info = streamInfo.Item1; directStreamProvider = streamInfo.Item2; } else { - info = await service.GetChannelStream(channel.ExternalId, mediaSourceId, cancellationToken).ConfigureAwait(false); + info = await service.GetChannelStream(GetItemExternalId(channel), mediaSourceId, cancellationToken).ConfigureAwait(false); } info.RequiresClosing = true; @@ -341,8 +353,8 @@ namespace Emby.Server.Implementations.LiveTv isVideo = !string.Equals(recording.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase); service = GetService(recording); - _logger.Info("Opening recording stream from {0}, external recording Id: {1}", service.Name, recording.ExternalId); - info = await service.GetRecordingStream(recording.ExternalId, null, cancellationToken).ConfigureAwait(false); + _logger.Info("Opening recording stream from {0}, external recording Id: {1}", service.Name, GetItemExternalId(recording)); + info = await service.GetRecordingStream(GetItemExternalId(recording), null, cancellationToken).ConfigureAwait(false); info.RequiresClosing = true; if (info.RequiresClosing) @@ -493,7 +505,7 @@ namespace Emby.Server.Implementations.LiveTv isNew = true; } - if (!string.Equals(channelInfo.Id, item.ExternalId)) + if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal)) { isNew = true; } @@ -601,7 +613,6 @@ namespace Emby.Server.Implementations.LiveTv item.EpisodeTitle = info.EpisodeTitle; item.ExternalId = info.Id; - item.ExternalSeriesIdLegacy = seriesId; if (!string.IsNullOrWhiteSpace(seriesId) && !string.Equals(item.ExternalSeriesId, seriesId, StringComparison.Ordinal)) { @@ -841,6 +852,13 @@ namespace Emby.Server.Implementations.LiveTv return item.Id; } + + + private string GetExternalSeriesIdLegacy(BaseItem item) + { + return item.GetProviderId("ProviderExternalSeriesId"); + } + public async Task GetProgram(string id, CancellationToken cancellationToken, User user = null) { var program = GetInternalProgram(id); @@ -848,7 +866,15 @@ namespace Emby.Server.Implementations.LiveTv var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user); var list = new List>(); - list.Add(new Tuple(dto, program.ServiceName, program.ExternalId, program.ExternalSeriesIdLegacy)); + + var externalSeriesId = program.ExternalSeriesId; + + if (string.IsNullOrWhiteSpace(externalSeriesId)) + { + externalSeriesId = GetExternalSeriesIdLegacy(program); + } + + list.Add(new Tuple(dto, program.ServiceName, GetItemExternalId(program), externalSeriesId)); await AddRecordingInfo(list, cancellationToken).ConfigureAwait(false); @@ -1283,7 +1309,7 @@ namespace Emby.Server.Implementations.LiveTv var isKids = false; var iSSeries = false; - var channelPrograms = await service.GetProgramsAsync(currentChannel.ExternalId, start, end, cancellationToken).ConfigureAwait(false); + var channelPrograms = await service.GetProgramsAsync(GetItemExternalId(currentChannel), start, end, cancellationToken).ConfigureAwait(false); var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery { @@ -1830,7 +1856,14 @@ namespace Emby.Server.Implementations.LiveTv dto.ServiceName = serviceName; } - programTuples.Add(new Tuple(dto, serviceName, program.ExternalId, program.ExternalSeriesIdLegacy)); + var externalSeriesId = program.ExternalSeriesId; + + if (string.IsNullOrWhiteSpace(externalSeriesId)) + { + externalSeriesId = GetExternalSeriesIdLegacy(program); + } + + programTuples.Add(new Tuple(dto, serviceName, GetItemExternalId(program), externalSeriesId)); } await AddRecordingInfo(programTuples, CancellationToken.None).ConfigureAwait(false); @@ -2006,7 +2039,7 @@ namespace Emby.Server.Implementations.LiveTv if (service is EmbyTV.EmbyTV) { // We can't trust that we'll be able to direct stream it through emby server, no matter what the provider says - return service.DeleteRecordingAsync(recording.ExternalId, CancellationToken.None); + return service.DeleteRecordingAsync(GetItemExternalId(recording), CancellationToken.None); } return Task.FromResult(true); @@ -2030,7 +2063,7 @@ namespace Emby.Server.Implementations.LiveTv try { - await service.DeleteRecordingAsync(recording.ExternalId, CancellationToken.None).ConfigureAwait(false); + await service.DeleteRecordingAsync(GetItemExternalId(recording), CancellationToken.None).ConfigureAwait(false); } catch (ResourceNotFoundException) { @@ -2289,12 +2322,12 @@ namespace Emby.Server.Implementations.LiveTv programInfo = new ProgramInfo { Audio = program.Audio, - ChannelId = channel.ExternalId, + ChannelId = GetItemExternalId(channel), CommunityRating = program.CommunityRating, EndDate = program.EndDate ?? DateTime.MinValue, EpisodeTitle = program.EpisodeTitle, Genres = program.Genres, - Id = program.ExternalId, + Id = GetItemExternalId(program), IsHD = program.IsHD, IsKids = program.IsKids, IsLive = program.IsLive, @@ -2360,7 +2393,7 @@ namespace Emby.Server.Implementations.LiveTv info.Name = program.Name; info.Overview = program.Overview; info.ProgramId = programDto.Id; - info.ExternalProgramId = program.ExternalId; + info.ExternalProgramId = GetItemExternalId(program); if (program.EndDate.HasValue) { @@ -2804,7 +2837,7 @@ namespace Emby.Server.Implementations.LiveTv public async Task SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings) { - info = _jsonSerializer.DeserializeFromString< ListingsProviderInfo>(_jsonSerializer.SerializeToString(info)); + info = _jsonSerializer.DeserializeFromString(_jsonSerializer.SerializeToString(info)); var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); diff --git a/Emby.Server.Implementations/LiveTv/ProgramImageProvider.cs b/Emby.Server.Implementations/LiveTv/ProgramImageProvider.cs index f5d298af4..5a0389b16 100644 --- a/Emby.Server.Implementations/LiveTv/ProgramImageProvider.cs +++ b/Emby.Server.Implementations/LiveTv/ProgramImageProvider.cs @@ -24,6 +24,18 @@ namespace Emby.Server.Implementations.LiveTv return new[] { ImageType.Primary }; } + private string GetItemExternalId(BaseItem item) + { + var externalId = item.ExternalId; + + if (string.IsNullOrWhiteSpace(externalId)) + { + externalId = item.GetProviderId("ProviderExternalId"); + } + + return externalId; + } + public async Task GetImage(IHasImages item, ImageType type, CancellationToken cancellationToken) { var liveTvItem = (LiveTvProgram)item; @@ -38,7 +50,7 @@ namespace Emby.Server.Implementations.LiveTv { var channel = _liveTvManager.GetInternalChannel(liveTvItem.ChannelId); - var response = await service.GetProgramImageAsync(liveTvItem.ExternalId, channel.ExternalId, cancellationToken).ConfigureAwait(false); + var response = await service.GetProgramImageAsync(GetItemExternalId(liveTvItem), GetItemExternalId(channel), cancellationToken).ConfigureAwait(false); if (response != null) { diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index bc0241766..37ab12366 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -128,7 +128,11 @@ namespace MediaBrowser.Api { // Don't clutter the log } - catch (IOException ex) + catch (IOException) + { + // Don't clutter the log + } + catch (Exception ex) { Logger.ErrorException("Error deleting encoded media cache", ex); } diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index d3bf03302..d2446ce46 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -6,12 +6,6 @@ namespace MediaBrowser.Common.Configuration /// public interface IApplicationPaths { - /// - /// Gets the application path. - /// - /// The application path. - string ApplicationPath { get; } - /// /// Gets the path to the program data folder /// diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index b079da97c..10cac7992 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -296,28 +296,11 @@ namespace MediaBrowser.Controller.Entities /// If this content came from an external service, the id of the content on that service /// [IgnoreDataMember] - public string ExternalId - { - get { return this.GetProviderId("ProviderExternalId"); } - set - { - this.SetProviderId("ProviderExternalId", value); - } - } + public string ExternalId { get; set; } [IgnoreDataMember] public string ExternalSeriesId { get; set; } - [IgnoreDataMember] - public string ExternalSeriesIdLegacy - { - get { return this.GetProviderId("ProviderExternalSeriesId"); } - set - { - this.SetProviderId("ProviderExternalSeriesId", value); - } - } - /// /// Gets or sets the etag. /// diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 6acdccf3d..1b8b3feec 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -37,20 +37,22 @@ namespace MediaBrowser.MediaEncoding.Encoder { output = GetProcessOutput(encoderAppPath, "-version"); } - catch + catch (Exception ex) { + if (logOutput) + { + _logger.ErrorException("Error validating encoder", ex); + } } - output = output ?? string.Empty; - - if (logOutput) + if (string.IsNullOrWhiteSpace(output)) { - _logger.Info("ffmpeg info: {0}", output); + return false; } - if (string.IsNullOrWhiteSpace(output)) + if (logOutput) { - return false; + _logger.Info("ffmpeg info: {0}", output); } if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1) diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index f219d9295..22e1e7758 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -308,6 +308,7 @@ namespace MediaBrowser.Model.IO void SetReadOnly(string path, bool isHidden); char DirectorySeparatorChar { get; } + char PathSeparator { get; } string GetFullPath(string path); diff --git a/MediaBrowser.Model/System/IEnvironmentInfo.cs b/MediaBrowser.Model/System/IEnvironmentInfo.cs index c5f493e7c..7e7d81e30 100644 --- a/MediaBrowser.Model/System/IEnvironmentInfo.cs +++ b/MediaBrowser.Model/System/IEnvironmentInfo.cs @@ -12,6 +12,8 @@ namespace MediaBrowser.Model.System string OperatingSystemName { get; } string OperatingSystemVersion { get; } Architecture SystemArchitecture { get; } + string GetEnvironmentVariable(string name); + string GetUserId(); } public enum OperatingSystem diff --git a/MediaBrowser.Server.Mono/MonoAppHost.cs b/MediaBrowser.Server.Mono/MonoAppHost.cs index 5f0ecde24..fd3c9f506 100644 --- a/MediaBrowser.Server.Mono/MonoAppHost.cs +++ b/MediaBrowser.Server.Mono/MonoAppHost.cs @@ -91,7 +91,7 @@ namespace MediaBrowser.Server.Mono MainClass.Shutdown(); } - protected override void AuthorizeServer(int udpPort, int httpServerPort, int httpsServerPort, string applicationPath, string tempDirectory) + protected override void AuthorizeServer() { throw new NotImplementedException(); } diff --git a/MediaBrowser.Server.Mono/Program.cs b/MediaBrowser.Server.Mono/Program.cs index 48390f078..470525ece 100644 --- a/MediaBrowser.Server.Mono/Program.cs +++ b/MediaBrowser.Server.Mono/Program.cs @@ -5,6 +5,7 @@ using MediaBrowser.Server.Startup.Common; using Microsoft.Win32; using System; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -74,7 +75,9 @@ namespace MediaBrowser.Server.Mono programDataPath = ApplicationPathHelper.GetProgramDataPath(applicationPath); } - return new ServerApplicationPaths(programDataPath, applicationPath, Path.GetDirectoryName(applicationPath)); + var appFolderPath = Path.GetDirectoryName(applicationPath); + + return new ServerApplicationPaths(programDataPath, appFolderPath, Path.GetDirectoryName(applicationPath)); } private static readonly TaskCompletionSource ApplicationTaskCompletionSource = new TaskCompletionSource(); @@ -305,5 +308,10 @@ namespace MediaBrowser.Server.Mono public class MonoEnvironmentInfo : EnvironmentInfo { public bool IsBsd { get; set; } + + public virtual string GetUserId() + { + return Syscall.getuid().ToString(CultureInfo.InvariantCulture); + } } } diff --git a/MediaBrowser.Server.Startup.Common/Persistence/SqliteExtensions.cs b/MediaBrowser.Server.Startup.Common/Persistence/SqliteExtensions.cs index 72ecfc1bf..22aeb53dd 100644 --- a/MediaBrowser.Server.Startup.Common/Persistence/SqliteExtensions.cs +++ b/MediaBrowser.Server.Startup.Common/Persistence/SqliteExtensions.cs @@ -14,7 +14,11 @@ namespace Emby.Server.Core.Data /// /// Connects to db. /// - public static async Task ConnectToDb(string dbPath, bool isReadOnly, bool enablePooling, int? cacheSize, ILogger logger) + public static async Task ConnectToDb(string dbPath, + bool isReadOnly, + bool enablePooling, + int? cacheSize, + ILogger logger) { if (string.IsNullOrEmpty(dbPath)) { diff --git a/MediaBrowser.ServerApplication/MainStartup.cs b/MediaBrowser.ServerApplication/MainStartup.cs index ab0a36aff..c0ebcde74 100644 --- a/MediaBrowser.ServerApplication/MainStartup.cs +++ b/MediaBrowser.ServerApplication/MainStartup.cs @@ -44,6 +44,8 @@ namespace MediaBrowser.ServerApplication [DllImport("kernel32.dll", SetLastError = true)] static extern bool SetDllDirectory(string lpPathName); + public static string ApplicationPath; + public static bool TryGetLocalFromUncDirectory(string local, out string unc) { if ((local == null) || (local == "")) @@ -81,14 +83,14 @@ namespace MediaBrowser.ServerApplication var currentProcess = Process.GetCurrentProcess(); - var applicationPath = currentProcess.MainModule.FileName; - var architecturePath = Path.Combine(Path.GetDirectoryName(applicationPath), Environment.Is64BitProcess ? "x64" : "x86"); + ApplicationPath = currentProcess.MainModule.FileName; + var architecturePath = Path.Combine(Path.GetDirectoryName(ApplicationPath), Environment.Is64BitProcess ? "x64" : "x86"); Wand.SetMagickCoderModulePath(architecturePath); var success = SetDllDirectory(architecturePath); - var appPaths = CreateApplicationPaths(applicationPath, IsRunningAsService); + var appPaths = CreateApplicationPaths(ApplicationPath, IsRunningAsService); var logManager = new NlogManager(appPaths.LogDirectoryPath, "server"); logManager.ReloadLogger(LogSeverity.Debug); @@ -102,7 +104,7 @@ namespace MediaBrowser.ServerApplication if (options.ContainsOption("-installservice")) { logger.Info("Performing service installation"); - InstallService(applicationPath, logger); + InstallService(ApplicationPath, logger); return; } @@ -110,7 +112,7 @@ namespace MediaBrowser.ServerApplication if (options.ContainsOption("-installserviceasadmin")) { logger.Info("Performing service installation"); - RunServiceInstallation(applicationPath); + RunServiceInstallation(ApplicationPath); return; } @@ -118,7 +120,7 @@ namespace MediaBrowser.ServerApplication if (options.ContainsOption("-uninstallservice")) { logger.Info("Performing service uninstallation"); - UninstallService(applicationPath, logger); + UninstallService(ApplicationPath, logger); return; } @@ -126,15 +128,15 @@ namespace MediaBrowser.ServerApplication if (options.ContainsOption("-uninstallserviceasadmin")) { logger.Info("Performing service uninstallation"); - RunServiceUninstallation(applicationPath); + RunServiceUninstallation(ApplicationPath); return; } AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; - RunServiceInstallationIfNeeded(applicationPath); + RunServiceInstallationIfNeeded(ApplicationPath); - if (IsAlreadyRunning(applicationPath, currentProcess)) + if (IsAlreadyRunning(ApplicationPath, currentProcess)) { logger.Info("Shutting down because another instance of Emby Server is already running."); return; @@ -250,6 +252,8 @@ namespace MediaBrowser.ServerApplication /// ServerApplicationPaths. private static ServerApplicationPaths CreateApplicationPaths(string applicationPath, bool runAsService) { + var appFolderPath = Path.GetDirectoryName(applicationPath); + var resourcesPath = Path.GetDirectoryName(applicationPath); if (runAsService) @@ -258,10 +262,10 @@ namespace MediaBrowser.ServerApplication var programDataPath = Path.GetDirectoryName(systemPath); - return new ServerApplicationPaths(programDataPath, applicationPath, resourcesPath); + return new ServerApplicationPaths(programDataPath, appFolderPath, resourcesPath); } - return new ServerApplicationPaths(ApplicationPathHelper.GetProgramDataPath(applicationPath), applicationPath, resourcesPath); + return new ServerApplicationPaths(ApplicationPathHelper.GetProgramDataPath(applicationPath), appFolderPath, resourcesPath); } /// @@ -663,7 +667,7 @@ namespace MediaBrowser.ServerApplication _logger.Info("Starting new instance"); //Application.Restart(); - Process.Start(_appHost.ServerConfigurationManager.ApplicationPaths.ApplicationPath); + Process.Start(ApplicationPath); ShutdownWindowsApplication(); } diff --git a/MediaBrowser.ServerApplication/Updates/ApplicationUpdater.cs b/MediaBrowser.ServerApplication/Updates/ApplicationUpdater.cs index c426395b7..97537b27d 100644 --- a/MediaBrowser.ServerApplication/Updates/ApplicationUpdater.cs +++ b/MediaBrowser.ServerApplication/Updates/ApplicationUpdater.cs @@ -44,7 +44,7 @@ namespace MediaBrowser.ServerApplication.Updates // startpath = executable to launch // systempath = folder containing installation var args = string.Format("product={0} archive=\"{1}\" caller={2} pismo=false version={3} service={4} installpath=\"{5}\" startpath=\"{6}\" systempath=\"{7}\"", - product, archive, Process.GetCurrentProcess().Id, version, restartServiceName ?? string.Empty, appPaths.ProgramDataPath, appPaths.ApplicationPath, systemPath); + product, archive, Process.GetCurrentProcess().Id, version, restartServiceName ?? string.Empty, appPaths.ProgramDataPath, MainStartup.ApplicationPath, systemPath); logger.Info("Args: {0}", args); Process.Start(tempUpdater, args); diff --git a/MediaBrowser.ServerApplication/WindowsAppHost.cs b/MediaBrowser.ServerApplication/WindowsAppHost.cs index 8fd718432..d9bead6b7 100644 --- a/MediaBrowser.ServerApplication/WindowsAppHost.cs +++ b/MediaBrowser.ServerApplication/WindowsAppHost.cs @@ -6,6 +6,7 @@ using System.Reflection; using Emby.Server.Core; using Emby.Server.Core.Data; using Emby.Server.Core.FFMpeg; +using Emby.Server.Implementations.EntryPoints; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.System; @@ -60,9 +61,13 @@ namespace MediaBrowser.ServerApplication MainStartup.Shutdown(); } - protected override void AuthorizeServer(int udpPort, int httpServerPort, int httpsServerPort, string applicationPath, string tempDirectory) + protected override void AuthorizeServer() { - ServerAuthorization.AuthorizeServer(udpPort, httpServerPort, httpsServerPort, applicationPath, tempDirectory); + ServerAuthorization.AuthorizeServer(UdpServerEntryPoint.PortNumber, + ServerConfigurationManager.Configuration.HttpServerPortNumber, + ServerConfigurationManager.Configuration.HttpsPortNumber, + MainStartup.ApplicationPath, + ConfigurationManager.CommonApplicationPaths.TempDirectory); } protected override IDbConnector GetDbConnector() diff --git a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs index 4c22f0246..b512939a7 100644 --- a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs @@ -90,8 +90,6 @@ namespace MediaBrowser.XbmcMetadata.Savers } } - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - protected override List GetTagsUsed() { var list = new List diff --git a/Nuget/MediaBrowser.Common.nuspec b/Nuget/MediaBrowser.Common.nuspec index 464a29eb2..daeb754ba 100644 --- a/Nuget/MediaBrowser.Common.nuspec +++ b/Nuget/MediaBrowser.Common.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Common - 3.0.689 + 3.0.691 Emby.Common Emby Team ebr,Luke,scottisafool diff --git a/Nuget/MediaBrowser.Server.Core.nuspec b/Nuget/MediaBrowser.Server.Core.nuspec index 96e9a697a..a34a9bee2 100644 --- a/Nuget/MediaBrowser.Server.Core.nuspec +++ b/Nuget/MediaBrowser.Server.Core.nuspec @@ -2,7 +2,7 @@ MediaBrowser.Server.Core - 3.0.689 + 3.0.691 Emby.Server.Core Emby Team ebr,Luke,scottisafool @@ -12,7 +12,7 @@ Contains core components required to build plugins for Emby Server. Copyright © Emby 2013 - + diff --git a/src/Emby.Server/ApplicationPathHelper.cs b/src/Emby.Server/ApplicationPathHelper.cs index 4da87b6a0..c611ff372 100644 --- a/src/Emby.Server/ApplicationPathHelper.cs +++ b/src/Emby.Server/ApplicationPathHelper.cs @@ -8,7 +8,7 @@ namespace Emby.Server { public class ApplicationPathHelper { - public static string GetProgramDataPath(string applicationPath) + public static string GetProgramDataPath(string appDirectory) { var useDebugPath = false; @@ -27,14 +27,7 @@ namespace Emby.Server // If it's a relative path, e.g. "..\" if (!Path.IsPathRooted(programDataPath)) { - var path = Path.GetDirectoryName(applicationPath); - - if (string.IsNullOrEmpty(path)) - { - throw new Exception("Unable to determine running assembly location"); - } - - programDataPath = Path.Combine(path, programDataPath); + programDataPath = Path.Combine(appDirectory, programDataPath); programDataPath = Path.GetFullPath(programDataPath); } diff --git a/src/Emby.Server/CoreAppHost.cs b/src/Emby.Server/CoreAppHost.cs index 1a1526513..21f6ae445 100644 --- a/src/Emby.Server/CoreAppHost.cs +++ b/src/Emby.Server/CoreAppHost.cs @@ -51,7 +51,7 @@ namespace Emby.Server return list; } - protected override void AuthorizeServer(int udpPort, int httpServerPort, int httpsServerPort, string applicationPath, string tempDirectory) + protected override void AuthorizeServer() { } diff --git a/src/Emby.Server/Program.cs b/src/Emby.Server/Program.cs index 64fc423a1..6f871990d 100644 --- a/src/Emby.Server/Program.cs +++ b/src/Emby.Server/Program.cs @@ -28,8 +28,6 @@ namespace Emby.Server private static ILogger _logger; - private static bool _isRunningAsService = false; - private static bool _canRestartService = false; private static bool _appHostDisposed; [DllImport("kernel32.dll", SetLastError = true)] @@ -41,39 +39,33 @@ namespace Emby.Server public static void Main(string[] args) { var options = new StartupOptions(); - _isRunningAsService = options.ContainsOption("-service"); - - if (_isRunningAsService) - { - //_canRestartService = CanRestartWindowsService(); - } var currentProcess = Process.GetCurrentProcess(); - - var applicationPath = currentProcess.MainModule.FileName; + + var baseDirectory = System.AppContext.BaseDirectory; //var architecturePath = Path.Combine(Path.GetDirectoryName(applicationPath), Environment.Is64BitProcess ? "x64" : "x86"); //Wand.SetMagickCoderModulePath(architecturePath); //var success = SetDllDirectory(architecturePath); - var appPaths = CreateApplicationPaths(applicationPath, _isRunningAsService); + var appPaths = CreateApplicationPaths(baseDirectory); var logManager = new NlogManager(appPaths.LogDirectoryPath, "server"); logManager.ReloadLogger(LogSeverity.Debug); logManager.AddConsoleOutput(); var logger = _logger = logManager.GetLogger("Main"); - + ApplicationHost.LogEnvironmentInfo(logger, appPaths, true); AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; - if (IsAlreadyRunning(applicationPath, currentProcess)) - { - logger.Info("Shutting down because another instance of Emby Server is already running."); - return; - } + //if (IsAlreadyRunning(applicationPath, currentProcess)) + //{ + // logger.Info("Shutting down because another instance of Emby Server is already running."); + // return; + //} if (PerformUpdateIfNeeded(appPaths, logger)) { @@ -81,14 +73,7 @@ namespace Emby.Server return; } - try - { - RunApplication(appPaths, logManager, _isRunningAsService, options); - } - finally - { - OnServiceShutdown(); - } + RunApplication(appPaths, logManager, options); } /// @@ -139,34 +124,17 @@ namespace Emby.Server } } - if (!_isRunningAsService) - { - return false; - } - return false; } /// /// Creates the application paths. /// - /// The application path. - /// if set to true [run as service]. - /// ServerApplicationPaths. - private static ServerApplicationPaths CreateApplicationPaths(string applicationPath, bool runAsService) + private static ServerApplicationPaths CreateApplicationPaths(string appDirectory) { - var resourcesPath = Path.GetDirectoryName(applicationPath); - - if (runAsService) - { - var systemPath = Path.GetDirectoryName(applicationPath); - - var programDataPath = Path.GetDirectoryName(systemPath); - - return new ServerApplicationPaths(programDataPath, applicationPath, resourcesPath); - } + var resourcesPath = appDirectory; - return new ServerApplicationPaths(ApplicationPathHelper.GetProgramDataPath(applicationPath), applicationPath, resourcesPath); + return new ServerApplicationPaths(ApplicationPathHelper.GetProgramDataPath(appDirectory), appDirectory, resourcesPath); } /// @@ -177,14 +145,7 @@ namespace Emby.Server { get { - if (_isRunningAsService) - { - return _canRestartService; - } - else - { - return true; - } + return true; } } @@ -196,14 +157,7 @@ namespace Emby.Server { get { - if (_isRunningAsService) - { - return _canRestartService; - } - else - { - return true; - } + return false; } } @@ -214,9 +168,8 @@ namespace Emby.Server /// /// The app paths. /// The log manager. - /// if set to true [run service]. /// The options. - private static void RunApplication(ServerApplicationPaths appPaths, ILogManager logManager, bool runService, StartupOptions options) + private static void RunApplication(ServerApplicationPaths appPaths, ILogManager logManager, StartupOptions options) { var fileSystem = new ManagedFileSystem(logManager.GetLogger("FileSystem"), true, true, true); @@ -240,29 +193,19 @@ namespace Emby.Server var initProgress = new Progress(); - if (!runService) - { - // Not crazy about this but it's the only way to suppress ffmpeg crash dialog boxes - SetErrorMode(ErrorModes.SEM_FAILCRITICALERRORS | ErrorModes.SEM_NOALIGNMENTFAULTEXCEPT | - ErrorModes.SEM_NOGPFAULTERRORBOX | ErrorModes.SEM_NOOPENFILEERRORBOX); - } + // Not crazy about this but it's the only way to suppress ffmpeg crash dialog boxes + SetErrorMode(ErrorModes.SEM_FAILCRITICALERRORS | ErrorModes.SEM_NOALIGNMENTFAULTEXCEPT | + ErrorModes.SEM_NOGPFAULTERRORBOX | ErrorModes.SEM_NOOPENFILEERRORBOX); var task = _appHost.Init(initProgress); Task.WaitAll(task); task = task.ContinueWith(new Action(a => _appHost.RunStartupTasks()), TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.AttachedToParent); - if (runService) - { - StartService(logManager); - } - else - { - Task.WaitAll(task); + Task.WaitAll(task); - task = ApplicationTaskCompletionSource.Task; - Task.WaitAll(task); - } + task = ApplicationTaskCompletionSource.Task; + Task.WaitAll(task); } private static void GenerateCertificate(string certPath, string certHost) @@ -270,31 +213,6 @@ namespace Emby.Server //CertificateGenerator.CreateSelfSignCertificatePfx(certPath, certHost, _logger); } - /// - /// Starts the service. - /// - private static void StartService(ILogManager logManager) - { - } - - /// - /// Handles the Disposed event of the service control. - /// - /// The source of the event. - /// The instance containing the event data. - static void service_Disposed(object sender, EventArgs e) - { - ApplicationTaskCompletionSource.SetResult(true); - OnServiceShutdown(); - } - - private static void OnServiceShutdown() - { - _logger.Info("Shutting down"); - - DisposeAppHost(); - } - /// /// Handles the UnhandledException event of the CurrentDomain control. /// @@ -306,10 +224,7 @@ namespace Emby.Server new UnhandledExceptionWriter(_appHost.ServerConfigurationManager.ApplicationPaths, _logger, _appHost.LogManager).Log(exception); - if (!_isRunningAsService) - { - ShowMessageBox("Unhandled exception: " + exception.Message); - } + ShowMessageBox("Unhandled exception: " + exception.Message); if (!Debugger.IsAttached) { @@ -325,29 +240,6 @@ namespace Emby.Server /// true if XXXX, false otherwise private static bool PerformUpdateIfNeeded(ServerApplicationPaths appPaths, ILogger logger) { - // Look for the existence of an update archive - var updateArchive = Path.Combine(appPaths.TempUpdatePath, "MBServer" + ".zip"); - if (File.Exists(updateArchive)) - { - logger.Info("An update is available from {0}", updateArchive); - - // Update is there - execute update - try - { - //var serviceName = _isRunningAsService ? BackgroundService.GetExistingServiceName() : string.Empty; - //new ApplicationUpdater().UpdateApplication(appPaths, updateArchive, logger, serviceName); - - // And just let the app exit so it can update - return true; - } - catch (Exception e) - { - logger.ErrorException("Error starting updater.", e); - - ShowMessageBox(string.Format("Error attempting to update application.\n\n{0}\n\n{1}", e.GetType().Name, e.Message)); - } - } - return false; } @@ -358,37 +250,25 @@ namespace Emby.Server public static void Shutdown() { - if (_isRunningAsService) - { - ShutdownWindowsService(); - } - else - { - DisposeAppHost(); + DisposeAppHost(); - ShutdownWindowsApplication(); - } + //_logger.Info("Calling Application.Exit"); + //Application.Exit(); + + _logger.Info("Calling Environment.Exit"); + Environment.Exit(0); + + _logger.Info("Calling ApplicationTaskCompletionSource.SetResult"); + ApplicationTaskCompletionSource.SetResult(true); } public static void Restart() { DisposeAppHost(); - if (_isRunningAsService) - { - RestartWindowsService(); - } - else - { - //_logger.Info("Hiding server notify icon"); - //_serverNotifyIcon.Visible = false; + // todo: start new instance - _logger.Info("Starting new instance"); - //Application.Restart(); - Process.Start(_appHost.ServerConfigurationManager.ApplicationPaths.ApplicationPath); - - ShutdownWindowsApplication(); - } + Shutdown(); } private static void DisposeAppHost() @@ -402,31 +282,6 @@ namespace Emby.Server } } - private static void ShutdownWindowsApplication() - { - //_logger.Info("Calling Application.Exit"); - //Application.Exit(); - - _logger.Info("Calling Environment.Exit"); - Environment.Exit(0); - - _logger.Info("Calling ApplicationTaskCompletionSource.SetResult"); - ApplicationTaskCompletionSource.SetResult(true); - } - - private static void ShutdownWindowsService() - { - } - - private static void RestartWindowsService() - { - } - - private static bool CanRestartWindowsService() - { - return false; - } - /// /// Sets the error mode. /// diff --git a/src/Emby.Server/project.json b/src/Emby.Server/project.json index c64db844f..55acee514 100644 --- a/src/Emby.Server/project.json +++ b/src/Emby.Server/project.json @@ -12,10 +12,10 @@ "version": "1.0.1" }, "Mono.Nat": "1.0.0-*", - "Microsoft.Win32.Registry": "4.0.0", - "System.Runtime.Extensions": "4.1.0", - "System.Diagnostics.Process": "4.1.0", - "Microsoft.Data.SQLite": "1.0.0" + "Microsoft.Win32.Registry": "4.0.0", + "System.Runtime.Extensions": "4.1.0", + "System.Diagnostics.Process": "4.1.0", + "Microsoft.Data.SQLite": "1.1.0-preview1-final" }, "frameworks": { -- cgit v1.2.3 From 65a1ef020b205b1676bd7dd70e7261a1fa29b7a2 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sat, 19 Nov 2016 00:52:49 -0500 Subject: move sync repository to portable project --- Emby.Common.Implementations/BaseApplicationHost.cs | 1 - .../ScheduledTasks/TaskManager.cs | 26 +- .../Updates/GithubUpdater.cs | 269 ------ Emby.Server.Core/ApplicationHost.cs | 24 +- Emby.Server.Core/Browser/BrowserLauncher.cs | 75 -- Emby.Server.Core/Data/SqliteItemRepository.cs | 29 +- .../EntryPoints/ExternalPortForwarding.cs | 3 +- Emby.Server.Core/EntryPoints/StartupWizard.cs | 59 -- Emby.Server.Core/HttpServerFactory.cs | 6 +- Emby.Server.Core/Migrations/DbMigration.cs | 64 -- .../Migrations/UpdateLevelMigration.cs | 131 --- Emby.Server.Core/Sync/SyncRepository.cs | 976 --------------------- .../Activity/ActivityRepository.cs | 2 +- .../Browser/BrowserLauncher.cs | 73 ++ .../Data/BaseSqliteRepository.cs | 20 +- .../Data/CleanDatabaseScheduledTask.cs | 148 +--- .../Emby.Server.Implementations.csproj | 4 + .../EntryPoints/StartupWizard.cs | 59 ++ .../FileOrganization/OrganizerScheduledTask.cs | 2 +- .../HttpServer/HttpListenerHost.cs | 8 +- .../Migrations/UpdateLevelMigration.cs | 130 +++ .../Security/AuthenticationRepository.cs | 4 +- Emby.Server.Implementations/Sync/SyncRepository.cs | 770 ++++++++++++++++ Emby.Server.Implementations/TV/TVSeriesManager.cs | 4 - MediaBrowser.Api/StartupWizardService.cs | 1 - MediaBrowser.Common/MediaBrowser.Common.csproj | 1 + MediaBrowser.Common/Updates/GithubUpdater.cs | 269 ++++++ .../Entities/InternalItemsQuery.cs | 1 - MediaBrowser.Controller/Entities/TV/Series.cs | 4 - .../Configuration/ServerConfiguration.cs | 4 - MediaBrowser.Model/Tasks/ITaskManager.cs | 2 - MediaBrowser.Server.Mono/MonoAppHost.cs | 26 + MediaBrowser.Server.Mono/Native/MonoFileSystem.cs | 3 +- MediaBrowser.ServerApplication/MainStartup.cs | 2 +- MediaBrowser.ServerApplication/ServerNotifyIcon.cs | 2 +- MediaBrowser.ServerApplication/WindowsAppHost.cs | 9 + 36 files changed, 1392 insertions(+), 1819 deletions(-) delete mode 100644 Emby.Common.Implementations/Updates/GithubUpdater.cs delete mode 100644 Emby.Server.Core/Browser/BrowserLauncher.cs delete mode 100644 Emby.Server.Core/EntryPoints/StartupWizard.cs delete mode 100644 Emby.Server.Core/Migrations/DbMigration.cs delete mode 100644 Emby.Server.Core/Migrations/UpdateLevelMigration.cs delete mode 100644 Emby.Server.Core/Sync/SyncRepository.cs create mode 100644 Emby.Server.Implementations/Browser/BrowserLauncher.cs create mode 100644 Emby.Server.Implementations/EntryPoints/StartupWizard.cs create mode 100644 Emby.Server.Implementations/Migrations/UpdateLevelMigration.cs create mode 100644 Emby.Server.Implementations/Sync/SyncRepository.cs create mode 100644 MediaBrowser.Common/Updates/GithubUpdater.cs (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Common.Implementations/BaseApplicationHost.cs b/Emby.Common.Implementations/BaseApplicationHost.cs index 1f194968c..02d7cb31f 100644 --- a/Emby.Common.Implementations/BaseApplicationHost.cs +++ b/Emby.Common.Implementations/BaseApplicationHost.cs @@ -4,7 +4,6 @@ using Emby.Common.Implementations.Devices; using Emby.Common.Implementations.IO; using Emby.Common.Implementations.ScheduledTasks; using Emby.Common.Implementations.Serialization; -using Emby.Common.Implementations.Updates; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Progress; diff --git a/Emby.Common.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Common.Implementations/ScheduledTasks/TaskManager.cs index 218af7ed5..b0153c588 100644 --- a/Emby.Common.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Common.Implementations/ScheduledTasks/TaskManager.cs @@ -55,25 +55,6 @@ namespace Emby.Common.Implementations.ScheduledTasks private ILogger Logger { get; set; } private readonly IFileSystem _fileSystem; - private bool _suspendTriggers; - - public bool SuspendTriggers - { - get { return _suspendTriggers; } - set - { - Logger.Info("Setting SuspendTriggers to {0}", value); - var executeQueued = _suspendTriggers && !value; - - _suspendTriggers = value; - - if (executeQueued) - { - ExecuteQueuedTasks(); - } - } - } - /// /// Initializes a new instance of the class. /// @@ -230,7 +211,7 @@ namespace Emby.Common.Implementations.ScheduledTasks lock (_taskQueue) { - if (task.State == TaskState.Idle && !SuspendTriggers) + if (task.State == TaskState.Idle) { Execute(task, options); return; @@ -322,11 +303,6 @@ namespace Emby.Common.Implementations.ScheduledTasks /// private void ExecuteQueuedTasks() { - if (SuspendTriggers) - { - return; - } - Logger.Info("ExecuteQueuedTasks"); // Execute queued tasks diff --git a/Emby.Common.Implementations/Updates/GithubUpdater.cs b/Emby.Common.Implementations/Updates/GithubUpdater.cs deleted file mode 100644 index 42bc29ed5..000000000 --- a/Emby.Common.Implementations/Updates/GithubUpdater.cs +++ /dev/null @@ -1,269 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Updates; - -namespace Emby.Common.Implementations.Updates -{ - public class GithubUpdater - { - private readonly IHttpClient _httpClient; - private readonly IJsonSerializer _jsonSerializer; - - public GithubUpdater(IHttpClient httpClient, IJsonSerializer jsonSerializer) - { - _httpClient = httpClient; - _jsonSerializer = jsonSerializer; - } - - public async Task CheckForUpdateResult(string organzation, string repository, Version minVersion, PackageVersionClass updateLevel, string assetFilename, string packageName, string targetFilename, TimeSpan cacheLength, CancellationToken cancellationToken) - { - var url = string.Format("https://api.github.com/repos/{0}/{1}/releases", organzation, repository); - - var options = new HttpRequestOptions - { - Url = url, - EnableKeepAlive = false, - CancellationToken = cancellationToken, - UserAgent = "Emby/3.0", - BufferContent = false - }; - - if (cacheLength.Ticks > 0) - { - options.CacheMode = CacheMode.Unconditional; - options.CacheLength = cacheLength; - } - - using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) - { - var obj = _jsonSerializer.DeserializeFromStream(stream); - - return CheckForUpdateResult(obj, minVersion, updateLevel, assetFilename, packageName, targetFilename); - } - } - - private CheckForUpdateResult CheckForUpdateResult(RootObject[] obj, Version minVersion, PackageVersionClass updateLevel, string assetFilename, string packageName, string targetFilename) - { - if (updateLevel == PackageVersionClass.Release) - { - // Technically all we need to do is check that it's not pre-release - // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly. - obj = obj.Where(i => !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) && !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase)).ToArray(); - } - else if (updateLevel == PackageVersionClass.Beta) - { - obj = obj.Where(i => !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase)).ToArray(); - } - else if (updateLevel == PackageVersionClass.Dev) - { - obj = obj.Where(i => !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) || i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase)).ToArray(); - } - - var availableUpdate = obj - .Select(i => CheckForUpdateResult(i, minVersion, assetFilename, packageName, targetFilename)) - .Where(i => i != null) - .OrderByDescending(i => Version.Parse(i.AvailableVersion)) - .FirstOrDefault(); - - return availableUpdate ?? new CheckForUpdateResult - { - IsUpdateAvailable = false - }; - } - - private bool MatchesUpdateLevel(RootObject i, PackageVersionClass updateLevel) - { - if (updateLevel == PackageVersionClass.Beta) - { - return !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase); - } - if (updateLevel == PackageVersionClass.Dev) - { - return !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) || - i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase); - } - - // Technically all we need to do is check that it's not pre-release - // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly. - return !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) && - !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase); - } - - public async Task> GetLatestReleases(string organzation, string repository, string assetFilename, CancellationToken cancellationToken) - { - var list = new List(); - - var url = string.Format("https://api.github.com/repos/{0}/{1}/releases", organzation, repository); - - var options = new HttpRequestOptions - { - Url = url, - EnableKeepAlive = false, - CancellationToken = cancellationToken, - UserAgent = "Emby/3.0", - BufferContent = false - }; - - using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) - { - var obj = _jsonSerializer.DeserializeFromStream(stream); - - obj = obj.Where(i => (i.assets ?? new List()).Any(a => IsAsset(a, assetFilename))).ToArray(); - - list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Release)).OrderByDescending(GetVersion).Take(1)); - list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Beta)).OrderByDescending(GetVersion).Take(1)); - list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Dev)).OrderByDescending(GetVersion).Take(1)); - - return list; - } - } - - public Version GetVersion(RootObject obj) - { - Version version; - if (!Version.TryParse(obj.tag_name, out version)) - { - return new Version(1, 0); - } - - return version; - } - - private CheckForUpdateResult CheckForUpdateResult(RootObject obj, Version minVersion, string assetFilename, string packageName, string targetFilename) - { - Version version; - if (!Version.TryParse(obj.tag_name, out version)) - { - return null; - } - - if (version < minVersion) - { - return null; - } - - var asset = (obj.assets ?? new List()).FirstOrDefault(i => IsAsset(i, assetFilename)); - - if (asset == null) - { - return null; - } - - return new CheckForUpdateResult - { - AvailableVersion = version.ToString(), - IsUpdateAvailable = version > minVersion, - Package = new PackageVersionInfo - { - classification = obj.prerelease ? - (obj.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase) ? PackageVersionClass.Dev : PackageVersionClass.Beta) : - PackageVersionClass.Release, - name = packageName, - sourceUrl = asset.browser_download_url, - targetFilename = targetFilename, - versionStr = version.ToString(), - requiredVersionStr = "1.0.0", - description = obj.body, - infoUrl = obj.html_url - } - }; - } - - private bool IsAsset(Asset asset, string assetFilename) - { - var downloadFilename = Path.GetFileName(asset.browser_download_url) ?? string.Empty; - - if (downloadFilename.IndexOf(assetFilename, StringComparison.OrdinalIgnoreCase) != -1) - { - return true; - } - - return string.Equals(assetFilename, downloadFilename, StringComparison.OrdinalIgnoreCase); - } - - public class Uploader - { - public string login { get; set; } - public int id { get; set; } - public string avatar_url { get; set; } - public string gravatar_id { get; set; } - public string url { get; set; } - public string html_url { get; set; } - public string followers_url { get; set; } - public string following_url { get; set; } - public string gists_url { get; set; } - public string starred_url { get; set; } - public string subscriptions_url { get; set; } - public string organizations_url { get; set; } - public string repos_url { get; set; } - public string events_url { get; set; } - public string received_events_url { get; set; } - public string type { get; set; } - public bool site_admin { get; set; } - } - - public class Asset - { - public string url { get; set; } - public int id { get; set; } - public string name { get; set; } - public object label { get; set; } - public Uploader uploader { get; set; } - public string content_type { get; set; } - public string state { get; set; } - public int size { get; set; } - public int download_count { get; set; } - public string created_at { get; set; } - public string updated_at { get; set; } - public string browser_download_url { get; set; } - } - - public class Author - { - public string login { get; set; } - public int id { get; set; } - public string avatar_url { get; set; } - public string gravatar_id { get; set; } - public string url { get; set; } - public string html_url { get; set; } - public string followers_url { get; set; } - public string following_url { get; set; } - public string gists_url { get; set; } - public string starred_url { get; set; } - public string subscriptions_url { get; set; } - public string organizations_url { get; set; } - public string repos_url { get; set; } - public string events_url { get; set; } - public string received_events_url { get; set; } - public string type { get; set; } - public bool site_admin { get; set; } - } - - public class RootObject - { - public string url { get; set; } - public string assets_url { get; set; } - public string upload_url { get; set; } - public string html_url { get; set; } - public int id { get; set; } - public string tag_name { get; set; } - public string target_commitish { get; set; } - public string name { get; set; } - public bool draft { get; set; } - public Author author { get; set; } - public bool prerelease { get; set; } - public string created_at { get; set; } - public string published_at { get; set; } - public List assets { get; set; } - public string tarball_url { get; set; } - public string zipball_url { get; set; } - public string body { get; set; } - } - } -} diff --git a/Emby.Server.Core/ApplicationHost.cs b/Emby.Server.Core/ApplicationHost.cs index 6073991b1..00b9b99e6 100644 --- a/Emby.Server.Core/ApplicationHost.cs +++ b/Emby.Server.Core/ApplicationHost.cs @@ -65,7 +65,6 @@ using Emby.Common.Implementations.Networking; using Emby.Common.Implementations.Reflection; using Emby.Common.Implementations.Serialization; using Emby.Common.Implementations.TextEncoding; -using Emby.Common.Implementations.Updates; using Emby.Common.Implementations.Xml; using Emby.Photos; using MediaBrowser.Model.IO; @@ -90,10 +89,10 @@ using Emby.Server.Implementations.Devices; using Emby.Server.Implementations.FFMpeg; using Emby.Server.Core.IO; using Emby.Server.Core.Localization; -using Emby.Server.Core.Migrations; +using Emby.Server.Implementations.Migrations; using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Social; -using Emby.Server.Core.Sync; +using Emby.Server.Implementations.Sync; using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Collections; using Emby.Server.Implementations.Connect; @@ -357,12 +356,6 @@ namespace Emby.Server.Core { await PerformPreInitMigrations().ConfigureAwait(false); - if (ServerConfigurationManager.Configuration.MigrationVersion < CleanDatabaseScheduledTask.MigrationVersion && - ServerConfigurationManager.Configuration.IsStartupWizardCompleted) - { - TaskManager.SuspendTriggers = true; - } - await base.RunStartupTasks().ConfigureAwait(false); await MediaEncoder.Init().ConfigureAwait(false); @@ -494,7 +487,6 @@ namespace Emby.Server.Core { var migrations = new List { - new DbMigration(ServerConfigurationManager, TaskManager) }; foreach (var task in migrations) @@ -568,7 +560,7 @@ namespace Emby.Server.Core AuthenticationRepository = await GetAuthenticationRepository().ConfigureAwait(false); RegisterSingleInstance(AuthenticationRepository); - SyncRepository = await GetSyncRepository().ConfigureAwait(false); + SyncRepository = GetSyncRepository(); RegisterSingleInstance(SyncRepository); UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, () => ConnectManager, this, JsonSerializer, FileSystemManager, CryptographyProvider, _defaultUserNameFactory()); @@ -591,7 +583,7 @@ namespace Emby.Server.Core CertificatePath = GetCertificatePath(true); Certificate = GetCertificate(CertificatePath); - HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamFactory, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate); + HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamFactory, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate, SupportsDualModeSockets); HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); RegisterSingleInstance(HttpServer, false); progress.Report(10); @@ -714,6 +706,8 @@ namespace Emby.Server.Core await ((UserManager)UserManager).Initialize().ConfigureAwait(false); } + protected abstract bool SupportsDualModeSockets { get; } + private ICertificate GetCertificate(string certificateLocation) { if (string.IsNullOrWhiteSpace(certificateLocation)) @@ -844,11 +838,11 @@ namespace Emby.Server.Core return repo; } - private async Task GetSyncRepository() + private ISyncRepository GetSyncRepository() { - var repo = new SyncRepository(LogManager, JsonSerializer, ServerConfigurationManager.ApplicationPaths, GetDbConnector()); + var repo = new SyncRepository(LogManager.GetLogger("SyncRepository"), JsonSerializer, ServerConfigurationManager.ApplicationPaths); - await repo.Initialize().ConfigureAwait(false); + repo.Initialize(); return repo; } diff --git a/Emby.Server.Core/Browser/BrowserLauncher.cs b/Emby.Server.Core/Browser/BrowserLauncher.cs deleted file mode 100644 index 4ccc41c30..000000000 --- a/Emby.Server.Core/Browser/BrowserLauncher.cs +++ /dev/null @@ -1,75 +0,0 @@ -using MediaBrowser.Controller; -using System; - -namespace Emby.Server.Core.Browser -{ - /// - /// Class BrowserLauncher - /// - public static class BrowserLauncher - { - /// - /// Opens the dashboard page. - /// - /// The page. - /// The app host. - public static void OpenDashboardPage(string page, IServerApplicationHost appHost) - { - var url = appHost.GetLocalApiUrl("localhost") + "/web/" + page; - - OpenUrl(appHost, url); - } - - /// - /// Opens the community. - /// - public static void OpenCommunity(IServerApplicationHost appHost) - { - OpenUrl(appHost, "http://emby.media/community"); - } - - public static void OpenEmbyPremiere(IServerApplicationHost appHost) - { - OpenDashboardPage("supporterkey.html", appHost); - } - - /// - /// Opens the web client. - /// - /// The app host. - public static void OpenWebClient(IServerApplicationHost appHost) - { - OpenDashboardPage("index.html", appHost); - } - - /// - /// Opens the dashboard. - /// - /// The app host. - public static void OpenDashboard(IServerApplicationHost appHost) - { - OpenDashboardPage("dashboard.html", appHost); - } - - /// - /// Opens the URL. - /// - /// The URL. - private static void OpenUrl(IServerApplicationHost appHost, string url) - { - try - { - appHost.LaunchUrl(url); - } - catch (NotImplementedException) - { - - } - catch (Exception ex) - { - Console.WriteLine("Error launching url: " + url); - Console.WriteLine(ex.Message); - } - } - } -} diff --git a/Emby.Server.Core/Data/SqliteItemRepository.cs b/Emby.Server.Core/Data/SqliteItemRepository.cs index ed03c0f67..2f08081f6 100644 --- a/Emby.Server.Core/Data/SqliteItemRepository.cs +++ b/Emby.Server.Core/Data/SqliteItemRepository.cs @@ -3060,18 +3060,6 @@ namespace Emby.Server.Core.Data { //whereClauses.Add("(UserId is null or UserId=@UserId)"); } - if (query.IsCurrentSchema.HasValue) - { - if (query.IsCurrentSchema.Value) - { - whereClauses.Add("(SchemaVersion not null AND SchemaVersion=@SchemaVersion)"); - } - else - { - whereClauses.Add("(SchemaVersion is null or SchemaVersion<>@SchemaVersion)"); - } - cmd.Parameters.Add(cmd, "@SchemaVersion", DbType.Int32).Value = LatestSchemaVersion; - } if (query.IsHD.HasValue) { whereClauses.Add("IsHD=@IsHD"); @@ -3454,7 +3442,7 @@ namespace Emby.Server.Core.Data cmd.Parameters.Add(cmd, "@NameLessThan", DbType.String).Value = query.NameLessThan.ToLower(); } - if (query.ImageTypes.Length > 0 && _config.Configuration.SchemaVersion >= 87) + if (query.ImageTypes.Length > 0) { foreach (var requiredImage in query.ImageTypes) { @@ -3738,15 +3726,8 @@ namespace Emby.Server.Core.Data } if (query.IsVirtualItem.HasValue) { - if (_config.Configuration.SchemaVersion >= 90) - { - whereClauses.Add("IsVirtualItem=@IsVirtualItem"); - cmd.Parameters.Add(cmd, "@IsVirtualItem", DbType.Boolean).Value = query.IsVirtualItem.Value; - } - else if (!query.IsVirtualItem.Value) - { - whereClauses.Add("LocationType<>'Virtual'"); - } + whereClauses.Add("IsVirtualItem=@IsVirtualItem"); + cmd.Parameters.Add(cmd, "@IsVirtualItem", DbType.Boolean).Value = query.IsVirtualItem.Value; } if (query.IsSpecialSeason.HasValue) { @@ -3770,7 +3751,7 @@ namespace Emby.Server.Core.Data whereClauses.Add("PremiereDate < DATETIME('now')"); } } - if (query.IsMissing.HasValue && _config.Configuration.SchemaVersion >= 90) + if (query.IsMissing.HasValue) { if (query.IsMissing.Value) { @@ -3781,7 +3762,7 @@ namespace Emby.Server.Core.Data whereClauses.Add("(IsVirtualItem=0 OR PremiereDate >= DATETIME('now'))"); } } - if (query.IsVirtualUnaired.HasValue && _config.Configuration.SchemaVersion >= 90) + if (query.IsVirtualUnaired.HasValue) { if (query.IsVirtualUnaired.Value) { diff --git a/Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs index c1a19a71f..11e275940 100644 --- a/Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs @@ -95,7 +95,7 @@ namespace Emby.Server.Core.EntryPoints NatUtility.StartDiscovery(); - _timer = _timerFactory.Create(ClearCreatedRules, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); + _timer = _timerFactory.Create(ClearCreatedRules, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; @@ -233,6 +233,7 @@ namespace Emby.Server.Core.EntryPoints await device.CreatePortMap(new Mapping(Protocol.Tcp, privatePort, publicPort) { Description = _appHost.Name + }).ConfigureAwait(false); } catch (Exception ex) diff --git a/Emby.Server.Core/EntryPoints/StartupWizard.cs b/Emby.Server.Core/EntryPoints/StartupWizard.cs deleted file mode 100644 index 30ceca073..000000000 --- a/Emby.Server.Core/EntryPoints/StartupWizard.cs +++ /dev/null @@ -1,59 +0,0 @@ -using MediaBrowser.Controller; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.Logging; -using Emby.Server.Core.Browser; - -namespace Emby.Server.Core.EntryPoints -{ - /// - /// Class StartupWizard - /// - public class StartupWizard : IServerEntryPoint - { - /// - /// The _app host - /// - private readonly IServerApplicationHost _appHost; - /// - /// The _user manager - /// - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The app host. - /// The logger. - public StartupWizard(IServerApplicationHost appHost, ILogger logger) - { - _appHost = appHost; - _logger = logger; - } - - /// - /// Runs this instance. - /// - public void Run() - { - if (_appHost.IsFirstRun) - { - LaunchStartupWizard(); - } - } - - /// - /// Launches the startup wizard. - /// - private void LaunchStartupWizard() - { - BrowserLauncher.OpenDashboardPage("wizardstart.html", _appHost); - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() - { - } - } -} \ No newline at end of file diff --git a/Emby.Server.Core/HttpServerFactory.cs b/Emby.Server.Core/HttpServerFactory.cs index d6c07e258..8ec5fb026 100644 --- a/Emby.Server.Core/HttpServerFactory.cs +++ b/Emby.Server.Core/HttpServerFactory.cs @@ -44,7 +44,8 @@ namespace Emby.Server.Core IJsonSerializer json, IXmlSerializer xml, IEnvironmentInfo environment, - ICertificate certificate) + ICertificate certificate, + bool enableDualModeSockets) { var logger = logManager.GetLogger("HttpServer"); @@ -63,7 +64,8 @@ namespace Emby.Server.Core environment, certificate, new StreamFactory(), - GetParseFn); + GetParseFn, + enableDualModeSockets); } private static Func GetParseFn(Type propertyType) diff --git a/Emby.Server.Core/Migrations/DbMigration.cs b/Emby.Server.Core/Migrations/DbMigration.cs deleted file mode 100644 index 5d652770f..000000000 --- a/Emby.Server.Core/Migrations/DbMigration.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Threading.Tasks; -using Emby.Server.Implementations.Data; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.Tasks; -using Emby.Server.Core.Data; -using Emby.Server.Implementations.Migrations; - -namespace Emby.Server.Core.Migrations -{ - public class DbMigration : IVersionMigration - { - private readonly IServerConfigurationManager _config; - private readonly ITaskManager _taskManager; - - public DbMigration(IServerConfigurationManager config, ITaskManager taskManager) - { - _config = config; - _taskManager = taskManager; - } - - public async Task Run() - { - // If a forced migration is required, do that now - if (_config.Configuration.MigrationVersion < CleanDatabaseScheduledTask.MigrationVersion) - { - if (!_config.Configuration.IsStartupWizardCompleted) - { - _config.Configuration.MigrationVersion = CleanDatabaseScheduledTask.MigrationVersion; - _config.SaveConfiguration(); - return; - } - - _taskManager.SuspendTriggers = true; - CleanDatabaseScheduledTask.EnableUnavailableMessage = true; - - Task.Run(async () => - { - await Task.Delay(1000).ConfigureAwait(false); - - _taskManager.Execute(); - }); - - return; - } - - if (_config.Configuration.SchemaVersion < SqliteItemRepository.LatestSchemaVersion) - { - if (!_config.Configuration.IsStartupWizardCompleted) - { - _config.Configuration.SchemaVersion = SqliteItemRepository.LatestSchemaVersion; - _config.SaveConfiguration(); - return; - } - - Task.Run(async () => - { - await Task.Delay(1000).ConfigureAwait(false); - - _taskManager.Execute(); - }); - } - } - } -} diff --git a/Emby.Server.Core/Migrations/UpdateLevelMigration.cs b/Emby.Server.Core/Migrations/UpdateLevelMigration.cs deleted file mode 100644 index c79dbabea..000000000 --- a/Emby.Server.Core/Migrations/UpdateLevelMigration.cs +++ /dev/null @@ -1,131 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Emby.Common.Implementations.Updates; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Updates; -using Emby.Server.Implementations.Migrations; - -namespace Emby.Server.Core.Migrations -{ - public class UpdateLevelMigration : IVersionMigration - { - private readonly IServerConfigurationManager _config; - private readonly IServerApplicationHost _appHost; - private readonly IHttpClient _httpClient; - private readonly IJsonSerializer _jsonSerializer; - private readonly string _releaseAssetFilename; - private readonly ILogger _logger; - - public UpdateLevelMigration(IServerConfigurationManager config, IServerApplicationHost appHost, IHttpClient httpClient, IJsonSerializer jsonSerializer, string releaseAssetFilename, ILogger logger) - { - _config = config; - _appHost = appHost; - _httpClient = httpClient; - _jsonSerializer = jsonSerializer; - _releaseAssetFilename = releaseAssetFilename; - _logger = logger; - } - - public async Task Run() - { - var lastVersion = _config.Configuration.LastVersion; - var currentVersion = _appHost.ApplicationVersion; - - if (string.Equals(lastVersion, currentVersion.ToString(), StringComparison.OrdinalIgnoreCase)) - { - return; - } - - try - { - var updateLevel = _config.Configuration.SystemUpdateLevel; - - await CheckVersion(currentVersion, updateLevel, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.ErrorException("Error in update migration", ex); - } - } - - private async Task CheckVersion(Version currentVersion, PackageVersionClass currentUpdateLevel, CancellationToken cancellationToken) - { - var releases = await new GithubUpdater(_httpClient, _jsonSerializer) - .GetLatestReleases("MediaBrowser", "Emby", _releaseAssetFilename, cancellationToken).ConfigureAwait(false); - - var newUpdateLevel = GetNewUpdateLevel(currentVersion, currentUpdateLevel, releases); - - if (newUpdateLevel != currentUpdateLevel) - { - _config.Configuration.SystemUpdateLevel = newUpdateLevel; - _config.SaveConfiguration(); - } - } - - private PackageVersionClass GetNewUpdateLevel(Version currentVersion, PackageVersionClass currentUpdateLevel, List releases) - { - var newUpdateLevel = currentUpdateLevel; - - // If the current version is later than current stable, set the update level to beta - if (releases.Count >= 1) - { - var release = releases[0]; - var version = ParseVersion(release.tag_name); - if (version != null) - { - if (currentVersion > version) - { - newUpdateLevel = PackageVersionClass.Beta; - } - else - { - return PackageVersionClass.Release; - } - } - } - - // If the current version is later than current beta, set the update level to dev - if (releases.Count >= 2) - { - var release = releases[1]; - var version = ParseVersion(release.tag_name); - if (version != null) - { - if (currentVersion > version) - { - newUpdateLevel = PackageVersionClass.Dev; - } - else - { - return PackageVersionClass.Beta; - } - } - } - - return newUpdateLevel; - } - - private Version ParseVersion(string versionString) - { - if (!string.IsNullOrWhiteSpace(versionString)) - { - var parts = versionString.Split('.'); - if (parts.Length == 3) - { - versionString += ".0"; - } - } - - Version version; - Version.TryParse(versionString, out version); - - return version; - } - } -} diff --git a/Emby.Server.Core/Sync/SyncRepository.cs b/Emby.Server.Core/Sync/SyncRepository.cs deleted file mode 100644 index bfcf76767..000000000 --- a/Emby.Server.Core/Sync/SyncRepository.cs +++ /dev/null @@ -1,976 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Emby.Server.Core.Data; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Sync; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Sync; - -namespace Emby.Server.Core.Sync -{ - public class SyncRepository : BaseSqliteRepository, ISyncRepository - { - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - private readonly IJsonSerializer _json; - - public SyncRepository(ILogManager logManager, IJsonSerializer json, IServerApplicationPaths appPaths, IDbConnector connector) - : base(logManager, connector) - { - _json = json; - DbFilePath = Path.Combine(appPaths.DataPath, "sync14.db"); - } - - private class SyncSummary - { - public Dictionary Items { get; set; } - - public SyncSummary() - { - Items = new Dictionary(); - } - } - - - public async Task Initialize() - { - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - string[] queries = { - - "create table if not exists SyncJobs (Id GUID PRIMARY KEY, TargetId TEXT NOT NULL, Name TEXT NOT NULL, Profile TEXT, Quality TEXT, Bitrate INT, Status TEXT NOT NULL, Progress FLOAT, UserId TEXT NOT NULL, ItemIds TEXT NOT NULL, Category TEXT, ParentId TEXT, UnwatchedOnly BIT, ItemLimit INT, SyncNewContent BIT, DateCreated DateTime, DateLastModified DateTime, ItemCount int)", - - "create table if not exists SyncJobItems (Id GUID PRIMARY KEY, ItemId TEXT, ItemName TEXT, MediaSourceId TEXT, JobId TEXT, TemporaryPath TEXT, OutputPath TEXT, Status TEXT, TargetId TEXT, DateCreated DateTime, Progress FLOAT, AdditionalFiles TEXT, MediaSource TEXT, IsMarkedForRemoval BIT, JobItemIndex INT, ItemDateModifiedTicks BIGINT)", - - "drop index if exists idx_SyncJobItems2", - "drop index if exists idx_SyncJobItems3", - "drop index if exists idx_SyncJobs1", - "drop index if exists idx_SyncJobs", - "drop index if exists idx_SyncJobItems1", - "create index if not exists idx_SyncJobItems4 on SyncJobItems(TargetId,ItemId,Status,Progress,DateCreated)", - "create index if not exists idx_SyncJobItems5 on SyncJobItems(TargetId,Status,ItemId,Progress)", - - "create index if not exists idx_SyncJobs2 on SyncJobs(TargetId,Status,ItemIds,Progress)", - - "pragma shrink_memory" - }; - - connection.RunQueries(queries, Logger); - - connection.AddColumn(Logger, "SyncJobs", "Profile", "TEXT"); - connection.AddColumn(Logger, "SyncJobs", "Bitrate", "INT"); - connection.AddColumn(Logger, "SyncJobItems", "ItemDateModifiedTicks", "BIGINT"); - } - } - - private const string BaseJobSelectText = "select Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount from SyncJobs"; - private const string BaseJobItemSelectText = "select Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks from SyncJobItems"; - - public SyncJob GetJob(string id) - { - if (string.IsNullOrEmpty(id)) - { - throw new ArgumentNullException("id"); - } - - CheckDisposed(); - - var guid = new Guid(id); - - if (guid == Guid.Empty) - { - throw new ArgumentNullException("id"); - } - - using (var connection = CreateConnection(true).Result) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = BaseJobSelectText + " where Id=@Id"; - - cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) - { - if (reader.Read()) - { - return GetJob(reader); - } - } - } - - return null; - } - } - - private SyncJob GetJob(IDataReader reader) - { - var info = new SyncJob - { - Id = reader.GetGuid(0).ToString("N"), - TargetId = reader.GetString(1), - Name = reader.GetString(2) - }; - - if (!reader.IsDBNull(3)) - { - info.Profile = reader.GetString(3); - } - - if (!reader.IsDBNull(4)) - { - info.Quality = reader.GetString(4); - } - - if (!reader.IsDBNull(5)) - { - info.Bitrate = reader.GetInt32(5); - } - - if (!reader.IsDBNull(6)) - { - info.Status = (SyncJobStatus)Enum.Parse(typeof(SyncJobStatus), reader.GetString(6), true); - } - - if (!reader.IsDBNull(7)) - { - info.Progress = reader.GetDouble(7); - } - - if (!reader.IsDBNull(8)) - { - info.UserId = reader.GetString(8); - } - - if (!reader.IsDBNull(9)) - { - info.RequestedItemIds = reader.GetString(9).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); - } - - if (!reader.IsDBNull(10)) - { - info.Category = (SyncCategory)Enum.Parse(typeof(SyncCategory), reader.GetString(10), true); - } - - if (!reader.IsDBNull(11)) - { - info.ParentId = reader.GetString(11); - } - - if (!reader.IsDBNull(12)) - { - info.UnwatchedOnly = reader.GetBoolean(12); - } - - if (!reader.IsDBNull(13)) - { - info.ItemLimit = reader.GetInt32(13); - } - - info.SyncNewContent = reader.GetBoolean(14); - - info.DateCreated = reader.GetDateTime(15).ToUniversalTime(); - info.DateLastModified = reader.GetDateTime(16).ToUniversalTime(); - info.ItemCount = reader.GetInt32(17); - - return info; - } - - public Task Create(SyncJob job) - { - return InsertOrUpdate(job, true); - } - - public Task Update(SyncJob job) - { - return InsertOrUpdate(job, false); - } - - private async Task InsertOrUpdate(SyncJob job, bool insert) - { - if (job == null) - { - throw new ArgumentNullException("job"); - } - - CheckDisposed(); - - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - using (var cmd = connection.CreateCommand()) - { - if (insert) - { - cmd.CommandText = "insert into SyncJobs (Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount) values (@Id, @TargetId, @Name, @Profile, @Quality, @Bitrate, @Status, @Progress, @UserId, @ItemIds, @Category, @ParentId, @UnwatchedOnly, @ItemLimit, @SyncNewContent, @DateCreated, @DateLastModified, @ItemCount)"; - - cmd.Parameters.Add(cmd, "@Id"); - cmd.Parameters.Add(cmd, "@TargetId"); - cmd.Parameters.Add(cmd, "@Name"); - cmd.Parameters.Add(cmd, "@Profile"); - cmd.Parameters.Add(cmd, "@Quality"); - cmd.Parameters.Add(cmd, "@Bitrate"); - cmd.Parameters.Add(cmd, "@Status"); - cmd.Parameters.Add(cmd, "@Progress"); - cmd.Parameters.Add(cmd, "@UserId"); - cmd.Parameters.Add(cmd, "@ItemIds"); - cmd.Parameters.Add(cmd, "@Category"); - cmd.Parameters.Add(cmd, "@ParentId"); - cmd.Parameters.Add(cmd, "@UnwatchedOnly"); - cmd.Parameters.Add(cmd, "@ItemLimit"); - cmd.Parameters.Add(cmd, "@SyncNewContent"); - cmd.Parameters.Add(cmd, "@DateCreated"); - cmd.Parameters.Add(cmd, "@DateLastModified"); - cmd.Parameters.Add(cmd, "@ItemCount"); - } - else - { - cmd.CommandText = "update SyncJobs set TargetId=@TargetId,Name=@Name,Profile=@Profile,Quality=@Quality,Bitrate=@Bitrate,Status=@Status,Progress=@Progress,UserId=@UserId,ItemIds=@ItemIds,Category=@Category,ParentId=@ParentId,UnwatchedOnly=@UnwatchedOnly,ItemLimit=@ItemLimit,SyncNewContent=@SyncNewContent,DateCreated=@DateCreated,DateLastModified=@DateLastModified,ItemCount=@ItemCount where Id=@Id"; - - cmd.Parameters.Add(cmd, "@Id"); - cmd.Parameters.Add(cmd, "@TargetId"); - cmd.Parameters.Add(cmd, "@Name"); - cmd.Parameters.Add(cmd, "@Profile"); - cmd.Parameters.Add(cmd, "@Quality"); - cmd.Parameters.Add(cmd, "@Bitrate"); - cmd.Parameters.Add(cmd, "@Status"); - cmd.Parameters.Add(cmd, "@Progress"); - cmd.Parameters.Add(cmd, "@UserId"); - cmd.Parameters.Add(cmd, "@ItemIds"); - cmd.Parameters.Add(cmd, "@Category"); - cmd.Parameters.Add(cmd, "@ParentId"); - cmd.Parameters.Add(cmd, "@UnwatchedOnly"); - cmd.Parameters.Add(cmd, "@ItemLimit"); - cmd.Parameters.Add(cmd, "@SyncNewContent"); - cmd.Parameters.Add(cmd, "@DateCreated"); - cmd.Parameters.Add(cmd, "@DateLastModified"); - cmd.Parameters.Add(cmd, "@ItemCount"); - } - - IDbTransaction transaction = null; - - try - { - transaction = connection.BeginTransaction(); - - var index = 0; - - cmd.GetParameter(index++).Value = new Guid(job.Id); - cmd.GetParameter(index++).Value = job.TargetId; - cmd.GetParameter(index++).Value = job.Name; - cmd.GetParameter(index++).Value = job.Profile; - cmd.GetParameter(index++).Value = job.Quality; - cmd.GetParameter(index++).Value = job.Bitrate; - cmd.GetParameter(index++).Value = job.Status.ToString(); - cmd.GetParameter(index++).Value = job.Progress; - cmd.GetParameter(index++).Value = job.UserId; - cmd.GetParameter(index++).Value = string.Join(",", job.RequestedItemIds.ToArray()); - cmd.GetParameter(index++).Value = job.Category; - cmd.GetParameter(index++).Value = job.ParentId; - cmd.GetParameter(index++).Value = job.UnwatchedOnly; - cmd.GetParameter(index++).Value = job.ItemLimit; - cmd.GetParameter(index++).Value = job.SyncNewContent; - cmd.GetParameter(index++).Value = job.DateCreated; - cmd.GetParameter(index++).Value = job.DateLastModified; - cmd.GetParameter(index++).Value = job.ItemCount; - - cmd.Transaction = transaction; - - cmd.ExecuteNonQuery(); - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save record:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - } - } - } - } - - public async Task DeleteJob(string id) - { - if (string.IsNullOrWhiteSpace(id)) - { - throw new ArgumentNullException("id"); - } - - CheckDisposed(); - - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - using (var deleteJobCommand = connection.CreateCommand()) - { - using (var deleteJobItemsCommand = connection.CreateCommand()) - { - IDbTransaction transaction = null; - - try - { - // _deleteJobCommand - deleteJobCommand.CommandText = "delete from SyncJobs where Id=@Id"; - deleteJobCommand.Parameters.Add(deleteJobCommand, "@Id"); - - transaction = connection.BeginTransaction(); - - deleteJobCommand.GetParameter(0).Value = new Guid(id); - deleteJobCommand.Transaction = transaction; - deleteJobCommand.ExecuteNonQuery(); - - // _deleteJobItemsCommand - deleteJobItemsCommand.CommandText = "delete from SyncJobItems where JobId=@JobId"; - deleteJobItemsCommand.Parameters.Add(deleteJobItemsCommand, "@JobId"); - - deleteJobItemsCommand.GetParameter(0).Value = id; - deleteJobItemsCommand.Transaction = transaction; - deleteJobItemsCommand.ExecuteNonQuery(); - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save record:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - } - } - } - } - } - - public QueryResult GetJobs(SyncJobQuery query) - { - if (query == null) - { - throw new ArgumentNullException("query"); - } - - CheckDisposed(); - - using (var connection = CreateConnection(true).Result) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = BaseJobSelectText; - - var whereClauses = new List(); - - if (query.Statuses.Length > 0) - { - var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); - - whereClauses.Add(string.Format("Status in ({0})", statuses)); - } - if (!string.IsNullOrWhiteSpace(query.TargetId)) - { - whereClauses.Add("TargetId=@TargetId"); - cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; - } - if (!string.IsNullOrWhiteSpace(query.ExcludeTargetIds)) - { - var excludeIds = (query.ExcludeTargetIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - if (excludeIds.Length == 1) - { - whereClauses.Add("TargetId<>@ExcludeTargetId"); - cmd.Parameters.Add(cmd, "@ExcludeTargetId", DbType.String).Value = excludeIds[0]; - } - else if (excludeIds.Length > 1) - { - whereClauses.Add("TargetId<>@ExcludeTargetId"); - cmd.Parameters.Add(cmd, "@ExcludeTargetId", DbType.String).Value = excludeIds[0]; - } - } - if (!string.IsNullOrWhiteSpace(query.UserId)) - { - whereClauses.Add("UserId=@UserId"); - cmd.Parameters.Add(cmd, "@UserId", DbType.String).Value = query.UserId; - } - if (query.SyncNewContent.HasValue) - { - whereClauses.Add("SyncNewContent=@SyncNewContent"); - cmd.Parameters.Add(cmd, "@SyncNewContent", DbType.Boolean).Value = query.SyncNewContent.Value; - } - - cmd.CommandText += " mainTable"; - - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - var startIndex = query.StartIndex ?? 0; - if (startIndex > 0) - { - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobs ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC LIMIT {0})", - startIndex.ToString(_usCulture))); - } - - if (whereClauses.Count > 0) - { - cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); - } - - cmd.CommandText += " ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC"; - - if (query.Limit.HasValue) - { - cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); - } - - cmd.CommandText += "; select count (Id) from SyncJobs" + whereTextWithoutPaging; - - var list = new List(); - var count = 0; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) - { - while (reader.Read()) - { - list.Add(GetJob(reader)); - } - - if (reader.NextResult() && reader.Read()) - { - count = reader.GetInt32(0); - } - } - - return new QueryResult() - { - Items = list.ToArray(), - TotalRecordCount = count - }; - } - } - } - - public SyncJobItem GetJobItem(string id) - { - if (string.IsNullOrEmpty(id)) - { - throw new ArgumentNullException("id"); - } - - CheckDisposed(); - - var guid = new Guid(id); - - using (var connection = CreateConnection(true).Result) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = BaseJobItemSelectText + " where Id=@Id"; - - cmd.Parameters.Add(cmd, "@Id", DbType.Guid).Value = guid; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) - { - if (reader.Read()) - { - return GetJobItem(reader); - } - } - } - - return null; - } - } - - private QueryResult GetJobItemReader(SyncJobItemQuery query, string baseSelectText, Func itemFactory) - { - if (query == null) - { - throw new ArgumentNullException("query"); - } - - using (var connection = CreateConnection(true).Result) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = baseSelectText; - - var whereClauses = new List(); - - if (!string.IsNullOrWhiteSpace(query.JobId)) - { - whereClauses.Add("JobId=@JobId"); - cmd.Parameters.Add(cmd, "@JobId", DbType.String).Value = query.JobId; - } - if (!string.IsNullOrWhiteSpace(query.ItemId)) - { - whereClauses.Add("ItemId=@ItemId"); - cmd.Parameters.Add(cmd, "@ItemId", DbType.String).Value = query.ItemId; - } - if (!string.IsNullOrWhiteSpace(query.TargetId)) - { - whereClauses.Add("TargetId=@TargetId"); - cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; - } - - if (query.Statuses.Length > 0) - { - var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); - - whereClauses.Add(string.Format("Status in ({0})", statuses)); - } - - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - var startIndex = query.StartIndex ?? 0; - if (startIndex > 0) - { - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobItems ORDER BY JobItemIndex, DateCreated LIMIT {0})", - startIndex.ToString(_usCulture))); - } - - if (whereClauses.Count > 0) - { - cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); - } - - cmd.CommandText += " ORDER BY JobItemIndex, DateCreated"; - - if (query.Limit.HasValue) - { - cmd.CommandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); - } - - cmd.CommandText += "; select count (Id) from SyncJobItems" + whereTextWithoutPaging; - - var list = new List(); - var count = 0; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) - { - while (reader.Read()) - { - list.Add(itemFactory(reader)); - } - - if (reader.NextResult() && reader.Read()) - { - count = reader.GetInt32(0); - } - } - - return new QueryResult() - { - Items = list.ToArray(), - TotalRecordCount = count - }; - } - } - } - - public Dictionary GetSyncedItemProgresses(SyncJobItemQuery query) - { - var result = new Dictionary(); - - var now = DateTime.UtcNow; - - using (var connection = CreateConnection(true).Result) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = "select ItemId,Status,Progress from SyncJobItems"; - - var whereClauses = new List(); - - if (!string.IsNullOrWhiteSpace(query.TargetId)) - { - whereClauses.Add("TargetId=@TargetId"); - cmd.Parameters.Add(cmd, "@TargetId", DbType.String).Value = query.TargetId; - } - - if (query.Statuses.Length > 0) - { - var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); - - whereClauses.Add(string.Format("Status in ({0})", statuses)); - } - - if (whereClauses.Count > 0) - { - cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); - } - - cmd.CommandText += ";" + cmd.CommandText - .Replace("select ItemId,Status,Progress from SyncJobItems", "select ItemIds,Status,Progress from SyncJobs") - .Replace("'Synced'", "'Completed','CompletedWithError'"); - - //Logger.Debug(cmd.CommandText); - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) - { - LogQueryTime("GetSyncedItemProgresses", cmd, now); - - while (reader.Read()) - { - AddStatusResult(reader, result, false); - } - - if (reader.NextResult()) - { - while (reader.Read()) - { - AddStatusResult(reader, result, true); - } - } - } - } - } - - return result; - } - - private void LogQueryTime(string methodName, IDbCommand cmd, DateTime startDate) - { - var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds; - - var slowThreshold = 1000; - -#if DEBUG - slowThreshold = 50; -#endif - - if (elapsed >= slowThreshold) - { - Logger.Debug("{2} query time (slow): {0}ms. Query: {1}", - Convert.ToInt32(elapsed), - cmd.CommandText, - methodName); - } - else - { - //Logger.Debug("{2} query time: {0}ms. Query: {1}", - // Convert.ToInt32(elapsed), - // cmd.CommandText, - // methodName); - } - } - - private void AddStatusResult(IDataReader reader, Dictionary result, bool multipleIds) - { - if (reader.IsDBNull(0)) - { - return; - } - - var itemIds = new List(); - - var ids = reader.GetString(0); - - if (multipleIds) - { - itemIds = ids.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); - } - else - { - itemIds.Add(ids); - } - - if (!reader.IsDBNull(1)) - { - SyncJobItemStatus status; - var statusString = reader.GetString(1); - if (string.Equals(statusString, "Completed", StringComparison.OrdinalIgnoreCase) || - string.Equals(statusString, "CompletedWithError", StringComparison.OrdinalIgnoreCase)) - { - status = SyncJobItemStatus.Synced; - } - else - { - status = (SyncJobItemStatus)Enum.Parse(typeof(SyncJobItemStatus), statusString, true); - } - - if (status == SyncJobItemStatus.Synced) - { - foreach (var itemId in itemIds) - { - result[itemId] = new SyncedItemProgress - { - Status = SyncJobItemStatus.Synced - }; - } - } - else - { - double progress = reader.IsDBNull(2) ? 0.0 : reader.GetDouble(2); - - foreach (var itemId in itemIds) - { - SyncedItemProgress currentStatus; - if (!result.TryGetValue(itemId, out currentStatus) || (currentStatus.Status != SyncJobItemStatus.Synced && progress >= currentStatus.Progress)) - { - result[itemId] = new SyncedItemProgress - { - Status = status, - Progress = progress - }; - } - } - } - } - } - - public QueryResult GetJobItems(SyncJobItemQuery query) - { - return GetJobItemReader(query, BaseJobItemSelectText, GetJobItem); - } - - public Task Create(SyncJobItem jobItem) - { - return InsertOrUpdate(jobItem, true); - } - - public Task Update(SyncJobItem jobItem) - { - return InsertOrUpdate(jobItem, false); - } - - private async Task InsertOrUpdate(SyncJobItem jobItem, bool insert) - { - if (jobItem == null) - { - throw new ArgumentNullException("jobItem"); - } - - CheckDisposed(); - - using (var connection = await CreateConnection().ConfigureAwait(false)) - { - using (var cmd = connection.CreateCommand()) - { - if (insert) - { - cmd.CommandText = "insert into SyncJobItems (Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks) values (@Id, @ItemId, @ItemName, @MediaSourceId, @JobId, @TemporaryPath, @OutputPath, @Status, @TargetId, @DateCreated, @Progress, @AdditionalFiles, @MediaSource, @IsMarkedForRemoval, @JobItemIndex, @ItemDateModifiedTicks)"; - - cmd.Parameters.Add(cmd, "@Id"); - cmd.Parameters.Add(cmd, "@ItemId"); - cmd.Parameters.Add(cmd, "@ItemName"); - cmd.Parameters.Add(cmd, "@MediaSourceId"); - cmd.Parameters.Add(cmd, "@JobId"); - cmd.Parameters.Add(cmd, "@TemporaryPath"); - cmd.Parameters.Add(cmd, "@OutputPath"); - cmd.Parameters.Add(cmd, "@Status"); - cmd.Parameters.Add(cmd, "@TargetId"); - cmd.Parameters.Add(cmd, "@DateCreated"); - cmd.Parameters.Add(cmd, "@Progress"); - cmd.Parameters.Add(cmd, "@AdditionalFiles"); - cmd.Parameters.Add(cmd, "@MediaSource"); - cmd.Parameters.Add(cmd, "@IsMarkedForRemoval"); - cmd.Parameters.Add(cmd, "@JobItemIndex"); - cmd.Parameters.Add(cmd, "@ItemDateModifiedTicks"); - } - else - { - // cmd - cmd.CommandText = "update SyncJobItems set ItemId=@ItemId,ItemName=@ItemName,MediaSourceId=@MediaSourceId,JobId=@JobId,TemporaryPath=@TemporaryPath,OutputPath=@OutputPath,Status=@Status,TargetId=@TargetId,DateCreated=@DateCreated,Progress=@Progress,AdditionalFiles=@AdditionalFiles,MediaSource=@MediaSource,IsMarkedForRemoval=@IsMarkedForRemoval,JobItemIndex=@JobItemIndex,ItemDateModifiedTicks=@ItemDateModifiedTicks where Id=@Id"; - - cmd.Parameters.Add(cmd, "@Id"); - cmd.Parameters.Add(cmd, "@ItemId"); - cmd.Parameters.Add(cmd, "@ItemName"); - cmd.Parameters.Add(cmd, "@MediaSourceId"); - cmd.Parameters.Add(cmd, "@JobId"); - cmd.Parameters.Add(cmd, "@TemporaryPath"); - cmd.Parameters.Add(cmd, "@OutputPath"); - cmd.Parameters.Add(cmd, "@Status"); - cmd.Parameters.Add(cmd, "@TargetId"); - cmd.Parameters.Add(cmd, "@DateCreated"); - cmd.Parameters.Add(cmd, "@Progress"); - cmd.Parameters.Add(cmd, "@AdditionalFiles"); - cmd.Parameters.Add(cmd, "@MediaSource"); - cmd.Parameters.Add(cmd, "@IsMarkedForRemoval"); - cmd.Parameters.Add(cmd, "@JobItemIndex"); - cmd.Parameters.Add(cmd, "@ItemDateModifiedTicks"); - } - - IDbTransaction transaction = null; - - try - { - transaction = connection.BeginTransaction(); - - var index = 0; - - cmd.GetParameter(index++).Value = new Guid(jobItem.Id); - cmd.GetParameter(index++).Value = jobItem.ItemId; - cmd.GetParameter(index++).Value = jobItem.ItemName; - cmd.GetParameter(index++).Value = jobItem.MediaSourceId; - cmd.GetParameter(index++).Value = jobItem.JobId; - cmd.GetParameter(index++).Value = jobItem.TemporaryPath; - cmd.GetParameter(index++).Value = jobItem.OutputPath; - cmd.GetParameter(index++).Value = jobItem.Status.ToString(); - cmd.GetParameter(index++).Value = jobItem.TargetId; - cmd.GetParameter(index++).Value = jobItem.DateCreated; - cmd.GetParameter(index++).Value = jobItem.Progress; - cmd.GetParameter(index++).Value = _json.SerializeToString(jobItem.AdditionalFiles); - cmd.GetParameter(index++).Value = jobItem.MediaSource == null ? null : _json.SerializeToString(jobItem.MediaSource); - cmd.GetParameter(index++).Value = jobItem.IsMarkedForRemoval; - cmd.GetParameter(index++).Value = jobItem.JobItemIndex; - cmd.GetParameter(index++).Value = jobItem.ItemDateModifiedTicks; - - cmd.Transaction = transaction; - - cmd.ExecuteNonQuery(); - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save record:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - } - } - } - } - - private SyncJobItem GetJobItem(IDataReader reader) - { - var info = new SyncJobItem - { - Id = reader.GetGuid(0).ToString("N"), - ItemId = reader.GetString(1) - }; - - if (!reader.IsDBNull(2)) - { - info.ItemName = reader.GetString(2); - } - - if (!reader.IsDBNull(3)) - { - info.MediaSourceId = reader.GetString(3); - } - - info.JobId = reader.GetString(4); - - if (!reader.IsDBNull(5)) - { - info.TemporaryPath = reader.GetString(5); - } - if (!reader.IsDBNull(6)) - { - info.OutputPath = reader.GetString(6); - } - - if (!reader.IsDBNull(7)) - { - info.Status = (SyncJobItemStatus)Enum.Parse(typeof(SyncJobItemStatus), reader.GetString(7), true); - } - - info.TargetId = reader.GetString(8); - - info.DateCreated = reader.GetDateTime(9).ToUniversalTime(); - - if (!reader.IsDBNull(10)) - { - info.Progress = reader.GetDouble(10); - } - - if (!reader.IsDBNull(11)) - { - var json = reader.GetString(11); - - if (!string.IsNullOrWhiteSpace(json)) - { - info.AdditionalFiles = _json.DeserializeFromString>(json); - } - } - - if (!reader.IsDBNull(12)) - { - var json = reader.GetString(12); - - if (!string.IsNullOrWhiteSpace(json)) - { - info.MediaSource = _json.DeserializeFromString(json); - } - } - - info.IsMarkedForRemoval = reader.GetBoolean(13); - info.JobItemIndex = reader.GetInt32(14); - - if (!reader.IsDBNull(15)) - { - info.ItemDateModifiedTicks = reader.GetInt64(15); - } - - return info; - } - } -} diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs index ea9e537c9..7bc77402e 100644 --- a/Emby.Server.Implementations/Activity/ActivityRepository.cs +++ b/Emby.Server.Implementations/Activity/ActivityRepository.cs @@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.Activity if (minDate.HasValue) { - whereClauses.Add("DateCreated>=@DateCreated"); + whereClauses.Add("DateCreated>=?"); paramList.Add(minDate.Value.ToDateTimeParamValue()); } diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs new file mode 100644 index 000000000..05cde91e2 --- /dev/null +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -0,0 +1,73 @@ +using MediaBrowser.Controller; +using System; + +namespace Emby.Server.Implementations.Browser +{ + /// + /// Class BrowserLauncher + /// + public static class BrowserLauncher + { + /// + /// Opens the dashboard page. + /// + /// The page. + /// The app host. + public static void OpenDashboardPage(string page, IServerApplicationHost appHost) + { + var url = appHost.GetLocalApiUrl("localhost") + "/web/" + page; + + OpenUrl(appHost, url); + } + + /// + /// Opens the community. + /// + public static void OpenCommunity(IServerApplicationHost appHost) + { + OpenUrl(appHost, "http://emby.media/community"); + } + + public static void OpenEmbyPremiere(IServerApplicationHost appHost) + { + OpenDashboardPage("supporterkey.html", appHost); + } + + /// + /// Opens the web client. + /// + /// The app host. + public static void OpenWebClient(IServerApplicationHost appHost) + { + OpenDashboardPage("index.html", appHost); + } + + /// + /// Opens the dashboard. + /// + /// The app host. + public static void OpenDashboard(IServerApplicationHost appHost) + { + OpenDashboardPage("dashboard.html", appHost); + } + + /// + /// Opens the URL. + /// + /// The URL. + private static void OpenUrl(IServerApplicationHost appHost, string url) + { + try + { + appHost.LaunchUrl(url); + } + catch (NotImplementedException) + { + + } + catch (Exception) + { + } + } + } +} diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 8febe83b2..c47a534d1 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Logging; using SQLitePCL.pretty; +using System.Linq; namespace Emby.Server.Implementations.Data { @@ -120,21 +121,30 @@ namespace Emby.Server.Implementations.Data } - protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type) + protected List GetColumnNames(IDatabaseConnection connection, string table) { + var list = new List(); + foreach (var row in connection.Query("PRAGMA table_info(" + table + ")")) { if (row[1].SQLiteType != SQLiteType.Null) { var name = row[1].ToString(); - if (string.Equals(name, columnName, StringComparison.OrdinalIgnoreCase)) - { - return; - } + list.Add(name); } } + return list; + } + + protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List existingColumnNames) + { + if (existingColumnNames.Contains(columnName, StringComparer.OrdinalIgnoreCase)) + { + return; + } + connection.ExecuteAll(string.Join(";", new string[] { "alter table " + table, diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index dd32e2cbd..3f11b6eb0 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -1,5 +1,4 @@ using MediaBrowser.Common.Progress; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; @@ -7,20 +6,13 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.IO; using MediaBrowser.Model.IO; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.IO; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; -using Emby.Server.Implementations.ScheduledTasks; namespace Emby.Server.Implementations.Data { @@ -29,26 +21,14 @@ namespace Emby.Server.Implementations.Data private readonly ILibraryManager _libraryManager; private readonly IItemRepository _itemRepo; private readonly ILogger _logger; - private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; - private readonly IHttpServer _httpServer; - private readonly ILocalizationManager _localization; - private readonly ITaskManager _taskManager; - public const int MigrationVersion = 23; - public static bool EnableUnavailableMessage = false; - const int LatestSchemaVersion = 109; - - public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IHttpServer httpServer, ILocalizationManager localization, ITaskManager taskManager) + public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IFileSystem fileSystem) { _libraryManager = libraryManager; _itemRepo = itemRepo; _logger = logger; - _config = config; _fileSystem = fileSystem; - _httpServer = httpServer; - _localization = localization; - _taskManager = taskManager; } public string Name @@ -68,8 +48,6 @@ namespace Emby.Server.Implementations.Data public async Task Execute(CancellationToken cancellationToken, IProgress progress) { - OnProgress(0); - // Ensure these objects are lazy loaded. // Without this there is a deadlock that will need to be investigated var rootChildren = _libraryManager.RootFolder.Children.ToList(); @@ -78,19 +56,7 @@ namespace Emby.Server.Implementations.Data var innerProgress = new ActionableProgress(); innerProgress.RegisterAction(p => { - double newPercentCommplete = .4 * p; - OnProgress(newPercentCommplete); - - progress.Report(newPercentCommplete); - }); - - await UpdateToLatestSchema(cancellationToken, innerProgress).ConfigureAwait(false); - - innerProgress = new ActionableProgress(); - innerProgress.RegisterAction(p => - { - double newPercentCommplete = 40 + .05 * p; - OnProgress(newPercentCommplete); + double newPercentCommplete = .45 * p; progress.Report(newPercentCommplete); }); await CleanDeadItems(cancellationToken, innerProgress).ConfigureAwait(false); @@ -100,122 +66,12 @@ namespace Emby.Server.Implementations.Data innerProgress.RegisterAction(p => { double newPercentCommplete = 45 + .55 * p; - OnProgress(newPercentCommplete); progress.Report(newPercentCommplete); }); await CleanDeletedItems(cancellationToken, innerProgress).ConfigureAwait(false); progress.Report(100); await _itemRepo.UpdateInheritedValues(cancellationToken).ConfigureAwait(false); - - if (_config.Configuration.MigrationVersion < MigrationVersion) - { - _config.Configuration.MigrationVersion = MigrationVersion; - _config.SaveConfiguration(); - } - - if (_config.Configuration.SchemaVersion < LatestSchemaVersion) - { - _config.Configuration.SchemaVersion = LatestSchemaVersion; - _config.SaveConfiguration(); - } - - if (EnableUnavailableMessage) - { - EnableUnavailableMessage = false; - _httpServer.GlobalResponse = null; - _taskManager.QueueScheduledTask(); - } - - _taskManager.SuspendTriggers = false; - } - - private void OnProgress(double newPercentCommplete) - { - if (EnableUnavailableMessage) - { - var html = "Emby"; - var text = _localization.GetLocalizedString("DbUpgradeMessage"); - html += string.Format(text, newPercentCommplete.ToString("N2", CultureInfo.InvariantCulture)); - - html += ""; - html += ""; - - _httpServer.GlobalResponse = html; - } - } - - private async Task UpdateToLatestSchema(CancellationToken cancellationToken, IProgress progress) - { - var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery - { - IsCurrentSchema = false, - ExcludeItemTypes = new[] { typeof(LiveTvProgram).Name } - }); - - var numComplete = 0; - var numItems = itemIds.Count; - - _logger.Debug("Upgrading schema for {0} items", numItems); - - var list = new List(); - - foreach (var itemId in itemIds) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (itemId != Guid.Empty) - { - // Somehow some invalid data got into the db. It probably predates the boundary checking - var item = _libraryManager.GetItemById(itemId); - - if (item != null) - { - list.Add(item); - } - } - - if (list.Count >= 1000) - { - try - { - await _itemRepo.SaveItems(list, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error saving item", ex); - } - - list.Clear(); - } - - numComplete++; - double percent = numComplete; - percent /= numItems; - progress.Report(percent * 100); - } - - if (list.Count > 0) - { - try - { - await _itemRepo.SaveItems(list, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error saving item", ex); - } - } - - progress.Report(100); } private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 653a6a9c1..973576a0d 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -36,6 +36,7 @@ + @@ -64,6 +65,7 @@ + @@ -174,6 +176,7 @@ + @@ -258,6 +261,7 @@ + diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs new file mode 100644 index 000000000..424153f22 --- /dev/null +++ b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs @@ -0,0 +1,59 @@ +using Emby.Server.Implementations.Browser; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Logging; + +namespace Emby.Server.Implementations.EntryPoints +{ + /// + /// Class StartupWizard + /// + public class StartupWizard : IServerEntryPoint + { + /// + /// The _app host + /// + private readonly IServerApplicationHost _appHost; + /// + /// The _user manager + /// + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The app host. + /// The logger. + public StartupWizard(IServerApplicationHost appHost, ILogger logger) + { + _appHost = appHost; + _logger = logger; + } + + /// + /// Runs this instance. + /// + public void Run() + { + if (_appHost.IsFirstRun) + { + LaunchStartupWizard(); + } + } + + /// + /// Launches the startup wizard. + /// + private void LaunchStartupWizard() + { + BrowserLauncher.OpenDashboardPage("wizardstart.html", _appHost); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs b/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs index 5be7ba7ad..48a85e0e0 100644 --- a/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs +++ b/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs @@ -74,7 +74,7 @@ namespace Emby.Server.Implementations.FileOrganization return new[] { // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromMinutes(5).Ticks} + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromMinutes(15).Ticks} }; } diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 876d140ec..c1758127a 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -59,12 +59,13 @@ namespace Emby.Server.Implementations.HttpServer private readonly IEnvironmentInfo _environment; private readonly IStreamFactory _streamFactory; private readonly Func> _funcParseFn; + private readonly bool _enableDualModeSockets; public HttpListenerHost(IServerApplicationHost applicationHost, ILogger logger, IServerConfigurationManager config, string serviceName, - string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, ICertificate certificate, IStreamFactory streamFactory, Func> funcParseFn) + string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, ICertificate certificate, IStreamFactory streamFactory, Func> funcParseFn, bool enableDualModeSockets) : base(serviceName) { _appHost = applicationHost; @@ -80,6 +81,7 @@ namespace Emby.Server.Implementations.HttpServer _certificate = certificate; _streamFactory = streamFactory; _funcParseFn = funcParseFn; + _enableDualModeSockets = enableDualModeSockets; _config = config; _logger = logger; @@ -179,8 +181,6 @@ namespace Emby.Server.Implementations.HttpServer private IHttpListener GetListener() { - var enableDualMode = _environment.OperatingSystem == OperatingSystem.Windows; - return new WebSocketSharpListener(_logger, _certificate, _memoryStreamProvider, @@ -189,7 +189,7 @@ namespace Emby.Server.Implementations.HttpServer _socketFactory, _cryptoProvider, _streamFactory, - enableDualMode, + _enableDualModeSockets, GetRequest); } diff --git a/Emby.Server.Implementations/Migrations/UpdateLevelMigration.cs b/Emby.Server.Implementations/Migrations/UpdateLevelMigration.cs new file mode 100644 index 000000000..c532ea08d --- /dev/null +++ b/Emby.Server.Implementations/Migrations/UpdateLevelMigration.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Common.Updates; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Updates; + +namespace Emby.Server.Implementations.Migrations +{ + public class UpdateLevelMigration : IVersionMigration + { + private readonly IServerConfigurationManager _config; + private readonly IServerApplicationHost _appHost; + private readonly IHttpClient _httpClient; + private readonly IJsonSerializer _jsonSerializer; + private readonly string _releaseAssetFilename; + private readonly ILogger _logger; + + public UpdateLevelMigration(IServerConfigurationManager config, IServerApplicationHost appHost, IHttpClient httpClient, IJsonSerializer jsonSerializer, string releaseAssetFilename, ILogger logger) + { + _config = config; + _appHost = appHost; + _httpClient = httpClient; + _jsonSerializer = jsonSerializer; + _releaseAssetFilename = releaseAssetFilename; + _logger = logger; + } + + public async Task Run() + { + var lastVersion = _config.Configuration.LastVersion; + var currentVersion = _appHost.ApplicationVersion; + + if (string.Equals(lastVersion, currentVersion.ToString(), StringComparison.OrdinalIgnoreCase)) + { + return; + } + + try + { + var updateLevel = _config.Configuration.SystemUpdateLevel; + + await CheckVersion(currentVersion, updateLevel, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.ErrorException("Error in update migration", ex); + } + } + + private async Task CheckVersion(Version currentVersion, PackageVersionClass currentUpdateLevel, CancellationToken cancellationToken) + { + var releases = await new GithubUpdater(_httpClient, _jsonSerializer) + .GetLatestReleases("MediaBrowser", "Emby", _releaseAssetFilename, cancellationToken).ConfigureAwait(false); + + var newUpdateLevel = GetNewUpdateLevel(currentVersion, currentUpdateLevel, releases); + + if (newUpdateLevel != currentUpdateLevel) + { + _config.Configuration.SystemUpdateLevel = newUpdateLevel; + _config.SaveConfiguration(); + } + } + + private PackageVersionClass GetNewUpdateLevel(Version currentVersion, PackageVersionClass currentUpdateLevel, List releases) + { + var newUpdateLevel = currentUpdateLevel; + + // If the current version is later than current stable, set the update level to beta + if (releases.Count >= 1) + { + var release = releases[0]; + var version = ParseVersion(release.tag_name); + if (version != null) + { + if (currentVersion > version) + { + newUpdateLevel = PackageVersionClass.Beta; + } + else + { + return PackageVersionClass.Release; + } + } + } + + // If the current version is later than current beta, set the update level to dev + if (releases.Count >= 2) + { + var release = releases[1]; + var version = ParseVersion(release.tag_name); + if (version != null) + { + if (currentVersion > version) + { + newUpdateLevel = PackageVersionClass.Dev; + } + else + { + return PackageVersionClass.Beta; + } + } + } + + return newUpdateLevel; + } + + private Version ParseVersion(string versionString) + { + if (!string.IsNullOrWhiteSpace(versionString)) + { + var parts = versionString.Split('.'); + if (parts.Length == 3) + { + versionString += ".0"; + } + } + + Version version; + Version.TryParse(versionString, out version); + + return version; + } + } +} diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index 5179bd258..f4cb42d29 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -40,7 +40,9 @@ namespace Emby.Server.Implementations.Security connection.RunInTransaction(db => { - AddColumn(db, "AccessTokens", "AppVersion", "TEXT"); + var existingColumnNames = GetColumnNames(db, "AccessTokens"); + + AddColumn(db, "AccessTokens", "AppVersion", "TEXT", existingColumnNames); }); } } diff --git a/Emby.Server.Implementations/Sync/SyncRepository.cs b/Emby.Server.Implementations/Sync/SyncRepository.cs new file mode 100644 index 000000000..fbc5772f3 --- /dev/null +++ b/Emby.Server.Implementations/Sync/SyncRepository.cs @@ -0,0 +1,770 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Emby.Server.Implementations.Data; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Sync; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Sync +{ + public class SyncRepository : BaseSqliteRepository, ISyncRepository + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + private readonly IJsonSerializer _json; + + public SyncRepository(ILogger logger, IJsonSerializer json, IServerApplicationPaths appPaths) + : base(logger) + { + _json = json; + DbFilePath = Path.Combine(appPaths.DataPath, "sync14.db"); + } + + private class SyncSummary + { + public Dictionary Items { get; set; } + + public SyncSummary() + { + Items = new Dictionary(); + } + } + + public void Initialize() + { + using (var connection = CreateConnection()) + { + string[] queries = { + + "create table if not exists SyncJobs (Id GUID PRIMARY KEY, TargetId TEXT NOT NULL, Name TEXT NOT NULL, Profile TEXT, Quality TEXT, Bitrate INT, Status TEXT NOT NULL, Progress FLOAT, UserId TEXT NOT NULL, ItemIds TEXT NOT NULL, Category TEXT, ParentId TEXT, UnwatchedOnly BIT, ItemLimit INT, SyncNewContent BIT, DateCreated DateTime, DateLastModified DateTime, ItemCount int)", + + "create table if not exists SyncJobItems (Id GUID PRIMARY KEY, ItemId TEXT, ItemName TEXT, MediaSourceId TEXT, JobId TEXT, TemporaryPath TEXT, OutputPath TEXT, Status TEXT, TargetId TEXT, DateCreated DateTime, Progress FLOAT, AdditionalFiles TEXT, MediaSource TEXT, IsMarkedForRemoval BIT, JobItemIndex INT, ItemDateModifiedTicks BIGINT)", + + "drop index if exists idx_SyncJobItems2", + "drop index if exists idx_SyncJobItems3", + "drop index if exists idx_SyncJobs1", + "drop index if exists idx_SyncJobs", + "drop index if exists idx_SyncJobItems1", + "create index if not exists idx_SyncJobItems4 on SyncJobItems(TargetId,ItemId,Status,Progress,DateCreated)", + "create index if not exists idx_SyncJobItems5 on SyncJobItems(TargetId,Status,ItemId,Progress)", + + "create index if not exists idx_SyncJobs2 on SyncJobs(TargetId,Status,ItemIds,Progress)", + + "pragma shrink_memory" + }; + + connection.RunQueries(queries); + + connection.RunInTransaction(db => + { + var existingColumnNames = GetColumnNames(db, "SyncJobs"); + AddColumn(db, "SyncJobs", "Profile", "TEXT", existingColumnNames); + AddColumn(db, "SyncJobs", "Bitrate", "INT", existingColumnNames); + + existingColumnNames = GetColumnNames(db, "SyncJobItems"); + AddColumn(db, "SyncJobItems", "ItemDateModifiedTicks", "BIGINT", existingColumnNames); + }); + } + } + + private const string BaseJobSelectText = "select Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount from SyncJobs"; + private const string BaseJobItemSelectText = "select Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks from SyncJobItems"; + + public SyncJob GetJob(string id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException("id"); + } + + CheckDisposed(); + + var guid = new Guid(id); + + if (guid == Guid.Empty) + { + throw new ArgumentNullException("id"); + } + + lock (WriteLock) + { + using (var connection = CreateConnection(true)) + { + var commandText = BaseJobSelectText + " where Id=?"; + var paramList = new List(); + + paramList.Add(guid.ToGuidParamValue()); + + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + return GetJob(row); + } + + return null; + } + } + } + + private SyncJob GetJob(IReadOnlyList reader) + { + var info = new SyncJob + { + Id = reader[0].ReadGuid().ToString("N"), + TargetId = reader[1].ToString(), + Name = reader[2].ToString() + }; + + if (reader[3].SQLiteType != SQLiteType.Null) + { + info.Profile = reader[3].ToString(); + } + + if (reader[4].SQLiteType != SQLiteType.Null) + { + info.Quality = reader[4].ToString(); + } + + if (reader[5].SQLiteType != SQLiteType.Null) + { + info.Bitrate = reader[5].ToInt(); + } + + if (reader[6].SQLiteType != SQLiteType.Null) + { + info.Status = (SyncJobStatus)Enum.Parse(typeof(SyncJobStatus), reader[6].ToString(), true); + } + + if (reader[7].SQLiteType != SQLiteType.Null) + { + info.Progress = reader[7].ToDouble(); + } + + if (reader[8].SQLiteType != SQLiteType.Null) + { + info.UserId = reader[8].ToString(); + } + + if (reader[9].SQLiteType != SQLiteType.Null) + { + info.RequestedItemIds = reader[9].ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + } + + if (reader[10].SQLiteType != SQLiteType.Null) + { + info.Category = (SyncCategory)Enum.Parse(typeof(SyncCategory), reader[10].ToString(), true); + } + + if (reader[11].SQLiteType != SQLiteType.Null) + { + info.ParentId = reader[11].ToString(); + } + + if (reader[12].SQLiteType != SQLiteType.Null) + { + info.UnwatchedOnly = reader[12].ToBool(); + } + + if (reader[13].SQLiteType != SQLiteType.Null) + { + info.ItemLimit = reader[13].ToInt(); + } + + info.SyncNewContent = reader[14].ToBool(); + + info.DateCreated = reader[15].ReadDateTime(); + info.DateLastModified = reader[16].ReadDateTime(); + info.ItemCount = reader[17].ToInt(); + + return info; + } + + public Task Create(SyncJob job) + { + return InsertOrUpdate(job, true); + } + + public Task Update(SyncJob job) + { + return InsertOrUpdate(job, false); + } + + private async Task InsertOrUpdate(SyncJob job, bool insert) + { + if (job == null) + { + throw new ArgumentNullException("job"); + } + + CheckDisposed(); + + lock (WriteLock) + { + using (var connection = CreateConnection()) + { + string commandText; + var paramList = new List(); + + if (insert) + { + commandText = "insert into SyncJobs (Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + } + else + { + commandText = "update SyncJobs set TargetId=?,Name=?,Profile=?,Quality=?,Bitrate=?,Status=?,Progress=?,UserId=?,ItemIds=?,Category=?,ParentId=?,UnwatchedOnly=?,ItemLimit=?,SyncNewContent=?,DateCreated=?,DateLastModified=?,ItemCount=? where Id=?"; + } + + paramList.Add(job.Id.ToGuidParamValue()); + paramList.Add(job.TargetId); + paramList.Add(job.Name); + paramList.Add(job.Profile); + paramList.Add(job.Quality); + paramList.Add(job.Bitrate); + paramList.Add(job.Status.ToString()); + paramList.Add(job.Progress); + paramList.Add(job.UserId); + + paramList.Add(string.Join(",", job.RequestedItemIds.ToArray())); + paramList.Add(job.Category); + paramList.Add(job.ParentId); + paramList.Add(job.UnwatchedOnly); + paramList.Add(job.ItemLimit); + paramList.Add(job.SyncNewContent); + paramList.Add(job.DateCreated.ToDateTimeParamValue()); + paramList.Add(job.DateLastModified.ToDateTimeParamValue()); + paramList.Add(job.ItemCount); + + connection.RunInTransaction(conn => + { + conn.Execute(commandText, paramList.ToArray()); + }); + } + } + } + + public async Task DeleteJob(string id) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException("id"); + } + + CheckDisposed(); + + lock (WriteLock) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(conn => + { + conn.Execute("delete from SyncJobs where Id=?", id.ToGuidParamValue()); + conn.Execute("delete from SyncJobItems where JobId=?", id); + }); + } + } + } + + public QueryResult GetJobs(SyncJobQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + CheckDisposed(); + + lock (WriteLock) + { + using (var connection = CreateConnection(true)) + { + var commandText = BaseJobSelectText; + var paramList = new List(); + + var whereClauses = new List(); + + if (query.Statuses.Length > 0) + { + var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); + + whereClauses.Add(string.Format("Status in ({0})", statuses)); + } + if (!string.IsNullOrWhiteSpace(query.TargetId)) + { + whereClauses.Add("TargetId=?"); + paramList.Add(query.TargetId); + } + if (!string.IsNullOrWhiteSpace(query.ExcludeTargetIds)) + { + var excludeIds = (query.ExcludeTargetIds ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + if (excludeIds.Length == 1) + { + whereClauses.Add("TargetId<>?"); + paramList.Add(excludeIds[0]); + } + else if (excludeIds.Length > 1) + { + whereClauses.Add("TargetId<>?"); + paramList.Add(excludeIds[0]); + } + } + if (!string.IsNullOrWhiteSpace(query.UserId)) + { + whereClauses.Add("UserId=?"); + paramList.Add(query.UserId); + } + if (query.SyncNewContent.HasValue) + { + whereClauses.Add("SyncNewContent=?"); + paramList.Add(query.SyncNewContent.Value); + } + + commandText += " mainTable"; + + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + var startIndex = query.StartIndex ?? 0; + if (startIndex > 0) + { + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobs ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC LIMIT {0})", + startIndex.ToString(_usCulture))); + } + + if (whereClauses.Count > 0) + { + commandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } + + commandText += " ORDER BY (Select Max(DateLastModified) from SyncJobs where TargetId=mainTable.TargetId) DESC, DateLastModified DESC"; + + if (query.Limit.HasValue) + { + commandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + } + + var list = new List(); + var count = connection.Query("select count (Id) from SyncJobs" + whereTextWithoutPaging, paramList.ToArray()) + .SelectScalarInt() + .First(); + + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + list.Add(GetJob(row)); + } + + return new QueryResult() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + } + + public SyncJobItem GetJobItem(string id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException("id"); + } + + CheckDisposed(); + + lock (WriteLock) + { + var guid = new Guid(id); + + using (var connection = CreateConnection(true)) + { + var commandText = BaseJobItemSelectText + " where Id=?"; + var paramList = new List(); + + paramList.Add(guid.ToGuidParamValue()); + + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + return GetJobItem(row); + } + + return null; + } + } + } + + private QueryResult GetJobItemReader(SyncJobItemQuery query, string baseSelectText, Func, T> itemFactory) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + lock (WriteLock) + { + using (var connection = CreateConnection(true)) + { + var commandText = baseSelectText; + var paramList = new List(); + + var whereClauses = new List(); + + if (!string.IsNullOrWhiteSpace(query.JobId)) + { + whereClauses.Add("JobId=?"); + paramList.Add(query.JobId); + } + if (!string.IsNullOrWhiteSpace(query.ItemId)) + { + whereClauses.Add("ItemId=?"); + paramList.Add(query.ItemId); + } + if (!string.IsNullOrWhiteSpace(query.TargetId)) + { + whereClauses.Add("TargetId=?"); + paramList.Add(query.TargetId); + } + + if (query.Statuses.Length > 0) + { + var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); + + whereClauses.Add(string.Format("Status in ({0})", statuses)); + } + + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + var startIndex = query.StartIndex ?? 0; + if (startIndex > 0) + { + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM SyncJobItems ORDER BY JobItemIndex, DateCreated LIMIT {0})", + startIndex.ToString(_usCulture))); + } + + if (whereClauses.Count > 0) + { + commandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } + + commandText += " ORDER BY JobItemIndex, DateCreated"; + + if (query.Limit.HasValue) + { + commandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + } + + var list = new List(); + var count = connection.Query("select count (Id) from SyncJobItems" + whereTextWithoutPaging, paramList.ToArray()) + .SelectScalarInt() + .First(); + + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + list.Add(itemFactory(row)); + } + + return new QueryResult() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + } + + public Dictionary GetSyncedItemProgresses(SyncJobItemQuery query) + { + var result = new Dictionary(); + + var now = DateTime.UtcNow; + + lock (WriteLock) + { + using (var connection = CreateConnection(true)) + { + var commandText = "select ItemId,Status,Progress from SyncJobItems"; + + var whereClauses = new List(); + var paramList = new List(); + + if (!string.IsNullOrWhiteSpace(query.TargetId)) + { + whereClauses.Add("TargetId=?"); + paramList.Add(query.TargetId); + } + + if (query.Statuses.Length > 0) + { + var statuses = string.Join(",", query.Statuses.Select(i => "'" + i.ToString() + "'").ToArray()); + + whereClauses.Add(string.Format("Status in ({0})", statuses)); + } + + if (whereClauses.Count > 0) + { + commandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } + + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + AddStatusResult(row, result, false); + } + LogQueryTime("GetSyncedItemProgresses", commandText, now); + + commandText = commandText + .Replace("select ItemId,Status,Progress from SyncJobItems", "select ItemIds,Status,Progress from SyncJobs") + .Replace("'Synced'", "'Completed','CompletedWithError'"); + + now = DateTime.UtcNow; + foreach (var row in connection.Query(commandText, paramList.ToArray())) + { + AddStatusResult(row, result, true); + } + LogQueryTime("GetSyncedItemProgresses", commandText, now); + } + } + + return result; + } + + private void LogQueryTime(string methodName, string commandText, DateTime startDate) + { + var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds; + + var slowThreshold = 1000; + +#if DEBUG + slowThreshold = 50; +#endif + + if (elapsed >= slowThreshold) + { + Logger.Debug("{2} query time (slow): {0}ms. Query: {1}", + Convert.ToInt32(elapsed), + commandText, + methodName); + } + else + { + //Logger.Debug("{2} query time: {0}ms. Query: {1}", + // Convert.ToInt32(elapsed), + // cmd.CommandText, + // methodName); + } + } + + private void AddStatusResult(IReadOnlyList reader, Dictionary result, bool multipleIds) + { + if (reader[0].SQLiteType == SQLiteType.Null) + { + return; + } + + var itemIds = new List(); + + var ids = reader[0].ToString(); + + if (multipleIds) + { + itemIds = ids.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); + } + else + { + itemIds.Add(ids); + } + + if (reader[1].SQLiteType != SQLiteType.Null) + { + SyncJobItemStatus status; + var statusString = reader[1].ToString(); + if (string.Equals(statusString, "Completed", StringComparison.OrdinalIgnoreCase) || + string.Equals(statusString, "CompletedWithError", StringComparison.OrdinalIgnoreCase)) + { + status = SyncJobItemStatus.Synced; + } + else + { + status = (SyncJobItemStatus)Enum.Parse(typeof(SyncJobItemStatus), statusString, true); + } + + if (status == SyncJobItemStatus.Synced) + { + foreach (var itemId in itemIds) + { + result[itemId] = new SyncedItemProgress + { + Status = SyncJobItemStatus.Synced + }; + } + } + else + { + double progress = reader[2].SQLiteType == SQLiteType.Null ? 0.0 : reader[2].ToDouble(); + + foreach (var itemId in itemIds) + { + SyncedItemProgress currentStatus; + if (!result.TryGetValue(itemId, out currentStatus) || (currentStatus.Status != SyncJobItemStatus.Synced && progress >= currentStatus.Progress)) + { + result[itemId] = new SyncedItemProgress + { + Status = status, + Progress = progress + }; + } + } + } + } + } + + public QueryResult GetJobItems(SyncJobItemQuery query) + { + return GetJobItemReader(query, BaseJobItemSelectText, GetJobItem); + } + + public Task Create(SyncJobItem jobItem) + { + return InsertOrUpdate(jobItem, true); + } + + public Task Update(SyncJobItem jobItem) + { + return InsertOrUpdate(jobItem, false); + } + + private async Task InsertOrUpdate(SyncJobItem jobItem, bool insert) + { + if (jobItem == null) + { + throw new ArgumentNullException("jobItem"); + } + + CheckDisposed(); + + lock (WriteLock) + { + using (var connection = CreateConnection()) + { + string commandText; + + if (insert) + { + commandText = "insert into SyncJobItems (Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + } + else + { + // cmd + commandText = "update SyncJobItems set ItemId=?,ItemName=?,MediaSourceId=?,JobId=?,TemporaryPath=?,OutputPath=?,Status=?,TargetId=?,DateCreated=?,Progress=?,AdditionalFiles=?,MediaSource=?,IsMarkedForRemoval=?,JobItemIndex=?,ItemDateModifiedTicks=? where Id=?"; + } + + var paramList = new List(); + paramList.Add(jobItem.Id.ToGuidParamValue()); + paramList.Add(jobItem.ItemId); + paramList.Add(jobItem.ItemName); + paramList.Add(jobItem.MediaSourceId); + paramList.Add(jobItem.JobId); + paramList.Add(jobItem.TemporaryPath); + paramList.Add(jobItem.OutputPath); + paramList.Add(jobItem.Status.ToString()); + + paramList.Add(jobItem.TargetId); + paramList.Add(jobItem.DateCreated.ToDateTimeParamValue()); + paramList.Add(jobItem.Progress); + paramList.Add(_json.SerializeToString(jobItem.AdditionalFiles)); + paramList.Add(jobItem.MediaSource == null ? null : _json.SerializeToString(jobItem.MediaSource)); + paramList.Add(jobItem.IsMarkedForRemoval); + paramList.Add(jobItem.JobItemIndex); + paramList.Add(jobItem.ItemDateModifiedTicks); + + connection.RunInTransaction(conn => + { + conn.Execute(commandText, paramList.ToArray()); + }); + } + } + } + + private SyncJobItem GetJobItem(IReadOnlyList reader) + { + var info = new SyncJobItem + { + Id = reader[0].ReadGuid().ToString("N"), + ItemId = reader[1].ToString() + }; + + if (reader[2].SQLiteType != SQLiteType.Null) + { + info.ItemName = reader[2].ToString(); + } + + if (reader[3].SQLiteType != SQLiteType.Null) + { + info.MediaSourceId = reader[3].ToString(); + } + + info.JobId = reader[4].ToString(); + + if (reader[5].SQLiteType != SQLiteType.Null) + { + info.TemporaryPath = reader[5].ToString(); + } + if (reader[6].SQLiteType != SQLiteType.Null) + { + info.OutputPath = reader[6].ToString(); + } + + if (reader[7].SQLiteType != SQLiteType.Null) + { + info.Status = (SyncJobItemStatus)Enum.Parse(typeof(SyncJobItemStatus), reader[7].ToString(), true); + } + + info.TargetId = reader[8].ToString(); + + info.DateCreated = reader[9].ReadDateTime(); + + if (reader[10].SQLiteType != SQLiteType.Null) + { + info.Progress = reader[10].ToDouble(); + } + + if (reader[11].SQLiteType != SQLiteType.Null) + { + var json = reader[11].ToString(); + + if (!string.IsNullOrWhiteSpace(json)) + { + info.AdditionalFiles = _json.DeserializeFromString>(json); + } + } + + if (reader[12].SQLiteType != SQLiteType.Null) + { + var json = reader[12].ToString(); + + if (!string.IsNullOrWhiteSpace(json)) + { + info.MediaSource = _json.DeserializeFromString(json); + } + } + + info.IsMarkedForRemoval = reader[13].ToBool(); + info.JobItemIndex = reader[14].ToInt(); + + if (reader[15].SQLiteType != SQLiteType.Null) + { + info.ItemDateModifiedTicks = reader[15].ToInt64(); + } + + return info; + } + } +} diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index f3bab7883..a47aaa305 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -148,10 +148,6 @@ namespace Emby.Server.Implementations.TV private string GetUniqueSeriesKey(BaseItem series) { - if (_config.Configuration.SchemaVersion < 97) - { - return series.Id.ToString("N"); - } return series.GetPresentationUniqueKey(); } diff --git a/MediaBrowser.Api/StartupWizardService.cs b/MediaBrowser.Api/StartupWizardService.cs index 49fdcece1..4e5047f78 100644 --- a/MediaBrowser.Api/StartupWizardService.cs +++ b/MediaBrowser.Api/StartupWizardService.cs @@ -115,7 +115,6 @@ namespace MediaBrowser.Api config.EnableStandaloneMusicKeys = true; config.EnableCaseSensitiveItemIds = true; config.EnableFolderView = true; - config.SchemaVersion = 109; config.EnableSimpleArtistDetection = true; config.SkipDeserializationForBasicTypes = true; config.SkipDeserializationForPrograms = true; diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 9e212219d..eb082f707 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -70,6 +70,7 @@ + diff --git a/MediaBrowser.Common/Updates/GithubUpdater.cs b/MediaBrowser.Common/Updates/GithubUpdater.cs new file mode 100644 index 000000000..c5000391d --- /dev/null +++ b/MediaBrowser.Common/Updates/GithubUpdater.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Updates; + +namespace MediaBrowser.Common.Updates +{ + public class GithubUpdater + { + private readonly IHttpClient _httpClient; + private readonly IJsonSerializer _jsonSerializer; + + public GithubUpdater(IHttpClient httpClient, IJsonSerializer jsonSerializer) + { + _httpClient = httpClient; + _jsonSerializer = jsonSerializer; + } + + public async Task CheckForUpdateResult(string organzation, string repository, Version minVersion, PackageVersionClass updateLevel, string assetFilename, string packageName, string targetFilename, TimeSpan cacheLength, CancellationToken cancellationToken) + { + var url = string.Format("https://api.github.com/repos/{0}/{1}/releases", organzation, repository); + + var options = new HttpRequestOptions + { + Url = url, + EnableKeepAlive = false, + CancellationToken = cancellationToken, + UserAgent = "Emby/3.0", + BufferContent = false + }; + + if (cacheLength.Ticks > 0) + { + options.CacheMode = CacheMode.Unconditional; + options.CacheLength = cacheLength; + } + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + var obj = _jsonSerializer.DeserializeFromStream(stream); + + return CheckForUpdateResult(obj, minVersion, updateLevel, assetFilename, packageName, targetFilename); + } + } + + private CheckForUpdateResult CheckForUpdateResult(RootObject[] obj, Version minVersion, PackageVersionClass updateLevel, string assetFilename, string packageName, string targetFilename) + { + if (updateLevel == PackageVersionClass.Release) + { + // Technically all we need to do is check that it's not pre-release + // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly. + obj = obj.Where(i => !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) && !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase)).ToArray(); + } + else if (updateLevel == PackageVersionClass.Beta) + { + obj = obj.Where(i => !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase)).ToArray(); + } + else if (updateLevel == PackageVersionClass.Dev) + { + obj = obj.Where(i => !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) || i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase)).ToArray(); + } + + var availableUpdate = obj + .Select(i => CheckForUpdateResult(i, minVersion, assetFilename, packageName, targetFilename)) + .Where(i => i != null) + .OrderByDescending(i => Version.Parse(i.AvailableVersion)) + .FirstOrDefault(); + + return availableUpdate ?? new CheckForUpdateResult + { + IsUpdateAvailable = false + }; + } + + private bool MatchesUpdateLevel(RootObject i, PackageVersionClass updateLevel) + { + if (updateLevel == PackageVersionClass.Beta) + { + return !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase); + } + if (updateLevel == PackageVersionClass.Dev) + { + return !i.prerelease || i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) || + i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase); + } + + // Technically all we need to do is check that it's not pre-release + // But let's addititional checks for -beta and -dev to handle builds that might be temporarily tagged incorrectly. + return !i.prerelease && !i.name.EndsWith("-beta", StringComparison.OrdinalIgnoreCase) && + !i.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase); + } + + public async Task> GetLatestReleases(string organzation, string repository, string assetFilename, CancellationToken cancellationToken) + { + var list = new List(); + + var url = string.Format("https://api.github.com/repos/{0}/{1}/releases", organzation, repository); + + var options = new HttpRequestOptions + { + Url = url, + EnableKeepAlive = false, + CancellationToken = cancellationToken, + UserAgent = "Emby/3.0", + BufferContent = false + }; + + using (var stream = await _httpClient.Get(options).ConfigureAwait(false)) + { + var obj = _jsonSerializer.DeserializeFromStream(stream); + + obj = obj.Where(i => (i.assets ?? new List()).Any(a => IsAsset(a, assetFilename))).ToArray(); + + list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Release)).OrderByDescending(GetVersion).Take(1)); + list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Beta)).OrderByDescending(GetVersion).Take(1)); + list.AddRange(obj.Where(i => MatchesUpdateLevel(i, PackageVersionClass.Dev)).OrderByDescending(GetVersion).Take(1)); + + return list; + } + } + + public Version GetVersion(RootObject obj) + { + Version version; + if (!Version.TryParse(obj.tag_name, out version)) + { + return new Version(1, 0); + } + + return version; + } + + private CheckForUpdateResult CheckForUpdateResult(RootObject obj, Version minVersion, string assetFilename, string packageName, string targetFilename) + { + Version version; + if (!Version.TryParse(obj.tag_name, out version)) + { + return null; + } + + if (version < minVersion) + { + return null; + } + + var asset = (obj.assets ?? new List()).FirstOrDefault(i => IsAsset(i, assetFilename)); + + if (asset == null) + { + return null; + } + + return new CheckForUpdateResult + { + AvailableVersion = version.ToString(), + IsUpdateAvailable = version > minVersion, + Package = new PackageVersionInfo + { + classification = obj.prerelease ? + (obj.name.EndsWith("-dev", StringComparison.OrdinalIgnoreCase) ? PackageVersionClass.Dev : PackageVersionClass.Beta) : + PackageVersionClass.Release, + name = packageName, + sourceUrl = asset.browser_download_url, + targetFilename = targetFilename, + versionStr = version.ToString(), + requiredVersionStr = "1.0.0", + description = obj.body, + infoUrl = obj.html_url + } + }; + } + + private bool IsAsset(Asset asset, string assetFilename) + { + var downloadFilename = Path.GetFileName(asset.browser_download_url) ?? string.Empty; + + if (downloadFilename.IndexOf(assetFilename, StringComparison.OrdinalIgnoreCase) != -1) + { + return true; + } + + return string.Equals(assetFilename, downloadFilename, StringComparison.OrdinalIgnoreCase); + } + + public class Uploader + { + public string login { get; set; } + public int id { get; set; } + public string avatar_url { get; set; } + public string gravatar_id { get; set; } + public string url { get; set; } + public string html_url { get; set; } + public string followers_url { get; set; } + public string following_url { get; set; } + public string gists_url { get; set; } + public string starred_url { get; set; } + public string subscriptions_url { get; set; } + public string organizations_url { get; set; } + public string repos_url { get; set; } + public string events_url { get; set; } + public string received_events_url { get; set; } + public string type { get; set; } + public bool site_admin { get; set; } + } + + public class Asset + { + public string url { get; set; } + public int id { get; set; } + public string name { get; set; } + public object label { get; set; } + public Uploader uploader { get; set; } + public string content_type { get; set; } + public string state { get; set; } + public int size { get; set; } + public int download_count { get; set; } + public string created_at { get; set; } + public string updated_at { get; set; } + public string browser_download_url { get; set; } + } + + public class Author + { + public string login { get; set; } + public int id { get; set; } + public string avatar_url { get; set; } + public string gravatar_id { get; set; } + public string url { get; set; } + public string html_url { get; set; } + public string followers_url { get; set; } + public string following_url { get; set; } + public string gists_url { get; set; } + public string starred_url { get; set; } + public string subscriptions_url { get; set; } + public string organizations_url { get; set; } + public string repos_url { get; set; } + public string events_url { get; set; } + public string received_events_url { get; set; } + public string type { get; set; } + public bool site_admin { get; set; } + } + + public class RootObject + { + public string url { get; set; } + public string assets_url { get; set; } + public string upload_url { get; set; } + public string html_url { get; set; } + public int id { get; set; } + public string tag_name { get; set; } + public string target_commitish { get; set; } + public string name { get; set; } + public bool draft { get; set; } + public Author author { get; set; } + public bool prerelease { get; set; } + public string created_at { get; set; } + public string published_at { get; set; } + public List assets { get; set; } + public string tarball_url { get; set; } + public string zipball_url { get; set; } + public string body { get; set; } + } + } +} diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index 3fb118a9c..94baacf13 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -123,7 +123,6 @@ namespace MediaBrowser.Controller.Entities public int? MinParentalRating { get; set; } public int? MaxParentalRating { get; set; } - public bool? IsCurrentSchema { get; set; } public bool? HasDeadParentId { get; set; } public bool? IsOffline { get; set; } public bool? IsVirtualItem { get; set; } diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index cca8e3c19..a997d3476 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -126,10 +126,6 @@ namespace MediaBrowser.Controller.Entities.TV private static string GetUniqueSeriesKey(BaseItem series) { - if (ConfigurationManager.Configuration.SchemaVersion < 97) - { - return series.Id.ToString("N"); - } return series.GetPresentationUniqueKey(); } diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 9715a624f..cdda858b7 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -192,9 +192,6 @@ namespace MediaBrowser.Model.Configuration public int SharingExpirationDays { get; set; } - public string[] Migrations { get; set; } - - public int MigrationVersion { get; set; } public int SchemaVersion { get; set; } public int SqliteCacheSize { get; set; } @@ -218,7 +215,6 @@ namespace MediaBrowser.Model.Configuration public ServerConfiguration() { LocalNetworkAddresses = new string[] { }; - Migrations = new string[] { }; CodecsUsed = new string[] { }; SqliteCacheSize = 0; ImageExtractionTimeoutMs = 0; diff --git a/MediaBrowser.Model/Tasks/ITaskManager.cs b/MediaBrowser.Model/Tasks/ITaskManager.cs index b6f847feb..fa3da97b3 100644 --- a/MediaBrowser.Model/Tasks/ITaskManager.cs +++ b/MediaBrowser.Model/Tasks/ITaskManager.cs @@ -74,7 +74,5 @@ namespace MediaBrowser.Model.Tasks event EventHandler> TaskExecuting; event EventHandler TaskCompleted; - - bool SuspendTriggers { get; set; } } } \ No newline at end of file diff --git a/MediaBrowser.Server.Mono/MonoAppHost.cs b/MediaBrowser.Server.Mono/MonoAppHost.cs index bb7db6a7c..d864c47d6 100644 --- a/MediaBrowser.Server.Mono/MonoAppHost.cs +++ b/MediaBrowser.Server.Mono/MonoAppHost.cs @@ -92,6 +92,32 @@ namespace MediaBrowser.Server.Mono MainClass.Shutdown(); } + protected override bool SupportsDualModeSockets + { + get + { + return GetMonoVersion() >= new Version(4, 6); + } + } + + private static Version GetMonoVersion() + { + Type type = Type.GetType("Mono.Runtime"); + if (type != null) + { + MethodInfo displayName = type.GetTypeInfo().GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static); + var displayNameValue = displayName.Invoke(null, null).ToString().Trim().Split(' ')[0]; + + Version version; + if (Version.TryParse(displayNameValue, out version)) + { + return version; + } + } + + return new Version(1, 0); + } + protected override void AuthorizeServer() { throw new NotImplementedException(); diff --git a/MediaBrowser.Server.Mono/Native/MonoFileSystem.cs b/MediaBrowser.Server.Mono/Native/MonoFileSystem.cs index 748b94604..3d6a3e35d 100644 --- a/MediaBrowser.Server.Mono/Native/MonoFileSystem.cs +++ b/MediaBrowser.Server.Mono/Native/MonoFileSystem.cs @@ -6,7 +6,8 @@ namespace MediaBrowser.Server.Mono.Native { public class MonoFileSystem : ManagedFileSystem { - public MonoFileSystem(ILogger logger, bool supportsAsyncFileStreams, bool enableManagedInvalidFileNameChars) : base(logger, supportsAsyncFileStreams, enableManagedInvalidFileNameChars, false) + public MonoFileSystem(ILogger logger, bool supportsAsyncFileStreams, bool enableManagedInvalidFileNameChars) + : base(logger, supportsAsyncFileStreams, enableManagedInvalidFileNameChars, true) { } diff --git a/MediaBrowser.ServerApplication/MainStartup.cs b/MediaBrowser.ServerApplication/MainStartup.cs index 9b634d12b..7eafc1721 100644 --- a/MediaBrowser.ServerApplication/MainStartup.cs +++ b/MediaBrowser.ServerApplication/MainStartup.cs @@ -23,8 +23,8 @@ using Emby.Common.Implementations.Logging; using Emby.Common.Implementations.Networking; using Emby.Common.Implementations.Security; using Emby.Server.Core; -using Emby.Server.Core.Browser; using Emby.Server.Implementations; +using Emby.Server.Implementations.Browser; using Emby.Server.Implementations.IO; using ImageMagickSharp; using MediaBrowser.Common.Net; diff --git a/MediaBrowser.ServerApplication/ServerNotifyIcon.cs b/MediaBrowser.ServerApplication/ServerNotifyIcon.cs index 139961e6f..c421dd9eb 100644 --- a/MediaBrowser.ServerApplication/ServerNotifyIcon.cs +++ b/MediaBrowser.ServerApplication/ServerNotifyIcon.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Logging; using System; using System.ComponentModel; using System.Windows.Forms; -using Emby.Server.Core.Browser; +using Emby.Server.Implementations.Browser; using MediaBrowser.Model.Globalization; namespace MediaBrowser.ServerApplication diff --git a/MediaBrowser.ServerApplication/WindowsAppHost.cs b/MediaBrowser.ServerApplication/WindowsAppHost.cs index b950de118..937762ed0 100644 --- a/MediaBrowser.ServerApplication/WindowsAppHost.cs +++ b/MediaBrowser.ServerApplication/WindowsAppHost.cs @@ -117,6 +117,14 @@ namespace MediaBrowser.ServerApplication } } + protected override bool SupportsDualModeSockets + { + get + { + return true; + } + } + public override void LaunchUrl(string url) { var process = new Process @@ -137,6 +145,7 @@ namespace MediaBrowser.ServerApplication } catch (Exception ex) { + Console.WriteLine("Error launching url: {0}", url); Logger.ErrorException("Error launching url: {0}", ex, url); throw; -- cgit v1.2.3 From 9606a2a710614404b4dda96cceff688314a1ec89 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 24 Nov 2016 11:29:23 -0500 Subject: filter duplicate recordings based on showId --- Emby.Common.Implementations/Net/UdpSocket.cs | 2 - .../HttpServer/HttpResultFactory.cs | 3 +- .../LiveTv/EmbyTV/EmbyTV.cs | 50 +++++++++++++++++++++- .../LiveTv/EmbyTV/RecordingHelper.cs | 1 + .../LiveTv/Listings/XmlTvListingsProvider.cs | 7 ++- MediaBrowser.Controller/LiveTv/TimerInfo.cs | 2 + .../Configuration/ServerConfiguration.cs | 2 - MediaBrowser.Model/Querying/ItemFields.cs | 2 + MediaBrowser.WebDashboard/Api/DashboardService.cs | 16 +++---- MediaBrowser.WebDashboard/Api/PackageCreator.cs | 32 +++++--------- 10 files changed, 79 insertions(+), 38 deletions(-) (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Common.Implementations/Net/UdpSocket.cs b/Emby.Common.Implementations/Net/UdpSocket.cs index b9b7d8a2d..367d2242c 100644 --- a/Emby.Common.Implementations/Net/UdpSocket.cs +++ b/Emby.Common.Implementations/Net/UdpSocket.cs @@ -33,8 +33,6 @@ namespace Emby.Common.Implementations.Net _LocalPort = localPort; _Socket.Bind(new IPEndPoint(ip, _LocalPort)); - if (_LocalPort == 0) - _LocalPort = (_Socket.LocalEndPoint as IPEndPoint).Port; } #endregion diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index f65331ec7..313db6a75 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -100,7 +100,8 @@ namespace Emby.Server.Implementations.HttpServer responseHeaders = new Dictionary(); } - if (addCachePrevention) + string expires; + if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out expires)) { responseHeaders["Expires"] = "-1"; } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index b7d2d1748..1fe5d87ce 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -645,6 +645,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV existingTimer.SeasonNumber = updatedTimer.SeasonNumber; existingTimer.ShortOverview = updatedTimer.ShortOverview; existingTimer.StartDate = updatedTimer.StartDate; + existingTimer.ShowId = updatedTimer.ShowId; } public Task GetChannelImageAsync(string channelId, CancellationToken cancellationToken) @@ -1836,6 +1837,39 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return seriesTimer.SkipEpisodesInLibrary && IsProgramAlreadyInLibrary(timer); } + private void HandleDuplicateShowIds(List timers) + { + foreach (var timer in timers.Skip(1)) + { + // TODO: Get smarter, prefer HD, etc + + timer.Status = RecordingStatus.Cancelled; + _timerProvider.Update(timer); + } + } + + private void SearchForDuplicateShowIds(List timers) + { + var groups = timers.ToLookup(i => i.ShowId ?? string.Empty).ToList(); + + foreach (var group in groups) + { + if (string.IsNullOrWhiteSpace(group.Key)) + { + continue; + } + + var groupTimers = group.ToList(); + + if (groupTimers.Count < 2) + { + continue; + } + + HandleDuplicateShowIds(groupTimers); + } + } + private async Task UpdateTimersForSeriesTimer(List epgData, SeriesTimerInfo seriesTimer, bool deleteInvalidTimers) { var allTimers = GetTimersForSeries(seriesTimer, epgData) @@ -1843,6 +1877,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var registration = await _liveTvManager.GetRegistrationInfo("seriesrecordings").ConfigureAwait(false); + var enabledTimersForSeries = new List(); + if (registration.IsValid) { foreach (var timer in allTimers) @@ -1855,6 +1891,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { timer.Status = RecordingStatus.Cancelled; } + else + { + enabledTimersForSeries.Add(timer); + } _timerProvider.Add(timer); } else @@ -1870,6 +1910,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV existingTimer.Status = RecordingStatus.Cancelled; } + if (existingTimer.Status != RecordingStatus.Cancelled) + { + enabledTimersForSeries.Add(existingTimer); + } + existingTimer.SeriesTimerId = seriesTimer.Id; _timerProvider.Update(existingTimer); } @@ -1877,6 +1922,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } + SearchForDuplicateShowIds(enabledTimersForSeries); + if (deleteInvalidTimers) { var allTimerIds = allTimers @@ -1901,8 +1948,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private IEnumerable GetTimersForSeries(SeriesTimerInfo seriesTimer, - IEnumerable allPrograms) + private IEnumerable GetTimersForSeries(SeriesTimerInfo seriesTimer, IEnumerable allPrograms) { if (seriesTimer == null) { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs index 0ae5971bc..1b6ddc73f 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs @@ -31,6 +31,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV timer.Name = parent.Name; timer.Overview = parent.Overview; timer.SeriesTimerId = seriesTimer.Id; + timer.ShowId = parent.ShowId; CopyProgramInfoToTimerInfo(parent, timer); diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index 33db69ee2..45158b3c2 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -124,12 +124,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings private ProgramInfo GetProgramInfo(XmlTvProgram p, ListingsProviderInfo info) { + var episodeTitle = p.Episode == null ? null : p.Episode.Title; + var programInfo = new ProgramInfo { ChannelId = p.ChannelId, EndDate = GetDate(p.EndDate), EpisodeNumber = p.Episode == null ? null : p.Episode.Episode, - EpisodeTitle = p.Episode == null ? null : p.Episode.Title, + EpisodeTitle = episodeTitle, Genres = p.Categories, Id = String.Format("{0}_{1:O}", p.ChannelId, p.StartDate), // Construct an id from the channel and start date, StartDate = GetDate(p.StartDate), @@ -149,7 +151,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings HasImage = p.Icon != null && !String.IsNullOrEmpty(p.Icon.Source), OfficialRating = p.Rating != null && !String.IsNullOrEmpty(p.Rating.Value) ? p.Rating.Value : null, CommunityRating = p.StarRating.HasValue ? p.StarRating.Value : (float?)null, - SeriesId = p.Episode != null ? p.Title.GetMD5().ToString("N") : null + SeriesId = p.Episode != null ? p.Title.GetMD5().ToString("N") : null, + ShowId = ((p.Title ?? string.Empty) + (episodeTitle ?? string.Empty)).GetMD5().ToString("N") }; if (programInfo.IsMovie) diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs index fd614253a..ee8dd5d3a 100644 --- a/MediaBrowser.Controller/LiveTv/TimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs @@ -34,6 +34,8 @@ namespace MediaBrowser.Controller.LiveTv /// The program identifier. public string ProgramId { get; set; } + public string ShowId { get; set; } + /// /// Name of the recording. /// diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index cdda858b7..64225ae76 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -154,7 +154,6 @@ namespace MediaBrowser.Model.Configuration /// /// true if [enable dashboard response caching]; otherwise, false. public bool EnableDashboardResponseCaching { get; set; } - public bool EnableDashboardResourceMinification { get; set; } /// /// Allows the dashboard to be served from a custom path. @@ -230,7 +229,6 @@ namespace MediaBrowser.Model.Configuration HttpsPortNumber = DefaultHttpsPort; EnableHttps = false; EnableDashboardResponseCaching = true; - EnableDashboardResourceMinification = true; EnableAnonymousUsageReporting = true; EnableAutomaticRestart = true; diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index dfca9e771..bf1d4991c 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -45,6 +45,8 @@ /// Chapters, + ChildCount, + /// /// The critic rating summary /// diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index ebd11ca9a..7fcfbfb13 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -113,6 +113,7 @@ namespace MediaBrowser.WebDashboard.Api private readonly ILocalizationManager _localization; private readonly IJsonSerializer _jsonSerializer; private readonly IAssemblyInfo _assemblyInfo; + private readonly IMemoryStreamFactory _memoryStreamFactory; /// /// Initializes a new instance of the class. @@ -120,7 +121,7 @@ namespace MediaBrowser.WebDashboard.Api /// The app host. /// The server configuration manager. /// The file system. - public DashboardService(IServerApplicationHost appHost, IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, IAssemblyInfo assemblyInfo, ILogger logger, IHttpResultFactory resultFactory) + public DashboardService(IServerApplicationHost appHost, IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, IAssemblyInfo assemblyInfo, ILogger logger, IHttpResultFactory resultFactory, IMemoryStreamFactory memoryStreamFactory) { _appHost = appHost; _serverConfigurationManager = serverConfigurationManager; @@ -130,6 +131,7 @@ namespace MediaBrowser.WebDashboard.Api _assemblyInfo = assemblyInfo; _logger = logger; _resultFactory = resultFactory; + _memoryStreamFactory = memoryStreamFactory; } /// @@ -161,7 +163,7 @@ namespace MediaBrowser.WebDashboard.Api if (plugin != null && stream != null) { - return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => GetPackageCreator().ModifyHtml("dummy.html", stream, null, _appHost.ApplicationVersion.ToString(), null, false)); + return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => GetPackageCreator().ModifyHtml("dummy.html", stream, null, _appHost.ApplicationVersion.ToString(), null)); } throw new ResourceNotFoundException(); @@ -294,7 +296,7 @@ namespace MediaBrowser.WebDashboard.Api cacheDuration = TimeSpan.FromDays(365); } - var cacheKey = (_appHost.ApplicationVersion.ToString() + (localizationCulture ?? string.Empty) + path).GetMD5(); + var cacheKey = (_appHost.ApplicationVersion + (localizationCulture ?? string.Empty) + path).GetMD5(); return await _resultFactory.GetStaticResult(Request, cacheKey, null, cacheDuration, contentType, () => GetResourceStream(path, localizationCulture)).ConfigureAwait(false); } @@ -312,15 +314,13 @@ namespace MediaBrowser.WebDashboard.Api /// Task{Stream}. private Task GetResourceStream(string path, string localizationCulture) { - var minify = _serverConfigurationManager.Configuration.EnableDashboardResourceMinification; - return GetPackageCreator() - .GetResource(path, null, localizationCulture, _appHost.ApplicationVersion.ToString(), minify); + .GetResource(path, null, localizationCulture, _appHost.ApplicationVersion.ToString()); } private PackageCreator GetPackageCreator() { - return new PackageCreator(_fileSystem, _localization, _logger, _serverConfigurationManager, _jsonSerializer); + return new PackageCreator(_fileSystem, _logger, _serverConfigurationManager, _memoryStreamFactory); } private List GetDeployIgnoreExtensions() @@ -507,7 +507,7 @@ namespace MediaBrowser.WebDashboard.Api private async Task DumpFile(string resourceVirtualPath, string destinationFilePath, string mode, string culture, string appVersion) { - using (var stream = await GetPackageCreator().GetResource(resourceVirtualPath, mode, culture, appVersion, false).ConfigureAwait(false)) + using (var stream = await GetPackageCreator().GetResource(resourceVirtualPath, mode, culture, appVersion).ConfigureAwait(false)) { using (var fs = _fileSystem.GetFileStream(destinationFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) { diff --git a/MediaBrowser.WebDashboard/Api/PackageCreator.cs b/MediaBrowser.WebDashboard/Api/PackageCreator.cs index 260352c7e..f2df01976 100644 --- a/MediaBrowser.WebDashboard/Api/PackageCreator.cs +++ b/MediaBrowser.WebDashboard/Api/PackageCreator.cs @@ -16,32 +16,28 @@ namespace MediaBrowser.WebDashboard.Api public class PackageCreator { private readonly IFileSystem _fileSystem; - private readonly ILocalizationManager _localization; private readonly ILogger _logger; private readonly IServerConfigurationManager _config; - private readonly IJsonSerializer _jsonSerializer; + private readonly IMemoryStreamFactory _memoryStreamFactory; - public PackageCreator(IFileSystem fileSystem, ILocalizationManager localization, ILogger logger, IServerConfigurationManager config, IJsonSerializer jsonSerializer) + public PackageCreator(IFileSystem fileSystem, ILogger logger, IServerConfigurationManager config, IMemoryStreamFactory memoryStreamFactory) { _fileSystem = fileSystem; - _localization = localization; _logger = logger; _config = config; - _jsonSerializer = jsonSerializer; + _memoryStreamFactory = memoryStreamFactory; } public async Task GetResource(string path, string mode, string localizationCulture, - string appVersion, - bool enableMinification) + string appVersion) { Stream resourceStream; if (path.Equals("css/all.css", StringComparison.OrdinalIgnoreCase)) { - resourceStream = await GetAllCss(enableMinification).ConfigureAwait(false); - enableMinification = false; + resourceStream = await GetAllCss().ConfigureAwait(false); } else { @@ -56,7 +52,7 @@ namespace MediaBrowser.WebDashboard.Api { if (IsCoreHtml(path)) { - resourceStream = await ModifyHtml(path, resourceStream, mode, appVersion, localizationCulture, enableMinification).ConfigureAwait(false); + resourceStream = await ModifyHtml(path, resourceStream, mode, appVersion, localizationCulture).ConfigureAwait(false); } } } @@ -140,20 +136,14 @@ namespace MediaBrowser.WebDashboard.Api /// /// Modifies the HTML by adding common meta tags, css and js. /// - /// The path. - /// The source stream. - /// The mode. - /// The application version. - /// The localization culture. - /// if set to true [enable minification]. /// Task{Stream}. - public async Task ModifyHtml(string path, Stream sourceStream, string mode, string appVersion, string localizationCulture, bool enableMinification) + public async Task ModifyHtml(string path, Stream sourceStream, string mode, string appVersion, string localizationCulture) { using (sourceStream) { string html; - using (var memoryStream = new MemoryStream()) + using (var memoryStream = _memoryStreamFactory.CreateNew()) { await sourceStream.CopyToAsync(memoryStream).ConfigureAwait(false); @@ -202,7 +192,7 @@ namespace MediaBrowser.WebDashboard.Api var bytes = Encoding.UTF8.GetBytes(html); - return new MemoryStream(bytes); + return _memoryStreamFactory.CreateNew(bytes); } } @@ -332,9 +322,9 @@ namespace MediaBrowser.WebDashboard.Api /// Gets all CSS. /// /// Task{Stream}. - private async Task GetAllCss(bool enableMinification) + private async Task GetAllCss() { - var memoryStream = new MemoryStream(); + var memoryStream = _memoryStreamFactory.CreateNew(); var files = new[] { -- cgit v1.2.3 From 921ec9cd11b71171cb69fa538e9d1ec1f2ffbbd5 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sat, 26 Nov 2016 19:40:15 -0500 Subject: save additional info to recording nfo's --- .../Data/SqliteItemRepository.cs | 13 +- .../HttpServer/HttpListenerHost.cs | 6 +- .../LiveTv/EmbyTV/EmbyTV.cs | 201 ++++++++++++++++++--- Emby.Server.Implementations/Sync/SyncRepository.cs | 20 +- MediaBrowser.Api/Library/LibraryService.cs | 17 +- .../Entities/InternalItemsQuery.cs | 1 + 6 files changed, 219 insertions(+), 39 deletions(-) (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 727a9c4bb..c6e5a6dcf 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -170,7 +170,9 @@ namespace Emby.Server.Implementations.Data createMediaStreamsTableCommand, - "create index if not exists idx_mediastreams1 on mediastreams(ItemId)" + "create index if not exists idx_mediastreams1 on mediastreams(ItemId)", + + "pragma shrink_memory" }; @@ -3591,6 +3593,15 @@ namespace Emby.Server.Implementations.Data } } + if (!string.IsNullOrWhiteSpace(query.ExternalId)) + { + whereClauses.Add("ExternalId=@ExternalId"); + if (statement != null) + { + statement.TryBind("@ExternalId", query.ExternalId); + } + } + if (!string.IsNullOrWhiteSpace(query.Name)) { whereClauses.Add("CleanName=@Name"); diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index c1758127a..8a5ae2c3a 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -414,9 +414,9 @@ namespace Emby.Server.Implementations.HttpServer httpRes.StatusCode = 200; httpRes.AddHeader("Access-Control-Allow-Origin", "*"); httpRes.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); - httpRes.AddHeader("Access-Control-Allow-Headers", - "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization"); - httpRes.ContentType = "text/html"; + httpRes.AddHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization"); + httpRes.ContentType = "text/plain"; + Write(httpRes, string.Empty); return; } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 1fe5d87ce..36a4dc608 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1601,6 +1601,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { try { + var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, + Limit = 1, + ExternalId = timer.ProgramId + + }).FirstOrDefault(); + if (timer.IsSports) { AddGenre(timer.Genres, "Sports"); @@ -1615,14 +1623,37 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV AddGenre(timer.Genres, "News"); } + // dummy this up + if (program == null) + { + program = new LiveTvProgram + { + Name = timer.Name, + HomePageUrl = timer.HomePageUrl, + ShortOverview = timer.ShortOverview, + Overview = timer.Overview, + Genres = timer.Genres, + CommunityRating = timer.CommunityRating, + OfficialRating = timer.OfficialRating, + ProductionYear = timer.ProductionYear, + PremiereDate = timer.OriginalAirDate, + IndexNumber = timer.EpisodeNumber, + ParentIndexNumber = timer.SeasonNumber + }; + } + if (timer.IsProgramSeries) { - SaveSeriesNfo(timer, recordingPath, seriesPath); - SaveVideoNfo(timer, recordingPath, false); + SaveSeriesNfo(timer, seriesPath); + SaveVideoNfo(timer, recordingPath, program, false); } else if (!timer.IsMovie || timer.IsSports || timer.IsNews) { - SaveVideoNfo(timer, recordingPath, true); + SaveVideoNfo(timer, recordingPath, program, true); + } + else + { + SaveVideoNfo(timer, recordingPath, program, false); } } catch (Exception ex) @@ -1631,7 +1662,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private void SaveSeriesNfo(TimerInfo timer, string recordingPath, string seriesPath) + private void SaveSeriesNfo(TimerInfo timer, string seriesPath) { var nfoPath = Path.Combine(seriesPath, "tvshow.nfo"); @@ -1676,7 +1707,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss"; - private void SaveVideoNfo(TimerInfo timer, string recordingPath, bool lockData) + private void SaveVideoNfo(TimerInfo timer, string recordingPath, BaseItem item, bool lockData) { var nfoPath = Path.ChangeExtension(recordingPath, ".nfo"); @@ -1694,6 +1725,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV CloseOutput = false }; + var options = _config.GetNfoConfiguration(); + using (XmlWriter writer = XmlWriter.Create(stream, settings)) { writer.WriteStartDocument(true); @@ -1707,45 +1740,64 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV writer.WriteElementString("title", timer.EpisodeTitle); } - if (timer.OriginalAirDate.HasValue) + if (item.PremiereDate.HasValue) { - var formatString = _config.GetNfoConfiguration().ReleaseDateFormat; + var formatString = options.ReleaseDateFormat; - writer.WriteElementString("aired", timer.OriginalAirDate.Value.ToLocalTime().ToString(formatString)); + writer.WriteElementString("aired", item.PremiereDate.Value.ToLocalTime().ToString(formatString)); } - if (timer.EpisodeNumber.HasValue) + if (item.IndexNumber.HasValue) { - writer.WriteElementString("episode", timer.EpisodeNumber.Value.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString("episode", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture)); } - if (timer.SeasonNumber.HasValue) + if (item.ParentIndexNumber.HasValue) { - writer.WriteElementString("season", timer.SeasonNumber.Value.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString("season", item.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture)); } } else { writer.WriteStartElement("movie"); - if (!string.IsNullOrWhiteSpace(timer.Name)) + if (!string.IsNullOrWhiteSpace(item.Name)) + { + writer.WriteElementString("title", item.Name); + } + + if (!string.IsNullOrWhiteSpace(item.OriginalTitle)) + { + writer.WriteElementString("originaltitle", item.OriginalTitle); + } + + if (item.PremiereDate.HasValue) { - writer.WriteElementString("title", timer.Name); + var formatString = options.ReleaseDateFormat; + + writer.WriteElementString("premiered", item.PremiereDate.Value.ToLocalTime().ToString(formatString)); + writer.WriteElementString("releasedate", item.PremiereDate.Value.ToLocalTime().ToString(formatString)); } } writer.WriteElementString("dateadded", DateTime.UtcNow.ToLocalTime().ToString(DateAddedFormat)); - if (timer.ProductionYear.HasValue) + if (item.ProductionYear.HasValue) { - writer.WriteElementString("year", timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString("year", item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)); } - if (!string.IsNullOrEmpty(timer.OfficialRating)) + + if (!string.IsNullOrEmpty(item.OfficialRating)) { - writer.WriteElementString("mpaa", timer.OfficialRating); + writer.WriteElementString("mpaa", item.OfficialRating); } - var overview = (timer.Overview ?? string.Empty) + if (!string.IsNullOrEmpty(item.OfficialRatingDescription)) + { + writer.WriteElementString("mpaadescription", item.OfficialRatingDescription); + } + + var overview = (item.Overview ?? string.Empty) .StripHtml() .Replace(""", "'"); @@ -1756,24 +1808,116 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV writer.WriteElementString("lockdata", true.ToString().ToLower()); } - if (timer.CommunityRating.HasValue) + if (item.CommunityRating.HasValue) { - writer.WriteElementString("rating", timer.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString("rating", item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)); } - foreach (var genre in timer.Genres) + foreach (var genre in item.Genres) { writer.WriteElementString("genre", genre); } - if (!string.IsNullOrWhiteSpace(timer.ShortOverview)) + if (!string.IsNullOrWhiteSpace(item.ShortOverview)) { - writer.WriteElementString("outline", timer.ShortOverview); + writer.WriteElementString("outline", item.ShortOverview); } - if (!string.IsNullOrWhiteSpace(timer.HomePageUrl)) + if (!string.IsNullOrWhiteSpace(item.HomePageUrl)) + { + writer.WriteElementString("website", item.HomePageUrl); + } + + var people = item.Id == Guid.Empty ? new List() : _libraryManager.GetPeople(item); + + var directors = people + .Where(i => IsPersonType(i, PersonType.Director)) + .Select(i => i.Name) + .ToList(); + + foreach (var person in directors) { - writer.WriteElementString("website", timer.HomePageUrl); + writer.WriteElementString("director", person); + } + + var writers = people + .Where(i => IsPersonType(i, PersonType.Writer)) + .Select(i => i.Name) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + foreach (var person in writers) + { + writer.WriteElementString("writer", person); + } + + foreach (var person in writers) + { + writer.WriteElementString("credits", person); + } + + var rt = item.GetProviderId(MetadataProviders.RottenTomatoes); + + if (!string.IsNullOrEmpty(rt)) + { + writer.WriteElementString("rottentomatoesid", rt); + } + + var tmdbCollection = item.GetProviderId(MetadataProviders.TmdbCollection); + + if (!string.IsNullOrEmpty(tmdbCollection)) + { + writer.WriteElementString("collectionnumber", tmdbCollection); + } + + var imdb = item.GetProviderId(MetadataProviders.Imdb); + if (!string.IsNullOrEmpty(imdb)) + { + if (item is Series) + { + writer.WriteElementString("imdb_id", imdb); + } + else + { + writer.WriteElementString("imdbid", imdb); + } + } + + var tvdb = item.GetProviderId(MetadataProviders.Tvdb); + if (!string.IsNullOrEmpty(tvdb)) + { + writer.WriteElementString("tvdbid", tvdb); + } + + var tmdb = item.GetProviderId(MetadataProviders.Tmdb); + if (!string.IsNullOrEmpty(tmdb)) + { + writer.WriteElementString("tmdbid", tmdb); + } + + if (item.CriticRating.HasValue) + { + writer.WriteElementString("criticrating", item.CriticRating.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (!string.IsNullOrEmpty(item.CriticRatingSummary)) + { + writer.WriteElementString("criticratingsummary", item.CriticRatingSummary); + } + + if (!string.IsNullOrWhiteSpace(item.Tagline)) + { + writer.WriteElementString("tagline", item.Tagline); + } + + foreach (var studio in item.Studios) + { + writer.WriteElementString("studio", studio); + } + + if (item.VoteCount.HasValue) + { + writer.WriteElementString("votes", item.VoteCount.Value.ToString(CultureInfo.InvariantCulture)); } writer.WriteEndElement(); @@ -1782,6 +1926,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } + private static bool IsPersonType(PersonInfo person, string type) + { + return string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase); + } + private void AddGenre(List genres, string genre) { if (!genres.Contains(genre, StringComparer.OrdinalIgnoreCase)) diff --git a/Emby.Server.Implementations/Sync/SyncRepository.cs b/Emby.Server.Implementations/Sync/SyncRepository.cs index d8bec1ce3..8cce7a8bf 100644 --- a/Emby.Server.Implementations/Sync/SyncRepository.cs +++ b/Emby.Server.Implementations/Sync/SyncRepository.cs @@ -229,7 +229,6 @@ namespace Emby.Server.Implementations.Sync commandText = "update SyncJobs set TargetId=?,Name=?,Profile=?,Quality=?,Bitrate=?,Status=?,Progress=?,UserId=?,ItemIds=?,Category=?,ParentId=?,UnwatchedOnly=?,ItemLimit=?,SyncNewContent=?,DateCreated=?,DateLastModified=?,ItemCount=? where Id=?"; } - paramList.Add(job.Id.ToGuidParamValue()); paramList.Add(job.TargetId); paramList.Add(job.Name); paramList.Add(job.Profile); @@ -249,6 +248,15 @@ namespace Emby.Server.Implementations.Sync paramList.Add(job.DateLastModified.ToDateTimeParamValue()); paramList.Add(job.ItemCount); + if (insert) + { + paramList.Insert(0, job.Id.ToGuidParamValue()); + } + else + { + paramList.Add(job.Id.ToGuidParamValue()); + } + connection.RunInTransaction(conn => { conn.Execute(commandText, paramList.ToArray()); @@ -698,7 +706,6 @@ namespace Emby.Server.Implementations.Sync } var paramList = new List(); - paramList.Add(jobItem.Id.ToGuidParamValue()); paramList.Add(jobItem.ItemId); paramList.Add(jobItem.ItemName); paramList.Add(jobItem.MediaSourceId); @@ -716,6 +723,15 @@ namespace Emby.Server.Implementations.Sync paramList.Add(jobItem.JobItemIndex); paramList.Add(jobItem.ItemDateModifiedTicks); + if (insert) + { + paramList.Insert(0, jobItem.Id.ToGuidParamValue()); + } + else + { + paramList.Add(jobItem.Id.ToGuidParamValue()); + } + connection.RunInTransaction(conn => { conn.Execute(commandText, paramList.ToArray()); diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index 695718a25..15c1cbe82 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -680,14 +680,17 @@ namespace MediaBrowser.Api.Library /// The request. public void Post(RefreshLibrary request) { - try - { - _libraryManager.ValidateMediaLibrary(new Progress(), CancellationToken.None); - } - catch (Exception ex) + Task.Run(() => { - Logger.ErrorException("Error refreshing library", ex); - } + try + { + _libraryManager.ValidateMediaLibrary(new Progress(), CancellationToken.None); + } + catch (Exception ex) + { + Logger.ErrorException("Error refreshing library", ex); + } + }); } /// diff --git a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs index be6e95ddd..17ef81db9 100644 --- a/MediaBrowser.Controller/Entities/InternalItemsQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalItemsQuery.cs @@ -141,6 +141,7 @@ namespace MediaBrowser.Controller.Entities public SeriesStatus[] SeriesStatuses { get; set; } public string AlbumArtistStartsWithOrGreater { get; set; } public string ExternalSeriesId { get; set; } + public string ExternalId { get; set; } public string[] AlbumNames { get; set; } public string[] ArtistNames { get; set; } -- cgit v1.2.3 From 26ef23d628c6f84baca5491203e1fe2a9a82d6b9 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 27 Nov 2016 14:36:56 -0500 Subject: update caching headers --- .../IO/ManagedFileSystem.cs | 30 +++++++++++--- Emby.Server.Core/ApplicationHost.cs | 2 +- .../Data/BaseSqliteRepository.cs | 17 ++++++-- .../Data/SqliteDisplayPreferencesRepository.cs | 10 ++--- .../Data/SqliteItemRepository.cs | 46 ++++++++++++++++++++-- .../Data/SqliteUserDataRepository.cs | 8 ++++ .../HttpServer/HttpResultFactory.cs | 13 ++++-- Emby.Server.Implementations/Sync/SyncRepository.cs | 8 ++++ MediaBrowser.Model/IO/IFileSystem.cs | 13 +----- MediaBrowser.Model/LiveTv/LiveTvOptions.cs | 1 - MediaBrowser.Providers/Manager/MetadataService.cs | 36 +++++++---------- MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs | 22 ++++++----- 12 files changed, 139 insertions(+), 67 deletions(-) (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Common.Implementations/IO/ManagedFileSystem.cs b/Emby.Common.Implementations/IO/ManagedFileSystem.cs index 4fb70d4e2..b5943e17b 100644 --- a/Emby.Common.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Common.Implementations/IO/ManagedFileSystem.cs @@ -397,16 +397,34 @@ namespace Emby.Common.Implementations.IO private FileAccess GetFileAccess(FileAccessMode mode) { - var val = (int)mode; - - return (FileAccess)val; + switch (mode) + { + case FileAccessMode.ReadWrite: + return FileAccess.ReadWrite; + case FileAccessMode.Write: + return FileAccess.Write; + case FileAccessMode.Read: + return FileAccess.Read; + default: + throw new Exception("Unrecognized FileAccessMode"); + } } private FileShare GetFileShare(FileShareMode mode) { - var val = (int)mode; - - return (FileShare)val; + switch (mode) + { + case FileShareMode.ReadWrite: + return FileShare.ReadWrite; + case FileShareMode.Write: + return FileShare.Write; + case FileShareMode.Read: + return FileShare.Read; + case FileShareMode.None: + return FileShare.None; + default: + throw new Exception("Unrecognized FileShareMode"); + } } public void SetHidden(string path, bool isHidden) diff --git a/Emby.Server.Core/ApplicationHost.cs b/Emby.Server.Core/ApplicationHost.cs index 7e5d6e31c..90848d930 100644 --- a/Emby.Server.Core/ApplicationHost.cs +++ b/Emby.Server.Core/ApplicationHost.cs @@ -551,7 +551,7 @@ namespace Emby.Server.Core DisplayPreferencesRepository = displayPreferencesRepo; RegisterSingleInstance(DisplayPreferencesRepository); - var itemRepo = new SqliteItemRepository(ServerConfigurationManager, JsonSerializer, LogManager.GetLogger("SqliteItemRepository"), MemoryStreamFactory, assemblyInfo, FileSystemManager, EnvironmentInfo); + var itemRepo = new SqliteItemRepository(ServerConfigurationManager, JsonSerializer, LogManager.GetLogger("SqliteItemRepository"), MemoryStreamFactory, assemblyInfo, FileSystemManager, EnvironmentInfo, TimerFactory); ItemRepository = itemRepo; RegisterSingleInstance(ItemRepository); diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 5c60a6f86..308b8356f 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -88,11 +88,14 @@ namespace Emby.Server.Implementations.Data var queries = new List { - "PRAGMA temp_store = memory", - //"PRAGMA journal_mode=WAL" //"PRAGMA cache size=-10000" }; + if (EnableTempStoreMemory) + { + queries.Add("PRAGMA temp_store = memory"); + } + //var cacheSize = CacheSize; //if (cacheSize.HasValue) //{ @@ -116,7 +119,7 @@ namespace Emby.Server.Implementations.Data db.ExecuteAll(string.Join(";", queries.ToArray())); } } - else + else if (queries.Count > 0) { db.ExecuteAll(string.Join(";", queries.ToArray())); } @@ -124,6 +127,14 @@ namespace Emby.Server.Implementations.Data return db; } + protected virtual bool EnableTempStoreMemory + { + get + { + return false; + } + } + protected virtual int? CacheSize { get diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs index 17afbcfa9..1bd64b21d 100644 --- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs @@ -63,8 +63,8 @@ namespace Emby.Server.Implementations.Data string[] queries = { - "create table if not exists userdisplaypreferences (id GUID, userId GUID, client text, data BLOB)", - "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" + "create table if not exists userdisplaypreferences (id GUID, userId GUID, client text, data BLOB)", + "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" }; connection.RunQueries(queries); @@ -107,10 +107,10 @@ namespace Emby.Server.Implementations.Data private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection) { - using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userid, @client, @data)")) - { - var serialized = _jsonSerializer.SerializeToBytes(displayPreferences, _memoryStreamProvider); + var serialized = _jsonSerializer.SerializeToBytes(displayPreferences, _memoryStreamProvider); + using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)")) + { statement.TryBind("@id", displayPreferences.Id.ToGuidParamValue()); statement.TryBind("@userId", userId.ToGuidParamValue()); statement.TryBind("@client", client); diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index c6e5a6dcf..29aacc059 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -30,6 +30,7 @@ using MediaBrowser.Server.Implementations.Playlists; using MediaBrowser.Model.Reflection; using SQLitePCL.pretty; using MediaBrowser.Model.System; +using MediaBrowser.Model.Threading; namespace Emby.Server.Implementations.Data { @@ -68,11 +69,13 @@ namespace Emby.Server.Implementations.Data private readonly IMemoryStreamFactory _memoryStreamProvider; private readonly IFileSystem _fileSystem; private readonly IEnvironmentInfo _environmentInfo; + private readonly ITimerFactory _timerFactory; + private ITimer _shrinkMemoryTimer; /// /// Initializes a new instance of the class. /// - public SqliteItemRepository(IServerConfigurationManager config, IJsonSerializer jsonSerializer, ILogger logger, IMemoryStreamFactory memoryStreamProvider, IAssemblyInfo assemblyInfo, IFileSystem fileSystem, IEnvironmentInfo environmentInfo) + public SqliteItemRepository(IServerConfigurationManager config, IJsonSerializer jsonSerializer, ILogger logger, IMemoryStreamFactory memoryStreamProvider, IAssemblyInfo assemblyInfo, IFileSystem fileSystem, IEnvironmentInfo environmentInfo, ITimerFactory timerFactory) : base(logger) { if (config == null) @@ -89,6 +92,7 @@ namespace Emby.Server.Implementations.Data _memoryStreamProvider = memoryStreamProvider; _fileSystem = fileSystem; _environmentInfo = environmentInfo; + _timerFactory = timerFactory; _typeMapper = new TypeMapper(assemblyInfo); _criticReviewsPath = Path.Combine(_config.ApplicationPaths.DataPath, "critic-reviews"); @@ -119,6 +123,14 @@ namespace Emby.Server.Implementations.Data } } + protected override bool EnableTempStoreMemory + { + get + { + return true; + } + } + private SQLiteDatabaseConnection _backgroundConnection; protected override void CloseConnection() { @@ -129,6 +141,12 @@ namespace Emby.Server.Implementations.Data _backgroundConnection.Dispose(); _backgroundConnection = null; } + + if (_shrinkMemoryTimer != null) + { + _shrinkMemoryTimer.Dispose(); + _shrinkMemoryTimer = null; + } } /// @@ -364,13 +382,35 @@ namespace Emby.Server.Implementations.Data connection.RunQueries(postQueries); - //SqliteExtensions.Attach(_connection, Path.Combine(_config.ApplicationPaths.DataPath, "userdata_v2.db"), "UserDataDb"); //await Vacuum(_connection).ConfigureAwait(false); } userDataRepo.Initialize(WriteLock); _backgroundConnection = CreateConnection(true); + + _shrinkMemoryTimer = _timerFactory.Create(OnShrinkMemoryTimerCallback, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(30)); + } + + private void OnShrinkMemoryTimerCallback(object state) + { + try + { + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunQueries(new string[] + { + "pragma shrink_memory" + }); + } + } + } + catch (Exception ex) + { + Logger.ErrorException("Error running shrink memory", ex); + } } private readonly string[] _retriveItemColumns = @@ -666,7 +706,7 @@ namespace Emby.Server.Implementations.Data { var requiresReset = false; - var statements = db.PrepareAll(string.Join(";", new string[] + var statements = db.PrepareAll(string.Join(";", new string[] { GetSaveItemCommandText(), "delete from AncestorIds where ItemId=@ItemId", diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 4c1b8fcd9..b01f215e0 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -84,6 +84,14 @@ namespace Emby.Server.Implementations.Data } } + protected override bool EnableTempStoreMemory + { + get + { + return true; + } + } + private void ImportUserDataIfNeeded(IDatabaseConnection connection) { if (!_fileSystem.FileExists(_importFile)) diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 313db6a75..995dc7b7b 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -735,7 +735,7 @@ namespace Emby.Server.Implementations.HttpServer /// true if [is not modified] [the specified cache key]; otherwise, false. private bool IsNotModified(IRequest requestContext, Guid? cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration) { - var isNotModified = true; + //var isNotModified = true; var ifModifiedSinceHeader = requestContext.Headers.Get("If-Modified-Since"); @@ -745,18 +745,23 @@ namespace Emby.Server.Implementations.HttpServer if (DateTime.TryParse(ifModifiedSinceHeader, out ifModifiedSince)) { - isNotModified = IsNotModified(ifModifiedSince.ToUniversalTime(), cacheDuration, lastDateModified); + if (IsNotModified(ifModifiedSince.ToUniversalTime(), cacheDuration, lastDateModified)) + { + return true; + } } } var ifNoneMatchHeader = requestContext.Headers.Get("If-None-Match"); // Validate If-None-Match - if (isNotModified && (cacheKey.HasValue || !string.IsNullOrEmpty(ifNoneMatchHeader))) + if ((cacheKey.HasValue || !string.IsNullOrEmpty(ifNoneMatchHeader))) { Guid ifNoneMatch; - if (Guid.TryParse(ifNoneMatchHeader ?? string.Empty, out ifNoneMatch)) + ifNoneMatchHeader = (ifNoneMatchHeader ?? string.Empty).Trim('\"'); + + if (Guid.TryParse(ifNoneMatchHeader, out ifNoneMatch)) { if (cacheKey.HasValue && cacheKey.Value == ifNoneMatch) { diff --git a/Emby.Server.Implementations/Sync/SyncRepository.cs b/Emby.Server.Implementations/Sync/SyncRepository.cs index 8cce7a8bf..b2d9fbcc9 100644 --- a/Emby.Server.Implementations/Sync/SyncRepository.cs +++ b/Emby.Server.Implementations/Sync/SyncRepository.cs @@ -83,6 +83,14 @@ namespace Emby.Server.Implementations.Sync } } + protected override bool EnableTempStoreMemory + { + get + { + return true; + } + } + private const string BaseJobSelectText = "select Id, TargetId, Name, Profile, Quality, Bitrate, Status, Progress, UserId, ItemIds, Category, ParentId, UnwatchedOnly, ItemLimit, SyncNewContent, DateCreated, DateLastModified, ItemCount from SyncJobs"; private const string BaseJobItemSelectText = "select Id, ItemId, ItemName, MediaSourceId, JobId, TemporaryPath, OutputPath, Status, TargetId, DateCreated, Progress, AdditionalFiles, MediaSource, IsMarkedForRemoval, JobItemIndex, ItemDateModifiedTicks from SyncJobItems"; diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index 22e1e7758..62bb66ea8 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -369,7 +369,6 @@ namespace MediaBrowser.Model.IO Append = 6 } - [Flags] public enum FileAccessMode { // @@ -388,7 +387,6 @@ namespace MediaBrowser.Model.IO ReadWrite = 3 } - [Flags] public enum FileShareMode { // @@ -417,16 +415,7 @@ namespace MediaBrowser.Model.IO // or another process) will fail until the file is closed. However, even if this // flag is specified, additional permissions might still be needed to access the // file. - ReadWrite = 3, - // - // Summary: - // Allows subsequent deleting of a file. - Delete = 4, - // - // Summary: - // Makes the file handle inheritable by child processes. This is not directly supported - // by Win32. - Inheritable = 16 + ReadWrite = 3 } } diff --git a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs index 8f7e94cf0..c5e140032 100644 --- a/MediaBrowser.Model/LiveTv/LiveTvOptions.cs +++ b/MediaBrowser.Model/LiveTv/LiveTvOptions.cs @@ -32,7 +32,6 @@ namespace MediaBrowser.Model.LiveTv public LiveTvOptions() { EnableMovieProviders = true; - EnableRecordingSubfolders = true; TunerHosts = new List(); ListingProviders = new List(); MediaLocationsCreated = new string[] { }; diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 9f8dca434..9c6d6a482 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -78,16 +78,15 @@ namespace MediaBrowser.Providers.Manager bool hasRefreshedMetadata = true; bool hasRefreshedImages = true; + var isFirstRefresh = item.DateLastRefreshed == default(DateTime); // Next run metadata providers if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None) { - var providers = GetProviders(item, refreshOptions, requiresRefresh) + var providers = GetProviders(item, refreshOptions, isFirstRefresh, requiresRefresh) .ToList(); - var dateLastRefresh = item.DateLastRefreshed; - - if (providers.Count > 0 || dateLastRefresh == default(DateTime)) + if (providers.Count > 0 || isFirstRefresh) { if (item.BeforeMetadataRefresh()) { @@ -110,11 +109,7 @@ namespace MediaBrowser.Providers.Manager var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, itemImageProvider, cancellationToken).ConfigureAwait(false); updateType = updateType | result.UpdateType; - if (result.Failures == 0) - { - hasRefreshedMetadata = true; - } - else + if (result.Failures > 0) { hasRefreshedMetadata = false; } @@ -138,19 +133,13 @@ namespace MediaBrowser.Providers.Manager var result = await itemImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions, config, cancellationToken).ConfigureAwait(false); updateType = updateType | result.UpdateType; - if (result.Failures == 0) - { - hasRefreshedImages = true; - } - else + if (result.Failures > 0) { hasRefreshedImages = false; } } } - var isFirstRefresh = item.DateLastRefreshed == default(DateTime); - var beforeSaveResult = await BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh, updateType).ConfigureAwait(false); updateType = updateType | beforeSaveResult; @@ -373,15 +362,18 @@ namespace MediaBrowser.Providers.Manager /// Gets the providers. /// /// IEnumerable{`0}. - protected IEnumerable GetProviders(IHasMetadata item, MetadataRefreshOptions options, bool requiresRefresh) + protected IEnumerable GetProviders(IHasMetadata item, MetadataRefreshOptions options, bool isFirstRefresh, bool requiresRefresh) { // Get providers to refresh var providers = ((ProviderManager)ProviderManager).GetMetadataProviders(item).ToList(); - var dateLastRefresh = item.DateLastRefreshed; + var metadataRefreshMode = options.MetadataRefreshMode; // Run all if either of these flags are true - var runAllProviders = options.ReplaceAllMetadata || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || dateLastRefresh == default(DateTime) || requiresRefresh; + var runAllProviders = options.ReplaceAllMetadata || + metadataRefreshMode == MetadataRefreshMode.FullRefresh || + (isFirstRefresh && metadataRefreshMode >= MetadataRefreshMode.Default) || + (requiresRefresh && metadataRefreshMode >= MetadataRefreshMode.Default); if (!runAllProviders) { @@ -404,6 +396,9 @@ namespace MediaBrowser.Providers.Manager } else { + var anyRemoteProvidersChanged = providersWithChanges.OfType() + .Any(); + providers = providers.Where(i => { // If any provider reports a change, always run local ones as well @@ -412,9 +407,6 @@ namespace MediaBrowser.Providers.Manager return true; } - var anyRemoteProvidersChanged = providersWithChanges.OfType() - .Any(); - // If any remote providers changed, run them all so that priorities can be honored if (i is IRemoteMetadataProvider) { diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 84dd095cd..125ac5291 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -911,17 +911,14 @@ namespace MediaBrowser.XbmcMetadata.Savers var image = item.GetImageInfo(ImageType.Primary, 0); - if (image != null && image.IsLocalFile) + if (image != null) { - writer.WriteElementString("poster", GetPathToSave(image.Path, libraryManager, config)); + writer.WriteElementString("poster", GetImagePathToSave(image, libraryManager, config)); } foreach (var backdrop in item.GetImages(ImageType.Backdrop)) { - if (backdrop.IsLocalFile) - { - writer.WriteElementString("fanart", GetPathToSave(backdrop.Path, libraryManager, config)); - } + writer.WriteElementString("fanart", GetImagePathToSave(backdrop, libraryManager, config)); } writer.WriteEndElement(); @@ -1012,9 +1009,9 @@ namespace MediaBrowser.XbmcMetadata.Savers var personEntity = libraryManager.GetPerson(person.Name); var image = personEntity.GetImageInfo(ImageType.Primary, 0); - if (image != null && image.IsLocalFile) + if (image != null) { - writer.WriteElementString("thumb", GetPathToSave(image.Path, libraryManager, config)); + writer.WriteElementString("thumb", GetImagePathToSave(image, libraryManager, config)); } } catch (Exception) @@ -1026,9 +1023,14 @@ namespace MediaBrowser.XbmcMetadata.Savers } } - private static string GetPathToSave(string path, ILibraryManager libraryManager, IServerConfigurationManager config) + private static string GetImagePathToSave(ItemImageInfo image, ILibraryManager libraryManager, IServerConfigurationManager config) { - return libraryManager.GetPathAfterNetworkSubstitution(path); + if (!image.IsLocalFile) + { + return image.Path; + } + + return libraryManager.GetPathAfterNetworkSubstitution(image.Path); } private static bool IsPersonType(PersonInfo person, string type) -- cgit v1.2.3 From 56b24da15165ef4c4b7107b673bab5b191d76afe Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 28 Nov 2016 00:38:41 -0500 Subject: update response stream parsing --- .../HttpServer/SocketSharp/WebSocketSharpResponse.cs | 12 ++++++++---- Emby.Server.Implementations/packages.config | 2 +- MediaBrowser.Api/Playback/Hls/BaseHlsService.cs | 3 ++- ServiceStack/HttpResponseExtensionsInternal.cs | 2 +- SocketHttpListener.Portable/Net/HttpConnection.cs | 4 +++- SocketHttpListener.Portable/Net/HttpListenerResponse.cs | 16 ++++++++++++++++ SocketHttpListener.Portable/Net/ResponseStream.cs | 2 +- 7 files changed, 32 insertions(+), 9 deletions(-) (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs index dc049cbde..36f795411 100644 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs @@ -4,6 +4,7 @@ using System.IO; using System.Net; using System.Text; using MediaBrowser.Model.Logging; +using SocketHttpListener.Net; using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse; using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; using IRequest = MediaBrowser.Model.Services.IRequest; @@ -101,12 +102,15 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp var outputStream = response.OutputStream; // This is needed with compression - //if (!string.IsNullOrWhiteSpace(GetHeader("Content-Encoding"))) + if (outputStream is ResponseStream) { - outputStream.Flush(); - } + //if (!string.IsNullOrWhiteSpace(GetHeader("Content-Encoding"))) + { + outputStream.Flush(); + } - outputStream.Dispose(); + outputStream.Dispose(); + } response.Close(); } catch (Exception ex) diff --git a/Emby.Server.Implementations/packages.config b/Emby.Server.Implementations/packages.config index 5d75af7a2..8464b9b37 100644 --- a/Emby.Server.Implementations/packages.config +++ b/Emby.Server.Implementations/packages.config @@ -1,6 +1,6 @@  - + diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 8d1c0a61e..690acb163 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -153,7 +153,8 @@ namespace MediaBrowser.Api.Playback.Hls var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(UsCulture); - text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(UsCulture), newDuration, StringComparison.OrdinalIgnoreCase); + text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(UsCulture), newDuration, StringComparison.OrdinalIgnoreCase); + //text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(UsCulture), newDuration, StringComparison.OrdinalIgnoreCase); return text; } diff --git a/ServiceStack/HttpResponseExtensionsInternal.cs b/ServiceStack/HttpResponseExtensionsInternal.cs index f78647721..feb18081a 100644 --- a/ServiceStack/HttpResponseExtensionsInternal.cs +++ b/ServiceStack/HttpResponseExtensionsInternal.cs @@ -130,7 +130,7 @@ namespace ServiceStack { foreach (var responseHeaders in responseOptions.Headers) { - if (responseHeaders.Key == "Content-Length") + if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)) { response.SetContentLength(long.Parse(responseHeaders.Value)); continue; diff --git a/SocketHttpListener.Portable/Net/HttpConnection.cs b/SocketHttpListener.Portable/Net/HttpConnection.cs index b09d02254..4b54fc013 100644 --- a/SocketHttpListener.Portable/Net/HttpConnection.cs +++ b/SocketHttpListener.Portable/Net/HttpConnection.cs @@ -209,7 +209,9 @@ namespace SocketHttpListener.Net // TODO: can we get this stream before reading the input? if (o_stream == null) { - if (context.Response.SendChunked || isExpect100Continue || context.Response.ContentLength64 <= 0) + context.Response.DetermineIfChunked(); + + if (context.Response.SendChunked || isExpect100Continue || context.Request.IsWebSocketRequest) { o_stream = new ResponseStream(stream, context.Response, _memoryStreamFactory, _textEncoding); } diff --git a/SocketHttpListener.Portable/Net/HttpListenerResponse.cs b/SocketHttpListener.Portable/Net/HttpListenerResponse.cs index 880473c0a..c1182de34 100644 --- a/SocketHttpListener.Portable/Net/HttpListenerResponse.cs +++ b/SocketHttpListener.Portable/Net/HttpListenerResponse.cs @@ -362,6 +362,22 @@ namespace SocketHttpListener.Net return false; } + public void DetermineIfChunked() + { + if (chunked) + { + return ; + } + + Version v = context.Request.ProtocolVersion; + if (!cl_set && !chunked && v >= HttpVersion.Version11) + chunked = true; + if (!chunked && string.Equals(headers["Transfer-Encoding"], "chunked")) + { + chunked = true; + } + } + internal void SendHeaders(bool closing, MemoryStream ms) { Encoding encoding = content_encoding; diff --git a/SocketHttpListener.Portable/Net/ResponseStream.cs b/SocketHttpListener.Portable/Net/ResponseStream.cs index 7a6425dea..6067a89ec 100644 --- a/SocketHttpListener.Portable/Net/ResponseStream.cs +++ b/SocketHttpListener.Portable/Net/ResponseStream.cs @@ -14,7 +14,7 @@ namespace SocketHttpListener.Net // Update: we send a single packet for the first non-chunked Write // What happens when we set content-length to X and write X-1 bytes then close? // what if we don't set content-length at all? - class ResponseStream : Stream + public class ResponseStream : Stream { HttpListenerResponse response; bool disposed; -- cgit v1.2.3 From 401a6b8f4a84f378f0f6682ee7aecccc6ab30935 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 4 Dec 2016 16:30:38 -0500 Subject: add request logging --- Emby.Common.Implementations/Net/SocketFactory.cs | 26 ++-- Emby.Common.Implementations/Net/UdpSocket.cs | 46 +++--- .../Networking/NetworkManager.cs | 2 +- Emby.Dlna/ConnectionManager/ControlHandler.cs | 4 +- Emby.Dlna/ContentDirectory/ControlHandler.cs | 45 ++++-- Emby.Dlna/Emby.Dlna.csproj | 1 - Emby.Dlna/Main/DlnaEntryPoint.cs | 6 +- Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs | 6 +- Emby.Dlna/Server/Headers.cs | 169 --------------------- Emby.Dlna/Service/BaseControlHandler.cs | 17 +-- .../HttpServer/HttpListenerHost.cs | 2 +- ServiceStack/HttpHandlerFactory.cs | 9 +- 12 files changed, 96 insertions(+), 237 deletions(-) delete mode 100644 Emby.Dlna/Server/Headers.cs (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Common.Implementations/Net/SocketFactory.cs b/Emby.Common.Implementations/Net/SocketFactory.cs index c65593242..124252097 100644 --- a/Emby.Common.Implementations/Net/SocketFactory.cs +++ b/Emby.Common.Implementations/Net/SocketFactory.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; using System.Net.Sockets; using System.Threading.Tasks; +using Emby.Common.Implementations.Networking; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; @@ -18,11 +19,6 @@ namespace Emby.Common.Implementations.Net // but that wasn't really the point so kept to YAGNI principal for now, even if the // interfaces are a bit ugly, specific and make assumptions. - /// - /// Used by RSSDP components to create implementations of the interface, to perform platform agnostic socket communications. - /// - private IPAddress _LocalIP; - private readonly ILogger _logger; public SocketFactory(ILogger logger) @@ -33,7 +29,6 @@ namespace Emby.Common.Implementations.Net } _logger = logger; - _LocalIP = IPAddress.Any; } public ISocket CreateSocket(IpAddressFamily family, MediaBrowser.Model.Net.SocketType socketType, MediaBrowser.Model.Net.ProtocolType protocolType, bool dualMode) @@ -66,7 +61,7 @@ namespace Emby.Common.Implementations.Net try { retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - return new UdpSocket(retVal, localPort, _LocalIP); + return new UdpSocket(retVal, localPort, IPAddress.Any); } catch { @@ -80,9 +75,8 @@ namespace Emby.Common.Implementations.Net /// /// Creates a new UDP socket that is a member of the SSDP multicast local admin group and binds it to the specified local port. /// - /// An integer specifying the local port to bind the socket to. /// An implementation of the interface used by RSSDP components to perform socket operations. - public IUdpSocket CreateSsdpUdpSocket(int localPort) + public IUdpSocket CreateSsdpUdpSocket(IpAddressInfo localIpAddress, int localPort) { if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort"); @@ -91,8 +85,11 @@ namespace Emby.Common.Implementations.Net { retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4); - retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), _LocalIP)); - return new UdpSocket(retVal, localPort, _LocalIP); + + var localIp = NetworkManager.ToIPAddress(localIpAddress); + + retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), localIp)); + return new UdpSocket(retVal, localPort, localIp); } catch { @@ -134,10 +131,13 @@ namespace Emby.Common.Implementations.Net //retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); - retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse(ipAddress), _LocalIP)); + + var localIp = IPAddress.Any; + + retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse(ipAddress), localIp)); retVal.MulticastLoopback = true; - return new UdpSocket(retVal, localPort, _LocalIP); + return new UdpSocket(retVal, localPort, localIp); } catch { diff --git a/Emby.Common.Implementations/Net/UdpSocket.cs b/Emby.Common.Implementations/Net/UdpSocket.cs index 367d2242c..b2af9d162 100644 --- a/Emby.Common.Implementations/Net/UdpSocket.cs +++ b/Emby.Common.Implementations/Net/UdpSocket.cs @@ -20,7 +20,6 @@ namespace Emby.Common.Implementations.Net private Socket _Socket; private int _LocalPort; - #endregion #region Constructors @@ -31,12 +30,19 @@ namespace Emby.Common.Implementations.Net _Socket = socket; _LocalPort = localPort; + LocalIPAddress = NetworkManager.ToIpAddressInfo(ip); _Socket.Bind(new IPEndPoint(ip, _LocalPort)); } #endregion + public IpAddressInfo LocalIPAddress + { + get; + private set; + } + #region IUdpSocket Members public Task ReceiveAsync() @@ -50,18 +56,18 @@ namespace Emby.Common.Implementations.Net state.TaskCompletionSource = tcs; #if NETSTANDARD1_6 - _Socket.ReceiveFromAsync(new ArraySegment(state.Buffer),SocketFlags.None, state.EndPoint) + _Socket.ReceiveFromAsync(new ArraySegment(state.Buffer),SocketFlags.None, state.RemoteEndPoint) .ContinueWith((task, asyncState) => { if (task.Status != TaskStatus.Faulted) { var receiveState = asyncState as AsyncReceiveState; - receiveState.EndPoint = task.Result.RemoteEndPoint; - ProcessResponse(receiveState, () => task.Result.ReceivedBytes); + receiveState.RemoteEndPoint = task.Result.RemoteEndPoint; + ProcessResponse(receiveState, () => task.Result.ReceivedBytes, LocalIPAddress); } }, state); #else - _Socket.BeginReceiveFrom(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, ref state.EndPoint, ProcessResponse, state); + _Socket.BeginReceiveFrom(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, ref state.RemoteEndPoint, ProcessResponse, state); #endif return tcs.Task; @@ -74,6 +80,8 @@ namespace Emby.Common.Implementations.Net if (buffer == null) throw new ArgumentNullException("messageData"); if (endPoint == null) throw new ArgumentNullException("endPoint"); + var ipEndPoint = NetworkManager.ToIPEndPoint(endPoint); + #if NETSTANDARD1_6 if (size != buffer.Length) @@ -83,14 +91,14 @@ namespace Emby.Common.Implementations.Net buffer = copy; } - _Socket.SendTo(buffer, new IPEndPoint(IPAddress.Parse(endPoint.IpAddress.ToString()), endPoint.Port)); + _Socket.SendTo(buffer, ipEndPoint); return Task.FromResult(true); #else var taskSource = new TaskCompletionSource(); try { - _Socket.BeginSendTo(buffer, 0, size, SocketFlags.None, new System.Net.IPEndPoint(IPAddress.Parse(endPoint.IpAddress.ToString()), endPoint.Port), result => + _Socket.BeginSendTo(buffer, 0, size, SocketFlags.None, ipEndPoint, result => { try { @@ -109,7 +117,7 @@ namespace Emby.Common.Implementations.Net taskSource.TrySetException(ex); } - //_Socket.SendTo(messageData, new System.Net.IPEndPoint(IPAddress.Parse(endPoint.IPAddress), endPoint.Port)); + //_Socket.SendTo(messageData, new System.Net.IPEndPoint(IPAddress.Parse(RemoteEndPoint.IPAddress), RemoteEndPoint.Port)); return taskSource.Task; #endif @@ -133,19 +141,20 @@ namespace Emby.Common.Implementations.Net #region Private Methods - private static void ProcessResponse(AsyncReceiveState state, Func receiveData) + private static void ProcessResponse(AsyncReceiveState state, Func receiveData, IpAddressInfo localIpAddress) { try { var bytesRead = receiveData(); - var ipEndPoint = state.EndPoint as IPEndPoint; + var ipEndPoint = state.RemoteEndPoint as IPEndPoint; state.TaskCompletionSource.SetResult( - new SocketReceiveResult() + new SocketReceiveResult { Buffer = state.Buffer, ReceivedBytes = bytesRead, - RemoteEndPoint = ToIpEndPointInfo(ipEndPoint) + RemoteEndPoint = ToIpEndPointInfo(ipEndPoint), + LocalIPAddress = localIpAddress } ); } @@ -182,15 +191,16 @@ namespace Emby.Common.Implementations.Net var state = asyncResult.AsyncState as AsyncReceiveState; try { - var bytesRead = state.Socket.EndReceiveFrom(asyncResult, ref state.EndPoint); + var bytesRead = state.Socket.EndReceiveFrom(asyncResult, ref state.RemoteEndPoint); - var ipEndPoint = state.EndPoint as IPEndPoint; + var ipEndPoint = state.RemoteEndPoint as IPEndPoint; state.TaskCompletionSource.SetResult( new SocketReceiveResult { Buffer = state.Buffer, ReceivedBytes = bytesRead, - RemoteEndPoint = ToIpEndPointInfo(ipEndPoint) + RemoteEndPoint = ToIpEndPointInfo(ipEndPoint), + LocalIPAddress = LocalIPAddress } ); } @@ -211,13 +221,13 @@ namespace Emby.Common.Implementations.Net private class AsyncReceiveState { - public AsyncReceiveState(Socket socket, EndPoint endPoint) + public AsyncReceiveState(Socket socket, EndPoint remoteEndPoint) { this.Socket = socket; - this.EndPoint = endPoint; + this.RemoteEndPoint = remoteEndPoint; } - public EndPoint EndPoint; + public EndPoint RemoteEndPoint; public byte[] Buffer = new byte[8192]; public Socket Socket { get; private set; } diff --git a/Emby.Common.Implementations/Networking/NetworkManager.cs b/Emby.Common.Implementations/Networking/NetworkManager.cs index a4f8f7ced..b9100f9db 100644 --- a/Emby.Common.Implementations/Networking/NetworkManager.cs +++ b/Emby.Common.Implementations/Networking/NetworkManager.cs @@ -27,7 +27,7 @@ namespace Emby.Common.Implementations.Networking private List _localIpAddresses; private readonly object _localIpAddressSyncLock = new object(); - public IEnumerable GetLocalIpAddresses() + public List GetLocalIpAddresses() { const int cacheMinutes = 5; diff --git a/Emby.Dlna/ConnectionManager/ControlHandler.cs b/Emby.Dlna/ConnectionManager/ControlHandler.cs index 0bc44db17..ae983c5e7 100644 --- a/Emby.Dlna/ConnectionManager/ControlHandler.cs +++ b/Emby.Dlna/ConnectionManager/ControlHandler.cs @@ -14,7 +14,7 @@ namespace Emby.Dlna.ConnectionManager { private readonly DeviceProfile _profile; - protected override IEnumerable> GetResult(string methodName, Headers methodParams) + protected override IEnumerable> GetResult(string methodName, IDictionary methodParams) { if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase)) { @@ -26,7 +26,7 @@ namespace Emby.Dlna.ConnectionManager private IEnumerable> HandleGetProtocolInfo() { - return new Headers(true) + return new Dictionary(StringComparer.OrdinalIgnoreCase) { { "Source", _profile.ProtocolInfo }, { "Sink", "" } diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index d77919e47..98a151f29 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -65,7 +65,7 @@ namespace Emby.Dlna.ContentDirectory _didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, Logger, libraryManager, mediaEncoder); } - protected override IEnumerable> GetResult(string methodName, Headers methodParams) + protected override IEnumerable> GetResult(string methodName, IDictionary methodParams) { var deviceId = "test"; @@ -118,17 +118,20 @@ namespace Emby.Dlna.ContentDirectory _userDataManager.SaveUserData(user.Id, item, userdata, UserDataSaveReason.TogglePlayed, CancellationToken.None); - return new Headers(); + return new Dictionary(StringComparer.OrdinalIgnoreCase); } private IEnumerable> HandleGetSearchCapabilities() { - return new Headers(true) { { "SearchCaps", "res@resolution,res@size,res@duration,dc:title,dc:creator,upnp:actor,upnp:artist,upnp:genre,upnp:album,dc:date,upnp:class,@id,@refID,@protocolInfo,upnp:author,dc:description,pv:avKeywords" } }; + return new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "SearchCaps", "res@resolution,res@size,res@duration,dc:title,dc:creator,upnp:actor,upnp:artist,upnp:genre,upnp:album,dc:date,upnp:class,@id,@refID,@protocolInfo,upnp:author,dc:description,pv:avKeywords" } + }; } private IEnumerable> HandleGetSortCapabilities() { - return new Headers(true) + return new Dictionary(StringComparer.OrdinalIgnoreCase) { { "SortCaps", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating" } }; @@ -136,7 +139,7 @@ namespace Emby.Dlna.ContentDirectory private IEnumerable> HandleGetSortExtensionCapabilities() { - return new Headers(true) + return new Dictionary(StringComparer.OrdinalIgnoreCase) { { "SortExtensionCaps", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating" } }; @@ -144,14 +147,14 @@ namespace Emby.Dlna.ContentDirectory private IEnumerable> HandleGetSystemUpdateID() { - var headers = new Headers(true); + var headers = new Dictionary(StringComparer.OrdinalIgnoreCase); headers.Add("Id", _systemUpdateId.ToString(_usCulture)); return headers; } private IEnumerable> HandleGetFeatureList() { - return new Headers(true) + return new Dictionary(StringComparer.OrdinalIgnoreCase) { { "FeatureList", GetFeatureListXml() } }; @@ -159,7 +162,7 @@ namespace Emby.Dlna.ContentDirectory private IEnumerable> HandleXGetFeatureList() { - return new Headers(true) + return new Dictionary(StringComparer.OrdinalIgnoreCase) { { "FeatureList", GetFeatureListXml() } }; @@ -183,12 +186,24 @@ namespace Emby.Dlna.ContentDirectory return builder.ToString(); } - private async Task>> HandleBrowse(Headers sparams, User user, string deviceId) + public string GetValueOrDefault(IDictionary sparams, string key, string defaultValue) + { + string val; + + if (sparams.TryGetValue(key, out val)) + { + return val; + } + + return defaultValue; + } + + private async Task>> HandleBrowse(IDictionary sparams, User user, string deviceId) { var id = sparams["ObjectID"]; var flag = sparams["BrowseFlag"]; - var filter = new Filter(sparams.GetValueOrDefault("Filter", "*")); - var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", "")); + var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); + var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", "")); var provided = 0; @@ -294,11 +309,11 @@ namespace Emby.Dlna.ContentDirectory }; } - private async Task>> HandleSearch(Headers sparams, User user, string deviceId) + private async Task>> HandleSearch(IDictionary sparams, User user, string deviceId) { - var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", "")); - var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", "")); - var filter = new Filter(sparams.GetValueOrDefault("Filter", "*")); + var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", "")); + var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", "")); + var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); // sort example: dc:title, dc:date diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index 0441cb3be..4d1aacfec 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -152,7 +152,6 @@ - diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 170b4cee0..858b1ae9e 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -54,6 +54,7 @@ namespace Emby.Dlna.Main private readonly ITimerFactory _timerFactory; private readonly ISocketFactory _socketFactory; private readonly IEnvironmentInfo _environmentInfo; + private readonly INetworkManager _networkManager; private ISsdpCommunicationsServer _communicationsServer; @@ -69,7 +70,7 @@ namespace Emby.Dlna.Main IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, - IDeviceDiscovery deviceDiscovery, IMediaEncoder mediaEncoder, ISocketFactory socketFactory, ITimerFactory timerFactory, IEnvironmentInfo environmentInfo) + IDeviceDiscovery deviceDiscovery, IMediaEncoder mediaEncoder, ISocketFactory socketFactory, ITimerFactory timerFactory, IEnvironmentInfo environmentInfo, INetworkManager networkManager) { _config = config; _appHost = appHost; @@ -87,6 +88,7 @@ namespace Emby.Dlna.Main _socketFactory = socketFactory; _timerFactory = timerFactory; _environmentInfo = environmentInfo; + _networkManager = networkManager; _logger = logManager.GetLogger("Dlna"); } @@ -156,7 +158,7 @@ namespace Emby.Dlna.Main { if (_communicationsServer == null) { - _communicationsServer = new SsdpCommunicationsServer(_socketFactory) + _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger) { IsShared = true }; diff --git a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs index 5e232aeac..daf46b106 100644 --- a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs @@ -11,7 +11,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar { public class ControlHandler : BaseControlHandler { - protected override IEnumerable> GetResult(string methodName, Headers methodParams) + protected override IEnumerable> GetResult(string methodName, IDictionary methodParams) { if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase)) return HandleIsAuthorized(); @@ -23,7 +23,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar private IEnumerable> HandleIsAuthorized() { - return new Headers(true) + return new Dictionary(StringComparer.OrdinalIgnoreCase) { { "Result", "1" } }; @@ -31,7 +31,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar private IEnumerable> HandleIsValidated() { - return new Headers(true) + return new Dictionary(StringComparer.OrdinalIgnoreCase) { { "Result", "1" } }; diff --git a/Emby.Dlna/Server/Headers.cs b/Emby.Dlna/Server/Headers.cs deleted file mode 100644 index 47dd8e321..000000000 --- a/Emby.Dlna/Server/Headers.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; - -namespace Emby.Dlna.Server -{ - public class Headers : IDictionary - { - private readonly bool _asIs = false; - private readonly Dictionary _dict = new Dictionary(); - private readonly static Regex Validator = new Regex(@"^[a-z\d][a-z\d_.-]+$", RegexOptions.IgnoreCase); - - public Headers(bool asIs) - { - _asIs = asIs; - } - - public Headers() - : this(asIs: false) - { - } - - public int Count - { - get - { - return _dict.Count; - } - } - public string HeaderBlock - { - get - { - var hb = new StringBuilder(); - foreach (var h in this) - { - hb.AppendFormat("{0}: {1}\r\n", h.Key, h.Value); - } - return hb.ToString(); - } - } - public bool IsReadOnly - { - get - { - return false; - } - } - public ICollection Keys - { - get - { - return _dict.Keys; - } - } - public ICollection Values - { - get - { - return _dict.Values; - } - } - - - public string this[string key] - { - get - { - return _dict[Normalize(key)]; - } - set - { - _dict[Normalize(key)] = value; - } - } - - - private string Normalize(string header) - { - if (!_asIs) - { - header = header.ToLower(); - } - header = header.Trim(); - if (!Validator.IsMatch(header)) - { - throw new ArgumentException("Invalid header: " + header); - } - return header; - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() - { - return _dict.GetEnumerator(); - } - - public void Add(KeyValuePair item) - { - Add(item.Key, item.Value); - } - - public void Add(string key, string value) - { - _dict.Add(Normalize(key), value); - } - - public void Clear() - { - _dict.Clear(); - } - - public bool Contains(KeyValuePair item) - { - var p = new KeyValuePair(Normalize(item.Key), item.Value); - return _dict.Contains(p); - } - - public bool ContainsKey(string key) - { - return _dict.ContainsKey(Normalize(key)); - } - - public void CopyTo(KeyValuePair[] array, int arrayIndex) - { - throw new NotImplementedException(); - } - - public IEnumerator> GetEnumerator() - { - return _dict.GetEnumerator(); - } - - public bool Remove(string key) - { - return _dict.Remove(Normalize(key)); - } - - public bool Remove(KeyValuePair item) - { - return Remove(item.Key); - } - - public override string ToString() - { - return string.Format("({0})", string.Join(", ", (from x in _dict - select string.Format("{0}={1}", x.Key, x.Value)))); - } - - public bool TryGetValue(string key, out string value) - { - return _dict.TryGetValue(Normalize(key), out value); - } - - public string GetValueOrDefault(string key, string defaultValue) - { - string val; - - if (TryGetValue(key, out val)) - { - return val; - } - - return defaultValue; - } - } -} diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs index 4ce047172..3092589c1 100644 --- a/Emby.Dlna/Service/BaseControlHandler.cs +++ b/Emby.Dlna/Service/BaseControlHandler.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text; using System.Xml; using Emby.Dlna.Didl; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Model.Xml; namespace Emby.Dlna.Service @@ -185,8 +186,7 @@ namespace Emby.Dlna.Service { using (var subReader = reader.ReadSubtree()) { - result.Headers = ParseFirstBodyChild(subReader); - + ParseFirstBodyChild(subReader, result.Headers); return result; } } @@ -204,10 +204,8 @@ namespace Emby.Dlna.Service return result; } - private Headers ParseFirstBodyChild(XmlReader reader) + private void ParseFirstBodyChild(XmlReader reader, IDictionary headers) { - var result = new Headers(); - reader.MoveToContent(); reader.Read(); @@ -216,25 +214,24 @@ namespace Emby.Dlna.Service { if (reader.NodeType == XmlNodeType.Element) { - result.Add(reader.LocalName, reader.ReadElementContentAsString()); + // TODO: Should we be doing this here, or should it be handled earlier when decoding the request? + headers[reader.LocalName.RemoveDiacritics()] = reader.ReadElementContentAsString(); } else { reader.Read(); } } - - return result; } private class ControlRequestInfo { public string LocalName; public string NamespaceURI; - public Headers Headers = new Headers(); + public IDictionary Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); } - protected abstract IEnumerable> GetResult(string methodName, Headers methodParams); + protected abstract IEnumerable> GetResult(string methodName, IDictionary methodParams); private void LogRequest(ControlRequest request) { diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 8a5ae2c3a..0e1f5a551 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -518,7 +518,7 @@ namespace Emby.Server.Implementations.HttpServer return; } - var handler = HttpHandlerFactory.GetHandler(httpReq); + var handler = HttpHandlerFactory.GetHandler(httpReq, _logger); if (handler != null) { diff --git a/ServiceStack/HttpHandlerFactory.cs b/ServiceStack/HttpHandlerFactory.cs index d48bfeb5f..5f4892d51 100644 --- a/ServiceStack/HttpHandlerFactory.cs +++ b/ServiceStack/HttpHandlerFactory.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using MediaBrowser.Model.Logging; using MediaBrowser.Model.Services; using ServiceStack.Host; @@ -9,12 +10,16 @@ namespace ServiceStack public class HttpHandlerFactory { // Entry point for HttpListener - public static RestHandler GetHandler(IHttpRequest httpReq) + public static RestHandler GetHandler(IHttpRequest httpReq, ILogger logger) { var pathInfo = httpReq.PathInfo; var pathParts = pathInfo.TrimStart('/').Split('/'); - if (pathParts.Length == 0) return null; + if (pathParts.Length == 0) + { + logger.Error("Path parts empty for PathInfo: {0}, Url: {1}", pathInfo, httpReq.RawUrl); + return null; + } string contentType; var restPath = RestHandler.FindMatchingRestPath(httpReq.HttpMethod, pathInfo, out contentType); -- cgit v1.2.3 From e1b880a5a072764cabace79cd6d1d65315ec65e4 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 13 Dec 2016 02:36:30 -0500 Subject: update series queries --- .../Activity/ActivityRepository.cs | 15 +- .../Data/BaseSqliteRepository.cs | 2 + .../Data/SqliteItemRepository.cs | 261 +++++++++++---------- .../Data/SqliteUserDataRepository.cs | 12 +- Emby.Server.Implementations/Dto/DtoService.cs | 105 ++++++--- .../FileOrganization/EpisodeFileOrganizer.cs | 6 + .../SocketSharp/WebSocketSharpListener.cs | 3 +- .../Library/LibraryManager.cs | 58 +++-- .../Library/UserDataManager.cs | 7 +- .../Notifications/SqliteNotificationsRepository.cs | 20 +- .../Security/AuthenticationRepository.cs | 12 +- MediaBrowser.Api/BaseApiService.cs | 32 ++- MediaBrowser.Api/ItemLookupService.cs | 15 +- MediaBrowser.Controller/Entities/BaseItem.cs | 3 +- .../Entities/CollectionFolder.cs | 68 ++++-- MediaBrowser.Controller/Entities/Folder.cs | 36 +-- MediaBrowser.Controller/Entities/IHasUserData.cs | 6 +- MediaBrowser.Controller/Library/ILibraryManager.cs | 2 + .../Library/IUserDataManager.cs | 6 +- MediaBrowser.Model/Querying/ItemFields.cs | 2 + RSSDP/SsdpDevicePublisherBase.cs | 8 +- 21 files changed, 418 insertions(+), 261 deletions(-) (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs index 7ac0e680c..5f6518a14 100644 --- a/Emby.Server.Implementations/Activity/ActivityRepository.cs +++ b/Emby.Server.Implementations/Activity/ActivityRepository.cs @@ -84,9 +84,6 @@ namespace Emby.Server.Implementations.Activity { using (var connection = CreateConnection(true)) { - var list = new List(); - var result = new QueryResult(); - var commandText = BaseActivitySelectText; var whereClauses = new List(); @@ -127,8 +124,11 @@ namespace Emby.Server.Implementations.Activity statementTexts.Add(commandText); statementTexts.Add("select count (Id) from ActivityLogEntries" + whereTextWithoutPaging); - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var list = new List(); + var result = new QueryResult(); + var statements = PrepareAllSafe(db, string.Join(";", statementTexts.ToArray())).ToList(); using (var statement = statements[0]) @@ -153,10 +153,11 @@ namespace Emby.Server.Implementations.Activity result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); } - }, ReadTransactionMode); - result.Items = list.ToArray(); - return result; + result.Items = list.ToArray(); + return result; + + }, ReadTransactionMode); } } } diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 9e60a43aa..2fc721f83 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -43,6 +43,7 @@ namespace Emby.Server.Implementations.Data //CheckOk(rc); rc = raw.sqlite3_config(raw.SQLITE_CONFIG_MULTITHREAD, 1); + //rc = raw.sqlite3_config(raw.SQLITE_CONFIG_SERIALIZED, 1); //CheckOk(rc); rc = raw.sqlite3_enable_shared_cache(1); @@ -94,6 +95,7 @@ namespace Emby.Server.Implementations.Data var queries = new List { //"PRAGMA cache size=-10000" + //"PRAGMA read_uncommitted = true" }; if (EnableTempStoreMemory) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 42cbf1965..803ebeca0 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -328,6 +328,8 @@ namespace Emby.Server.Implementations.Data "drop table if exists Images", "drop index if exists idx_Images", "drop index if exists idx_TypeSeriesPresentationUniqueKey", + "drop index if exists idx_SeriesPresentationUniqueKey", + "drop index if exists idx_TypeSeriesPresentationUniqueKey2", "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", @@ -343,8 +345,9 @@ namespace Emby.Server.Implementations.Data // series "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)", - // series next up - "create index if not exists idx_SeriesPresentationUniqueKey on TypedBaseItems(SeriesPresentationUniqueKey)", + // series counts + // seriesdateplayed sort order + "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)", // live tv programs "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)", @@ -2079,25 +2082,29 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException("id"); } - var list = new List(); - using (WriteLock.Read()) { using (var connection = CreateConnection(true)) { - using (var statement = PrepareStatementSafe(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) + return connection.RunInTransaction(db => { - statement.TryBind("@ItemId", id); + var list = new List(); - foreach (var row in statement.ExecuteQuery()) + using (var statement = PrepareStatementSafe(db, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) { - list.Add(GetChapter(row)); + statement.TryBind("@ItemId", id); + + foreach (var row in statement.ExecuteQuery()) + { + list.Add(GetChapter(row)); + } } - } + + return list; + + }, ReadTransactionMode); } } - - return list; } /// @@ -2470,32 +2477,33 @@ namespace Emby.Server.Implementations.Data //commandText += GetGroupBy(query); - int count = 0; - using (WriteLock.Read()) { using (var connection = CreateConnection(true)) { - using (var statement = PrepareStatementSafe(connection, commandText)) + return connection.RunInTransaction(db => { - if (EnableJoinUserData(query)) + using (var statement = PrepareStatementSafe(db, commandText)) { - statement.TryBind("@UserId", query.User.Id); - } + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } - BindSimilarParams(query, statement); + BindSimilarParams(query, statement); - // Running this again will bind the params - GetWhereClauses(query, statement); + // Running this again will bind the params + GetWhereClauses(query, statement); - count = statement.ExecuteQuery().SelectScalarInt().First(); - } + var count = statement.ExecuteQuery().SelectScalarInt().First(); + LogQueryTime("GetCount", commandText, now); + return count; + } + + }, ReadTransactionMode); } - LogQueryTime("GetCount", commandText, now); } - - return count; } public List GetItemList(InternalItemsQuery query) @@ -2511,8 +2519,6 @@ namespace Emby.Server.Implementations.Data var now = DateTime.UtcNow; - var list = new List(); - // Hack for right now since we currently don't support filtering out these duplicates within a query if (query.Limit.HasValue && query.EnableGroupByMetadataKey) { @@ -2553,53 +2559,59 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - using (var statement = PrepareStatementSafe(connection, commandText)) + return connection.RunInTransaction(db => { - if (EnableJoinUserData(query)) + var list = new List(); + + using (var statement = PrepareStatementSafe(db, commandText)) { - statement.TryBind("@UserId", query.User.Id); - } + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } - BindSimilarParams(query, statement); + BindSimilarParams(query, statement); - // Running this again will bind the params - GetWhereClauses(query, statement); + // Running this again will bind the params + GetWhereClauses(query, statement); - foreach (var row in statement.ExecuteQuery()) - { - var item = GetItem(row, query); - if (item != null) + foreach (var row in statement.ExecuteQuery()) { - list.Add(item); + var item = GetItem(row, query); + if (item != null) + { + list.Add(item); + } } } - } - } - LogQueryTime("GetItemList", commandText, now); - } + // Hack for right now since we currently don't support filtering out these duplicates within a query + if (query.EnableGroupByMetadataKey) + { + var limit = query.Limit ?? int.MaxValue; + limit -= 4; + var newList = new List(); - // Hack for right now since we currently don't support filtering out these duplicates within a query - if (query.EnableGroupByMetadataKey) - { - var limit = query.Limit ?? int.MaxValue; - limit -= 4; - var newList = new List(); + foreach (var item in list) + { + AddItem(newList, item); - foreach (var item in list) - { - AddItem(newList, item); + if (newList.Count >= limit) + { + break; + } + } - if (newList.Count >= limit) - { - break; - } - } + list = newList; + } - list = newList; - } + LogQueryTime("GetItemList", commandText, now); - return list; + return list; + + }, ReadTransactionMode); + } + } } private void AddItem(List items, BaseItem newItem) @@ -2637,7 +2649,7 @@ namespace Emby.Server.Implementations.Data var slowThreshold = 1000; #if DEBUG - slowThreshold = 50; + slowThreshold = 2; #endif if (elapsed >= slowThreshold) @@ -2718,7 +2730,6 @@ namespace Emby.Server.Implementations.Data } } - var result = new QueryResult(); var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; var statementTexts = new List(); @@ -2748,8 +2759,9 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var result = new QueryResult(); var statements = PrepareAllSafe(db, string.Join(";", statementTexts.ToArray())) .ToList(); @@ -2796,12 +2808,12 @@ namespace Emby.Server.Implementations.Data } } - }, ReadTransactionMode); + LogQueryTime("GetItems", commandText, now); - LogQueryTime("GetItems", commandText, now); + result.Items = list.ToArray(); + return result; - result.Items = list.ToArray(); - return result; + }, ReadTransactionMode); } } } @@ -2962,34 +2974,38 @@ namespace Emby.Server.Implementations.Data } } - var list = new List(); - using (WriteLock.Read()) { using (var connection = CreateConnection(true)) { - using (var statement = PrepareStatementSafe(connection, commandText)) + return connection.RunInTransaction(db => { - if (EnableJoinUserData(query)) + var list = new List(); + + using (var statement = PrepareStatementSafe(db, commandText)) { - statement.TryBind("@UserId", query.User.Id); - } + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } - BindSimilarParams(query, statement); + BindSimilarParams(query, statement); - // Running this again will bind the params - GetWhereClauses(query, statement); + // Running this again will bind the params + GetWhereClauses(query, statement); - foreach (var row in statement.ExecuteQuery()) - { - list.Add(row[0].ReadGuid()); + foreach (var row in statement.ExecuteQuery()) + { + list.Add(row[0].ReadGuid()); + } } - } - } - LogQueryTime("GetItemList", commandText, now); + LogQueryTime("GetItemList", commandText, now); - return list; + return list; + + }, ReadTransactionMode); + } } } @@ -3153,10 +3169,10 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - var result = new QueryResult(); - - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var result = new QueryResult(); + var statements = PrepareAllSafe(db, string.Join(";", statementTexts.ToArray())) .ToList(); @@ -3199,12 +3215,12 @@ namespace Emby.Server.Implementations.Data } } - }, ReadTransactionMode); + LogQueryTime("GetItemIds", commandText, now); - LogQueryTime("GetItemIds", commandText, now); + result.Items = list.ToArray(); + return result; - result.Items = list.ToArray(); - return result; + }, ReadTransactionMode); } } } @@ -4653,13 +4669,13 @@ namespace Emby.Server.Implementations.Data commandText += " order by ListOrder"; - var list = new List(); using (WriteLock.Read()) { using (var connection = CreateConnection(true)) { - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var list = new List(); using (var statement = PrepareStatementSafe(db, commandText)) { // Run this again to bind the params @@ -4670,9 +4686,9 @@ namespace Emby.Server.Implementations.Data list.Add(row.GetString(0)); } } + return list; }, ReadTransactionMode); } - return list; } } @@ -4696,14 +4712,14 @@ namespace Emby.Server.Implementations.Data commandText += " order by ListOrder"; - var list = new List(); - using (WriteLock.Read()) { using (var connection = CreateConnection(true)) { - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var list = new List(); + using (var statement = PrepareStatementSafe(db, commandText)) { // Run this again to bind the params @@ -4714,11 +4730,11 @@ namespace Emby.Server.Implementations.Data list.Add(GetPerson(row)); } } + + return list; }, ReadTransactionMode); } } - - return list; } private List GetPeopleWhereClauses(InternalPeopleQuery query, IStatement statement) @@ -4899,8 +4915,6 @@ namespace Emby.Server.Implementations.Data ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) : ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture)).ToArray()) + ")"); - var list = new List(); - var commandText = "Select Value From ItemValues where " + typeClause; if (withItemTypes.Count > 0) @@ -4920,8 +4934,10 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var list = new List(); + using (var statement = PrepareStatementSafe(db, commandText)) { foreach (var row in statement.ExecuteQuery()) @@ -4932,12 +4948,13 @@ namespace Emby.Server.Implementations.Data } } } + + LogQueryTime("GetItemValueNames", commandText, now); + + return list; }, ReadTransactionMode); } } - LogQueryTime("GetItemValueNames", commandText, now); - - return list; } private QueryResult> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType) @@ -5081,9 +5098,6 @@ namespace Emby.Server.Implementations.Data var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - var list = new List>(); - var result = new QueryResult>(); - var statementTexts = new List(); if (!isReturningZeroItems) { @@ -5102,8 +5116,11 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var list = new List>(); + var result = new QueryResult>(); + var statements = PrepareAllSafe(db, string.Join(";", statementTexts.ToArray())).ToList(); if (!isReturningZeroItems) @@ -5167,17 +5184,18 @@ namespace Emby.Server.Implementations.Data LogQueryTime("GetItemValues", commandText, now); } } + + if (result.TotalRecordCount == 0) + { + result.TotalRecordCount = list.Count; + } + result.Items = list.ToArray(); + + return result; + }, ReadTransactionMode); } } - - if (result.TotalRecordCount == 0) - { - result.TotalRecordCount = list.Count; - } - result.Items = list.ToArray(); - - return result; } private ItemCounts GetItemCounts(IReadOnlyList reader, int countStartColumn, List typesToCount) @@ -5390,8 +5408,6 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException("query"); } - var list = new List(); - var cmdText = "select " + string.Join(",", _mediaStreamSaveColumns) + " from mediastreams where"; cmdText += " ItemId=@ItemId"; @@ -5412,8 +5428,10 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var list = new List(); + using (var statement = PrepareStatementSafe(db, cmdText)) { statement.TryBind("@ItemId", query.ItemId.ToGuidParamValue()); @@ -5433,11 +5451,12 @@ namespace Emby.Server.Implementations.Data list.Add(GetMediaStream(row)); } } + + return list; + }, ReadTransactionMode); } } - - return list; } public async Task SaveMediaStreams(Guid id, List streams, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 7afb5720e..7767ae892 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -300,9 +300,7 @@ namespace Emby.Server.Implementations.Data { using (var connection = CreateConnection(true)) { - UserItemData result = null; - - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { using (var statement = db.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where key =@Key and userId=@UserId")) { @@ -311,13 +309,13 @@ namespace Emby.Server.Implementations.Data foreach (var row in statement.ExecuteQuery()) { - result = ReadRow(row); - break; + return ReadRow(row); } } - }, ReadTransactionMode); - return result; + return null; + + }, ReadTransactionMode); } } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 2b2c3e000..d0c473777 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -459,12 +459,21 @@ namespace Emby.Server.Implementations.Dto if (dtoOptions.EnableUserData) { - dto.UserData = await _userDataRepository.GetUserDataDto(item, dto, user).ConfigureAwait(false); + dto.UserData = await _userDataRepository.GetUserDataDto(item, dto, user, dtoOptions.Fields).ConfigureAwait(false); } if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library) { - dto.ChildCount = GetChildCount(folder, user); + // For these types we can try to optimize and assume these values will be equal + if (item is MusicAlbum || item is Season) + { + dto.ChildCount = dto.RecursiveItemCount; + } + + if (dtoOptions.Fields.Contains(ItemFields.ChildCount)) + { + dto.ChildCount = dto.ChildCount ?? GetChildCount(folder, user); + } } if (fields.Contains(ItemFields.CumulativeRunTimeTicks)) @@ -1151,28 +1160,29 @@ namespace Emby.Server.Implementations.Dto { dto.Artists = hasArtist.Artists; - var artistItems = _libraryManager.GetArtists(new InternalItemsQuery - { - EnableTotalRecordCount = false, - ItemIds = new[] { item.Id.ToString("N") } - }); - - dto.ArtistItems = artistItems.Items - .Select(i => - { - var artist = i.Item1; - return new NameIdPair - { - Name = artist.Name, - Id = artist.Id.ToString("N") - }; - }) - .ToList(); + //var artistItems = _libraryManager.GetArtists(new InternalItemsQuery + //{ + // EnableTotalRecordCount = false, + // ItemIds = new[] { item.Id.ToString("N") } + //}); + + //dto.ArtistItems = artistItems.Items + // .Select(i => + // { + // var artist = i.Item1; + // return new NameIdPair + // { + // Name = artist.Name, + // Id = artist.Id.ToString("N") + // }; + // }) + // .ToList(); // Include artists that are not in the database yet, e.g., just added via metadata editor - var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); + //var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); + dto.ArtistItems = new List(); dto.ArtistItems.AddRange(hasArtist.Artists - .Except(foundArtists, new DistinctNameComparer()) + //.Except(foundArtists, new DistinctNameComparer()) .Select(i => { // This should not be necessary but we're seeing some cases of it @@ -1201,23 +1211,48 @@ namespace Emby.Server.Implementations.Dto { dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); - var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery - { - EnableTotalRecordCount = false, - ItemIds = new[] { item.Id.ToString("N") } - }); - - dto.AlbumArtists = artistItems.Items + //var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery + //{ + // EnableTotalRecordCount = false, + // ItemIds = new[] { item.Id.ToString("N") } + //}); + + //dto.AlbumArtists = artistItems.Items + // .Select(i => + // { + // var artist = i.Item1; + // return new NameIdPair + // { + // Name = artist.Name, + // Id = artist.Id.ToString("N") + // }; + // }) + // .ToList(); + + dto.AlbumArtists = new List(); + dto.AlbumArtists.AddRange(hasAlbumArtist.AlbumArtists + //.Except(foundArtists, new DistinctNameComparer()) .Select(i => { - var artist = i.Item1; - return new NameIdPair + // This should not be necessary but we're seeing some cases of it + if (string.IsNullOrWhiteSpace(i)) { - Name = artist.Name, - Id = artist.Id.ToString("N") - }; - }) - .ToList(); + return null; + } + + var artist = _libraryManager.GetArtist(i); + if (artist != null) + { + return new NameIdPair + { + Name = artist.Name, + Id = artist.Id.ToString("N") + }; + } + + return null; + + }).Where(i => i != null)); } // Add video info diff --git a/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs index cc2dcb6fd..5bb21d02a 100644 --- a/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs +++ b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs @@ -519,6 +519,12 @@ namespace Emby.Server.Implementations.FileOrganization private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result) { + // We should probably handle this earlier so that we never even make it this far + if (string.Equals(result.OriginalPath, result.TargetPath, StringComparison.OrdinalIgnoreCase)) + { + return; + } + _libraryMonitor.ReportFileSystemChangeBeginning(result.TargetPath); _fileSystem.CreateDirectory(Path.GetDirectoryName(result.TargetPath)); diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs index 94cc383a7..4606d0e31 100644 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs @@ -76,7 +76,8 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp private void ProcessContext(HttpListenerContext context) { - Task.Factory.StartNew(() => InitTask(context)); + //Task.Factory.StartNew(() => InitTask(context), TaskCreationOptions.DenyChildAttach | TaskCreationOptions.PreferFairness); + Task.Run(() => InitTask(context)); } private Task InitTask(HttpListenerContext context) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index ad91988e5..1ff61286f 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -817,7 +817,31 @@ namespace Emby.Server.Implementations.Library return _userRootFolder; } - + + public Guid? FindIdByPath(string path, bool? isFolder) + { + // If this returns multiple items it could be tricky figuring out which one is correct. + // In most cases, the newest one will be and the others obsolete but not yet cleaned up + + var query = new InternalItemsQuery + { + Path = path, + IsFolder = isFolder, + SortBy = new[] { ItemSortBy.DateCreated }, + SortOrder = SortOrder.Descending, + Limit = 1 + }; + + var id = GetItemIds(query); + + if (id.Count == 0) + { + return null; + } + + return id[0]; + } + public BaseItem FindByPath(string path, bool? isFolder) { // If this returns multiple items it could be tricky figuring out which one is correct. @@ -1430,7 +1454,7 @@ namespace Emby.Server.Implementations.Library })) { // Optimize by querying against top level views - query.TopParentIds = parents.SelectMany(i => GetTopParentsForQuery(i, query.User)).Select(i => i.Id.ToString("N")).ToArray(); + query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).Select(i => i.ToString("N")).ToArray(); query.AncestorIds = new string[] { }; } } @@ -1489,7 +1513,7 @@ namespace Emby.Server.Implementations.Library })) { // Optimize by querying against top level views - query.TopParentIds = parents.SelectMany(i => GetTopParentsForQuery(i, query.User)).Select(i => i.Id.ToString("N")).ToArray(); + query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).Select(i => i.ToString("N")).ToArray(); } else { @@ -1515,11 +1539,11 @@ namespace Emby.Server.Implementations.Library }, CancellationToken.None).Result.ToList(); - query.TopParentIds = userViews.SelectMany(i => GetTopParentsForQuery(i, user)).Select(i => i.Id.ToString("N")).ToArray(); + query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).Select(i => i.ToString("N")).ToArray(); } } - private IEnumerable GetTopParentsForQuery(BaseItem item, User user) + private IEnumerable GetTopParentIdsForQuery(BaseItem item, User user) { var view = item as UserView; @@ -1527,7 +1551,7 @@ namespace Emby.Server.Implementations.Library { if (string.Equals(view.ViewType, CollectionType.LiveTv)) { - return new[] { view }; + return new[] { view.Id }; } if (string.Equals(view.ViewType, CollectionType.Channels)) { @@ -1537,7 +1561,7 @@ namespace Emby.Server.Implementations.Library }, CancellationToken.None).Result; - return channelResult.Items; + return channelResult.Items.Select(i => i.Id); } // Translate view into folders @@ -1546,18 +1570,18 @@ namespace Emby.Server.Implementations.Library var displayParent = GetItemById(view.DisplayParentId); if (displayParent != null) { - return GetTopParentsForQuery(displayParent, user); + return GetTopParentIdsForQuery(displayParent, user); } - return new BaseItem[] { }; + return new Guid[] { }; } if (view.ParentId != Guid.Empty) { var displayParent = GetItemById(view.ParentId); if (displayParent != null) { - return GetTopParentsForQuery(displayParent, user); + return GetTopParentIdsForQuery(displayParent, user); } - return new BaseItem[] { }; + return new Guid[] { }; } // Handle grouping @@ -1568,23 +1592,23 @@ namespace Emby.Server.Implementations.Library .OfType() .Where(i => string.IsNullOrWhiteSpace(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase)) .Where(i => user.IsFolderGrouped(i.Id)) - .SelectMany(i => GetTopParentsForQuery(i, user)); + .SelectMany(i => GetTopParentIdsForQuery(i, user)); } - return new BaseItem[] { }; + return new Guid[] { }; } var collectionFolder = item as CollectionFolder; if (collectionFolder != null) { - return collectionFolder.GetPhysicalParents(); + return collectionFolder.PhysicalFolderIds; } - + var topParent = item.GetTopParent(); if (topParent != null) { - return new[] { topParent }; + return new[] { topParent.Id }; } - return new BaseItem[] { }; + return new Guid[] { }; } /// diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index f4a30fc00..5a14edf13 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -12,6 +12,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Library { @@ -186,16 +187,16 @@ namespace Emby.Server.Implementations.Library var userData = GetUserData(user.Id, item); var dto = GetUserItemDataDto(userData); - await item.FillUserDataDtoValues(dto, userData, null, user).ConfigureAwait(false); + await item.FillUserDataDtoValues(dto, userData, null, user, new List()).ConfigureAwait(false); return dto; } - public async Task GetUserDataDto(IHasUserData item, BaseItemDto itemDto, User user) + public async Task GetUserDataDto(IHasUserData item, BaseItemDto itemDto, User user, List fields) { var userData = GetUserData(user.Id, item); var dto = GetUserItemDataDto(userData); - await item.FillUserDataDtoValues(dto, userData, itemDto, user).ConfigureAwait(false); + await item.FillUserDataDtoValues(dto, userData, itemDto, user, fields).ConfigureAwait(false); return dto; } diff --git a/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs b/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs index 43e19da65..f18278cb2 100644 --- a/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs +++ b/Emby.Server.Implementations/Notifications/SqliteNotificationsRepository.cs @@ -65,9 +65,9 @@ namespace Emby.Server.Implementations.Notifications var whereClause = " where " + string.Join(" And ", clauses.ToArray()); - using (var connection = CreateConnection(true)) + using (WriteLock.Read()) { - lock (WriteLock) + using (var connection = CreateConnection(true)) { result.TotalRecordCount = connection.Query("select count(Id) from Notifications" + whereClause, paramList.ToArray()).SelectScalarInt().First(); @@ -106,9 +106,9 @@ namespace Emby.Server.Implementations.Notifications { var result = new NotificationsSummary(); - using (var connection = CreateConnection(true)) + using (WriteLock.Read()) { - lock (WriteLock) + using (var connection = CreateConnection(true)) { using (var statement = connection.PrepareStatement("select Level from Notifications where UserId=@UserId and IsRead=@IsRead")) { @@ -223,9 +223,9 @@ namespace Emby.Server.Implementations.Notifications cancellationToken.ThrowIfCancellationRequested(); - using (var connection = CreateConnection()) + lock (WriteLock) { - lock (WriteLock) + using (var connection = CreateConnection()) { connection.RunInTransaction(conn => { @@ -286,9 +286,9 @@ namespace Emby.Server.Implementations.Notifications { cancellationToken.ThrowIfCancellationRequested(); - using (var connection = CreateConnection()) + using (WriteLock.Write()) { - lock (WriteLock) + using (var connection = CreateConnection()) { connection.RunInTransaction(conn => { @@ -308,9 +308,9 @@ namespace Emby.Server.Implementations.Notifications { cancellationToken.ThrowIfCancellationRequested(); - using (var connection = CreateConnection()) + using (WriteLock.Write()) { - lock (WriteLock) + using (var connection = CreateConnection()) { connection.RunInTransaction(conn => { diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index 392db6935..7199f4f4d 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -206,10 +206,10 @@ namespace Emby.Server.Implementations.Security { using (var connection = CreateConnection(true)) { - var result = new QueryResult(); - - connection.RunInTransaction(db => + return connection.RunInTransaction(db => { + var result = new QueryResult(); + var statementTexts = new List(); statementTexts.Add(commandText); statementTexts.Add("select count (Id) from AccessTokens" + whereTextWithoutPaging); @@ -236,10 +236,10 @@ namespace Emby.Server.Implementations.Security } } - }, ReadTransactionMode); + result.Items = list.ToArray(); + return result; - result.Items = list.ToArray(); - return result; + }, ReadTransactionMode); } } } diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index 73a2bedb9..7a8951396 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -121,7 +121,9 @@ namespace MediaBrowser.Api { var options = new DtoOptions(); - options.DeviceId = authContext.GetAuthorizationInfo(Request).DeviceId; + var authInfo = authContext.GetAuthorizationInfo(Request); + + options.DeviceId = authInfo.DeviceId; var hasFields = request as IHasItemFields; if (hasFields != null) @@ -129,6 +131,34 @@ namespace MediaBrowser.Api options.Fields = hasFields.GetItemFields().ToList(); } + var client = authInfo.Client ?? string.Empty; + if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1) + { + options.Fields.Add(Model.Querying.ItemFields.RecursiveItemCount); + } + + if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("wmc", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("roku", StringComparison.OrdinalIgnoreCase) != -1 || + client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1) + { + options.Fields.Add(Model.Querying.ItemFields.ChildCount); + } + + if (client.IndexOf("web", StringComparison.OrdinalIgnoreCase) == -1 && + client.IndexOf("mobile", StringComparison.OrdinalIgnoreCase) == -1 && + client.IndexOf("ios", StringComparison.OrdinalIgnoreCase) == -1 && + client.IndexOf("android", StringComparison.OrdinalIgnoreCase) == -1 && + client.IndexOf("theater", StringComparison.OrdinalIgnoreCase) == -1) + { + options.Fields.Add(Model.Querying.ItemFields.ChildCount); + } + var hasDtoOptions = request as IHasDtoOptions; if (hasDtoOptions != null) { diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs index 0ae1fbff4..b5c51bfef 100644 --- a/MediaBrowser.Api/ItemLookupService.cs +++ b/MediaBrowser.Api/ItemLookupService.cs @@ -14,8 +14,6 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.IO; -using MediaBrowser.Controller.IO; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; @@ -88,6 +86,12 @@ namespace MediaBrowser.Api { } + [Route("/Items/RemoteSearch/Book", "POST")] + [Authenticated] + public class GetBookRemoteSearchResults : RemoteSearchQuery, IReturn> + { + } + [Route("/Items/RemoteSearch/Image", "GET", Summary = "Gets a remote image")] public class GetRemoteSearchImage { @@ -147,6 +151,13 @@ namespace MediaBrowser.Api return ToOptimizedResult(result); } + public async Task Post(GetBookRemoteSearchResults request) + { + var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); + + return ToOptimizedResult(result); + } + public async Task Post(GetMovieRemoteSearchResults request) { var result = await _providerManager.GetRemoteSearchResults(request, CancellationToken.None).ConfigureAwait(false); diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 9f4503466..2aa53d651 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -30,6 +30,7 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Providers; +using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities @@ -2191,7 +2192,7 @@ namespace MediaBrowser.Controller.Entities return path; } - public virtual Task FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user) + public virtual Task FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, List itemFields) { if (RunTimeTicks.HasValue) { diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index ebc55ca8a..c505aefb3 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -27,6 +27,7 @@ namespace MediaBrowser.Controller.Entities public CollectionFolder() { PhysicalLocationsList = new List(); + PhysicalFolderIds = new List(); } [IgnoreDataMember] @@ -153,6 +154,7 @@ namespace MediaBrowser.Controller.Entities } public List PhysicalLocationsList { get; set; } + public List PhysicalFolderIds { get; set; } protected override IEnumerable GetFileSystemChildren(IDirectoryService directoryService) { @@ -176,6 +178,18 @@ namespace MediaBrowser.Controller.Entities } } + if (!changed) + { + var folderIds = PhysicalFolderIds.ToList(); + + var newFolderIds = GetPhysicalFolders(false).Select(i => i.Id).ToList(); + + if (!folderIds.SequenceEqual(newFolderIds)) + { + changed = true; + } + } + return changed; } @@ -186,6 +200,31 @@ namespace MediaBrowser.Controller.Entities return changed; } + protected override bool RefreshLinkedChildren(IEnumerable fileSystemChildren) + { + var physicalFolders = GetPhysicalFolders(false) + .ToList(); + + var linkedChildren = physicalFolders + .SelectMany(c => c.LinkedChildren) + .ToList(); + + var changed = !linkedChildren.SequenceEqual(LinkedChildren, new LinkedChildComparer()); + + LinkedChildren = linkedChildren; + + var folderIds = PhysicalFolderIds.ToList(); + var newFolderIds = physicalFolders.Select(i => i.Id).ToList(); + + if (!folderIds.SequenceEqual(newFolderIds)) + { + changed = true; + PhysicalFolderIds = newFolderIds.ToList(); + } + + return changed; + } + internal override bool IsValidFromResolver(BaseItem newItem) { var newCollectionFolder = newItem as CollectionFolder; @@ -260,26 +299,6 @@ namespace MediaBrowser.Controller.Entities return Task.FromResult(true); } - /// - /// Our children are actually just references to the ones in the physical root... - /// - /// The linked children. - [IgnoreDataMember] - public override List LinkedChildren - { - get { return GetLinkedChildrenInternal(); } - set - { - base.LinkedChildren = value; - } - } - private List GetLinkedChildrenInternal() - { - return GetPhysicalParents() - .SelectMany(c => c.LinkedChildren) - .ToList(); - } - /// /// Our children are actually just references to the ones in the physical root... /// @@ -292,11 +311,16 @@ namespace MediaBrowser.Controller.Entities private IEnumerable GetActualChildren() { - return GetPhysicalParents().SelectMany(c => c.Children); + return GetPhysicalFolders(true).SelectMany(c => c.Children); } - public IEnumerable GetPhysicalParents() + private IEnumerable GetPhysicalFolders(bool enableCache) { + if (enableCache) + { + return PhysicalFolderIds.Select(i => LibraryManager.GetItemById(i)).OfType(); + } + var rootChildren = LibraryManager.RootFolder.Children .OfType() .ToList(); diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 4705f03fa..a84e9a5d2 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1222,7 +1222,7 @@ namespace MediaBrowser.Controller.Entities /// Refreshes the linked children. /// /// true if XXXX, false otherwise - private bool RefreshLinkedChildren(IEnumerable fileSystemChildren) + protected virtual bool RefreshLinkedChildren(IEnumerable fileSystemChildren) { var currentManualLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Manual).ToList(); var currentShortcutLinks = LinkedChildren.Where(i => i.Type == LinkedChildType.Shortcut).ToList(); @@ -1410,23 +1410,24 @@ namespace MediaBrowser.Controller.Entities } } - public override async Task FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user) + public override async Task FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, List itemFields) { if (!SupportsUserDataFromChildren) { return; } - var recursiveItemCount = GetRecursiveChildCount(user); - if (itemDto != null) { - itemDto.RecursiveItemCount = recursiveItemCount; + if (itemFields.Contains(ItemFields.RecursiveItemCount)) + { + itemDto.RecursiveItemCount = GetRecursiveChildCount(user); + } } - if (recursiveItemCount > 0 && SupportsPlayedStatus) + if (SupportsPlayedStatus) { - var unplayedQueryResult = recursiveItemCount > 0 ? await GetItems(new InternalItemsQuery(user) + var unplayedQueryResult = await GetItems(new InternalItemsQuery(user) { Recursive = true, IsFolder = false, @@ -1435,21 +1436,24 @@ namespace MediaBrowser.Controller.Entities Limit = 0, IsPlayed = false - }).ConfigureAwait(false) : new QueryResult(); + }).ConfigureAwait(false); double unplayedCount = unplayedQueryResult.TotalRecordCount; - var unplayedPercentage = (unplayedCount / recursiveItemCount) * 100; - dto.PlayedPercentage = 100 - unplayedPercentage; - dto.Played = dto.PlayedPercentage.Value >= 100; dto.UnplayedItemCount = unplayedQueryResult.TotalRecordCount; - } - if (itemDto != null) - { - if (this is Season || this is MusicAlbum) + if (itemDto != null && itemDto.RecursiveItemCount.HasValue) + { + if (itemDto.RecursiveItemCount.Value > 0) + { + var unplayedPercentage = (unplayedCount/itemDto.RecursiveItemCount.Value)*100; + dto.PlayedPercentage = 100 - unplayedPercentage; + dto.Played = dto.PlayedPercentage.Value >= 100; + } + } + else { - itemDto.ChildCount = recursiveItemCount; + dto.Played = (dto.UnplayedItemCount ?? 0) == 0; } } } diff --git a/MediaBrowser.Controller/Entities/IHasUserData.cs b/MediaBrowser.Controller/Entities/IHasUserData.cs index c21e170ae..0b3b7dc8d 100644 --- a/MediaBrowser.Controller/Entities/IHasUserData.cs +++ b/MediaBrowser.Controller/Entities/IHasUserData.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Entities { @@ -14,10 +15,7 @@ namespace MediaBrowser.Controller.Entities /// /// Fills the user data dto values. /// - /// The dto. - /// The user data. - /// The user. - Task FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user); + Task FillUserDataDtoValues(UserItemDataDto dto, UserItemData userData, BaseItemDto itemDto, User user, List fields); bool EnableRememberingTrackSelections { get; } diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index d297fd006..bf9a07626 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -62,6 +62,8 @@ namespace MediaBrowser.Controller.Library /// BaseItem. BaseItem FindByPath(string path, bool? isFolder); + Guid? FindIdByPath(string path, bool? isFolder); + /// /// Gets the artist. /// diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index 86c52c4c3..5940c7e29 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -5,6 +5,7 @@ using MediaBrowser.Model.Entities; using System; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Querying; namespace MediaBrowser.Controller.Library { @@ -37,12 +38,9 @@ namespace MediaBrowser.Controller.Library /// /// Gets the user data dto. /// - /// The item. - /// The user. - /// UserItemDataDto. Task GetUserDataDto(IHasUserData item, User user); - Task GetUserDataDto(IHasUserData item, BaseItemDto itemDto, User user); + Task GetUserDataDto(IHasUserData item, BaseItemDto itemDto, User user, List fields); /// /// Get all user data for the given user diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index bf1d4991c..e36abf16e 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -171,6 +171,8 @@ /// PrimaryImageAspectRatio, + RecursiveItemCount, + /// /// The revenue /// diff --git a/RSSDP/SsdpDevicePublisherBase.cs b/RSSDP/SsdpDevicePublisherBase.cs index 260c00896..8ab35d661 100644 --- a/RSSDP/SsdpDevicePublisherBase.cs +++ b/RSSDP/SsdpDevicePublisherBase.cs @@ -291,7 +291,7 @@ namespace Rssdp.Infrastructure if (devices != null) { var deviceList = devices.ToList(); - WriteTrace(String.Format("Sending {0} search responses", deviceList.Count)); + //WriteTrace(String.Format("Sending {0} search responses", deviceList.Count)); foreach (var device in deviceList) { @@ -300,7 +300,7 @@ namespace Rssdp.Infrastructure } else { - WriteTrace(String.Format("Sending 0 search responses.")); + //WriteTrace(String.Format("Sending 0 search responses.")); } }); } @@ -413,7 +413,7 @@ namespace Rssdp.Infrastructure //DisposeRebroadcastTimer(); - WriteTrace("Begin Sending Alive Notifications For All Devices"); + //WriteTrace("Begin Sending Alive Notifications For All Devices"); _LastNotificationTime = DateTime.Now; @@ -430,7 +430,7 @@ namespace Rssdp.Infrastructure SendAliveNotifications(device, true); } - WriteTrace("Completed Sending Alive Notifications For All Devices"); + //WriteTrace("Completed Sending Alive Notifications For All Devices"); } catch (ObjectDisposedException ex) { -- cgit v1.2.3 From 524e7facc87e746745af9095a3f100dcec1799b6 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Tue, 13 Dec 2016 18:38:26 -0500 Subject: fix socket errors on linux under .net core --- Emby.Common.Implementations/Net/SocketFactory.cs | 12 ++--- .../Networking/NetworkManager.cs | 2 +- .../HttpServer/SocketSharp/SharpWebSocket.cs | 12 +---- MediaBrowser.Api/BaseApiService.cs | 3 +- SocketHttpListener.Portable/WebSocket.cs | 55 +++++++++------------- src/Emby.Server/Program.cs | 9 ++-- 6 files changed, 39 insertions(+), 54 deletions(-) (limited to 'Emby.Server.Implementations/HttpServer') diff --git a/Emby.Common.Implementations/Net/SocketFactory.cs b/Emby.Common.Implementations/Net/SocketFactory.cs index 1f41ffff5..70c7ba845 100644 --- a/Emby.Common.Implementations/Net/SocketFactory.cs +++ b/Emby.Common.Implementations/Net/SocketFactory.cs @@ -125,15 +125,15 @@ namespace Emby.Common.Implementations.Net try { -#if NETSTANDARD1_3 - // The ExclusiveAddressUse socket option is a Windows-specific option that, when set to "true," tells Windows not to allow another socket to use the same local address as this socket - // See https://github.com/dotnet/corefx/pull/11509 for more details - if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) +#if NET46 + retVal.ExclusiveAddressUse = false; +#else + // The ExclusiveAddressUse socket option is a Windows-specific option that, when set to "true," tells Windows not to allow another socket to use the same local address as this socket + // See https://github.com/dotnet/corefx/pull/11509 for more details + if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) { retVal.ExclusiveAddressUse = false; } -#else - retVal.ExclusiveAddressUse = false; #endif //retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); diff --git a/Emby.Common.Implementations/Networking/NetworkManager.cs b/Emby.Common.Implementations/Networking/NetworkManager.cs index a4e6d47d4..4485e8b14 100644 --- a/Emby.Common.Implementations/Networking/NetworkManager.cs +++ b/Emby.Common.Implementations/Networking/NetworkManager.cs @@ -245,7 +245,7 @@ namespace Emby.Common.Implementations.Networking //} return ipProperties.UnicastAddresses - .Where(i => i.IsDnsEligible) + //.Where(i => i.IsDnsEligible) .Select(i => i.Address) .Where(i => i.AddressFamily == AddressFamily.InterNetwork) .ToList(); diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs index 0a312f7b9..9823a2ff5 100644 --- a/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs @@ -102,11 +102,7 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp /// Task. public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken) { - var completionSource = new TaskCompletionSource(); - - WebSocket.SendAsync(bytes, res => completionSource.TrySetResult(true)); - - return completionSource.Task; + return WebSocket.SendAsync(bytes); } /// @@ -118,11 +114,7 @@ namespace Emby.Server.Implementations.HttpServer.SocketSharp /// Task. public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken) { - var completionSource = new TaskCompletionSource(); - - WebSocket.SendAsync(text, res => completionSource.TrySetResult(true)); - - return completionSource.Task; + return WebSocket.SendAsync(text); } /// diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index 7a8951396..07ec6d955 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -151,9 +151,10 @@ namespace MediaBrowser.Api } if (client.IndexOf("web", StringComparison.OrdinalIgnoreCase) == -1 && + + // covers both emby mobile and emby for android mobile client.IndexOf("mobile", StringComparison.OrdinalIgnoreCase) == -1 && client.IndexOf("ios", StringComparison.OrdinalIgnoreCase) == -1 && - client.IndexOf("android", StringComparison.OrdinalIgnoreCase) == -1 && client.IndexOf("theater", StringComparison.OrdinalIgnoreCase) == -1) { options.Fields.Add(Model.Querying.ItemFields.ChildCount); diff --git a/SocketHttpListener.Portable/WebSocket.cs b/SocketHttpListener.Portable/WebSocket.cs index 889880387..9966d3fcf 100644 --- a/SocketHttpListener.Portable/WebSocket.cs +++ b/SocketHttpListener.Portable/WebSocket.cs @@ -5,6 +5,7 @@ using System.IO; using System.Net; using System.Text; using System.Threading; +using System.Threading.Tasks; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.IO; using SocketHttpListener.Net.WebSockets; @@ -621,26 +622,22 @@ namespace SocketHttpListener } } - private void sendAsync(Opcode opcode, Stream stream, Action completed) + private Task sendAsync(Opcode opcode, Stream stream) { - Func sender = send; - sender.BeginInvoke( - opcode, - stream, - ar => - { - try - { - var sent = sender.EndInvoke(ar); - if (completed != null) - completed(sent); - } - catch (Exception ex) - { - error("An exception has occurred while callback.", ex); - } - }, - null); + var completionSource = new TaskCompletionSource(); + Task.Run(() => + { + try + { + send(opcode, stream); + completionSource.TrySetResult(true); + } + catch (Exception ex) + { + completionSource.TrySetException(ex); + } + }); + return completionSource.Task; } // As server @@ -833,22 +830,18 @@ namespace SocketHttpListener /// /// An array of that represents the binary data to send. /// - /// /// An Action<bool> delegate that references the method(s) called when the send is /// complete. A passed to this delegate is true if the send is /// complete successfully; otherwise, false. - /// - public void SendAsync(byte[] data, Action completed) + public Task SendAsync(byte[] data) { var msg = _readyState.CheckIfOpen() ?? data.CheckIfValidSendData(); if (msg != null) { - error(msg); - - return; + throw new Exception(msg); } - sendAsync(Opcode.Binary, _memoryStreamFactory.CreateNew(data), completed); + return sendAsync(Opcode.Binary, _memoryStreamFactory.CreateNew(data)); } /// @@ -860,22 +853,18 @@ namespace SocketHttpListener /// /// A that represents the text data to send. /// - /// /// An Action<bool> delegate that references the method(s) called when the send is /// complete. A passed to this delegate is true if the send is /// complete successfully; otherwise, false. - /// - public void SendAsync(string data, Action completed) + public Task SendAsync(string data) { var msg = _readyState.CheckIfOpen() ?? data.CheckIfValidSendData(); if (msg != null) { - error(msg); - - return; + throw new Exception(msg); } - sendAsync(Opcode.Text, _memoryStreamFactory.CreateNew(Encoding.UTF8.GetBytes(data)), completed); + return sendAsync(Opcode.Text, _memoryStreamFactory.CreateNew(Encoding.UTF8.GetBytes(data))); } #endregion diff --git a/src/Emby.Server/Program.cs b/src/Emby.Server/Program.cs index 24e391c73..26141a0ce 100644 --- a/src/Emby.Server/Program.cs +++ b/src/Emby.Server/Program.cs @@ -215,9 +215,12 @@ namespace Emby.Server var initProgress = new Progress(); - // Not crazy about this but it's the only way to suppress ffmpeg crash dialog boxes - SetErrorMode(ErrorModes.SEM_FAILCRITICALERRORS | ErrorModes.SEM_NOALIGNMENTFAULTEXCEPT | - ErrorModes.SEM_NOGPFAULTERRORBOX | ErrorModes.SEM_NOOPENFILEERRORBOX); + if (environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows) + { + // Not crazy about this but it's the only way to suppress ffmpeg crash dialog boxes + SetErrorMode(ErrorModes.SEM_FAILCRITICALERRORS | ErrorModes.SEM_NOALIGNMENTFAULTEXCEPT | + ErrorModes.SEM_NOGPFAULTERRORBOX | ErrorModes.SEM_NOOPENFILEERRORBOX); + } var task = _appHost.Init(initProgress); Task.WaitAll(task); -- cgit v1.2.3