#region License /* * HttpServer.cs * * A simple HTTP server that allows to accept the WebSocket connection requests. * * The MIT License * * Copyright (c) 2012-2014 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 #region Contributors /* * Contributors: * Juan Manuel Lallana */ #endregion using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using System.Threading; using WebSocketSharp.Net; using WebSocketSharp.Net.WebSockets; namespace WebSocketSharp.Server { /// /// Provides a simple HTTP server that allows to accept the WebSocket connection requests. /// /// /// The HttpServer class can provide the multi WebSocket services. /// public class HttpServer { #region Private Fields private HttpListener _listener; private Logger _logger; private int _port; private Thread _receiveRequestThread; private string _rootPath; private bool _secure; private WebSocketServiceManager _services; private volatile ServerState _state; private object _sync; private bool _windows; #endregion #region Public Constructors /// /// Initializes a new instance of the class. /// /// /// An instance initialized by this constructor listens for the incoming requests on port 80. /// public HttpServer () : this (80) { } /// /// Initializes a new instance of the class with the specified /// . /// /// /// /// An instance initialized by this constructor listens for the incoming requests on /// . /// /// /// And if is 443, that instance provides a secure connection. /// /// /// /// An that represents the port number on which to listen. /// /// /// isn't between 1 and 65535. /// public HttpServer (int port) : this (port, port == 443) { } /// /// Initializes a new instance of the class with the specified /// and . /// /// /// An instance initialized by this constructor listens for the incoming requests on /// . /// /// /// An that represents the port number on which to listen. /// /// /// A that indicates providing a secure connection or not. (true /// indicates providing a secure connection.) /// /// /// isn't between 1 and 65535. /// /// /// Pair of and is invalid. /// public HttpServer (int port, bool secure) { if (!port.IsPortNumber ()) throw new ArgumentOutOfRangeException ("port", "Must be between 1 and 65535: " + 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; _listener = new HttpListener (); _logger = new Logger (); _services = new WebSocketServiceManager (_logger); _state = ServerState.Ready; _sync = new object (); 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); } #endregion #region Public Properties /// /// Gets or sets the scheme used to authenticate the clients. /// /// /// One of the enum values, indicates /// the scheme used to authenticate the clients. /// The default value is . /// public AuthenticationSchemes AuthenticationSchemes { get { return _listener.AuthenticationSchemes; } set { if (!canSet ("AuthenticationSchemes")) return; _listener.AuthenticationSchemes = value; } } /// /// 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 (!canSet ("Certificate")) return; if (EndPointListener.CertificateExists (_port, _listener.CertificateFolderPath)) _logger.Warn ("The server certificate associated with the port number already exists."); _listener.DefaultCertificate = value; } } /// /// Gets a value indicating whether the server has started. /// /// /// true if the server has started; otherwise, false. /// public bool IsListening { get { return _state == ServerState.Start; } } /// /// Gets a value indicating whether the server provides a secure connection. /// /// /// true if the server provides a secure connection; otherwise, false. /// public bool IsSecure { get { return _secure; } } /// /// Gets or sets a value indicating whether the server cleans up the inactive sessions in the /// WebSocket services periodically. /// /// /// true if the server cleans up the inactive sessions every 60 seconds; otherwise, /// false. The default value is true. /// public bool KeepClean { get { return _services.KeepClean; } set { _services.KeepClean = value; } } /// /// Gets the logging functions. /// /// /// The default logging level is . If you would like to change it, /// you should set the Log.Level property to any of the enum /// values. /// /// /// A that provides the logging functions. /// public Logger Log { get { return _logger; } } /// /// Gets the port on which to listen for incoming requests. /// /// /// An that represents the port number on which to listen. /// public int Port { get { return _port; } } /// /// Gets or sets the name of the realm associated with the server. /// /// /// A that represents the name of the realm. The default value is /// SECRET AREA. /// public string Realm { get { return _listener.Realm; } set { if (!canSet ("Realm")) return; _listener.Realm = value; } } /// /// Gets or sets the document root path of the server. /// /// /// A that represents the document root path of the server. The default /// value is ./Public. /// public string RootPath { get { return _rootPath.IsNullOrEmpty () ? (_rootPath = "./Public") : _rootPath; } set { if (!canSet ("RootPath")) return; _rootPath = value; } } /// /// Gets or sets the delegate called to find the credentials for an identity used to /// authenticate a client. /// /// /// A Func<, > delegate that /// references the method(s) used to find the credentials. The default value is a function /// that only returns . /// public Func UserCredentialsFinder { get { return _listener.UserCredentialsFinder; } set { if (!canSet ("UserCredentialsFinder")) return; _listener.UserCredentialsFinder = value; } } /// /// Gets the access to the WebSocket services provided by the server. /// /// /// A that manages the WebSocket services. /// public WebSocketServiceManager WebSocketServices { get { return _services; } } #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 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 abort () { lock (_sync) { if (!IsListening) return; _state = ServerState.ShuttingDown; } _services.Stop ( ((ushort) CloseStatusCode.ServerError).ToByteArrayInternally (ByteOrder.Big), true); _listener.Abort (); _state = ServerState.Stop; } private void acceptHttpRequest (HttpListenerContext context) { var args = new HttpRequestEventArgs (context); var method = context.Request.HttpMethod; if (method == "GET") { if (OnGet != null) { OnGet (this, args); return; } } else if (method == "HEAD") { if (OnHead != null) { OnHead (this, args); return; } } else if (method == "POST") { if (OnPost != null) { OnPost (this, args); return; } } else if (method == "PUT") { if (OnPut != null) { OnPut (this, args); return; } } else if (method == "DELETE") { if (OnDelete != null) { OnDelete (this, args); return; } } else if (method == "OPTIONS") { if (OnOptions != null) { OnOptions (this, args); return; } } else if (method == "TRACE") { if (OnTrace != null) { OnTrace (this, args); return; } } else if (method == "CONNECT") { if (OnConnect != null) { OnConnect (this, args); return; } } else if (method == "PATCH") { if (OnPatch != null) { OnPatch (this, args); return; } } context.Response.StatusCode = (int) HttpStatusCode.NotImplemented; } private void acceptRequestAsync (HttpListenerContext context) { ThreadPool.QueueUserWorkItem ( state => { try { var authScheme = _listener.SelectAuthenticationScheme (context); if (authScheme != AuthenticationSchemes.Anonymous && !authenticateRequest (authScheme, context)) return; if (context.Request.IsUpgradeTo ("websocket")) { acceptWebSocketRequest (context.AcceptWebSocket (_logger)); return; } acceptHttpRequest (context); context.Response.Close (); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); context.Connection.Close (true); } }); } private void acceptWebSocketRequest (HttpListenerWebSocketContext context) { var path = context.Path; WebSocketServiceHost host; if (path == null || !_services.TryGetServiceHostInternally (path, out host)) { context.Close (HttpStatusCode.NotImplemented); return; } host.StartSession (context); } private bool authenticateRequest (AuthenticationSchemes scheme, HttpListenerContext context) { if (context.Request.IsAuthenticated) return true; if (scheme == AuthenticationSchemes.Basic) context.Response.CloseWithAuthChallenge ( HttpUtility.CreateBasicAuthChallenge (_listener.Realm)); else if (scheme == AuthenticationSchemes.Digest) context.Response.CloseWithAuthChallenge ( HttpUtility.CreateDigestAuthChallenge (_listener.Realm)); else context.Response.Close (HttpStatusCode.Forbidden); return false; } private bool canSet (string property) { if (_state == ServerState.Start || _state == ServerState.ShuttingDown) { _logger.Error ( String.Format ( "Set operation of {0} isn't available because the server has already started.", property)); return false; } return true; } private string checkIfCertExists () { return _secure && !EndPointListener.CertificateExists (_port, _listener.CertificateFolderPath) && Certificate == null ? "The secure connection requires a server certificate." : null; } private void receiveRequest () { while (true) { try { acceptRequestAsync (_listener.GetContext ()); } catch (HttpListenerException ex) { _logger.Warn ("Receiving has been stopped.\nreason: " + ex.Message); break; } catch (Exception ex) { _logger.Fatal (ex.ToString ()); break; } } if (IsListening) abort (); } private void startReceiving () { _receiveRequestThread = new Thread (new ThreadStart (receiveRequest)); _receiveRequestThread.IsBackground = true; _receiveRequestThread.Start (); } private void stopListener (int millisecondsTimeout) { _listener.Close (); _receiveRequestThread.Join (millisecondsTimeout); } #endregion #region Public Methods /// /// Adds the specified typed WebSocket service with the specified . /// /// /// This method converts to URL-decoded string and removes '/' /// from tail end of . /// /// /// A that represents the absolute path to the WebSocket service to add. /// /// /// The type of the WebSocket service. /// The TWithNew must inherit the class and must have a public /// parameterless constructor. /// public void AddWebSocketService (string path) where TWithNew : WebSocketService, new () { AddWebSocketService (path, () => new TWithNew ()); } /// /// Adds the specified typed WebSocket service with the specified and /// . /// /// /// /// This method converts to URL-decoded string and removes '/' /// from tail end of . /// /// /// returns a initialized specified typed /// instance. /// /// /// /// A that represents the absolute path to the WebSocket service to add. /// /// /// A Func<T> delegate that references the method used to initialize a new specified /// typed instance (a new /// instance). /// /// /// The type of the WebSocket service. The T must inherit the /// class. /// public void AddWebSocketService (string path, Func constructor) where T : WebSocketService { var msg = path.CheckIfValidServicePath () ?? (constructor == null ? "'constructor' must not be null." : null); if (msg != null) { _logger.Error (String.Format ("{0}\nservice path: {1}", msg, path)); return; } var host = new WebSocketServiceHost (path, constructor, _logger); if (!KeepClean) host.KeepClean = false; _services.Add (host.Path, host); } /// /// Gets the contents of the file with the specified . /// /// /// An array of that receives the contents of the file if it exists; /// otherwise, . /// /// /// A that represents the virtual path to the file to find. /// 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 . /// /// /// This method converts to URL-decoded string and removes '/' /// from tail end of . /// /// /// true if the WebSocket service is successfully found and removed; otherwise, /// false. /// /// /// A that represents the absolute path to the WebSocket service to find. /// public bool RemoveWebSocketService (string path) { var msg = path.CheckIfValidServicePath (); if (msg != null) { _logger.Error (String.Format ("{0}\nservice path: {1}", msg, path)); return false; } return _services.Remove (path); } /// /// Starts receiving the HTTP requests. /// public void Start () { lock (_sync) { var msg = _state.CheckIfStartable () ?? checkIfCertExists (); if (msg != null) { _logger.Error (String.Format ("{0}\nstate: {1}\nsecure: {2}", msg, _state, _secure)); return; } _services.Start (); _listener.Start (); startReceiving (); _state = ServerState.Start; } } /// /// Stops receiving the HTTP requests. /// public void Stop () { lock (_sync) { var msg = _state.CheckIfStart (); if (msg != null) { _logger.Error (String.Format ("{0}\nstate: {1}", msg, _state)); return; } _state = ServerState.ShuttingDown; } _services.Stop (new byte [0], true); stopListener (5000); _state = ServerState.Stop; } /// /// Stops receiving the HTTP requests with the specified and /// used to stop the WebSocket services. /// /// /// A that represents the status code indicating the reason for stop. /// /// /// A that represents the reason for stop. /// public void Stop (ushort code, string reason) { byte [] data = null; lock (_sync) { var msg = _state.CheckIfStart () ?? code.CheckIfValidCloseStatusCode () ?? (data = code.Append (reason)).CheckIfValidControlData ("reason"); if (msg != null) { _logger.Error ( String.Format ("{0}\nstate: {1}\ncode: {2}\nreason: {3}", msg, _state, code, reason)); return; } _state = ServerState.ShuttingDown; } _services.Stop (data, !code.IsReserved ()); stopListener (5000); _state = ServerState.Stop; } /// /// Stops receiving the HTTP requests with the specified and /// used to stop the WebSocket services. /// /// /// One of the enum values, represents the status code indicating /// the reasons for stop. /// /// /// A that represents the reason for stop. /// public void Stop (CloseStatusCode code, string reason) { byte [] data = null; lock (_sync) { var msg = _state.CheckIfStart () ?? (data = ((ushort) code).Append (reason)).CheckIfValidControlData ("reason"); if (msg != null) { _logger.Error (String.Format ("{0}\nstate: {1}\nreason: {2}", msg, _state, reason)); return; } _state = ServerState.ShuttingDown; } _services.Stop (data, !code.IsReserved ()); stopListener (5000); _state = ServerState.Stop; } #endregion } }