#region License /* * HttpListener.cs * * This code is derived from System.Net.HttpListener.cs of Mono * (http://www.mono-project.com). * * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) * Copyright (c) 2012-2014 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 Authors /* * Authors: * Gonzalo Paniagua Javier */ #endregion using System; using System.Collections; using System.Collections.Generic; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using System.Threading; // TODO: logging namespace WebSocketSharp.Net { /// /// Provides a simple, programmatically controlled HTTP listener. /// public sealed class HttpListener : IDisposable { #region Private Fields private AuthenticationSchemes _authSchemes; private AuthenticationSchemeSelector _authSchemeSelector; private string _certFolderPath; private Dictionary _connections; private List _contextQueue; private Func _credentialsFinder; private X509Certificate2 _defaultCert; private bool _disposed; private bool _ignoreWriteExceptions; private bool _listening; private HttpListenerPrefixCollection _prefixes; private string _realm; private Dictionary _registry; private List _waitQueue; #endregion #region Public Constructors /// /// Initializes a new instance of the class. /// public HttpListener () { _authSchemes = AuthenticationSchemes.Anonymous; _connections = new Dictionary (); _contextQueue = new List (); _prefixes = new HttpListenerPrefixCollection (this); _registry = new Dictionary (); _waitQueue = new List (); } #endregion #region Internal Properties internal bool IsDisposed { get { return _disposed; } } #endregion #region Public Properties /// /// 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 . /// /// /// This object has been closed. /// public AuthenticationSchemes AuthenticationSchemes { get { CheckDisposed (); return _authSchemes; } set { CheckDisposed (); _authSchemes = value; } } /// /// Gets or sets the delegate called to determine the scheme used to /// authenticate clients. /// /// /// A delegate that invokes the /// method(s) used to select an authentication scheme. The default value is /// . /// /// /// This object has been closed. /// public AuthenticationSchemeSelector AuthenticationSchemeSelectorDelegate { get { CheckDisposed (); return _authSchemeSelector; } set { CheckDisposed (); _authSchemeSelector = value; } } /// /// Gets or sets the path to the folder stored the certificate files used to /// authenticate the server on the secure connection. /// /// /// This property represents the path to the folder stored the certificate /// files associated with the port number of each added URI prefix. A set of /// the certificate files is a pair of the 'port number'.cer (DER) and /// 'port number'.key (DER, RSA Private Key). /// /// /// A that contains the path to the certificate folder. /// The default value is the result of Environment.GetFolderPath /// (). /// /// /// This object has been closed. /// public string CertificateFolderPath { get { CheckDisposed (); return _certFolderPath == null || _certFolderPath.Length == 0 ? (_certFolderPath = Environment.GetFolderPath ( Environment.SpecialFolder.ApplicationData)) : _certFolderPath; } set { CheckDisposed (); _certFolderPath = value; } } /// /// Gets or sets the default certificate used to authenticate the server on /// the secure connection. /// /// /// A used to authenticate the server if the /// certificate associated with the port number of each added URI prefix is /// not found in the . /// /// /// This object has been closed. /// public X509Certificate2 DefaultCertificate { get { CheckDisposed (); return _defaultCert; } set { CheckDisposed (); _defaultCert = value; } } /// /// Gets or sets a value indicating whether the /// returns exceptions that occur when sending the response to the client. /// /// /// true if the doesn't return exceptions /// that occur when sending the response to the client; otherwise, /// false. The default value is false. /// /// /// This object has been closed. /// public bool IgnoreWriteExceptions { get { CheckDisposed (); return _ignoreWriteExceptions; } set { CheckDisposed (); _ignoreWriteExceptions = value; } } /// /// Gets a value indicating whether the has been /// started. /// /// /// true if the has been started; otherwise, /// false. /// public bool IsListening { get { return _listening; } } /// /// Gets a value indicating whether the can be /// used with the current operating system. /// /// /// true. /// public static bool IsSupported { get { return true; } } /// /// Gets the URI prefixes handled by the . /// /// /// A that contains the URI /// prefixes. /// /// /// This object has been closed. /// public HttpListenerPrefixCollection Prefixes { get { CheckDisposed (); return _prefixes; } } /// /// 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. /// /// /// This object has been closed. /// public string Realm { get { CheckDisposed (); return _realm == null || _realm.Length == 0 ? (_realm = "SECRET AREA") : _realm; } set { CheckDisposed (); _realm = value; } } /// /// Gets or sets a value indicating whether, when NTLM authentication is used, /// the authentication information of first request is used to authenticate /// additional requests on the same connection. /// /// /// This property isn't currently supported and always throws /// a . /// /// /// true if the authentication information of first request is used; /// otherwise, false. /// /// /// Any use of this property. /// public bool UnsafeConnectionNtlmAuthentication { get { throw new NotSupportedException (); } set { throw new NotSupportedException (); } } /// /// 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 . /// /// /// This object has been closed. /// public Func UserCredentialsFinder { get { CheckDisposed (); return _credentialsFinder ?? (_credentialsFinder = identity => null); } set { CheckDisposed (); _credentialsFinder = value; } } #endregion #region Private Methods private void cleanup (bool force) { lock (((ICollection) _registry).SyncRoot) { if (!force) sendServiceUnavailable (); cleanupContextRegistry (); cleanupConnections (); cleanupWaitQueue (); } } private void cleanupConnections () { lock (((ICollection) _connections).SyncRoot) { if (_connections.Count == 0) return; // Need to copy this since closing will call RemoveConnection var keys = _connections.Keys; var conns = new HttpConnection [keys.Count]; keys.CopyTo (conns, 0); _connections.Clear (); for (var i = conns.Length - 1; i >= 0; i--) conns [i].Close (true); } } private void cleanupContextRegistry () { lock (((ICollection) _registry).SyncRoot) { if (_registry.Count == 0) return; // Need to copy this since closing will call UnregisterContext var keys = _registry.Keys; var all = new HttpListenerContext [keys.Count]; keys.CopyTo (all, 0); _registry.Clear (); for (var i = all.Length - 1; i >= 0; i--) all [i].Connection.Close (true); } } private void cleanupWaitQueue () { lock (((ICollection) _waitQueue).SyncRoot) { if (_waitQueue.Count == 0) return; var ex = new ObjectDisposedException (GetType ().ToString ()); foreach (var ares in _waitQueue) { ares.Complete (ex); } _waitQueue.Clear (); } } private void close (bool force) { EndPointManager.RemoveListener (this); cleanup (force); } // Must be called with a lock on _contextQueue private HttpListenerContext getContextFromQueue () { if (_contextQueue.Count == 0) return null; var context = _contextQueue [0]; _contextQueue.RemoveAt (0); return context; } private void sendServiceUnavailable () { lock (((ICollection) _contextQueue).SyncRoot) { if (_contextQueue.Count == 0) return; var contexts = _contextQueue.ToArray (); _contextQueue.Clear (); foreach (var context in contexts) { var res = context.Response; res.StatusCode = (int) HttpStatusCode.ServiceUnavailable; res.Close (); } } } #endregion #region Internal Methods internal void AddConnection (HttpConnection connection) { _connections [connection] = connection; } internal ListenerAsyncResult BeginGetContext (ListenerAsyncResult asyncResult) { CheckDisposed (); if (_prefixes.Count == 0) throw new InvalidOperationException ( "Please, call AddPrefix before using this method."); if (!_listening) throw new InvalidOperationException ( "Please, call Start before using this method."); // Lock _waitQueue early to avoid race conditions lock (((ICollection) _waitQueue).SyncRoot) { lock (((ICollection) _contextQueue).SyncRoot) { var context = getContextFromQueue (); if (context != null) { asyncResult.Complete (context, true); return asyncResult; } } _waitQueue.Add (asyncResult); } return asyncResult; } internal void CheckDisposed () { if (_disposed) throw new ObjectDisposedException (GetType ().ToString ()); } internal void RegisterContext (HttpListenerContext context) { lock (((ICollection) _registry).SyncRoot) _registry [context] = context; ListenerAsyncResult ares = null; lock (((ICollection) _waitQueue).SyncRoot) { if (_waitQueue.Count == 0) { lock (((ICollection) _contextQueue).SyncRoot) _contextQueue.Add (context); } else { ares = _waitQueue [0]; _waitQueue.RemoveAt (0); } } if (ares != null) ares.Complete (context); } internal void RemoveConnection (HttpConnection connection) { _connections.Remove (connection); } internal AuthenticationSchemes SelectAuthenticationScheme ( HttpListenerContext context) { return AuthenticationSchemeSelectorDelegate != null ? AuthenticationSchemeSelectorDelegate (context.Request) : _authSchemes; } internal void UnregisterContext (HttpListenerContext context) { lock (((ICollection) _registry).SyncRoot) _registry.Remove (context); lock (((ICollection) _contextQueue).SyncRoot) { var i = _contextQueue.IndexOf (context); if (i >= 0) _contextQueue.RemoveAt (i); } } #endregion #region Public Methods /// /// Shuts down the immediately. /// public void Abort () { if (_disposed) return; close (true); _disposed = true; } /// /// Begins getting an incoming request information asynchronously. /// /// /// This asynchronous operation must be completed by calling the /// EndGetContext method. Typically, that method is invoked by the /// delegate. /// /// /// An that contains the status of the /// asynchronous operation. /// /// /// An delegate that references the method(s) /// called when the asynchronous operation completes. /// /// /// An that contains a user defined object to pass to /// the delegate. /// /// /// /// The does not have any URI prefixes to listen /// on. /// /// /// -or- /// /// /// The has not been started or is stopped /// currently. /// /// /// /// This object has been closed. /// public IAsyncResult BeginGetContext (AsyncCallback callback, Object state) { return BeginGetContext (new ListenerAsyncResult (callback, state)); } /// /// Shuts down the . /// public void Close () { if (_disposed) return; close (false); _disposed = true; } /// /// Ends an asynchronous operation to get an incoming request information. /// /// /// This method completes an asynchronous operation started by calling the /// BeginGetContext method. /// /// /// A that contains a client's request /// information. /// /// /// An obtained by calling the /// BeginGetContext method. /// /// /// was not obtained by calling the /// BeginGetContext method. /// /// /// is . /// /// /// This method was already called for the specified /// . /// /// /// This object has been closed. /// public HttpListenerContext EndGetContext (IAsyncResult asyncResult) { CheckDisposed (); if (asyncResult == null) throw new ArgumentNullException ("asyncResult"); var ares = asyncResult as ListenerAsyncResult; if (ares == null) throw new ArgumentException ("Wrong IAsyncResult.", "asyncResult"); if (ares.EndCalled) throw new InvalidOperationException ("Cannot reuse this IAsyncResult."); ares.EndCalled = true; if (!ares.IsCompleted) ares.AsyncWaitHandle.WaitOne (); lock (((ICollection) _waitQueue).SyncRoot) { var i = _waitQueue.IndexOf (ares); if (i >= 0) _waitQueue.RemoveAt (i); } var context = ares.GetContext (); var authScheme = SelectAuthenticationScheme (context); if (authScheme != AuthenticationSchemes.Anonymous) context.SetUser (authScheme, Realm, UserCredentialsFinder); return context; // This will throw on error. } /// /// Gets an incoming request information. /// /// /// This method waits for an incoming request and returns the request /// information when received the request. /// /// /// A that contains a client's request /// information. /// /// /// /// The does not have any URI prefixes to listen /// on. /// /// /// -or- /// /// /// The has not been started or is stopped /// currently. /// /// /// /// This object has been closed. /// public HttpListenerContext GetContext () { var ares = BeginGetContext (new ListenerAsyncResult (null, null)); ares.InGet = true; return EndGetContext (ares); } /// /// Starts to receive incoming requests. /// /// /// This object has been closed. /// public void Start () { CheckDisposed (); if (_listening) return; EndPointManager.AddListener (this); _listening = true; } /// /// Stops receiving incoming requests. /// /// /// This object has been closed. /// public void Stop () { CheckDisposed (); if (!_listening) return; _listening = false; EndPointManager.RemoveListener (this); sendServiceUnavailable (); } #endregion #region Explicit Interface Implementation /// /// Releases all resource used by the . /// void IDisposable.Dispose () { if (_disposed) return; close (true); // TODO: Should we force here or not? _disposed = true; } #endregion } }