using Funq; using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using ServiceStack.Api.Swagger; using ServiceStack.Common.Web; using ServiceStack.Configuration; using ServiceStack.Logging.NLogger; 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.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; } private ContainerAdapter _containerAdapter; /// /// Initializes a new instance of the class. /// /// The application host. /// The logger. /// Name of the server. /// The default redirectpath. /// urlPrefix public HttpServer(IApplicationHost applicationHost, ILogger logger, string serverName, string defaultRedirectpath) : base() { if (logger == null) { throw new ArgumentNullException("logger"); } 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 = logger; EndpointHostConfig.Instance.ServiceStackHandlerFactoryPath = null; EndpointHostConfig.Instance.MetadataRedirectPath = "metadata"; _containerAdapter = new ContainerAdapter(applicationHost); } 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, // Tell SS to bubble exceptions up to here WriteErrorsToResponse = false }); container.Adapter = _containerAdapter; Plugins.Add(new SwaggerFeature()); Plugins.Add(new CorsFeature()); ServiceStack.Logging.LogManager.LogFactory = new NLogFactory(); ResponseFilters.Add((req, res, dto) => { var exception = dto as Exception; if (exception != null) { _logger.ErrorException("Error processing request for {0}", exception, req.RawUrl); if (!string.IsNullOrEmpty(exception.Message)) { res.AddHeader("X-Application-Error-Code", exception.Message.Replace(Environment.NewLine, " ")); } } 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; } } } }); } /// /// 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) { LogHttpRequest(context); if (context.Request.IsWebSocketRequest) { await ProcessWebSocketRequest(context).ConfigureAwait(false); return; } Task.Run(() => { RaiseReceiveWebRequest(context); try { ProcessRequest(context); } catch (InvalidOperationException ex) { HandleException(context.Response, ex, 422); throw; } catch (ResourceNotFoundException ex) { HandleException(context.Response, ex, 404); throw; } catch (FileNotFoundException ex) { HandleException(context.Response, ex, 404); throw; } catch (DirectoryNotFoundException ex) { HandleException(context.Response, ex, 404); throw; } catch (UnauthorizedAccessException ex) { HandleException(context.Response, ex, 401); throw; } catch (ArgumentException ex) { HandleException(context.Response, ex, 400); throw; } catch (Exception ex) { HandleException(context.Response, ex, 500); throw; } }); } /// /// Processes the web socket request. /// /// The CTX. /// Task. private async Task ProcessWebSocketRequest(HttpListenerContext ctx) { 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(); } } /// /// Logs the HTTP request. /// /// The CTX. private void LogHttpRequest(HttpListenerContext ctx) { 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; if (EnableHttpRequestLogging) { _logger.LogMultiline(type + " request received from " + ctx.Request.RemoteEndPoint, LogSeverity.Debug, log); } } /// /// Appends the error message. /// /// The response. /// The ex. /// The status code. private void HandleException(HttpListenerResponse response, Exception ex, int statusCode) { _logger.ErrorException("Error processing request", ex); response.StatusCode = statusCode; response.Headers.Add("Status", statusCode.ToString(new CultureInfo("en-US"))); response.Headers.Remove("Age"); response.Headers.Remove("Expires"); response.Headers.Remove("Cache-Control"); response.Headers.Remove("Etag"); response.Headers.Remove("Last-Modified"); response.ContentType = "text/plain"; if (!string.IsNullOrEmpty(ex.Message)) { response.AddHeader("X-Application-Error-Code", ex.Message); } // This could fail, but try to add the stack trace as the body content try { var sb = new StringBuilder(); sb.AppendLine("{"); sb.AppendLine("\"ResponseStatus\":{"); sb.AppendFormat(" \"ErrorCode\":{0},\n", ex.GetType().Name.EncodeJson()); sb.AppendFormat(" \"Message\":{0},\n", ex.Message.EncodeJson()); sb.AppendFormat(" \"StackTrace\":{0}\n", ex.StackTrace.EncodeJson()); sb.AppendLine("}"); sb.AppendLine("}"); response.StatusCode = 500; response.ContentType = ContentType.Json; var sbBytes = sb.ToString().ToUtf8Bytes(); response.OutputStream.Write(sbBytes, 0, sbBytes.Length); response.Close(); } catch (Exception errorEx) { _logger.ErrorException("Error processing failed request", errorEx); } } /// /// 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 url = context.Request.Url.ToString(); var endPoint = context.Request.RemoteEndPoint; 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); LogResponse(context, url, endPoint); httpRes.Close(); return; } throw new NotImplementedException("Cannot execute handler: " + handler + " at PathInfo: " + httpReq.PathInfo); } /// /// Logs the response. /// /// The CTX. /// The URL. /// The end point. private void LogResponse(HttpListenerContext ctx, string url, IPEndPoint endPoint) { if (!EnableHttpRequestLogging) { return; } var statusode = 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 msg = "Http Response Sent (" + statusode + ") to " + endPoint; _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 (!_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(); } } /// /// 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 } } }