#region License /* * WebSocketBehavior.cs * * The MIT License * * Copyright (c) 2012-2016 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 using System; using System.Collections.Specialized; using System.IO; using WebSocketSharp.Net; using WebSocketSharp.Net.WebSockets; namespace WebSocketSharp.Server { /// /// Exposes a set of methods and properties used to define the behavior of /// a WebSocket service provided by the or /// . /// /// /// This class is an abstract class. /// public abstract class WebSocketBehavior : IWebSocketSession { #region Private Fields private WebSocketContext _context; private Func _cookiesValidator; private bool _emitOnPing; private string _id; private bool _ignoreExtensions; private Func _originValidator; private string _protocol; private WebSocketSessionManager _sessions; private DateTime _startTime; private WebSocket _websocket; #endregion #region Protected Constructors /// /// Initializes a new instance of the class. /// protected WebSocketBehavior () { _startTime = DateTime.MaxValue; } #endregion #region Protected Properties /// /// Gets the HTTP headers included in a WebSocket handshake request. /// /// /// /// A that contains the headers. /// /// /// if the session has not started yet. /// /// protected NameValueCollection Headers { get { return _context != null ? _context.Headers : null; } } /// /// Gets the logging function. /// /// /// /// A that provides the logging function. /// /// /// if the session has not started yet. /// /// [Obsolete ("This property will be removed.")] protected Logger Log { get { return _websocket != null ? _websocket.Log : null; } } /// /// Gets the query string included in a WebSocket handshake request. /// /// /// /// A that contains the query /// parameters. /// /// /// An empty collection if not included. /// /// /// if the session has not started yet. /// /// protected NameValueCollection QueryString { get { return _context != null ? _context.QueryString : null; } } /// /// Gets the management function for the sessions in the service. /// /// /// /// A that manages the sessions in /// the service. /// /// /// if the session has not started yet. /// /// protected WebSocketSessionManager Sessions { get { return _sessions; } } #endregion #region Public Properties /// /// Gets the current state of the WebSocket connection for a session. /// /// /// /// One of the enum values. /// /// /// It indicates the current state of the connection. /// /// /// if the session has not /// started yet. /// /// public WebSocketState ConnectionState { get { return _websocket != null ? _websocket.ReadyState : WebSocketState.Connecting; } } /// /// Gets the information in a WebSocket handshake request to the service. /// /// /// /// A instance that provides the access to /// the information in the handshake request. /// /// /// if the session has not started yet. /// /// public WebSocketContext Context { get { return _context; } } /// /// Gets or sets the delegate used to validate the HTTP cookies included in /// a WebSocket handshake request to the service. /// /// /// /// A Func<CookieCollection, CookieCollection, bool> delegate /// or if not needed. /// /// /// The delegate invokes the method called when the WebSocket instance /// for a session validates the handshake request. /// /// /// 1st parameter passed to the method /// contains the cookies to validate if present. /// /// /// 2nd parameter passed to the method /// receives the cookies to send to the client. /// /// /// The method must return true if the cookies are valid. /// /// /// The default value is . /// /// public Func CookiesValidator { get { return _cookiesValidator; } set { _cookiesValidator = value; } } /// /// Gets or sets a value indicating whether the WebSocket instance for /// a session emits the message event when receives a ping. /// /// /// /// true if the WebSocket instance emits the message event /// when receives a ping; otherwise, false. /// /// /// The default value is false. /// /// public bool EmitOnPing { get { return _websocket != null ? _websocket.EmitOnPing : _emitOnPing; } set { if (_websocket != null) { _websocket.EmitOnPing = value; return; } _emitOnPing = value; } } /// /// Gets the unique ID of a session. /// /// /// /// A that represents the unique ID of the session. /// /// /// if the session has not started yet. /// /// public string ID { get { return _id; } } /// /// Gets or sets a value indicating whether the service ignores /// the Sec-WebSocket-Extensions header included in a WebSocket /// handshake request. /// /// /// /// true if the service ignores the extensions requested /// from a client; otherwise, false. /// /// /// The default value is false. /// /// public bool IgnoreExtensions { get { return _ignoreExtensions; } set { _ignoreExtensions = value; } } /// /// Gets or sets the delegate used to validate the Origin header included in /// a WebSocket handshake request to the service. /// /// /// /// A Func<string, bool> delegate or /// if not needed. /// /// /// The delegate invokes the method called when the WebSocket instance /// for a session validates the handshake request. /// /// /// The parameter passed to the method is the value /// of the Origin header or if the header is not /// present. /// /// /// The method must return true if the header value is valid. /// /// /// The default value is . /// /// public Func OriginValidator { get { return _originValidator; } set { _originValidator = value; } } /// /// Gets or sets the name of the WebSocket subprotocol for the service. /// /// /// /// A that represents the name of the subprotocol. /// /// /// The value specified for a set must be a token defined in /// /// RFC 2616. /// /// /// The default value is an empty string. /// /// /// /// The set operation is not available if the session has already started. /// /// /// The value specified for a set operation is not a token. /// public string Protocol { get { return _websocket != null ? _websocket.Protocol : (_protocol ?? String.Empty); } set { if (ConnectionState != WebSocketState.Connecting) { var msg = "The session has already started."; throw new InvalidOperationException (msg); } if (value == null || value.Length == 0) { _protocol = null; return; } if (!value.IsToken ()) throw new ArgumentException ("Not a token.", "value"); _protocol = value; } } /// /// Gets the time that a session has started. /// /// /// /// A that represents the time that the session /// has started. /// /// /// if the session has not started yet. /// /// public DateTime StartTime { get { return _startTime; } } #endregion #region Private Methods private string checkHandshakeRequest (WebSocketContext context) { if (_originValidator != null) { if (!_originValidator (context.Origin)) return "It includes no Origin header or an invalid one."; } if (_cookiesValidator != null) { var req = context.CookieCollection; var res = context.WebSocket.CookieCollection; if (!_cookiesValidator (req, res)) return "It includes no cookie or an invalid one."; } return null; } private void onClose (object sender, CloseEventArgs e) { if (_id == null) return; _sessions.Remove (_id); OnClose (e); } private void onError (object sender, ErrorEventArgs e) { OnError (e); } private void onMessage (object sender, MessageEventArgs e) { OnMessage (e); } private void onOpen (object sender, EventArgs e) { _id = _sessions.Add (this); if (_id == null) { _websocket.Close (CloseStatusCode.Away); return; } _startTime = DateTime.Now; OnOpen (); } #endregion #region Internal Methods internal void Start (WebSocketContext context, WebSocketSessionManager sessions) { if (_websocket != null) { _websocket.Log.Error ("A session instance cannot be reused."); context.WebSocket.Close (HttpStatusCode.ServiceUnavailable); return; } _context = context; _sessions = sessions; _websocket = context.WebSocket; _websocket.CustomHandshakeRequestChecker = checkHandshakeRequest; _websocket.EmitOnPing = _emitOnPing; _websocket.IgnoreExtensions = _ignoreExtensions; _websocket.Protocol = _protocol; var waitTime = sessions.WaitTime; if (waitTime != _websocket.WaitTime) _websocket.WaitTime = waitTime; _websocket.OnOpen += onOpen; _websocket.OnMessage += onMessage; _websocket.OnError += onError; _websocket.OnClose += onClose; _websocket.InternalAccept (); } #endregion #region Protected Methods /// /// Closes the WebSocket connection for a session. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// /// The session has not started yet. /// protected void Close () { if (_websocket == null) { var msg = "The session has not started yet."; throw new InvalidOperationException (msg); } _websocket.Close (); } /// /// Closes the WebSocket connection for a session with the specified /// code and reason. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// /// /// 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. /// /// /// /// The session has not started yet. /// /// /// /// 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. /// /// protected void Close (ushort code, string reason) { if (_websocket == null) { var msg = "The session has not started yet."; throw new InvalidOperationException (msg); } _websocket.Close (code, reason); } /// /// Closes the WebSocket connection for a session with the specified /// code and reason. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// /// /// 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. /// /// /// /// The session has not started yet. /// /// /// The size of is greater than 123 bytes. /// /// /// /// is /// . /// /// /// -or- /// /// /// is /// and there is reason. /// /// /// -or- /// /// /// could not be UTF-8-encoded. /// /// protected void Close (CloseStatusCode code, string reason) { if (_websocket == null) { var msg = "The session has not started yet."; throw new InvalidOperationException (msg); } _websocket.Close (code, reason); } /// /// Closes the WebSocket connection for a session asynchronously. /// /// /// /// This method does not wait for the close to be complete. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// /// /// The session has not started yet. /// protected void CloseAsync () { if (_websocket == null) { var msg = "The session has not started yet."; throw new InvalidOperationException (msg); } _websocket.CloseAsync (); } /// /// Closes the WebSocket connection for a session asynchronously with /// the specified code and reason. /// /// /// /// This method does not wait for the close to be complete. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// /// /// /// 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. /// /// /// /// The session has not started yet. /// /// /// /// 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. /// /// protected void CloseAsync (ushort code, string reason) { if (_websocket == null) { var msg = "The session has not started yet."; throw new InvalidOperationException (msg); } _websocket.CloseAsync (code, reason); } /// /// Closes the WebSocket connection for a session asynchronously with /// the specified code and reason. /// /// /// /// This method does not wait for the close to be complete. /// /// /// This method does nothing if the current state of the connection is /// Closing or Closed. /// /// /// /// /// 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. /// /// /// /// The session has not started yet. /// /// /// /// is /// . /// /// /// -or- /// /// /// is /// and there is reason. /// /// /// -or- /// /// /// could not be UTF-8-encoded. /// /// /// /// The size of is greater than 123 bytes. /// protected void CloseAsync (CloseStatusCode code, string reason) { if (_websocket == null) { var msg = "The session has not started yet."; throw new InvalidOperationException (msg); } _websocket.CloseAsync (code, reason); } /// /// Calls the method with the specified message. /// /// /// A that represents the error message. /// /// /// An instance that represents the cause of /// the error if present. /// /// /// is . /// /// /// is an empty string. /// [Obsolete ("This method will be removed.")] protected void Error (string message, Exception exception) { if (message == null) throw new ArgumentNullException ("message"); if (message.Length == 0) throw new ArgumentException ("An empty string.", "message"); OnError (new ErrorEventArgs (message, exception)); } /// /// Called when the WebSocket connection for a session has been closed. /// /// /// A that represents the event data passed /// from a event. /// protected virtual void OnClose (CloseEventArgs e) { } /// /// Called when the WebSocket instance for a session gets an error. /// /// /// A that represents the event data passed /// from a event. /// protected virtual void OnError (ErrorEventArgs e) { } /// /// Called when the WebSocket instance for a session receives a message. /// /// /// A that represents the event data passed /// from a event. /// protected virtual void OnMessage (MessageEventArgs e) { } /// /// Called when the WebSocket connection for a session has been established. /// protected virtual void OnOpen () { } /// /// Sends the specified data to a client using the WebSocket connection. /// /// /// An array of that represents the binary data to send. /// /// /// The current state of the connection is not Open. /// /// /// is . /// protected void Send (byte[] data) { if (_websocket == null) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } _websocket.Send (data); } /// /// Sends the specified file to a client using the WebSocket connection. /// /// /// /// A that specifies the file to send. /// /// /// The file is sent as the binary data. /// /// /// /// The current state of the connection is not Open. /// /// /// is . /// /// /// /// The file does not exist. /// /// /// -or- /// /// /// The file could not be opened. /// /// protected void Send (FileInfo fileInfo) { if (_websocket == null) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } _websocket.Send (fileInfo); } /// /// Sends the specified data to a client using the WebSocket connection. /// /// /// A that represents the text data to send. /// /// /// The current state of the connection is not Open. /// /// /// is . /// /// /// could not be UTF-8-encoded. /// protected void Send (string data) { if (_websocket == null) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } _websocket.Send (data); } /// /// Sends the data from the specified stream to a client using /// the WebSocket connection. /// /// /// /// A instance from which to read the data to send. /// /// /// The data is sent as the binary data. /// /// /// /// An that specifies the number of bytes to send. /// /// /// The current state of the connection is not Open. /// /// /// is . /// /// /// /// cannot be read. /// /// /// -or- /// /// /// is less than 1. /// /// /// -or- /// /// /// No data could be read from . /// /// protected void Send (Stream stream, int length) { if (_websocket == null) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } _websocket.Send (stream, length); } /// /// Sends the specified data to a client asynchronously using /// the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// An array of that represents the binary data to send. /// /// /// /// An Action<bool> delegate or /// if not needed. /// /// /// The delegate invokes the method called when the send is complete. /// /// /// true is passed to the method if the send has done with /// no error; otherwise, false. /// /// /// /// The current state of the connection is not Open. /// /// /// is . /// protected void SendAsync (byte[] data, Action completed) { if (_websocket == null) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } _websocket.SendAsync (data, completed); } /// /// Sends the specified file to a client asynchronously using /// the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// /// A that specifies the file to send. /// /// /// The file is sent as the binary data. /// /// /// /// /// An Action<bool> delegate or /// if not needed. /// /// /// The delegate invokes the method called when the send is complete. /// /// /// true is passed to the method if the send has done with /// no error; otherwise, false. /// /// /// /// The current state of the connection is not Open. /// /// /// is . /// /// /// /// The file does not exist. /// /// /// -or- /// /// /// The file could not be opened. /// /// protected void SendAsync (FileInfo fileInfo, Action completed) { if (_websocket == null) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } _websocket.SendAsync (fileInfo, completed); } /// /// Sends the specified data to a client asynchronously using /// the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// A that represents the text data to send. /// /// /// /// An Action<bool> delegate or /// if not needed. /// /// /// The delegate invokes the method called when the send is complete. /// /// /// true is passed to the method if the send has done with /// no error; otherwise, false. /// /// /// /// The current state of the connection is not Open. /// /// /// is . /// /// /// could not be UTF-8-encoded. /// protected void SendAsync (string data, Action completed) { if (_websocket == null) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } _websocket.SendAsync (data, completed); } /// /// Sends the data from the specified stream to a client asynchronously /// using the WebSocket connection. /// /// /// This method does not wait for the send to be complete. /// /// /// /// A instance from which to read the data to send. /// /// /// The data is sent as the binary data. /// /// /// /// An that specifies the number of bytes to send. /// /// /// /// An Action<bool> delegate or /// if not needed. /// /// /// The delegate invokes the method called when the send is complete. /// /// /// true is passed to the method if the send has done with /// no error; otherwise, false. /// /// /// /// The current state of the connection is not Open. /// /// /// is . /// /// /// /// cannot be read. /// /// /// -or- /// /// /// is less than 1. /// /// /// -or- /// /// /// No data could be read from . /// /// protected void SendAsync (Stream stream, int length, Action completed) { if (_websocket == null) { var msg = "The current state of the connection is not Open."; throw new InvalidOperationException (msg); } _websocket.SendAsync (stream, length, completed); } #endregion } }