#region License /* * WebSocketServer.cs * * A C# implementation of the WebSocket protocol server. * * 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 #region Contributors /* * Contributors: * Juan Manuel Lallana */ #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 the functions of the server that receives the WebSocket connection /// requests. /// /// /// The WebSocketServer class provides the multi WebSocket service. /// public class WebSocketServer { #region Private Fields private System.Net.IPAddress _address; private AuthenticationSchemes _authSchemes; private X509Certificate2 _cert; private Func _credentialsFinder; private TcpListener _listener; private Logger _logger; private int _port; private string _realm; private Thread _receiveRequestThread; private bool _secure; private WebSocketServiceHostManager _serviceHosts; private volatile ServerState _state; private object _sync; private Uri _uri; #endregion #region Public Constructors /// /// Initializes a new instance of the class /// that listens for incoming requests on port 80. /// public WebSocketServer () : this (80) { } /// /// Initializes a new instance of the class /// that listens for incoming connection attempts on the specified /// . /// /// /// An that contains a port number. /// /// /// is not between 1 and 65535. /// public WebSocketServer (int port) : this (System.Net.IPAddress.Any, port) { } /// /// Initializes a new instance of the class /// that listens for incoming connection attempts on the specified WebSocket /// URL. /// /// /// A that contains a WebSocket URL. /// /// /// is . /// /// /// is invalid. /// public WebSocketServer (string url) { if (url == null) throw new ArgumentNullException ("url"); string msg; if (!tryCreateUri (url, out _uri, out msg)) throw new ArgumentException (msg, "url"); var host = _uri.DnsSafeHost; _address = host.ToIPAddress (); if (_address == null || !_address.IsLocal ()) throw new ArgumentException ( String.Format ( "The host part must be the local host name: {0}", host), "url"); _port = _uri.Port; _secure = _uri.Scheme == "wss" ? true : false; init (); } /// /// Initializes a new instance of the class /// that listens for incoming connection attempts 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 not between 1 and 65535. /// /// /// Pair of and is invalid. /// public WebSocketServer (int port, bool secure) : this (System.Net.IPAddress.Any, port, secure) { } /// /// Initializes a new instance of the class /// that listens for incoming connection attempts on the specified /// and . /// /// /// A that represents the local IP /// address. /// /// /// An that contains a port number. /// /// /// is . /// /// /// is not between 1 and 65535. /// /// /// is not the local IP address. /// public WebSocketServer (System.Net.IPAddress address, int port) : this (address, port, port == 443 ? true : false) { } /// /// Initializes a new instance of the class /// that listens for incoming connection attempts on the specified /// , and /// . /// /// /// A that represents the local IP /// address. /// /// /// An that contains a port number. /// /// /// A that indicates providing a secure connection or not. /// (true indicates providing a secure connection.) /// /// /// is . /// /// /// is not between 1 and 65535. /// /// /// /// is not the local IP address. /// /// /// -or- /// /// /// Pair of and is /// invalid. /// /// public WebSocketServer (System.Net.IPAddress address, int port, bool secure) { if (!address.IsLocal ()) throw new ArgumentException ( String.Format ( "Must be the local IP address: {0}", address), "address"); 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)); _address = address; _port = port; _secure = secure; _uri = "/".ToUri (); init (); } #endregion #region Public Properties /// /// Gets the local IP address on which to listen for incoming connection /// attempts. /// /// /// A that represents the local IP /// address. /// public System.Net.IPAddress Address { get { return _address; } } /// /// Gets or sets the scheme used to authenticate the clients. /// /// /// One of the values /// that indicates the scheme used to authenticate the clients. The default /// value is . /// public AuthenticationSchemes AuthenticationSchemes { get { return _authSchemes; } set { if (!canSet ("AuthenticationSchemes")) return; _authSchemes = 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 _cert; } set { if (!canSet ("Certificate")) return; _cert = 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 _state == ServerState.START; } } /// /// 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 /// 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 _serviceHosts.KeepClean; } set { _serviceHosts.KeepClean = value; } } /// /// Gets the logging functions. /// /// /// The default logging level is the . If you /// change the current logging level, you set the Log.Level property /// to any of the values. /// /// /// A that provides the logging functions. /// public Logger Log { get { return _logger; } } /// /// Gets the port on which to listen for incoming connection attempts. /// /// /// An that contains a port number. /// public int Port { get { return _port; } } /// /// Gets or sets the name of the realm associated with the /// . /// /// /// A that contains the name of the realm. /// The default value is SECRET AREA. /// public string Realm { get { return _realm ?? (_realm = "SECRET AREA"); } set { if (!canSet ("Realm")) return; _realm = 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 _credentialsFinder ?? (_credentialsFinder = identity => null); } set { if (!canSet ("UserCredentialsFinder")) return; _credentialsFinder = value; } } /// /// Gets the functions for the WebSocket services provided by the /// . /// /// /// A that manages the WebSocket /// services. /// public WebSocketServiceHostManager WebSocketServices { get { return _serviceHosts; } } #endregion #region Private Methods private void abort () { lock (_sync) { if (!IsListening) return; _state = ServerState.SHUTDOWN; } _listener.Stop (); _serviceHosts.Stop ( ((ushort) CloseStatusCode.SERVER_ERROR).ToByteArrayInternally (ByteOrder.BIG), true); _state = ServerState.STOP; } private void acceptRequestAsync (TcpClient client) { ThreadPool.QueueUserWorkItem ( state => { try { var context = client.GetWebSocketContext (_cert, _secure, _logger); if (_authSchemes != AuthenticationSchemes.Anonymous && !authenticateRequest (_authSchemes, context)) return; acceptWebSocket (context); } catch (Exception ex) { _logger.Fatal (ex.ToString ()); client.Close (); } }); } private void acceptWebSocket (TcpListenerWebSocketContext context) { var path = context.Path; WebSocketServiceHost host; if (path == null || !_serviceHosts.TryGetServiceHostInternally (path, out host)) { context.Close (HttpStatusCode.NotImplemented); return; } if (_uri.IsAbsoluteUri) context.WebSocket.Url = new Uri (_uri, path); host.StartSession (context); } private bool authenticateRequest ( AuthenticationSchemes authScheme, TcpListenerWebSocketContext context) { var challenge = authScheme == AuthenticationSchemes.Basic ? HttpUtility.CreateBasicAuthChallenge (Realm) : authScheme == AuthenticationSchemes.Digest ? HttpUtility.CreateDigestAuthChallenge (Realm) : null; if (challenge == null) { context.Close (HttpStatusCode.Forbidden); return false; } var retry = -1; var expected = authScheme.ToString (); var realm = Realm; var credentialsFinder = UserCredentialsFinder; Func auth = null; auth = () => { retry++; if (retry > 99) { context.Close (HttpStatusCode.Forbidden); return false; } var header = context.Headers ["Authorization"]; if (header == null || !header.StartsWith (expected, StringComparison.OrdinalIgnoreCase)) { context.SendAuthChallenge (challenge); return auth (); } context.SetUser (authScheme, realm, credentialsFinder); if (context.IsAuthenticated) return true; context.SendAuthChallenge (challenge); return auth (); }; return auth (); } private bool canSet (string property) { if (_state == ServerState.START || _state == ServerState.SHUTDOWN) { _logger.Error ( String.Format ( "The '{0}' property cannot set a value because the server has already been started.", property)); return false; } return true; } private string checkIfCertExists () { return _secure && _cert == null ? "The secure connection requires a server certificate." : null; } private void init () { _authSchemes = AuthenticationSchemes.Anonymous; _listener = new TcpListener (_address, _port); _logger = new Logger (); _serviceHosts = new WebSocketServiceHostManager (_logger); _state = ServerState.READY; _sync = new object (); } private void receiveRequest () { while (true) { try { acceptRequestAsync (_listener.AcceptTcpClient ()); } catch (SocketException ex) { _logger.Warn ( String.Format ( "Receiving has been stopped.\nreason: {0}.", 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 timeOut) { _listener.Stop (); _receiveRequestThread.Join (timeOut); } 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 = "Must not contain the path or query component: " + uriString; return false; } return true; } #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 contains an absolute path to the WebSocket /// service. /// /// /// The type of the WebSocket service. The TWithNew must inherit the /// class and must have a public parameterless /// constructor. /// public void AddWebSocketService (string servicePath) where TWithNew : WebSocketService, new () { AddWebSocketService (servicePath, () => 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 WebSocket service instance. /// /// /// /// A that contains an absolute path to the WebSocket /// service. /// /// /// A Func<T> delegate that references the method used to initialize /// a new WebSocket service instance (a new WebSocket session). /// /// /// The type of the WebSocket service. The T must inherit the /// class. /// public void AddWebSocketService (string servicePath, Func serviceConstructor) where T : WebSocketService { var msg = servicePath.CheckIfValidServicePath () ?? (serviceConstructor == null ? "'serviceConstructor' must not be null." : null); if (msg != null) { _logger.Error ( String.Format ("{0}\nservice path: {1}", msg, servicePath ?? "")); return; } var host = new WebSocketServiceHost ( servicePath, serviceConstructor, _logger); if (!KeepClean) host.KeepClean = false; _serviceHosts.Add (host.ServicePath, host); } /// /// 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 contains an absolute path to the WebSocket /// service to find. /// public bool RemoveWebSocketService (string servicePath) { var msg = servicePath.CheckIfValidServicePath (); if (msg != null) { _logger.Error ( String.Format ("{0}\nservice path: {1}", msg, servicePath)); return false; } return _serviceHosts.Remove (servicePath); } /// /// Starts receiving the WebSocket connection requests. /// public void Start () { lock (_sync) { var msg = _state.CheckIfStopped () ?? checkIfCertExists (); if (msg != null) { _logger.Error ( String.Format ( "{0}\nstate: {1}\nsecure: {2}", msg, _state, _secure)); return; } _serviceHosts.Start (); _listener.Start (); startReceiving (); _state = ServerState.START; } } /// /// Stops receiving the WebSocket connection requests. /// public void Stop () { lock (_sync) { var msg = _state.CheckIfStarted (); if (msg != null) { _logger.Error (String.Format ("{0}\nstate: {1}", msg, _state)); return; } _state = ServerState.SHUTDOWN; } stopListener (5000); _serviceHosts.Stop (new byte [0], true); _state = ServerState.STOP; } /// /// Stops receiving the WebSocket connection requests with the specified /// and . /// /// /// 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) { byte [] data = null; lock (_sync) { var msg = _state.CheckIfStarted () ?? code.CheckIfValidCloseStatusCode () ?? (data = code.Append (reason)).CheckIfValidCloseData (); if (msg != null) { _logger.Error ( String.Format ( "{0}\nstate: {1}\ncode: {2}\nreason: {3}", msg, _state, code, reason)); return; } _state = ServerState.SHUTDOWN; } stopListener (5000); _serviceHosts.Stop (data, !code.IsReserved ()); _state = ServerState.STOP; } /// /// Stops receiving the WebSocket connection requests with the specified /// and . /// /// /// One of the values that represent the status /// codes indicating the reasons for stop. /// /// /// A that contains the reason for stop. /// public void Stop (CloseStatusCode code, string reason) { byte [] data = null; lock (_sync) { var msg = _state.CheckIfStarted () ?? (data = ((ushort) code).Append (reason)).CheckIfValidCloseData (); if (msg != null) { _logger.Error ( String.Format ("{0}\nstate: {1}\nreason: {2}", msg, _state, reason)); return; } _state = ServerState.SHUTDOWN; } stopListener (5000); _serviceHosts.Stop (data, !code.IsReserved ()); _state = ServerState.STOP; } #endregion } }