aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/HttpServer
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/HttpServer')
-rw-r--r--Emby.Server.Implementations/HttpServer/IHttpListener.cs46
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs246
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs195
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/SessionContext.cs67
-rw-r--r--Emby.Server.Implementations/HttpServer/StreamWriter.cs127
5 files changed, 681 insertions, 0 deletions
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
+ {
+ /// <summary>
+ /// Gets or sets the error handler.
+ /// </summary>
+ /// <value>The error handler.</value>
+ Action<Exception, IRequest> ErrorHandler { get; set; }
+
+ /// <summary>
+ /// Gets or sets the request handler.
+ /// </summary>
+ /// <value>The request handler.</value>
+ Func<IHttpRequest, Uri, Task> RequestHandler { get; set; }
+
+ /// <summary>
+ /// Gets or sets the web socket handler.
+ /// </summary>
+ /// <value>The web socket handler.</value>
+ Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
+
+ /// <summary>
+ /// Gets or sets the web socket connecting.
+ /// </summary>
+ /// <value>The web socket connecting.</value>
+ Action<WebSocketConnectingEventArgs> WebSocketConnecting { get; set; }
+
+ /// <summary>
+ /// Starts this instance.
+ /// </summary>
+ /// <param name="urlPrefixes">The URL prefixes.</param>
+ void Start(IEnumerable<string> urlPrefixes);
+
+ /// <summary>
+ /// Stops this instance.
+ /// </summary>
+ 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; }
+
+ /// <summary>
+ /// Redirect the client to a specific URL if authentication failed.
+ /// If this property is null, simply `401 Unauthorized` is returned.
+ /// </summary>
+ 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<string> 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);
+ }
+
+ /// <summary>
+ /// Gets the authorization.
+ /// </summary>
+ /// <param name="httpReq">The HTTP req.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ 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;
+ }
+
+ /// <summary>
+ /// Gets the auth.
+ /// </summary>
+ /// <param name="httpReq">The HTTP req.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ private Dictionary<string, string> GetAuthorizationDictionary(IServiceRequest httpReq)
+ {
+ var auth = httpReq.Headers["X-Emby-Authorization"];
+
+ if (string.IsNullOrWhiteSpace(auth))
+ {
+ auth = httpReq.Headers["Authorization"];
+ }
+
+ return GetAuthorization(auth);
+ }
+
+ /// <summary>
+ /// Gets the authorization.
+ /// </summary>
+ /// <param name="authorizationHeader">The authorization header.</param>
+ /// <returns>Dictionary{System.StringSystem.String}.</returns>
+ private Dictionary<string, string> 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<string, string>(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<SessionInfo> 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<SessionInfo> GetSession(object requestContext)
+ {
+ var req = new ServiceRequest((IRequest)requestContext);
+ return GetSession(req);
+ }
+
+ public async Task<User> GetUser(IServiceRequest requestContext)
+ {
+ var session = await GetSession(requestContext).ConfigureAwait(false);
+
+ return session == null || !session.UserId.HasValue ? null : _userManager.GetUserById(session.UserId.Value);
+ }
+
+ public Task<User> 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
+{
+ /// <summary>
+ /// Class StreamWriter
+ /// </summary>
+ public class StreamWriter : IAsyncStreamWriter, IHasHeaders
+ {
+ private ILogger Logger { get; set; }
+
+ private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
+
+ /// <summary>
+ /// Gets or sets the source stream.
+ /// </summary>
+ /// <value>The source stream.</value>
+ private Stream SourceStream { get; set; }
+
+ /// <summary>
+ /// The _options
+ /// </summary>
+ private readonly IDictionary<string, string> _options = new Dictionary<string, string>();
+ /// <summary>
+ /// Gets the options.
+ /// </summary>
+ /// <value>The options.</value>
+ public IDictionary<string, string> Headers
+ {
+ get { return _options; }
+ }
+
+ public Action OnComplete { get; set; }
+ public Action OnError { get; set; }
+ private readonly byte[] _bytes;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StreamWriter" /> class.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="contentType">Type of the content.</param>
+ /// <param name="logger">The logger.</param>
+ 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);
+ }
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="StreamWriter"/> class.
+ /// </summary>
+ /// <param name="source">The source.</param>
+ /// <param name="contentType">Type of the content.</param>
+ /// <param name="logger">The logger.</param>
+ 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();
+ }
+ }
+ }
+ }
+}