using Funq; using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Model.Logging; using ServiceStack.Api.Swagger; using ServiceStack.Common.Web; using ServiceStack.Configuration; using ServiceStack.Logging; using ServiceStack.ServiceHost; using ServiceStack.ServiceInterface.Cors; using ServiceStack.Text; using ServiceStack.WebHost.Endpoints; using ServiceStack.WebHost.Endpoints.Extensions; using ServiceStack.WebHost.Endpoints.Support; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.WebSockets; using System.Reactive.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace MediaBrowser.Server.Implementations.HttpServer { /// /// Class HttpServer /// public class HttpServer : HttpListenerBase, IHttpServer { /// /// The logger /// private readonly ILogger _logger; /// /// Gets the URL prefix. /// /// The URL prefix. public string UrlPrefix { get; private set; } /// /// The _rest services /// private readonly List _restServices = new List(); /// /// This subscribes to HttpListener requests and finds the appropriate BaseHandler to process it /// /// The HTTP listener. private IDisposable HttpListener { get; set; } /// /// Occurs when [web socket connected]. /// public event EventHandler WebSocketConnected; /// /// Gets the default redirect path. /// /// The default redirect path. private string DefaultRedirectPath { get; set; } /// /// Gets or sets the name of the server. /// /// The name of the server. private string ServerName { get; set; } /// /// The _container adapter /// private readonly ContainerAdapter _containerAdapter; private readonly ConcurrentDictionary _localEndPoints = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); /// /// Gets the local end points. /// /// The local end points. public IEnumerable LocalEndPoints { get { return _localEndPoints.Keys.ToList(); } } /// /// Initializes a new instance of the class. /// /// The application host. /// The log manager. /// Name of the server. /// The default redirectpath. /// urlPrefix public HttpServer(IApplicationHost applicationHost, ILogManager logManager, string serverName, string defaultRedirectpath) : base() { if (logManager == null) { throw new ArgumentNullException("logManager"); } if (applicationHost == null) { throw new ArgumentNullException("applicationHost"); } if (string.IsNullOrEmpty(serverName)) { throw new ArgumentNullException("serverName"); } if (string.IsNullOrEmpty(defaultRedirectpath)) { throw new ArgumentNullException("defaultRedirectpath"); } ServerName = serverName; DefaultRedirectPath = defaultRedirectpath; _logger = logManager.GetLogger("HttpServer"); LogManager.LogFactory = new ServerLogFactory(logManager); EndpointHostConfig.Instance.ServiceStackHandlerFactoryPath = null; EndpointHostConfig.Instance.MetadataRedirectPath = "metadata"; _containerAdapter = new ContainerAdapter(applicationHost); } /// /// The us culture /// protected static readonly CultureInfo UsCulture = new CultureInfo("en-US"); /// /// Configures the specified container. /// /// The container. public override void Configure(Container container) { JsConfig.DateHandler = JsonDateHandler.ISO8601; JsConfig.ExcludeTypeInfo = true; JsConfig.IncludeNullValues = false; SetConfig(new EndpointHostConfig { DefaultRedirectPath = DefaultRedirectPath, MapExceptionToStatusCode = { { typeof(InvalidOperationException), 422 }, { typeof(ResourceNotFoundException), 404 }, { typeof(FileNotFoundException), 404 }, { typeof(DirectoryNotFoundException), 404 } }, DebugMode = true, ServiceName = ServerName, LogFactory = LogManager.LogFactory, // The Markdown feature causes slow startup times (5 mins+) on cold boots for some users // Custom format allows images EnableFeatures = Feature.Csv | Feature.Html | Feature.Json | Feature.Jsv | Feature.Metadata | Feature.Xml | Feature.CustomFormat }); container.Adapter = _containerAdapter; Plugins.Add(new SwaggerFeature()); Plugins.Add(new CorsFeature()); ResponseFilters.Add(FilterResponse); } /// /// Filters the response. /// /// The req. /// The res. /// The dto. private void FilterResponse(IHttpRequest req, IHttpResponse res, object dto) { // Try to prevent compatibility view res.AddHeader("X-UA-Compatible", "IE=Edge"); var exception = dto as Exception; if (exception != null) { _logger.ErrorException("Error processing request for {0}", exception, req.RawUrl); if (!string.IsNullOrEmpty(exception.Message)) { var error = exception.Message.Replace(Environment.NewLine, " "); error = RemoveControlCharacters(error); res.AddHeader("X-Application-Error-Code", error); } } if (dto is CompressedResult) { // Per Google PageSpeed // This instructs the proxies to cache two versions of the resource: one compressed, and one uncompressed. // The correct version of the resource is delivered based on the client request header. // This is a good choice for applications that are singly homed and depend on public proxies for user locality. res.AddHeader("Vary", "Accept-Encoding"); } var hasOptions = dto as IHasOptions; if (hasOptions != null) { // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy string contentLength; if (hasOptions.Options.TryGetValue("Content-Length", out contentLength) && !string.IsNullOrEmpty(contentLength)) { var length = long.Parse(contentLength, UsCulture); if (length > 0) { var response = (HttpListenerResponse)res.OriginalResponse; response.ContentLength64 = length; // Disable chunked encoding. Technically this is only needed when using Content-Range, but // anytime we know the content length there's no need for it response.SendChunked = false; } } } } /// /// Removes the control characters. /// /// The in string. /// System.String. private static string RemoveControlCharacters(string inString) { if (inString == null) return null; var newString = new StringBuilder(); foreach (var ch in inString) { if (!char.IsControl(ch)) { newString.Append(ch); } } return newString.ToString(); } /// /// Starts the Web Service /// /// A Uri that acts as the base that the server is listening on. /// Format should be: http://127.0.0.1:8080/ or http://127.0.0.1:8080/somevirtual/ /// Note: the trailing slash is required! For more info see the /// HttpListener.Prefixes property on MSDN. /// urlBase public override void Start(string urlBase) { if (string.IsNullOrEmpty(urlBase)) { throw new ArgumentNullException("urlBase"); } // *** Already running - just leave it in place if (IsStarted) { return; } if (Listener == null) { _logger.Info("Creating HttpListner"); Listener = new HttpListener(); } EndpointHost.Config.ServiceStackHandlerFactoryPath = HttpListenerRequestWrapper.GetHandlerPathIfAny(urlBase); UrlPrefix = urlBase; _logger.Info("Adding HttpListener Prefixes"); Listener.Prefixes.Add(urlBase); IsStarted = true; _logger.Info("Starting HttpListner"); Listener.Start(); _logger.Info("Creating HttpListner observable stream"); HttpListener = CreateObservableStream().Subscribe(ProcessHttpRequestAsync); } /// /// Creates the observable stream. /// /// IObservable{HttpListenerContext}. private IObservable CreateObservableStream() { return Observable.Create(obs => Observable.FromAsync(() => Listener.GetContextAsync()) .Subscribe(obs)) .Repeat() .Retry() .Publish() .RefCount(); } /// /// Processes incoming http requests by routing them to the appropiate handler /// /// The CTX. private async void ProcessHttpRequestAsync(HttpListenerContext context) { var date = DateTime.Now; LogHttpRequest(context); if (context.Request.IsWebSocketRequest) { await ProcessWebSocketRequest(context).ConfigureAwait(false); return; } var localPath = context.Request.Url.LocalPath; if (string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase)) { context.Response.Redirect(DefaultRedirectPath); context.Response.Close(); return; } if (string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase)) { context.Response.Redirect("mediabrowser/" + DefaultRedirectPath); context.Response.Close(); return; } if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)) { context.Response.Redirect("mediabrowser/" + DefaultRedirectPath); context.Response.Close(); return; } if (string.IsNullOrEmpty(localPath)) { context.Response.Redirect("/mediabrowser/" + DefaultRedirectPath); context.Response.Close(); return; } RaiseReceiveWebRequest(context); await Task.Factory.StartNew(() => { try { var url = context.Request.Url.ToString(); var endPoint = context.Request.RemoteEndPoint; ProcessRequest(context); var duration = DateTime.Now - date; LogResponse(context, url, endPoint, duration); } catch (Exception ex) { _logger.ErrorException("ProcessRequest failure", ex); } }).ConfigureAwait(false); } /// /// Processes the web socket request. /// /// The CTX. /// Task. private async Task ProcessWebSocketRequest(HttpListenerContext ctx) { #if __MonoCS__ #else try { var webSocketContext = await ctx.AcceptWebSocketAsync(null).ConfigureAwait(false); if (WebSocketConnected != null) { WebSocketConnected(this, new WebSocketConnectEventArgs { WebSocket = new NativeWebSocket(webSocketContext.WebSocket, _logger), Endpoint = ctx.Request.RemoteEndPoint.ToString() }); } } catch (Exception ex) { _logger.ErrorException("AcceptWebSocketAsync error", ex); ctx.Response.StatusCode = 500; ctx.Response.Close(); } #endif } /// /// Logs the HTTP request. /// /// The CTX. private void LogHttpRequest(HttpListenerContext ctx) { var endpoint = ctx.Request.LocalEndPoint; if (endpoint != null) { var address = endpoint.ToString(); _localEndPoints.GetOrAdd(address, address); } if (EnableHttpRequestLogging) { var log = new StringBuilder(); log.AppendLine("Url: " + ctx.Request.Url); log.AppendLine("Headers: " + string.Join(",", ctx.Request.Headers.AllKeys.Select(k => k + "=" + ctx.Request.Headers[k]))); var type = ctx.Request.IsWebSocketRequest ? "Web Socket" : "HTTP " + ctx.Request.HttpMethod; _logger.LogMultiline(type + " request received from " + ctx.Request.RemoteEndPoint, LogSeverity.Debug, log); } } /// /// Overridable method that can be used to implement a custom hnandler /// /// The context. /// Cannot execute handler: + handler + at PathInfo: + httpReq.PathInfo protected override void ProcessRequest(HttpListenerContext context) { if (string.IsNullOrEmpty(context.Request.RawUrl)) return; var operationName = context.Request.GetOperationName(); var httpReq = new HttpListenerRequestWrapper(operationName, context.Request); var httpRes = new HttpListenerResponseWrapper(context.Response); var handler = ServiceStackHttpHandlerFactory.GetHandler(httpReq); var serviceStackHandler = handler as IServiceStackHttpHandler; if (serviceStackHandler != null) { var restHandler = serviceStackHandler as RestHandler; if (restHandler != null) { httpReq.OperationName = operationName = restHandler.RestPath.RequestType.Name; } serviceStackHandler.ProcessRequest(httpReq, httpRes, operationName); return; } throw new NotImplementedException("Cannot execute handler: " + handler + " at PathInfo: " + httpReq.PathInfo); } /// /// Logs the response. /// /// The CTX. /// The URL. /// The end point. /// The duration. private void LogResponse(HttpListenerContext ctx, string url, IPEndPoint endPoint, TimeSpan duration) { if (!EnableHttpRequestLogging) { return; } var statusCode = ctx.Response.StatusCode; var log = new StringBuilder(); log.AppendLine(string.Format("Url: {0}", url)); log.AppendLine("Headers: " + string.Join(",", ctx.Response.Headers.AllKeys.Select(k => k + "=" + ctx.Response.Headers[k]))); var responseTime = string.Format(". Response time: {0} ms", duration.TotalMilliseconds); var msg = "Response code " + statusCode + " sent to " + endPoint + responseTime; _logger.LogMultiline(msg, LogSeverity.Debug, log); } /// /// Creates the service manager. /// /// The assemblies with services. /// ServiceManager. protected override ServiceManager CreateServiceManager(params Assembly[] assembliesWithServices) { var types = _restServices.Select(r => r.GetType()).ToArray(); return new ServiceManager(new Container(), new ServiceController(() => types)); } /// /// Shut down the Web Service /// public override void Stop() { if (HttpListener != null) { HttpListener.Dispose(); HttpListener = null; } if (Listener != null) { Listener.Prefixes.Remove(UrlPrefix); } base.Stop(); } /// /// The _supports native web socket /// private bool? _supportsNativeWebSocket; /// /// Gets a value indicating whether [supports web sockets]. /// /// true if [supports web sockets]; otherwise, false. public bool SupportsWebSockets { get { #if __MonoCS__ return false; #else #endif if (!_supportsNativeWebSocket.HasValue) { try { new ClientWebSocket(); _supportsNativeWebSocket = true; } catch (PlatformNotSupportedException) { _supportsNativeWebSocket = false; } } return _supportsNativeWebSocket.Value; } } /// /// Gets or sets a value indicating whether [enable HTTP request logging]. /// /// true if [enable HTTP request logging]; otherwise, false. public bool EnableHttpRequestLogging { get; set; } /// /// Adds the rest handlers. /// /// The services. public void Init(IEnumerable services) { _restServices.AddRange(services); _logger.Info("Calling EndpointHost.ConfigureHost"); EndpointHost.ConfigureHost(this, ServerName, CreateServiceManager()); _logger.Info("Calling ServiceStack AppHost.Init"); Init(); } /// /// Releases the specified instance. /// /// The instance. public override void Release(object instance) { // Leave this empty so SS doesn't try to dispose our objects } } /// /// Class ContainerAdapter /// class ContainerAdapter : IContainerAdapter, IRelease { /// /// The _app host /// private readonly IApplicationHost _appHost; /// /// Initializes a new instance of the class. /// /// The app host. public ContainerAdapter(IApplicationHost appHost) { _appHost = appHost; } /// /// Resolves this instance. /// /// /// ``0. public T Resolve() { return _appHost.Resolve(); } /// /// Tries the resolve. /// /// /// ``0. public T TryResolve() { return _appHost.TryResolve(); } /// /// Releases the specified instance. /// /// The instance. public void Release(object instance) { // Leave this empty so SS doesn't try to dispose our objects } } }