From 28ccfb1bd17eceb683d428d1c0e2d2ea52a2f7ff Mon Sep 17 00:00:00 2001 From: LukePulverenti Date: Sun, 24 Feb 2013 19:13:45 -0500 Subject: extracted httpclient dependancy --- MediaBrowser.Networking/HttpServer/HttpServer.cs | 554 +++++++++++++++++++++++ 1 file changed, 554 insertions(+) create mode 100644 MediaBrowser.Networking/HttpServer/HttpServer.cs (limited to 'MediaBrowser.Networking/HttpServer/HttpServer.cs') diff --git a/MediaBrowser.Networking/HttpServer/HttpServer.cs b/MediaBrowser.Networking/HttpServer/HttpServer.cs new file mode 100644 index 000000000..b6250527d --- /dev/null +++ b/MediaBrowser.Networking/HttpServer/HttpServer.cs @@ -0,0 +1,554 @@ +using System.Net.WebSockets; +using Funq; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Kernel; +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.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.Networking.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; } + + /// + /// Gets or sets the kernel. + /// + /// The kernel. + private IKernel Kernel { get; set; } + + /// + /// Gets or sets the application host. + /// + /// The application host. + private IApplicationHost ApplicationHost { get; set; } + + /// + /// This subscribes to HttpListener requests and finds the appropriate BaseHandler to process it + /// + /// The HTTP listener. + private IDisposable HttpListener { get; set; } + + /// + /// Gets or sets the protobuf serializer. + /// + /// The protobuf serializer. + private IProtobufSerializer ProtobufSerializer { 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; } + + /// + /// Initializes a new instance of the class. + /// + /// The application host. + /// The kernel. + /// The protobuf serializer. + /// The logger. + /// Name of the server. + /// The default redirectpath. + /// urlPrefix + public HttpServer(IApplicationHost applicationHost, IKernel kernel, IProtobufSerializer protobufSerializer, ILogger logger, string serverName, string defaultRedirectpath) + : base() + { + if (kernel == null) + { + throw new ArgumentNullException("kernel"); + } + if (protobufSerializer == null) + { + throw new ArgumentNullException("protobufSerializer"); + } + 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; + ProtobufSerializer = protobufSerializer; + _logger = logger; + ApplicationHost = applicationHost; + + EndpointHostConfig.Instance.ServiceStackHandlerFactoryPath = null; + EndpointHostConfig.Instance.MetadataRedirectPath = "metadata"; + + Kernel = kernel; + + EndpointHost.ConfigureHost(this, ServerName, CreateServiceManager()); + ContentTypeFilters.Register(ContentType.ProtoBuf, (reqCtx, res, stream) => ProtobufSerializer.SerializeToStream(res, stream), (type, stream) => ProtobufSerializer.DeserializeFromStream(stream, type)); + + Init(); + } + + /// + /// 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, + + DebugMode = true + }); + + container.Adapter = new ContainerAdapter(ApplicationHost); + + foreach (var service in Kernel.RestServices) + { + service.Configure(this); + } + + Plugins.Add(new SwaggerFeature()); + Plugins.Add(new CorsFeature()); + + ServiceStack.Logging.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) + { + if (string.IsNullOrEmpty(urlBase)) + { + throw new ArgumentNullException("urlBase"); + } + + // *** Already running - just leave it in place + if (IsStarted) + { + return; + } + + if (Listener == null) + { + Listener = new HttpListener(); + } + + EndpointHost.Config.ServiceStackHandlerFactoryPath = HttpListenerRequestWrapper.GetHandlerPathIfAny(urlBase); + + UrlPrefix = 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; + } + + + 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 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 (EnableHttpRequestLogging) + { + _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)); + } + + /// + /// 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; } + } + + /// + /// Class ContainerAdapter + /// + class ContainerAdapter : IContainerAdapter + { + /// + /// 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(); + } + } +} \ No newline at end of file -- cgit v1.2.3