#region License /* * HttpServer.cs * * The MIT License * * Copyright (c) 2012-2013 sta.blockhead * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #endregion using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Security.Cryptography.X509Certificates; using System.Threading; using WebSocketSharp.Net; namespace WebSocketSharp.Server { /// /// Provides a simple HTTP server that allows to accept the WebSocket connection requests. /// /// /// The HttpServer instance can provide the multi WebSocket services. /// public class HttpServer { #region Private Fields private HttpListener _listener; private bool _listening; private Logger _logger; private int _port; private Thread _receiveRequestThread; private string _rootPath; private bool _secure; private ServiceHostManager _serviceHosts; private bool _windows; #endregion #region Public Constructors /// /// Initializes a new instance of the class that listens for incoming requests /// on port 80. /// public HttpServer () : this (80) { } /// /// Initializes a new instance of the class that listens for incoming requests /// on the specified . /// /// /// An that contains a port number. /// /// /// is 0 or less, or 65536 or greater. /// public HttpServer (int port) : this (port, port == 443 ? true : false) { } /// /// Initializes a new instance of the class that listens for incoming requests /// on the specified and . /// /// /// An that contains a port number. /// /// /// A that indicates providing a secure connection or not. /// (true indicates providing a secure connection.) /// /// /// is 0 or less, or 65536 or greater. /// /// /// Pair of and is invalid. /// public HttpServer (int port, bool secure) { if (!port.IsPortNumber ()) throw new ArgumentOutOfRangeException ("port", "Invalid port number: " + port); if (port == 80 && secure || port == 443 && !secure) throw new ArgumentException (String.Format ( "Invalid pair of 'port' and 'secure': {0}, {1}", port, secure)); _port = port; _secure = secure; init (); } #endregion #region Public Properties /// /// Gets or sets the certificate used to authenticate the server on the secure connection. /// /// /// A used to authenticate the server. /// public X509Certificate2 Certificate { get { return _listener.DefaultCertificate; } set { if (_listening) { var msg = "The value of Certificate property cannot be changed because the server has already been started."; _logger.Error (msg); error (msg); return; } if (EndPointListener.CertificateExists (_port, _listener.CertificateFolderPath)) _logger.Warn ("Server certificate associated with the port number already exists."); _listener.DefaultCertificate = value; } } /// /// Gets a value indicating whether the server has been started. /// /// /// true if the server has been started; otherwise, false. /// public bool IsListening { get { return _listening; } } /// /// Gets a value indicating whether the server provides secure connection. /// /// /// true if the server provides secure connection; otherwise, false. /// public bool IsSecure { get { return _secure; } } /// /// Gets or sets a value indicating whether the server cleans up the inactive WebSocket service /// instances periodically. /// /// /// true if the server cleans up the inactive WebSocket service instances every 60 seconds; /// otherwise, false. The default value is true. /// public bool KeepClean { get { return _serviceHosts.KeepClean; } set { _serviceHosts.KeepClean = value; } } /// /// Gets the logging functions. /// /// /// The default logging level is the . /// If you want to change the current logging level, you set the Log.Level property /// to one of the values which you want. /// /// /// A that provides the logging functions. /// public Logger Log { get { return _logger; } } /// /// Gets the port on which to listen for incoming requests. /// /// /// An that contains a port number. /// public int Port { get { return _port; } } /// /// Gets or sets the document root path of server. /// /// /// A that contains the document root path of server. /// The default value is ./Public. /// public string RootPath { get { return _rootPath.IsNullOrEmpty () ? (_rootPath = "./Public") : _rootPath; } set { if (_listening) { var msg = "The value of RootPath property cannot be changed because the server has already been started."; _logger.Error (msg); error (msg); return; } _rootPath = value; } } /// /// Gets the collection of paths associated with the every WebSocket services that the server provides. /// /// /// An IEnumerable<string> that contains the collection of paths. /// public IEnumerable ServicePaths { get { return _serviceHosts.Paths; } } #endregion #region Public Events /// /// Occurs when the server receives an HTTP CONNECT request. /// public event EventHandler OnConnect; /// /// Occurs when the server receives an HTTP DELETE request. /// public event EventHandler OnDelete; /// /// Occurs when the server gets an error. /// public event EventHandler OnError; /// /// Occurs when the server receives an HTTP GET request. /// public event EventHandler OnGet; /// /// Occurs when the server receives an HTTP HEAD request. /// public event EventHandler OnHead; /// /// Occurs when the server receives an HTTP OPTIONS request. /// public event EventHandler OnOptions; /// /// Occurs when the server receives an HTTP PATCH request. /// public event EventHandler OnPatch; /// /// Occurs when the server receives an HTTP POST request. /// public event EventHandler OnPost; /// /// Occurs when the server receives an HTTP PUT request. /// public event EventHandler OnPut; /// /// Occurs when the server receives an HTTP TRACE request. /// public event EventHandler OnTrace; #endregion #region Private Methods private void error (string message) { OnError.Emit (this, new ErrorEventArgs (message)); } private void init () { _listener = new HttpListener (); _listening = false; _logger = new Logger (); _serviceHosts = new ServiceHostManager (_logger); _windows = false; var os = Environment.OSVersion; if (os.Platform != PlatformID.Unix && os.Platform != PlatformID.MacOSX) _windows = true; var prefix = String.Format ("http{0}://*:{1}/", _secure ? "s" : "", _port); _listener.Prefixes.Add (prefix); } private void processHttpRequest (HttpListenerContext context) { var eventArgs = new HttpRequestEventArgs (context); var method = context.Request.HttpMethod; if (method == "GET" && OnGet != null) { OnGet (this, eventArgs); return; } if (method == "HEAD" && OnHead != null) { OnHead (this, eventArgs); return; } if (method == "POST" && OnPost != null) { OnPost (this, eventArgs); return; } if (method == "PUT" && OnPut != null) { OnPut (this, eventArgs); return; } if (method == "DELETE" && OnDelete != null) { OnDelete (this, eventArgs); return; } if (method == "OPTIONS" && OnOptions != null) { OnOptions (this, eventArgs); return; } if (method == "TRACE" && OnTrace != null) { OnTrace (this, eventArgs); return; } if (method == "CONNECT" && OnConnect != null) { OnConnect (this, eventArgs); return; } if (method == "PATCH" && OnPatch != null) { OnPatch (this, eventArgs); return; } context.Response.StatusCode = (int) HttpStatusCode.NotImplemented; } private bool processWebSocketRequest (HttpListenerContext context) { var wsContext = context.AcceptWebSocket (); var path = wsContext.Path.UrlDecode (); IServiceHost host; if (!_serviceHosts.TryGetServiceHost (path, out host)) { context.Response.StatusCode = (int) HttpStatusCode.NotImplemented; return false; } wsContext.WebSocket.Log = _logger; host.BindWebSocket (wsContext); return true; } private void processRequestAsync (HttpListenerContext context) { WaitCallback callback = state => { try { if (context.Request.IsUpgradeTo ("websocket")) { if (processWebSocketRequest (context)) return; } else { processHttpRequest (context); } context.Response.Close (); } catch (Exception ex) { _logger.Fatal (ex.Message); error ("An exception has occured."); } }; ThreadPool.QueueUserWorkItem (callback); } private void receiveRequest () { while (true) { try { processRequestAsync (_listener.GetContext ()); } catch (HttpListenerException) { _logger.Info ("HttpListener has been stopped."); break; } catch (Exception ex) { _logger.Fatal (ex.Message); error ("An exception has occured."); break; } } } private void startReceiveRequestThread () { _receiveRequestThread = new Thread (new ThreadStart (receiveRequest)); _receiveRequestThread.IsBackground = true; _receiveRequestThread.Start (); } private void stop (ushort code, string reason, bool ignoreArgs) { if (!ignoreArgs) { var data = code.Append (reason); if (data.Length > 125) { var msg = "The payload length of a Close frame must be 125 bytes or less."; _logger.Error (String.Format ("{0}\ncode: {1}\nreason: {2}", msg, code, reason)); error (msg); return; } } _listener.Close (); _receiveRequestThread.Join (5 * 1000); if (ignoreArgs) _serviceHosts.Stop (); else _serviceHosts.Stop (code, reason); _listening = false; } #endregion #region Public Methods /// /// Adds the specified typed WebSocket service with the specified . /// /// /// A that contains an absolute path to the WebSocket service. /// /// /// The type of the WebSocket service. The T must inherit the class. /// public void AddWebSocketService (string servicePath) where T : WebSocketService, new () { string msg; if (!servicePath.IsValidAbsolutePath (out msg)) { _logger.Error (msg); error (msg); return; } var host = new WebSocketServiceHost (_logger); host.Uri = servicePath.ToUri (); if (!KeepClean) host.KeepClean = false; _serviceHosts.Add (servicePath, host); } /// /// Gets the contents of the file with the specified . /// /// /// An array of that contains the contents of the file if exists; /// otherwise, . /// /// /// A that contains a virtual path to the file to get. /// public byte[] GetFile (string path) { var filePath = RootPath + path; if (_windows) filePath = filePath.Replace ("/", "\\"); return File.Exists (filePath) ? File.ReadAllBytes (filePath) : null; } /// /// Removes the WebSocket service with the specified . /// /// /// true if the WebSocket service is successfully found and removed; otherwise, false. /// /// /// A that contains an absolute path to the WebSocket service to find. /// public bool RemoveWebSocketService (string servicePath) { if (servicePath.IsNullOrEmpty ()) { var msg = "'servicePath' must not be null or empty."; _logger.Error (msg); error (msg); return false; } return _serviceHosts.Remove (servicePath); } /// /// Starts to receive the HTTP requests. /// public void Start () { if (_listening) return; if (_secure && !EndPointListener.CertificateExists (_port, _listener.CertificateFolderPath) && Certificate == null ) { var msg = "Secure connection requires a server certificate."; _logger.Error (msg); error (msg); return; } _listener.Start (); startReceiveRequestThread (); _listening = true; } /// /// Stops receiving the HTTP requests. /// public void Stop () { if (!_listening) return; stop (0, null, true); } /// /// Stops receiving the HTTP requests with the specified and /// used to stop the WebSocket services. /// /// /// A that contains a status code indicating the reason for stop. /// /// /// A that contains the reason for stop. /// public void Stop (ushort code, string reason) { if (!_listening) return; if (!code.IsCloseStatusCode ()) { var msg = "Invalid status code for stop."; _logger.Error (String.Format ("{0}\ncode: {1}", msg, code)); error (msg); return; } stop (code, reason, false); } /// /// Stops receiving the HTTP requests with the specified /// and used to stop the WebSocket services. /// /// /// A that contains a status code indicating the reason for stop. /// /// /// A that contains the reason for stop. /// public void Stop (CloseStatusCode code, string reason) { if (!_listening) return; stop ((ushort) code, reason, false); } #endregion } }