using Funq; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Kernel; using MediaBrowser.Model.Logging; using ServiceStack.Api.Swagger; using ServiceStack.Common.Web; using ServiceStack.Logging; 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.Globalization; using System.IO; using System.Linq; using System.Net; using System.Reactive.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace MediaBrowser.Common.Net { /// /// Class HttpServer /// public class HttpServer : HttpListenerBase { /// /// The logger /// private static ILogger Logger = Logging.LogManager.GetLogger("HttpServer"); /// /// Gets the URL prefix. /// /// The URL prefix. public string UrlPrefix { get; private set; } /// /// Gets or sets the kernel. /// /// The kernel. private IKernel Kernel { get; set; } /// /// 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. public string DefaultRedirectPath { get; private set; } /// /// Initializes a new instance of the class. /// /// The URL. /// Name of the product. /// The kernel. /// The default redirectpath. /// urlPrefix public HttpServer(string urlPrefix, string serverName, IKernel kernel, string defaultRedirectpath = null) : base() { if (string.IsNullOrEmpty(urlPrefix)) { throw new ArgumentNullException("urlPrefix"); } DefaultRedirectPath = defaultRedirectpath; EndpointHostConfig.Instance.ServiceStackHandlerFactoryPath = null; EndpointHostConfig.Instance.MetadataRedirectPath = "metadata"; UrlPrefix = urlPrefix; Kernel = kernel; EndpointHost.ConfigureHost(this, serverName, CreateServiceManager()); ContentTypeFilters.Register(ContentType.ProtoBuf, (reqCtx, res, stream) => Kernel.ProtobufSerializer.SerializeToStream(res, stream), (type, stream) => Kernel.ProtobufSerializer.DeserializeFromStream(stream, type)); Init(); Start(urlPrefix); } /// /// 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(); } /// /// Configures the specified container. /// /// The container. public override void Configure(Container container) { if (!string.IsNullOrEmpty(DefaultRedirectPath)) { SetConfig(new EndpointHostConfig { DefaultRedirectPath = DefaultRedirectPath, // Tell SS to bubble exceptions up to here WriteErrorsToResponse = false, DebugMode = true }); } container.Register(Kernel); foreach (var service in Kernel.RestServices) { service.Configure(this); } Plugins.Add(new SwaggerFeature()); Plugins.Add(new CorsFeature()); Serialization.JsonSerializer.Configure(); LogManager.LogFactory = new NLogFactory(); } /// /// 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. public override void Start(string urlBase) { // *** Already running - just leave it in place if (IsStarted) { return; } if (Listener == null) { Listener = new HttpListener(); } EndpointHost.Config.ServiceStackHandlerFactoryPath = HttpListenerRequestWrapper.GetHandlerPathIfAny(urlBase); Listener.Prefixes.Add(urlBase); IsStarted = true; Listener.Start(); 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; } 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), Endpoint = ctx.Request.RemoteEndPoint }); } } 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 (Kernel.Configuration.EnableHttpLevelLogging) { 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 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); httpRes.Close(); return; } throw new NotImplementedException("Cannot execute handler: " + handler + " at PathInfo: " + httpReq.PathInfo); } /// /// Logs the response. /// /// The CTX. private void LogResponse(HttpListenerContext ctx) { var statusode = ctx.Response.StatusCode; var log = new StringBuilder(); log.AppendLine(string.Format("Url: {0}", ctx.Request.Url)); log.AppendLine("Headers: " + string.Join(",", ctx.Response.Headers.AllKeys.Select(k => k + "=" + ctx.Response.Headers[k]))); var msg = "Http Response Sent (" + statusode + ") to " + ctx.Request.RemoteEndPoint; if (Kernel.Configuration.EnableHttpLevelLogging) { Logger.LogMultiline(msg, LogSeverity.Debug, log); } } /// /// Creates the service manager. /// /// The assemblies with services. /// ServiceManager. protected override ServiceManager CreateServiceManager(params Assembly[] assembliesWithServices) { var types = Kernel.RestServices.Select(r => r.GetType()).ToArray(); return new ServiceManager(new Container(), new ServiceController(() => types)); } } /// /// Class WebSocketConnectEventArgs /// public class WebSocketConnectEventArgs : EventArgs { /// /// Gets or sets the web socket. /// /// The web socket. public IWebSocket WebSocket { get; set; } /// /// Gets or sets the endpoint. /// /// The endpoint. public IPEndPoint Endpoint { get; set; } } }