#region License /* * HttpServer.cs * * The MIT License * * Copyright (c) 2012-2022 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 * - Liryna * - Rohan Singh */ #endregion using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using System.Text; using System.Threading; using WebSocketSharp.Net; using WebSocketSharp.Net.WebSockets; namespace WebSocketSharp.Server { /// /// Provides a simple HTTP server. /// /// /// /// The server supports HTTP/1.1 version request and response. /// /// /// And the server allows to accept WebSocket handshake requests. /// /// /// This class can provide multiple WebSocket services. /// /// public class HttpServer { #region Private Fields private System.Net.IPAddress _address; private string _docRootPath; private string _hostname; private HttpListener _listener; private Logger _log; private int _port; private Thread _receiveThread; private bool _secure; private WebSocketServiceManager _services; private volatile ServerState _state; private object _sync; #endregion #region Public Constructors /// /// Initializes a new instance of the class. /// /// /// The new instance listens for incoming requests on /// and port 80. /// public HttpServer () { init ("*", System.Net.IPAddress.Any, 80, false); } /// /// Initializes a new instance of the class with /// the specified port. /// /// /// /// The new instance listens for incoming requests on /// and . /// /// /// It provides secure connections if is 443. /// /// /// /// An that specifies the number of the port on which /// to listen. /// /// /// is less than 1 or greater than 65535. /// public HttpServer (int port) : this (port, port == 443) { } /// /// Initializes a new instance of the class with /// the specified URL. /// /// /// /// The new instance listens for incoming requests on the IP address and /// port of . /// /// /// Either port 80 or 443 is used if includes /// no port. Port 443 is used if the scheme of /// is https; otherwise, port 80 is used. /// /// /// The new instance provides secure connections if the scheme of /// is https. /// /// /// /// A that specifies the HTTP URL of the server. /// /// /// is . /// /// /// /// is an empty string. /// /// /// -or- /// /// /// is invalid. /// /// public HttpServer (string url) { if (url == null) throw new ArgumentNullException ("url"); if (url.Length == 0) throw new ArgumentException ("An empty string.", "url"); Uri uri; string msg; if (!tryCreateUri (url, out uri, out msg)) throw new ArgumentException (msg, "url"); var host = uri.GetDnsSafeHost (true); var addr = host.ToIPAddress (); if (addr == null) { msg = "The host part could not be converted to an IP address."; throw new ArgumentException (msg, "url"); } if (!addr.IsLocal ()) { msg = "The IP address of the host is not a local IP address."; throw new ArgumentException (msg, "url"); } init (host, addr, uri.Port, uri.Scheme == "https"); } /// /// Initializes a new instance of the class with /// the specified port and boolean if secure or not. /// /// /// The new instance listens for incoming requests on /// and . /// /// /// An that specifies the number of the port on which /// to listen. /// /// /// A : true if the new instance provides /// secure connections; otherwise, false. /// /// /// is less than 1 or greater than 65535. /// public HttpServer (int port, bool secure) { if (!port.IsPortNumber ()) { var msg = "It is less than 1 or greater than 65535."; throw new ArgumentOutOfRangeException ("port", msg); } init ("*", System.Net.IPAddress.Any, port, secure); } /// /// Initializes a new instance of the class with /// the specified IP address and port. /// /// /// /// The new instance listens for incoming requests on /// and . /// /// /// It provides secure connections if is 443. /// /// /// /// A that specifies the local IP /// address on which to listen. /// /// /// An that specifies the number of the port on which /// to listen. /// /// /// is . /// /// /// is not a local IP address. /// /// /// is less than 1 or greater than 65535. /// public HttpServer (System.Net.IPAddress address, int port) : this (address, port, port == 443) { } /// /// Initializes a new instance of the class with /// the specified IP address, port, and boolean if secure or not. /// /// /// The new instance listens for incoming requests on /// and . /// /// /// A that specifies the local IP /// address on which to listen. /// /// /// An that specifies the number of the port on which /// to listen. /// /// /// A : true if the new instance provides /// secure connections; otherwise, false. /// /// /// is . /// /// /// is not a local IP address. /// /// /// is less than 1 or greater than 65535. /// public HttpServer (System.Net.IPAddress address, int port, bool secure) { if (address == null) throw new ArgumentNullException ("address"); if (!address.IsLocal ()) { var msg = "It is not a local IP address."; throw new ArgumentException (msg, "address"); } if (!port.IsPortNumber ()) { var msg = "It is less than 1 or greater than 65535."; throw new ArgumentOutOfRangeException ("port", msg); } init (address.ToString (true), address, port, secure); } #endregion #region Public Properties /// /// Gets the IP address of the server. /// /// /// A that represents the local IP /// address on which to listen for incoming requests. /// public System.Net.IPAddress Address { get { return _address; } } /// /// Gets or sets the scheme used to authenticate the clients. /// /// /// The set operation does nothing if the server has already started or /// it is shutting down. /// /// /// /// One of the /// enum values. /// /// /// It represents the scheme used to authenticate the clients. /// /// /// The default value is /// . /// /// public AuthenticationSchemes AuthenticationSchemes { get { return _listener.AuthenticationSchemes; } set { lock (_sync) { if (!canSet ()) return; _listener.AuthenticationSchemes = value; } } } /// /// Gets or sets the path to the document folder of the server. /// /// /// /// '/' or '\' is trimmed from the end of the value if any. /// /// /// The set operation does nothing if the server has already /// started or it is shutting down. /// /// /// /// /// A that represents a path to the folder /// from which to find the requested file. /// /// /// The default value is "./Public". /// /// /// /// The value specified for a set operation is . /// /// /// /// The value specified for a set operation is an empty string. /// /// /// -or- /// /// /// The value specified for a set operation is an absolute root. /// /// /// -or- /// /// /// The value specified for a set operation is an invalid path string. /// /// public string DocumentRootPath { get { return _docRootPath; } set { if (value == null) throw new ArgumentNullException ("value"); if (value.Length == 0) throw new ArgumentException ("An empty string.", "value"); value = value.TrimSlashOrBackslashFromEnd (); if (value == "/") throw new ArgumentException ("An absolute root.", "value"); if (value == "\\") throw new ArgumentException ("An absolute root.", "value"); if (value.Length == 2 && value[1] == ':') throw new ArgumentException ("An absolute root.", "value"); string full = null; try { full = Path.GetFullPath (value); } catch (Exception ex) { throw new ArgumentException ("An invalid path string.", "value", ex); } if (full == "/") throw new ArgumentException ("An absolute root.", "value"); full = full.TrimSlashOrBackslashFromEnd (); if (full.Length == 2 && full[1] == ':') throw new ArgumentException ("An absolute root.", "value"); lock (_sync) { if (!canSet ()) return; _docRootPath = 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 secure connections are provided. /// /// /// true if this instance provides secure connections; otherwise, /// false. /// public bool IsSecure { get { return _secure; } } /// /// Gets or sets a value indicating whether the server cleans up the /// inactive sessions periodically. /// /// /// The set operation does nothing if the server has already started or /// it is shutting down. /// /// /// /// 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 function for the server. /// /// /// The default logging level is . /// /// /// A that provides the logging function. /// public Logger Log { get { return _log; } } /// /// Gets the port of the server. /// /// /// An that represents the number of the port on which /// to listen for incoming requests. /// public int Port { get { return _port; } } /// /// Gets or sets the name of the realm associated with the server. /// /// /// /// "SECRET AREA" is used as the name of the realm if the value is /// or an empty string. /// /// /// The set operation does nothing if the server has already started /// or it is shutting down. /// /// /// /// /// A that represents the name of the realm or /// . /// /// /// The default value is . /// /// public string Realm { get { return _listener.Realm; } set { lock (_sync) { if (!canSet ()) return; _listener.Realm = value; } } } /// /// Gets or sets a value indicating whether the server is allowed to /// be bound to an address that is already in use. /// /// /// /// You should set this property to true if you would like to /// resolve to wait for socket in TIME_WAIT state. /// /// /// The set operation does nothing if the server has already started /// or it is shutting down. /// /// /// /// /// true if the server is allowed to be bound to an address /// that is already in use; otherwise, false. /// /// /// The default value is false. /// /// public bool ReuseAddress { get { return _listener.ReuseAddress; } set { lock (_sync) { if (!canSet ()) return; _listener.ReuseAddress = value; } } } /// /// Gets the configuration for secure connection. /// /// /// The configuration will be referenced when attempts to start, /// so it must be configured before the start method is called. /// /// /// A that represents /// the configuration used to provide secure connections. /// /// /// This server does not provide secure connections. /// public ServerSslConfiguration SslConfiguration { get { if (!_secure) { var msg = "The server does not provide secure connections."; throw new InvalidOperationException (msg); } return _listener.SslConfiguration; } } /// /// Gets or sets the delegate used to find the credentials for /// an identity. /// /// /// /// No credentials are found if the method invoked by /// the delegate returns or /// the value is . /// /// /// The set operation does nothing if the server has /// already started or it is shutting down. /// /// /// /// /// A Func<, /// > delegate or /// if not needed. /// /// /// The delegate invokes the method called for finding /// the credentials used to authenticate a client. /// /// /// The default value is . /// /// public Func UserCredentialsFinder { get { return _listener.UserCredentialsFinder; } set { lock (_sync) { if (!canSet ()) return; _listener.UserCredentialsFinder = value; } } } /// /// Gets or sets the time to wait for the response to the WebSocket /// Ping or Close. /// /// /// The set operation does nothing if the server has already started or /// it is shutting down. /// /// /// /// A to wait for the response. /// /// /// The default value is the same as 1 second. /// /// /// /// The value specified for a set operation is zero or less. /// public TimeSpan WaitTime { get { return _services.WaitTime; } set { _services.WaitTime = value; } } /// /// Gets the management function for the WebSocket services provided by /// the server. /// /// /// A that manages the WebSocket /// services provided by the server. /// 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 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 (_state != ServerState.Start) return; _state = ServerState.ShuttingDown; } try { _services.Stop (1006, String.Empty); } catch (Exception ex) { _log.Fatal (ex.Message); _log.Debug (ex.ToString ()); } try { _listener.Abort (); } catch (Exception ex) { _log.Fatal (ex.Message); _log.Debug (ex.ToString ()); } _state = ServerState.Stop; } private bool canSet () { return _state == ServerState.Ready || _state == ServerState.Stop; } private bool checkCertificate (out string message) { message = null; var byUser = _listener.SslConfiguration.ServerCertificate != null; var path = _listener.CertificateFolderPath; var withPort = EndPointListener.CertificateExists (_port, path); var either = byUser || withPort; if (!either) { message = "There is no server certificate for secure connection."; return false; } var both = byUser && withPort; if (both) { var msg = "The server certificate associated with the port is used."; _log.Warn (msg); } return true; } private static HttpListener createListener ( string hostname, int port, bool secure ) { var lsnr = new HttpListener (); var schm = secure ? "https" : "http"; var pref = String.Format ("{0}://{1}:{2}/", schm, hostname, port); lsnr.Prefixes.Add (pref); return lsnr; } private void init ( string hostname, System.Net.IPAddress address, int port, bool secure ) { _hostname = hostname; _address = address; _port = port; _secure = secure; _docRootPath = "./Public"; _listener = createListener (_hostname, _port, _secure); _log = _listener.Log; _services = new WebSocketServiceManager (_log); _sync = new object (); } private void processRequest (HttpListenerContext context) { var method = context.Request.HttpMethod; var evt = method == "GET" ? OnGet : method == "HEAD" ? OnHead : method == "POST" ? OnPost : method == "PUT" ? OnPut : method == "DELETE" ? OnDelete : method == "CONNECT" ? OnConnect : method == "OPTIONS" ? OnOptions : method == "TRACE" ? OnTrace : null; if (evt == null) { context.ErrorStatusCode = 501; context.SendError (); return; } var e = new HttpRequestEventArgs (context, _docRootPath); evt (this, e); context.Response.Close (); } private void processRequest (HttpListenerWebSocketContext context) { var uri = context.RequestUri; if (uri == null) { context.Close (HttpStatusCode.BadRequest); return; } var path = uri.AbsolutePath; if (path.IndexOfAny (new[] { '%', '+' }) > -1) path = HttpUtility.UrlDecode (path, Encoding.UTF8); WebSocketServiceHost host; if (!_services.InternalTryGetServiceHost (path, out host)) { context.Close (HttpStatusCode.NotImplemented); return; } host.StartSession (context); } private void receiveRequest () { while (true) { HttpListenerContext ctx = null; try { ctx = _listener.GetContext (); ThreadPool.QueueUserWorkItem ( state => { try { if (ctx.Request.IsUpgradeRequest ("websocket")) { processRequest (ctx.GetWebSocketContext (null)); return; } processRequest (ctx); } catch (Exception ex) { _log.Error (ex.Message); _log.Debug (ex.ToString ()); ctx.Connection.Close (true); } } ); } catch (HttpListenerException ex) { if (_state == ServerState.ShuttingDown) { _log.Info ("The underlying listener is stopped."); return; } _log.Fatal (ex.Message); _log.Debug (ex.ToString ()); break; } catch (InvalidOperationException ex) { if (_state == ServerState.ShuttingDown) { _log.Info ("The underlying listener is stopped."); return; } _log.Fatal (ex.Message); _log.Debug (ex.ToString ()); break; } catch (Exception ex) { _log.Fatal (ex.Message); _log.Debug (ex.ToString ()); if (ctx != null) ctx.Connection.Close (true); if (_state == ServerState.ShuttingDown) return; break; } } abort (); } private void start () { lock (_sync) { if (_state == ServerState.Start || _state == ServerState.ShuttingDown) return; if (_secure) { string msg; if (!checkCertificate (out msg)) throw new InvalidOperationException (msg); } _services.Start (); try { startReceiving (); } catch { _services.Stop (1011, String.Empty); throw; } _state = ServerState.Start; } } private void startReceiving () { try { _listener.Start (); } catch (Exception ex) { var msg = "The underlying listener has failed to start."; throw new InvalidOperationException (msg, ex); } var receiver = new ThreadStart (receiveRequest); _receiveThread = new Thread (receiver); _receiveThread.IsBackground = true; _receiveThread.Start (); } private void stop (ushort code, string reason) { lock (_sync) { if (_state != ServerState.Start) return; _state = ServerState.ShuttingDown; } try { _services.Stop (code, reason); } catch (Exception ex) { _log.Fatal (ex.Message); _log.Debug (ex.ToString ()); } try { stopReceiving (5000); } catch (Exception ex) { _log.Fatal (ex.Message); _log.Debug (ex.ToString ()); } _state = ServerState.Stop; } private void stopReceiving (int millisecondsTimeout) { _listener.Stop (); _receiveThread.Join (millisecondsTimeout); } private static bool tryCreateUri ( string uriString, out Uri result, out string message ) { result = null; message = null; var uri = uriString.ToUri (); if (uri == null) { message = "An invalid URI string."; return false; } if (!uri.IsAbsoluteUri) { message = "A relative URI."; return false; } var schm = uri.Scheme; var http = schm == "http" || schm == "https"; if (!http) { message = "The scheme part is not 'http' or 'https'."; return false; } if (uri.PathAndQuery != "/") { message = "It includes either or both path and query components."; return false; } if (uri.Fragment.Length > 0) { message = "It includes the fragment component."; return false; } if (uri.Port == 0) { message = "The port part is zero."; return false; } result = uri; return true; } #endregion #region Public Methods /// /// Adds a WebSocket service with the specified behavior and path. /// /// /// /// A that specifies an absolute path to /// the service to add. /// /// /// / is trimmed from the end of the string if present. /// /// /// /// /// The type of the behavior for the service. /// /// /// It must inherit the class. /// /// /// And also, it must have a public parameterless constructor. /// /// /// /// is . /// /// /// /// is an empty string. /// /// /// -or- /// /// /// is not an absolute path. /// /// /// -or- /// /// /// includes either or both /// query and fragment components. /// /// /// -or- /// /// /// is already in use. /// /// public void AddWebSocketService (string path) where TBehavior : WebSocketBehavior, new () { _services.AddService (path, null); } /// /// Adds a WebSocket service with the specified behavior, path, /// and delegate. /// /// /// /// A that specifies an absolute path to /// the service to add. /// /// /// / is trimmed from the end of the string if present. /// /// /// /// /// An Action<TBehavior> delegate or /// if not needed. /// /// /// The delegate invokes the method called when initializing /// a new session instance for the service. /// /// /// /// /// The type of the behavior for the service. /// /// /// It must inherit the class. /// /// /// And also, it must have a public parameterless constructor. /// /// /// /// is . /// /// /// /// is an empty string. /// /// /// -or- /// /// /// is not an absolute path. /// /// /// -or- /// /// /// includes either or both /// query and fragment components. /// /// /// -or- /// /// /// is already in use. /// /// public void AddWebSocketService ( string path, Action initializer ) where TBehavior : WebSocketBehavior, new () { _services.AddService (path, initializer); } /// /// Removes a WebSocket service with the specified path. /// /// /// The service is stopped with close status 1001 (going away) /// if it has already started. /// /// /// true if the service is successfully found and removed; /// otherwise, false. /// /// /// /// A that specifies an absolute path to /// the service to remove. /// /// /// / is trimmed from the end of the string if present. /// /// /// /// is . /// /// /// /// is an empty string. /// /// /// -or- /// /// /// is not an absolute path. /// /// /// -or- /// /// /// includes either or both /// query and fragment components. /// /// public bool RemoveWebSocketService (string path) { return _services.RemoveService (path); } /// /// Starts receiving incoming requests. /// /// /// This method does nothing if the server has already started or /// it is shutting down. /// /// /// /// There is no server certificate for secure connection. /// /// /// -or- /// /// /// The underlying has failed to start. /// /// public void Start () { if (_state == ServerState.Start || _state == ServerState.ShuttingDown) return; start (); } /// /// Stops receiving incoming requests. /// /// /// This method does nothing if the server is not started, /// it is shutting down, or it has already stopped. /// public void Stop () { if (_state != ServerState.Start) return; stop (1001, String.Empty); } #endregion } }