#region License /* * WebSocketServer.cs * * 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. /// /// /// This class can provide multiple WebSocket services. /// public class WebSocketServer { #region Private Fields private System.Net.IPAddress _address; private bool _allowForwardedRequest; private AuthenticationSchemes _authSchemes; private static readonly string _defaultRealm; private bool _dnsStyle; private string _hostname; private TcpListener _listener; private Logger _log; private int _port; private string _realm; private string _realmInUse; private Thread _receiveThread; private bool _reuseAddress; private bool _secure; private WebSocketServiceManager _services; private ServerSslConfiguration _sslConfig; private ServerSslConfiguration _sslConfigInUse; 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. /// /// /// The new instance listens for incoming handshake requests on /// and port 80. /// public WebSocketServer () { var addr = System.Net.IPAddress.Any; init (addr.ToString (), addr, 80, false); } /// /// Initializes a new instance of the class /// with the specified . /// /// /// /// The new instance listens for incoming handshake requests on /// and . /// /// /// It provides secure connections if is 443. /// /// /// /// An that represents the number of the port /// on which to listen. /// /// /// is less than 1 or greater than 65535. /// public WebSocketServer (int port) : this (port, port == 443) { } /// /// Initializes a new instance of the class /// with the specified . /// /// /// /// The new instance listens for incoming handshake requests on /// the IP address of the host of and /// the port of . /// /// /// Either port 80 or 443 is used if includes /// no port. Port 443 is used if the scheme of /// is wss; otherwise, port 80 is used. /// /// /// The new instance provides secure connections if the scheme of /// is wss. /// /// /// /// A that represents the WebSocket URL of the server. /// /// /// is . /// /// /// /// is an empty string. /// /// /// -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 == 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 == "wss"); } /// /// Initializes a new instance of the class /// with the specified and . /// /// /// The new instance listens for incoming handshake requests on /// and . /// /// /// An that represents 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 WebSocketServer (int port, bool secure) { if (!port.IsPortNumber ()) { var msg = "Less than 1 or greater than 65535."; throw new ArgumentOutOfRangeException ("port", msg); } var addr = System.Net.IPAddress.Any; init (addr.ToString (), addr, port, secure); } /// /// Initializes a new instance of the class /// with the specified and . /// /// /// /// The new instance listens for incoming handshake requests on /// and . /// /// /// It provides secure connections if is 443. /// /// /// /// A that represents the local /// IP address on which to listen. /// /// /// An that represents 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 WebSocketServer (System.Net.IPAddress address, int port) : this (address, port, port == 443) { } /// /// Initializes a new instance of the class /// with the specified , , /// and . /// /// /// The new instance listens for incoming handshake requests on /// and . /// /// /// A that represents the local /// IP address on which to listen. /// /// /// An that represents 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 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"); if (!port.IsPortNumber ()) { var msg = "Less than 1 or greater than 65535."; throw new ArgumentOutOfRangeException ("port", msg); } init (address.ToString (), 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 handshake requests. /// public System.Net.IPAddress Address { get { return _address; } } /// /// Gets or sets a value indicating whether the server accepts every /// handshake request without checking the request URI. /// /// /// The set operation does nothing if the server has already started or /// it is shutting down. /// /// /// /// true if the server accepts every handshake request without /// checking the request URI; otherwise, false. /// /// /// The default value is false. /// /// public bool AllowForwardedRequest { get { return _allowForwardedRequest; } set { string msg; if (!canSet (out msg)) { _log.Warn (msg); return; } lock (_sync) { if (!canSet (out msg)) { _log.Warn (msg); return; } _allowForwardedRequest = value; } } } /// /// 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 _authSchemes; } set { string msg; if (!canSet (out msg)) { _log.Warn (msg); return; } lock (_sync) { if (!canSet (out msg)) { _log.Warn (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 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 handshake requests. /// public int Port { get { return _port; } } /// /// Gets or sets the realm used for authentication. /// /// /// /// "SECRET AREA" is used as 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 or by default. /// /// /// That string represents the name of the realm. /// /// public string Realm { get { return _realm; } set { string msg; if (!canSet (out msg)) { _log.Warn (msg); return; } lock (_sync) { if (!canSet (out msg)) { _log.Warn (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. /// /// /// /// 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 _reuseAddress; } set { string msg; if (!canSet (out msg)) { _log.Warn (msg); return; } lock (_sync) { if (!canSet (out msg)) { _log.Warn (msg); return; } _reuseAddress = value; } } } /// /// Gets the configuration for secure connection. /// /// /// This 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 instance does not provide secure connections. /// public ServerSslConfiguration SslConfiguration { get { if (!_secure) { var msg = "This instance does not provide secure connections."; throw new InvalidOperationException (msg); } return getSslConfiguration (); } } /// /// 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. /// /// /// That delegate invokes the method called for finding /// the credentials used to authenticate a client. /// /// /// The default value is . /// /// public Func UserCredentialsFinder { get { return _userCredFinder; } set { string msg; if (!canSet (out msg)) { _log.Warn (msg); return; } lock (_sync) { if (!canSet (out msg)) { _log.Warn (msg); return; } _userCredFinder = 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 Private Methods private void abort () { lock (_sync) { if (_state != ServerState.Start) return; _state = ServerState.ShuttingDown; } try { try { _listener.Stop (); } finally { _services.Stop (1006, String.Empty); } } catch { } _state = ServerState.Stop; } private bool authenticateClient (TcpListenerWebSocketContext context) { if (_authSchemes == AuthenticationSchemes.Anonymous) return true; if (_authSchemes == AuthenticationSchemes.None) return false; return context.Authenticate (_authSchemes, _realmInUse, _userCredFinder); } private bool canSet (out string message) { message = null; if (_state == ServerState.Start) { message = "The server has already started."; return false; } if (_state == ServerState.ShuttingDown) { message = "The server is shutting down."; return false; } return true; } private bool checkHostNameForRequest (string name) { return !_dnsStyle || Uri.CheckHostName (name) != UriHostNameType.Dns || name == _hostname; } private static bool checkSslConfiguration ( ServerSslConfiguration configuration, out string message ) { message = null; if (configuration.ServerCertificate == null) { message = "There is no server certificate for secure connection."; return false; } return true; } private string getRealm () { var realm = _realm; return realm != null && realm.Length > 0 ? realm : _defaultRealm; } private ServerSslConfiguration getSslConfiguration () { if (_sslConfig == null) _sslConfig = new ServerSslConfiguration (); return _sslConfig; } private void init ( string hostname, System.Net.IPAddress address, int port, bool secure ) { _hostname = hostname; _address = address; _port = port; _secure = secure; _authSchemes = AuthenticationSchemes.Anonymous; _dnsStyle = Uri.CheckHostName (hostname) == UriHostNameType.Dns; _listener = new TcpListener (address, port); _log = new Logger (); _services = new WebSocketServiceManager (_log); _sync = new object (); } private void processRequest (TcpListenerWebSocketContext context) { if (!authenticateClient (context)) { context.Close (HttpStatusCode.Forbidden); return; } var uri = context.RequestUri; if (uri == null) { context.Close (HttpStatusCode.BadRequest); return; } if (!_allowForwardedRequest) { if (uri.Port != _port) { context.Close (HttpStatusCode.BadRequest); return; } if (!checkHostNameForRequest (uri.DnsSafeHost)) { context.Close (HttpStatusCode.NotFound); 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) { TcpClient cl = null; try { cl = _listener.AcceptTcpClient (); ThreadPool.QueueUserWorkItem ( state => { try { var ctx = new TcpListenerWebSocketContext ( cl, null, _secure, _sslConfigInUse, _log ); processRequest (ctx); } catch (Exception ex) { _log.Error (ex.Message); _log.Debug (ex.ToString ()); cl.Close (); } } ); } catch (SocketException ex) { if (_state == ServerState.ShuttingDown) { _log.Info ("The underlying listener is stopped."); break; } _log.Fatal (ex.Message); _log.Debug (ex.ToString ()); break; } catch (Exception ex) { _log.Fatal (ex.Message); _log.Debug (ex.ToString ()); if (cl != null) cl.Close (); break; } } if (_state != ServerState.ShuttingDown) abort (); } private void start (ServerSslConfiguration sslConfig) { if (_state == ServerState.Start) { _log.Info ("The server has already started."); return; } if (_state == ServerState.ShuttingDown) { _log.Warn ("The server is shutting down."); return; } lock (_sync) { if (_state == ServerState.Start) { _log.Info ("The server has already started."); return; } if (_state == ServerState.ShuttingDown) { _log.Warn ("The server is shutting down."); return; } _sslConfigInUse = sslConfig; _realmInUse = getRealm (); _services.Start (); try { startReceiving (); } catch { _services.Stop (1011, String.Empty); throw; } _state = ServerState.Start; } } private void startReceiving () { if (_reuseAddress) { _listener.Server.SetSocketOption ( SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true ); } try { _listener.Start (); } catch (Exception ex) { var msg = "The underlying listener has failed to start."; throw new InvalidOperationException (msg, ex); } _receiveThread = new Thread (new ThreadStart (receiveRequest)); _receiveThread.IsBackground = true; _receiveThread.Start (); } private void stop (ushort code, string reason) { if (_state == ServerState.Ready) { _log.Info ("The server is not started."); return; } if (_state == ServerState.ShuttingDown) { _log.Info ("The server is shutting down."); return; } if (_state == ServerState.Stop) { _log.Info ("The server has already stopped."); return; } lock (_sync) { if (_state == ServerState.ShuttingDown) { _log.Info ("The server is shutting down."); return; } if (_state == ServerState.Stop) { _log.Info ("The server has already stopped."); return; } _state = ServerState.ShuttingDown; } try { var threw = false; try { stopReceiving (5000); } catch { threw = true; throw; } finally { try { _services.Stop (code, reason); } catch { if (!threw) throw; } } } finally { _state = ServerState.Stop; } } private void stopReceiving (int millisecondsTimeout) { try { _listener.Stop (); } catch (Exception ex) { var msg = "The underlying listener has failed to stop."; throw new InvalidOperationException (msg, ex); } _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 = "It includes either or both path and query components."; return false; } return true; } #endregion #region Public Methods /// /// Adds a WebSocket service with the specified behavior, path, /// and delegate. /// /// /// /// A that represents an absolute path to /// the service to add. /// /// /// / is trimmed from the end of the string if present. /// /// /// /// /// A Func<TBehavior> delegate. /// /// /// It invokes the method called when creating a new session /// instance for the service. /// /// /// The method must create a new instance of the specified /// behavior class and return it. /// /// /// /// /// The type of the behavior for the service. /// /// /// It must inherit the class. /// /// /// /// /// is . /// /// /// -or- /// /// /// 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. /// /// [Obsolete ("This method will be removed. Use added one instead.")] public void AddWebSocketService ( string path, Func creator ) where TBehavior : WebSocketBehavior { if (path == null) throw new ArgumentNullException ("path"); if (creator == null) throw new ArgumentNullException ("creator"); if (path.Length == 0) throw new ArgumentException ("An empty string.", "path"); if (path[0] != '/') throw new ArgumentException ("Not an absolute path.", "path"); if (path.IndexOfAny (new[] { '?', '#' }) > -1) { var msg = "It includes either or both query and fragment components."; throw new ArgumentException (msg, "path"); } _services.Add (path, creator); } /// /// Adds a WebSocket service with the specified behavior and path. /// /// /// /// A that represents 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 TBehaviorWithNew : WebSocketBehavior, new () { _services.AddService (path, null); } /// /// Adds a WebSocket service with the specified behavior, path, /// and delegate. /// /// /// /// A that represents an absolute path to /// the service to add. /// /// /// / is trimmed from the end of the string if present. /// /// /// /// /// An Action<TBehaviorWithNew> 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 TBehaviorWithNew : 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 represents 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 handshake 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 () { ServerSslConfiguration sslConfig = null; if (_secure) { sslConfig = new ServerSslConfiguration (getSslConfiguration ()); string msg; if (!checkSslConfiguration (sslConfig, out msg)) throw new InvalidOperationException (msg); } start (sslConfig); } /// /// Stops receiving incoming handshake requests. /// /// /// The underlying has failed to stop. /// public void Stop () { stop (1001, String.Empty); } /// /// Stops receiving incoming handshake requests and closes each connection /// with the specified code and reason. /// /// /// /// 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 in UTF-8. /// /// /// /// /// is less than 1000 or greater than 4999. /// /// /// -or- /// /// /// The size of is greater than 123 bytes. /// /// /// /// /// is 1010 (mandatory extension). /// /// /// -or- /// /// /// is 1005 (no status) and there is reason. /// /// /// -or- /// /// /// could not be UTF-8-encoded. /// /// /// /// The underlying has failed to stop. /// [Obsolete ("This method will be removed.")] public void Stop (ushort code, string reason) { if (!code.IsCloseStatusCode ()) { var msg = "Less than 1000 or greater than 4999."; throw new ArgumentOutOfRangeException ("code", msg); } if (code == 1010) { var msg = "1010 cannot be used."; throw new ArgumentException (msg, "code"); } if (!reason.IsNullOrEmpty ()) { if (code == 1005) { var msg = "1005 cannot be used."; throw new ArgumentException (msg, "code"); } byte[] bytes; if (!reason.TryGetUTF8EncodedBytes (out bytes)) { var msg = "It could not be UTF-8-encoded."; throw new ArgumentException (msg, "reason"); } if (bytes.Length > 123) { var msg = "Its size is greater than 123 bytes."; throw new ArgumentOutOfRangeException ("reason", msg); } } stop (code, reason); } /// /// Stops receiving incoming handshake requests and closes each connection /// with the specified code and reason. /// /// /// /// One of the enum values. /// /// /// It represents the status code indicating the reason for the close. /// /// /// /// /// A that represents the reason for the close. /// /// /// The size must be 123 bytes or less in UTF-8. /// /// /// /// /// is /// . /// /// /// -or- /// /// /// is /// and there is reason. /// /// /// -or- /// /// /// could not be UTF-8-encoded. /// /// /// /// The size of is greater than 123 bytes. /// /// /// The underlying has failed to stop. /// [Obsolete ("This method will be removed.")] public void Stop (CloseStatusCode code, string reason) { if (code == CloseStatusCode.MandatoryExtension) { var msg = "MandatoryExtension cannot be used."; throw new ArgumentException (msg, "code"); } if (!reason.IsNullOrEmpty ()) { if (code == CloseStatusCode.NoStatus) { var msg = "NoStatus cannot be used."; throw new ArgumentException (msg, "code"); } byte[] bytes; if (!reason.TryGetUTF8EncodedBytes (out bytes)) { var msg = "It could not be UTF-8-encoded."; throw new ArgumentException (msg, "reason"); } if (bytes.Length > 123) { var msg = "Its size is greater than 123 bytes."; throw new ArgumentOutOfRangeException ("reason", msg); } } stop ((ushort) code, reason); } #endregion } }