#region License /* * WebSocketServer.cs * * A C# implementation of the WebSocket protocol server. * * The MIT License * * Copyright (c) 2012-2015 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 * - Jonas Hovgaard * - Liryna * - Rohan Singh */ #endregion using System; using System.Collections.Generic; 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 WebSocket protocol server. /// /// /// The WebSocketServer class can provide multiple WebSocket services. /// public class WebSocketServer { #region Private Fields private System.Net.IPAddress _address; private AuthenticationSchemes _authSchemes; private static readonly string _defaultRealm; private bool _dnsStyle; private string _hostname; private TcpListener _listener; private Logger _logger; private int _port; private string _realm; private Thread _receiveThread; private bool _reuseAddress; private bool _secure; private WebSocketServiceManager _services; private ServerSslConfiguration _sslConfig; private volatile ServerState _state; private object _sync; private Func _userCredFinder; #endregion #region Static Constructor static WebSocketServer () { _defaultRealm = "SECRET AREA"; } #endregion #region Public Constructors /// /// Initializes a new instance of the class. /// /// /// An instance initialized by this constructor listens for the incoming connection requests on /// port 80. /// public WebSocketServer () { init (null, System.Net.IPAddress.Any, 80, false); } /// /// Initializes a new instance of the class with /// the specified . /// /// /// /// An instance initialized by this constructor listens for the incoming connection requests /// on . /// /// /// 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 inclusive. /// public WebSocketServer (int port) : this (port, port == 443) { } /// /// Initializes a new instance of the class with /// the specified WebSocket URL. /// /// /// /// An instance initialized by this constructor listens for the incoming connection requests /// on the host name and port in . /// /// /// If doesn't include a port, either port 80 or 443 is used on /// which to listen. It's determined by the scheme (ws or wss) in . /// (Port 80 if the scheme is ws.) /// /// /// /// A that represents the WebSocket URL of the server. /// /// /// is . /// /// /// /// is empty. /// /// /// -or- /// /// /// is invalid. /// /// public WebSocketServer (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.DnsSafeHost; var addr = host.ToIPAddress (); if (!addr.IsLocal ()) throw new ArgumentException ("The host part isn't a local host name: " + url, "url"); init (host, addr, uri.Port, uri.Scheme == "wss"); } /// /// Initializes a new instance of the class with /// the specified and . /// /// /// An instance initialized by this constructor listens for the incoming connection 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 inclusive. /// public WebSocketServer (int port, bool secure) { if (!port.IsPortNumber ()) throw new ArgumentOutOfRangeException ( "port", "Not between 1 and 65535 inclusive: " + port); init (null, System.Net.IPAddress.Any, port, secure); } /// /// Initializes a new instance of the class with /// the specified and . /// /// /// /// An instance initialized by this constructor listens for the incoming connection requests /// on and . /// /// /// If is 443, that instance provides a secure connection. /// /// /// /// A that represents the local IP address of the server. /// /// /// An that represents the port number on which to listen. /// /// /// is . /// /// /// isn't a local IP address. /// /// /// isn't between 1 and 65535 inclusive. /// public WebSocketServer (System.Net.IPAddress address, int port) : this (address, port, port == 443) { } /// /// Initializes a new instance of the class with /// the specified , , /// and . /// /// /// An instance initialized by this constructor listens for the incoming connection requests on /// and . /// /// /// A that represents the local IP address of the server. /// /// /// 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.) /// /// /// is . /// /// /// isn't a local IP address. /// /// /// isn't between 1 and 65535 inclusive. /// public WebSocketServer (System.Net.IPAddress address, int port, bool secure) { if (address == null) throw new ArgumentNullException ("address"); if (!address.IsLocal ()) throw new ArgumentException ("Not a local IP address: " + address, "address"); if (!port.IsPortNumber ()) throw new ArgumentOutOfRangeException ( "port", "Not between 1 and 65535 inclusive: " + port); init (null, address, port, secure); } #endregion #region Public Properties /// /// Gets the local IP address of the server. /// /// /// A that represents the local IP address of the server. /// public System.Net.IPAddress Address { get { return _address; } } /// /// 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 _authSchemes; } set { var msg = _state.CheckIfAvailable (true, false, false); if (msg != null) { _logger.Error (msg); return; } _authSchemes = 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 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 { var msg = _state.CheckIfAvailable (true, false, false); if (msg != null) { _logger.Error (msg); return; } _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 connection 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. /// /// /// If this property is or empty, "SECRET AREA" will be used as /// the name of the realm. /// /// /// A that represents the name of the realm. The default value is /// . /// public string Realm { get { return _realm; } set { var msg = _state.CheckIfAvailable (true, false, false); if (msg != null) { _logger.Error (msg); return; } _realm = value; } } /// /// Gets or sets a value indicating whether the server is allowed to be bound to /// an address that is already in use. /// /// /// If you would like to resolve to wait for socket in TIME_WAIT state, /// you should set this property to true. /// /// /// 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 _reuseAddress; } set { var msg = _state.CheckIfAvailable (true, false, false); if (msg != null) { _logger.Error (msg); return; } _reuseAddress = value; } } /// /// Gets or sets the SSL configuration used to authenticate the server and /// optionally the client for secure connection. /// /// /// A that represents the configuration used to /// authenticate the server and optionally the client for secure connection. /// public ServerSslConfiguration SslConfiguration { get { return _sslConfig ?? (_sslConfig = new ServerSslConfiguration (null)); } set { var msg = _state.CheckIfAvailable (true, false, false); if (msg != null) { _logger.Error (msg); return; } _sslConfig = 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 /// . /// public Func UserCredentialsFinder { get { return _userCredFinder; } set { var msg = _state.CheckIfAvailable (true, false, false); if (msg != null) { _logger.Error (msg); return; } _userCredFinder = value; } } /// /// Gets or sets the wait time for the response to the WebSocket Ping or Close. /// /// /// A that represents the wait time. The default value is /// the same as 1 second. /// public TimeSpan WaitTime { get { return _services.WaitTime; } set { var msg = _state.CheckIfAvailable (true, false, false) ?? value.CheckIfValidWaitTime (); if (msg != null) { _logger.Error (msg); return; } _services.WaitTime = 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 Private Methods private void abort () { lock (_sync) { if (!IsListening) return; _state = ServerState.ShuttingDown; } _listener.Stop (); _services.Stop (new CloseEventArgs (CloseStatusCode.ServerError), true, false); _state = ServerState.Stop; } private bool checkIfAvailable ( bool ready, bool start, bool shutting, bool stop, out string message ) { message = null; if (!ready && _state == ServerState.Ready) { message = "This operation is not available in: ready"; return false; } if (!start && _state == ServerState.Start) { message = "This operation is not available in: start"; return false; } if (!shutting && _state == ServerState.ShuttingDown) { message = "This operation is not available in: shutting down"; return false; } if (!stop && _state == ServerState.Stop) { message = "This operation is not available in: stop"; return false; } return true; } private string checkIfCertificateExists () { return _secure && (_sslConfig == null || _sslConfig.ServerCertificate == null) ? "The secure connection requires a server certificate." : null; } private string getRealm () { var realm = _realm; return realm != null && realm.Length > 0 ? realm : _defaultRealm; } private void init (string hostname, System.Net.IPAddress address, int port, bool secure) { _hostname = hostname ?? address.ToString (); _address = address; _port = port; _secure = secure; _authSchemes = AuthenticationSchemes.Anonymous; _dnsStyle = Uri.CheckHostName (hostname) == UriHostNameType.Dns; _listener = new TcpListener (address, port); _logger = new Logger (); _services = new WebSocketServiceManager (_logger); _sync = new object (); } private void processRequest (TcpListenerWebSocketContext context) { var uri = context.RequestUri; if (uri == null || uri.Port != _port) { context.Close (HttpStatusCode.BadRequest); return; } if (_dnsStyle) { var hostname = uri.DnsSafeHost; if (Uri.CheckHostName (hostname) == UriHostNameType.Dns && hostname != _hostname) { context.Close (HttpStatusCode.NotFound); return; } } WebSocketServiceHost host; if (!_services.InternalTryGetServiceHost (uri.AbsolutePath, out host)) { context.Close (HttpStatusCode.NotImplemented); return; } host.StartSession (context); } private void receiveRequest () { while (true) { try { var cl = _listener.AcceptTcpClient (); ThreadPool.QueueUserWorkItem ( state => { try { var ctx = cl.GetWebSocketContext (null, _secure, _sslConfig, _logger); if (!ctx.Authenticate (_authSchemes, getRealm (), _userCredFinder)) return; processRequest (ctx); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); cl.Close (); } } ); } catch (SocketException ex) { _logger.Warn ("Receiving has been stopped.\n reason: " + ex.Message); break; } catch (Exception ex) { _logger.Fatal (ex.ToString ()); break; } } if (IsListening) abort (); } private void startReceiving () { if (_reuseAddress) _listener.Server.SetSocketOption ( SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); _listener.Start (); _receiveThread = new Thread (new ThreadStart (receiveRequest)); _receiveThread.IsBackground = true; _receiveThread.Start (); } private void stopReceiving (int millisecondsTimeout) { _listener.Stop (); _receiveThread.Join (millisecondsTimeout); } private static bool tryCreateUri (string uriString, out Uri result, out string message) { if (!uriString.TryCreateWebSocketUri (out result, out message)) return false; if (result.PathAndQuery != "/") { result = null; message = "Includes the path or query component: " + uriString; return false; } return true; } #endregion #region Public Methods /// /// Adds a WebSocket service with the specified behavior, , /// and . /// /// /// /// This method converts to URL-decoded string, /// and removes '/' from tail end of . /// /// /// returns an initialized specified typed /// instance. /// /// /// /// A that represents the absolute path to the 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 behavior of the service to add. The TBehavior must inherit /// the class. /// public void AddWebSocketService (string path, Func initializer) where TBehavior : WebSocketBehavior { var msg = path.CheckIfValidServicePath () ?? (initializer == null ? "'initializer' is null." : null); if (msg != null) { _logger.Error (msg); return; } _services.Add (path, initializer); } /// /// Adds a WebSocket service with the specified behavior and . /// /// /// This method converts to URL-decoded string, /// and removes '/' from tail end of . /// /// /// A that represents the absolute path to the service to add. /// /// /// The type of the behavior of the service to add. The TBehaviorWithNew must inherit /// the class, and must have a public parameterless /// constructor. /// public void AddWebSocketService (string path) where TBehaviorWithNew : WebSocketBehavior, new () { AddWebSocketService (path, () => new TBehaviorWithNew ()); } /// /// Removes the WebSocket service with the specified . /// /// /// This method converts to URL-decoded string, /// and removes '/' from tail end of . /// /// /// true if the service is successfully found and removed; otherwise, false. /// /// /// A that represents the absolute path to the service to find. /// public bool RemoveWebSocketService (string path) { var msg = path.CheckIfValidServicePath (); if (msg != null) { _logger.Error (msg); return false; } return _services.Remove (path); } /// /// Starts receiving the WebSocket connection requests. /// public void Start () { lock (_sync) { var msg = _state.CheckIfAvailable (true, false, false) ?? checkIfCertificateExists (); if (msg != null) { _logger.Error (msg); return; } _services.Start (); startReceiving (); _state = ServerState.Start; } } /// /// Stops receiving the WebSocket handshake requests, and closes /// the WebSocket connections. /// public void Stop () { string msg; if (!checkIfAvailable (false, true, false, false, out msg)) { _logger.Error (msg); return; } lock (_sync) { if (!checkIfAvailable (false, true, false, false, out msg)) { _logger.Error (msg); return; } _state = ServerState.ShuttingDown; } stopReceiving (5000); _services.Stop (new CloseEventArgs (), true, true); _state = ServerState.Stop; } /// /// Stops receiving the WebSocket handshake requests, and closes /// the WebSocket connections with the specified and /// . /// /// /// A that represents the status code indicating /// the reason for the close. The status codes are defined in /// /// Section 7.4 of RFC 6455. /// /// /// A that represents the reason for the close. /// The size must be 123 bytes or less. /// public void Stop (ushort code, string reason) { string msg; if (!checkIfAvailable (false, true, false, false, out msg)) { _logger.Error (msg); return; } if (!WebSocket.CheckParametersForClose (code, reason, false, out msg)) { _logger.Error (msg); return; } lock (_sync) { if (!checkIfAvailable (false, true, false, false, out msg)) { _logger.Error (msg); return; } _state = ServerState.ShuttingDown; } stopReceiving (5000); if (code == (ushort) CloseStatusCode.NoStatus) { _services.Stop (new CloseEventArgs (), true, true); } else { var send = !code.IsReserved (); _services.Stop (new CloseEventArgs (code, reason), send, send); } _state = ServerState.Stop; } /// /// Stops receiving the WebSocket connection requests with /// the specified and . /// /// /// One of the enum values, represents the status code indicating /// the reason for the stop. /// /// /// A that represents the reason for the stop. /// public void Stop (CloseStatusCode code, string reason) { string msg; if (!checkIfAvailable (false, true, false, false, out msg)) { _logger.Error (msg); return; } if (!WebSocket.CheckParametersForClose (code, reason, false, out msg)) { _logger.Error (msg); return; } lock (_sync) { if (!checkIfAvailable (false, true, false, false, out msg)) { _logger.Error (msg); return; } _state = ServerState.ShuttingDown; } stopReceiving (5000); if (code == CloseStatusCode.NoStatus) { _services.Stop (new CloseEventArgs (), true, true); } else { var send = !code.IsReserved (); _services.Stop (new CloseEventArgs (code, reason), send, send); } _state = ServerState.Stop; } #endregion } }