#region License /* * HttpListener.cs * * This code is derived from HttpListener.cs (System.Net) of Mono * (http://www.mono-project.com). * * The MIT License * * Copyright (c) 2005 Novell, Inc. (http://www.novell.com) * 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 Authors /* * Authors: * - Gonzalo Paniagua Javier */ #endregion #region Contributors /* * Contributors: * - Liryna */ #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 Func _authSchemeSelector; private string _certFolderPath; private Dictionary _connections; private object _connectionsSync; private List _ctxQueue; private object _ctxQueueSync; private Dictionary _ctxRegistry; private object _ctxRegistrySync; private Func _credFinder; private bool _disposed; private bool _ignoreWriteExceptions; private volatile bool _listening; private Logger _logger; private HttpListenerPrefixCollection _prefixes; private string _realm; private bool _reuseAddress; private ServerSslConfiguration _sslConfig; private List _waitQueue; private object _waitQueueSync; #endregion #region Public Constructors /// /// Initializes a new instance of the class. /// public HttpListener () { _authSchemes = AuthenticationSchemes.Anonymous; _connections = new Dictionary (); _connectionsSync = ((ICollection) _connections).SyncRoot; _ctxQueue = new List (); _ctxQueueSync = ((ICollection) _ctxQueue).SyncRoot; _ctxRegistry = new Dictionary (); _ctxRegistrySync = ((ICollection) _ctxRegistry).SyncRoot; _logger = new Logger (); _prefixes = new HttpListenerPrefixCollection (this); _waitQueue = new List (); _waitQueueSync = ((ICollection) _waitQueue).SyncRoot; } #endregion #region Internal Properties internal bool IsDisposed { get { return _disposed; } } internal bool ReuseAddress { get { return _reuseAddress; } set { _reuseAddress = value; } } #endregion #region Public Properties /// /// Gets or sets the scheme used to authenticate the clients. /// /// /// One of the enum values, /// represents the scheme used to authenticate the clients. The default value is /// . /// /// /// This listener has been closed. /// public AuthenticationSchemes AuthenticationSchemes { get { CheckDisposed (); return _authSchemes; } set { CheckDisposed (); _authSchemes = value; } } /// /// Gets or sets the delegate called to select the scheme used to authenticate the clients. /// /// /// If you set this property, the listener uses the authentication scheme selected by /// the delegate for each request. Or if you don't set, the listener uses the value of /// the property as the authentication /// scheme for all requests. /// /// /// A Func<, > /// delegate that references the method used to select an authentication scheme. The default /// value is . /// /// /// This listener has been closed. /// public Func AuthenticationSchemeSelector { get { CheckDisposed (); return _authSchemeSelector; } set { CheckDisposed (); _authSchemeSelector = value; } } /// /// Gets or sets the path to the folder in which stores the certificate files used to /// authenticate the server on the secure connection. /// /// /// /// This property represents the path to the folder in which stores the certificate files /// associated with each port number of added URI prefixes. A set of the certificate files /// is a pair of the 'port number'.cer (DER) and 'port number'.key /// (DER, RSA Private Key). /// /// /// If this property is or empty, the result of /// System.Environment.GetFolderPath /// () is used as the default path. /// /// /// /// A that represents the path to the folder in which stores /// the certificate files. The default value is . /// /// /// This listener has been closed. /// public string CertificateFolderPath { get { CheckDisposed (); return _certFolderPath; } set { CheckDisposed (); _certFolderPath = value; } } /// /// Gets or sets a value indicating whether the listener returns exceptions that occur when /// sending the response to the client. /// /// /// true if the listener shouldn't return those exceptions; otherwise, false. /// The default value is false. /// /// /// This listener has been closed. /// public bool IgnoreWriteExceptions { get { CheckDisposed (); return _ignoreWriteExceptions; } set { CheckDisposed (); _ignoreWriteExceptions = value; } } /// /// Gets a value indicating whether the listener has been started. /// /// /// true if the listener has been started; otherwise, false. /// public bool IsListening { get { return _listening; } } /// /// Gets a value indicating whether the listener can be used with the current operating system. /// /// /// true. /// public static bool IsSupported { get { return true; } } /// /// 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 URI prefixes handled by the listener. /// /// /// A that contains the URI prefixes. /// /// /// This listener has been closed. /// public HttpListenerPrefixCollection Prefixes { get { CheckDisposed (); return _prefixes; } } /// /// Gets or sets the name of the realm associated with the listener. /// /// /// A that represents the name of the realm. The default value is /// "SECRET AREA". /// /// /// This listener has been closed. /// public string Realm { get { CheckDisposed (); return _realm != null && _realm.Length > 0 ? _realm : (_realm = "SECRET AREA"); } set { CheckDisposed (); _realm = 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. /// /// /// This listener has been closed. /// public ServerSslConfiguration SslConfiguration { get { CheckDisposed (); return _sslConfig ?? (_sslConfig = new ServerSslConfiguration (null)); } set { CheckDisposed (); _sslConfig = 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 used to find the credentials. The default value is a function that /// only returns . /// /// /// This listener has been closed. /// public Func UserCredentialsFinder { get { CheckDisposed (); return _credFinder ?? (_credFinder = id => null); } set { CheckDisposed (); _credFinder = value; } } #endregion #region Private Methods private void cleanupConnections () { HttpConnection[] conns = null; lock (_connectionsSync) { if (_connections.Count == 0) return; // Need to copy this since closing will call the RemoveConnection method. var keys = _connections.Keys; 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 () { HttpListenerContext[] ctxs = null; lock (_ctxRegistrySync) { if (_ctxRegistry.Count == 0) return; // Need to copy this since closing will call the UnregisterContext method. var keys = _ctxRegistry.Keys; ctxs = new HttpListenerContext[keys.Count]; keys.CopyTo (ctxs, 0); _ctxRegistry.Clear (); } for (var i = ctxs.Length - 1; i >= 0; i--) ctxs[i].Connection.Close (true); } private void cleanupWaitQueue (Exception exception) { HttpListenerAsyncResult[] aress = null; lock (_waitQueueSync) { if (_waitQueue.Count == 0) return; aress = _waitQueue.ToArray (); _waitQueue.Clear (); } foreach (var ares in aress) ares.Complete (exception); } private void close (bool force) { if (_listening) { _listening = false; EndPointManager.RemoveListener (this); } lock (_ctxRegistrySync) { if (!force) sendServiceUnavailable (); } cleanupContextRegistry (); cleanupConnections (); cleanupWaitQueue (new ObjectDisposedException (GetType ().ToString ())); _disposed = true; } private HttpListenerContext getContextFromQueue () { lock (_ctxQueueSync) { if (_ctxQueue.Count == 0) return null; var ctx = _ctxQueue[0]; _ctxQueue.RemoveAt (0); return ctx; } } private void sendServiceUnavailable () { HttpListenerContext[] ctxs = null; lock (_ctxQueueSync) { if (_ctxQueue.Count == 0) return; ctxs = _ctxQueue.ToArray (); _ctxQueue.Clear (); } foreach (var ctx in ctxs) { var res = ctx.Response; res.StatusCode = (int) HttpStatusCode.ServiceUnavailable; res.Close (); } } #endregion #region Internal Methods internal bool AddConnection (HttpConnection connection) { if (!_listening) return false; lock (_connectionsSync) { if (!_listening) return false; _connections[connection] = connection; } return true; } internal bool Authenticate (HttpListenerContext context) { var schm = SelectAuthenticationScheme (context); if (schm == AuthenticationSchemes.Anonymous) return true; if (schm != AuthenticationSchemes.Basic && schm != AuthenticationSchemes.Digest) { context.Response.Close (HttpStatusCode.Forbidden); return false; } var realm = Realm; var req = context.Request; var user = HttpUtility.CreateUser ( req.Headers["Authorization"], schm, realm, req.HttpMethod, UserCredentialsFinder); if (user != null && user.Identity.IsAuthenticated) { context.User = user; return true; } if (schm == AuthenticationSchemes.Basic) context.Response.CloseWithAuthChallenge ( AuthenticationChallenge.CreateBasicChallenge (realm).ToBasicString ()); if (schm == AuthenticationSchemes.Digest) context.Response.CloseWithAuthChallenge ( AuthenticationChallenge.CreateDigestChallenge (realm).ToDigestString ()); return false; } internal HttpListenerAsyncResult BeginGetContext (HttpListenerAsyncResult asyncResult) { // Lock _ctxRegistrySync early to avoid race conditions. lock (_ctxRegistrySync) { if (!_listening) throw new InvalidOperationException ("The listener is stopped/closed."); lock (_waitQueueSync) { var ctx = getContextFromQueue (); if (ctx != null) { asyncResult.Complete (ctx, true); return asyncResult; } _waitQueue.Add (asyncResult); } } return asyncResult; } internal void CheckDisposed () { if (_disposed) throw new ObjectDisposedException (GetType ().ToString ()); } internal bool RegisterContext (HttpListenerContext context) { if (!_listening) return false; HttpListenerAsyncResult ares = null; lock (_ctxRegistrySync) { if (!_listening) return false; _ctxRegistry[context] = context; lock (_waitQueueSync) { if (_waitQueue.Count == 0) { lock (_ctxQueueSync) _ctxQueue.Add (context); } else { ares = _waitQueue[0]; _waitQueue.RemoveAt (0); } } } if (ares != null) ares.Complete (context); return true; } internal void RemoveConnection (HttpConnection connection) { lock (_connectionsSync) _connections.Remove (connection); } internal AuthenticationSchemes SelectAuthenticationScheme (HttpListenerContext context) { return AuthenticationSchemeSelector != null ? AuthenticationSchemeSelector (context.Request) : _authSchemes; } internal void UnregisterContext (HttpListenerContext context) { lock (_ctxRegistrySync) _ctxRegistry.Remove (context); lock (_ctxQueueSync) { var idx = _ctxQueue.IndexOf (context); if (idx >= 0) _ctxQueue.RemoveAt (idx); } } #endregion #region Public Methods /// /// Shuts down the listener immediately. /// public void Abort () { if (_disposed) return; close (true); } /// /// Begins getting an incoming request asynchronously. /// /// /// This asynchronous operation must be completed by calling the EndGetContext method. /// Typically, the method is invoked by the delegate. /// /// /// An that represents the status of the asynchronous operation. /// /// /// An delegate that references the method to invoke when /// the asynchronous operation completes. /// /// /// An that represents a user defined object to pass to /// the delegate. /// /// /// /// This listener has no URI prefix on which listens. /// /// /// -or- /// /// /// This listener hasn't been started, or is currently stopped. /// /// /// /// This listener has been closed. /// public IAsyncResult BeginGetContext (AsyncCallback callback, Object state) { CheckDisposed (); if (_prefixes.Count == 0) throw new InvalidOperationException ("The listener has no URI prefix on which listens."); if (!_listening) throw new InvalidOperationException ("The listener hasn't been started."); return BeginGetContext (new HttpListenerAsyncResult (callback, state)); } /// /// Shuts down the listener. /// public void Close () { if (_disposed) return; close (false); } /// /// Ends an asynchronous operation to get an incoming request. /// /// /// This method completes an asynchronous operation started by calling /// the BeginGetContext method. /// /// /// A that represents a request. /// /// /// An obtained by calling the BeginGetContext method. /// /// /// is . /// /// /// wasn't obtained by calling the BeginGetContext method. /// /// /// This method was already called for the specified . /// /// /// This listener has been closed. /// public HttpListenerContext EndGetContext (IAsyncResult asyncResult) { CheckDisposed (); if (asyncResult == null) throw new ArgumentNullException ("asyncResult"); var ares = asyncResult as HttpListenerAsyncResult; if (ares == null) throw new ArgumentException ("A wrong IAsyncResult.", "asyncResult"); if (ares.EndCalled) throw new InvalidOperationException ("This IAsyncResult cannot be reused."); ares.EndCalled = true; if (!ares.IsCompleted) ares.AsyncWaitHandle.WaitOne (); return ares.GetContext (); // This may throw an exception. } /// /// Gets an incoming request. /// /// /// This method waits for an incoming request, and returns when a request is received. /// /// /// A that represents a request. /// /// /// /// This listener has no URI prefix on which listens. /// /// /// -or- /// /// /// This listener hasn't been started, or is currently stopped. /// /// /// /// This listener has been closed. /// public HttpListenerContext GetContext () { CheckDisposed (); if (_prefixes.Count == 0) throw new InvalidOperationException ("The listener has no URI prefix on which listens."); if (!_listening) throw new InvalidOperationException ("The listener hasn't been started."); var ares = BeginGetContext (new HttpListenerAsyncResult (null, null)); ares.InGet = true; return EndGetContext (ares); } /// /// Starts receiving incoming requests. /// /// /// This listener has been closed. /// public void Start () { CheckDisposed (); if (_listening) return; EndPointManager.AddListener (this); _listening = true; } /// /// Stops receiving incoming requests. /// /// /// This listener has been closed. /// public void Stop () { CheckDisposed (); if (!_listening) return; _listening = false; EndPointManager.RemoveListener (this); lock (_ctxRegistrySync) sendServiceUnavailable (); cleanupContextRegistry (); cleanupConnections (); cleanupWaitQueue (new HttpListenerException (995, "The listener is stopped.")); } #endregion #region Explicit Interface Implementations /// /// Releases all resources used by the listener. /// void IDisposable.Dispose () { if (_disposed) return; close (true); } #endregion } }