diff options
| author | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-07-02 00:57:18 -0400 |
|---|---|---|
| committer | Luke Pulverenti <luke.pulverenti@gmail.com> | 2014-07-02 00:57:18 -0400 |
| commit | 389390b82ecfbb48e0486f8f132046ddf8624e00 (patch) | |
| tree | c03ffa22f3a2fe668bb9be7078ad83fea3177796 /MediaBrowser.Server.Implementations/HttpServer | |
| parent | 3bef6ead9cec4c33d43b6348ae4fc33c9b70316a (diff) | |
fixes #789 - Security Issue: API allows access to any folder of the PC running MediaBrowser
Diffstat (limited to 'MediaBrowser.Server.Implementations/HttpServer')
6 files changed, 302 insertions, 14 deletions
diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs index 0fc9265f6..833dfc5e4 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -1,13 +1,13 @@ -using System.Net.Sockets; -using System.Runtime.Serialization; -using Funq; +using Funq; using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Logging; +using MediaBrowser.Server.Implementations.HttpServer.Security; using ServiceStack; using ServiceStack.Api.Swagger; +using ServiceStack.Auth; using ServiceStack.Host; using ServiceStack.Host.Handlers; using ServiceStack.Host.HttpListener; @@ -27,7 +27,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer { public class HttpListenerHost : ServiceStackHost, IHttpServer { - private string ServerName { get; set; } private string HandlerPath { get; set; } private string DefaultRedirectPath { get; set; } @@ -59,7 +58,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer : base(serviceName, assembliesWithServices) { DefaultRedirectPath = defaultRedirectPath; - ServerName = serviceName; HandlerPath = handlerPath; _logger = logManager.GetLogger("HttpServer"); @@ -95,7 +93,12 @@ namespace MediaBrowser.Server.Implementations.HttpServer container.Adapter = _containerAdapter; Plugins.Add(new SwaggerFeature()); - Plugins.Add(new CorsFeature(allowedHeaders: "Content-Type, Authorization")); + Plugins.Add(new CorsFeature(allowedHeaders: "Content-Type, Authorization")); + + Plugins.Add(new AuthFeature(() => new AuthUserSession(), new IAuthProvider[] { + new SessionAuthProvider(_containerAdapter.Resolve<ISessionContext>()), + })); + HostContext.GlobalResponseFilters.Add(new ResponseFilter(_logger).FilterResponse); } @@ -112,7 +115,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer Config.HandlerFactoryPath = string.IsNullOrEmpty(HandlerPath) ? null - : HandlerPath; + : "/" + HandlerPath; Config.MetadataRedirectPath = string.IsNullOrEmpty(HandlerPath) ? "metadata" @@ -161,8 +164,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer if (Listener == null) Listener = new HttpListener(); - HostContext.Config.HandlerFactoryPath = ListenerRequest.GetHandlerPathIfAny(UrlPrefixes.First()); - foreach (var prefix in UrlPrefixes) { _logger.Info("Adding HttpListener prefix " + prefix); @@ -172,6 +173,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer IsStarted = true; _logger.Info("Starting HttpListner"); Listener.Start(); + _logger.Info("HttpListener started"); for (var i = 0; i < _autoResetEvents.Count; i++) { @@ -263,27 +265,27 @@ namespace MediaBrowser.Server.Implementations.HttpServer var localPath = request.Url.LocalPath; - if (string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(localPath, "/" + HandlerPath + "/", StringComparison.OrdinalIgnoreCase)) { context.Response.Redirect(DefaultRedirectPath); context.Response.Close(); return; } - if (string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(localPath, "/" + HandlerPath, StringComparison.OrdinalIgnoreCase)) { - context.Response.Redirect("mediabrowser/" + DefaultRedirectPath); + context.Response.Redirect(HandlerPath + "/" + DefaultRedirectPath); context.Response.Close(); return; } if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)) { - context.Response.Redirect("mediabrowser/" + DefaultRedirectPath); + context.Response.Redirect(HandlerPath + "/" + DefaultRedirectPath); context.Response.Close(); return; } if (string.IsNullOrEmpty(localPath)) { - context.Response.Redirect("/mediabrowser/" + DefaultRedirectPath); + context.Response.Redirect("/" + HandlerPath + "/" + DefaultRedirectPath); context.Response.Close(); return; } @@ -410,6 +412,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer { var req = new ListenerRequest(httpContext, operationName, RequestAttributes.None); req.RequestAttributes = req.GetAttributes(); + return req; } @@ -442,7 +445,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer var httpReq = GetRequest(context, operationName); var httpRes = httpReq.Response; + //var pathInfo = httpReq.PathInfo; + var handler = HttpHandlerFactory.GetHandler(httpReq); + //var handler = HttpHandlerFactory.GetHandlerForPathInfo(httpReq.HttpMethod, pathInfo, pathInfo, httpReq.GetPhysicalPath()); var serviceStackHandler = handler as IServiceStackHandler; if (serviceStackHandler != null) diff --git a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs index 1ff199eb4..9de6972f8 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -228,5 +228,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer } public string StatusDescription { get; set; } + + public int PaddingLength { get; set; } } }
\ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs new file mode 100644 index 000000000..ddb583f5d --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs @@ -0,0 +1,113 @@ +using MediaBrowser.Controller.Net; +using ServiceStack; +using ServiceStack.Auth; +using ServiceStack.Web; +using System; +using System.Collections.Specialized; +using System.Linq; + +namespace MediaBrowser.Server.Implementations.HttpServer.Security +{ + public class AuthService : IAuthService + { + /// <summary> + /// Restrict authentication to a specific <see cref="IAuthProvider"/>. + /// For example, if this attribute should only permit access + /// if the user is authenticated with <see cref="BasicAuthProvider"/>, + /// you should set this property to <see cref="BasicAuthProvider.Name"/>. + /// </summary> + public string Provider { get; 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(IRequest req, IResponse res, object requestDto) + { + if (HostContext.HasValidAuthSecret(req)) + return; + + ExecuteBasic(req, res, requestDto); //first check if session is authenticated + if (res.IsClosed) return; //AuthenticateAttribute already closed the request (ie auth failed) + + ValidateUser(req); + } + + private void ValidateUser(IRequest req) + { + var user = req.TryResolve<ISessionContext>().GetUser(req); + + if (user == null || user.Configuration.IsDisabled) + { + throw new UnauthorizedAccessException("Unauthorized access."); + } + } + + private void ExecuteBasic(IRequest req, IResponse res, object requestDto) + { + if (AuthenticateService.AuthProviders == null) + throw new InvalidOperationException( + "The AuthService must be initialized by calling AuthService.Init to use an authenticate attribute"); + + var matchingOAuthConfigs = AuthenticateService.AuthProviders.Where(x => + this.Provider.IsNullOrEmpty() + || x.Provider == this.Provider).ToList(); + + if (matchingOAuthConfigs.Count == 0) + { + res.WriteError(req, requestDto, "No OAuth Configs found matching {0} provider" + .Fmt(this.Provider ?? "any")); + res.EndRequest(); + } + + matchingOAuthConfigs.OfType<IAuthWithRequest>() + .Each(x => x.PreAuthenticate(req, res)); + + var session = req.GetSession(); + if (session == null || !matchingOAuthConfigs.Any(x => session.IsAuthorized(x.Provider))) + { + if (this.DoHtmlRedirectIfConfigured(req, res, true)) return; + + AuthProvider.HandleFailedAuth(matchingOAuthConfigs[0], session, req, res); + } + } + + protected bool DoHtmlRedirectIfConfigured(IRequest req, IResponse res, bool includeRedirectParam = false) + { + var htmlRedirect = this.HtmlRedirect ?? AuthenticateService.HtmlRedirect; + if (htmlRedirect != null && req.ResponseContentType.MatchesContentType(MimeTypes.Html)) + { + DoHtmlRedirect(htmlRedirect, req, res, includeRedirectParam); + return true; + } + return false; + } + + public static void DoHtmlRedirect(string redirectUrl, IRequest req, IResponse res, bool includeRedirectParam) + { + var url = req.ResolveAbsoluteUrl(redirectUrl); + if (includeRedirectParam) + { + var absoluteRequestPath = req.ResolveAbsoluteUrl("~" + req.PathInfo + ToQueryString(req.QueryString)); + url = url.AddQueryParam(HostContext.ResolveLocalizedString(LocalizedStrings.Redirect), absoluteRequestPath); + } + + res.RedirectToUrl(url); + } + + private static string ToQueryString(INameValueCollection queryStringCollection) + { + return ToQueryString((NameValueCollection)queryStringCollection.Original); + } + + private static string ToQueryString(NameValueCollection queryStringCollection) + { + if (queryStringCollection == null || queryStringCollection.Count == 0) + return String.Empty; + + return "?" + queryStringCollection.ToFormUrlEncoded(); + } + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs new file mode 100644 index 000000000..6ea77f251 --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Net; +using ServiceStack.Web; + +namespace MediaBrowser.Server.Implementations.HttpServer.Security +{ + public class AuthorizationContext : IAuthorizationContext + { + public AuthorizationInfo GetAuthorizationInfo(IRequest requestContext) + { + return GetAuthorization(requestContext); + } + + /// <summary> + /// Gets the authorization. + /// </summary> + /// <param name="httpReq">The HTTP req.</param> + /// <returns>Dictionary{System.StringSystem.String}.</returns> + private static AuthorizationInfo GetAuthorization(IRequest httpReq) + { + var auth = GetAuthorizationDictionary(httpReq); + + string userId = null; + string deviceId = null; + string device = null; + string client = null; + string version = null; + + if (auth != null) + { + auth.TryGetValue("UserId", out userId); + auth.TryGetValue("DeviceId", out deviceId); + auth.TryGetValue("Device", out device); + auth.TryGetValue("Client", out client); + auth.TryGetValue("Version", out version); + } + + return new AuthorizationInfo + { + Client = client, + Device = device, + DeviceId = deviceId, + UserId = userId, + Version = version + }; + } + + /// <summary> + /// Gets the auth. + /// </summary> + /// <param name="httpReq">The HTTP req.</param> + /// <returns>Dictionary{System.StringSystem.String}.</returns> + private static Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq) + { + var 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 static Dictionary<string, string> GetAuthorization(string authorizationHeader) + { + if (authorizationHeader == null) return null; + + var parts = authorizationHeader.Split(' '); + + // 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 = authorizationHeader.Substring(authorizationHeader.IndexOf(' ')); + parts = authorizationHeader.Split(','); + + var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + foreach (var item in parts) + { + var param = item.Trim().Split(new[] { '=' }, 2); + result.Add(param[0], param[1].Trim(new[] { '"' })); + } + + return result; + } + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/SessionAuthProvider.cs b/MediaBrowser.Server.Implementations/HttpServer/Security/SessionAuthProvider.cs new file mode 100644 index 000000000..7c3173101 --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/Security/SessionAuthProvider.cs @@ -0,0 +1,35 @@ +using MediaBrowser.Controller.Net; +using ServiceStack; +using ServiceStack.Auth; + +namespace MediaBrowser.Server.Implementations.HttpServer.Security +{ + public class SessionAuthProvider : CredentialsAuthProvider + { + private readonly ISessionContext _sessionContext; + + public SessionAuthProvider(ISessionContext sessionContext) + { + _sessionContext = sessionContext; + } + + public override bool TryAuthenticate(IServiceBase authService, string userName, string password) + { + return true; + } + + public override bool IsAuthorized(IAuthSession session, IAuthTokens tokens, Authenticate request = null) + { + return true; + } + + protected override void SaveUserAuth(IServiceBase authService, IAuthSession session, IAuthRepository authRepo, IAuthTokens tokens) + { + } + + public override object Authenticate(IServiceBase authService, IAuthSession session, Authenticate request) + { + return base.Authenticate(authService, session, request); + } + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/SessionContext.cs b/MediaBrowser.Server.Implementations/HttpServer/Security/SessionContext.cs new file mode 100644 index 000000000..f67c643c8 --- /dev/null +++ b/MediaBrowser.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -0,0 +1,36 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Session; +using ServiceStack.Web; + +namespace MediaBrowser.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 SessionInfo GetSession(IRequest requestContext) + { + var authorization = _authContext.GetAuthorizationInfo(requestContext); + + return _sessionManager.GetSession(authorization.DeviceId, authorization.Client, authorization.Version); + } + + public User GetUser(IRequest requestContext) + { + var session = GetSession(requestContext); + + return session == null || !session.UserId.HasValue ? null : _userManager.GetUserById(session.UserId.Value); + } + } +} |
