#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-2021 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 Queue _contextQueue; private LinkedList _contextRegistry; private object _contextRegistrySync; private static readonly string _defaultRealm; private bool _disposed; private bool _ignoreWriteExceptions; private volatile bool _listening; private Logger _log; private string _objectName; private HttpListenerPrefixCollection _prefixes; private string _realm; private bool _reuseAddress; private ServerSslConfiguration _sslConfig; private Func _userCredFinder; private Queue _waitQueue; #endregion #region Static Constructor static HttpListener () { _defaultRealm = "SECRET AREA"; } #endregion #region Public Constructors /// /// Initializes a new instance of the class. /// public HttpListener () { _authSchemes = AuthenticationSchemes.Anonymous; _contextQueue = new Queue (); _contextRegistry = new LinkedList (); _contextRegistrySync = ((ICollection) _contextRegistry).SyncRoot; _log = new Logger (); _objectName = GetType ().ToString (); _prefixes = new HttpListenerPrefixCollection (this); _waitQueue = new Queue (); } #endregion #region Internal Properties 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. /// /// /// It represents the scheme used to authenticate the clients. /// /// /// The default value is /// . /// /// /// /// This listener has been closed. /// public AuthenticationSchemes AuthenticationSchemes { get { if (_disposed) throw new ObjectDisposedException (_objectName); return _authSchemes; } set { if (_disposed) throw new ObjectDisposedException (_objectName); _authSchemes = value; } } /// /// Gets or sets the delegate called to select the scheme used to /// authenticate the clients. /// /// /// /// If this property is set, the listener uses the authentication /// scheme selected by the delegate for each request. /// /// /// Or if this property is not set, the listener uses the value of /// the property /// as the authentication scheme for all requests. /// /// /// /// /// A Func<, /// > delegate or /// if not needed. /// /// /// The delegate references the method used to select /// an authentication scheme. /// /// /// The default value is . /// /// /// /// This listener has been closed. /// public Func AuthenticationSchemeSelector { get { if (_disposed) throw new ObjectDisposedException (_objectName); return _authSchemeSelector; } set { if (_disposed) throw new ObjectDisposedException (_objectName); _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 <port number>.cer /// (DER) and <port number>.key (DER, RSA Private Key). /// /// /// If this property is or an empty string, /// 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 { if (_disposed) throw new ObjectDisposedException (_objectName); return _certFolderPath; } set { if (_disposed) throw new ObjectDisposedException (_objectName); _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 should not return those exceptions; /// otherwise, false. /// /// /// The default value is false. /// /// /// /// This listener has been closed. /// public bool IgnoreWriteExceptions { get { if (_disposed) throw new ObjectDisposedException (_objectName); return _ignoreWriteExceptions; } set { if (_disposed) throw new ObjectDisposedException (_objectName); _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 _log; } } /// /// Gets the URI prefixes handled by the listener. /// /// /// A that contains the URI /// prefixes. /// /// /// This listener has been closed. /// public HttpListenerPrefixCollection Prefixes { get { if (_disposed) throw new ObjectDisposedException (_objectName); return _prefixes; } } /// /// Gets or sets the name of the realm associated with the listener. /// /// /// If this property is or an empty string, /// "SECRET AREA" will be used as the name of the realm. /// /// /// /// A that represents the name of the realm. /// /// /// The default value is . /// /// /// /// This listener has been closed. /// public string Realm { get { if (_disposed) throw new ObjectDisposedException (_objectName); return _realm; } set { if (_disposed) throw new ObjectDisposedException (_objectName); _realm = value; } } /// /// Gets the SSL configuration used to authenticate the server and /// optionally the client for secure connection. /// /// /// A that represents the SSL /// configuration for secure connection. /// /// /// This listener has been closed. /// public ServerSslConfiguration SslConfiguration { get { if (_disposed) throw new ObjectDisposedException (_objectName); if (_sslConfig == null) _sslConfig = new ServerSslConfiguration (); return _sslConfig; } } /// /// 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 is not 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 or /// if not needed. /// /// /// It references the method used to find the credentials. /// /// /// The default value is . /// /// /// /// This listener has been closed. /// public Func UserCredentialsFinder { get { if (_disposed) throw new ObjectDisposedException (_objectName); return _userCredFinder; } set { if (_disposed) throw new ObjectDisposedException (_objectName); _userCredFinder = value; } } #endregion #region Private Methods private HttpListenerAsyncResult beginGetContext ( AsyncCallback callback, object state ) { lock (_contextRegistrySync) { if (!_listening) { var msg = _disposed ? "The listener is closed." : "The listener is stopped."; throw new HttpListenerException (995, msg); } var ares = new HttpListenerAsyncResult (callback, state); if (_contextQueue.Count == 0) { _waitQueue.Enqueue (ares); } else { var ctx = _contextQueue.Dequeue (); ares.Complete (ctx, true); } return ares; } } private void cleanupContextQueue (bool force) { if (_contextQueue.Count == 0) return; if (force) { _contextQueue.Clear (); return; } var ctxs = _contextQueue.ToArray (); _contextQueue.Clear (); foreach (var ctx in ctxs) { ctx.ErrorStatusCode = 503; ctx.SendError (); } } private void cleanupContextRegistry () { var cnt = _contextRegistry.Count; if (cnt == 0) return; var ctxs = new HttpListenerContext[cnt]; _contextRegistry.CopyTo (ctxs, 0); _contextRegistry.Clear (); foreach (var ctx in ctxs) ctx.Connection.Close (true); } private void cleanupWaitQueue (string message) { if (_waitQueue.Count == 0) return; var aress = _waitQueue.ToArray (); _waitQueue.Clear (); foreach (var ares in aress) { var ex = new HttpListenerException (995, message); ares.Complete (ex); } } private void close (bool force) { if (!_listening) { _disposed = true; return; } _listening = false; cleanupContextQueue (force); cleanupContextRegistry (); var msg = "The listener is closed."; cleanupWaitQueue (msg); EndPointManager.RemoveListener (this); _disposed = true; } private string getRealm () { var realm = _realm; return realm != null && realm.Length > 0 ? realm : _defaultRealm; } private AuthenticationSchemes selectAuthenticationScheme ( HttpListenerRequest request ) { var selector = _authSchemeSelector; if (selector == null) return _authSchemes; try { return selector (request); } catch { return AuthenticationSchemes.None; } } #endregion #region Internal Methods internal bool AuthenticateContext (HttpListenerContext context) { var req = context.Request; var schm = selectAuthenticationScheme (req); if (schm == AuthenticationSchemes.Anonymous) return true; if (schm == AuthenticationSchemes.None) { context.ErrorStatusCode = 403; context.ErrorMessage = "Authentication not allowed"; context.SendError (); return false; } var realm = getRealm (); var user = HttpUtility.CreateUser ( req.Headers["Authorization"], schm, realm, req.HttpMethod, _userCredFinder ); var authenticated = user != null && user.Identity.IsAuthenticated; if (!authenticated) { context.SendAuthenticationChallenge (schm, realm); return false; } context.User = user; return true; } internal void CheckDisposed () { if (_disposed) throw new ObjectDisposedException (_objectName); } internal bool RegisterContext (HttpListenerContext context) { if (!_listening) return false; lock (_contextRegistrySync) { if (!_listening) return false; context.Listener = this; _contextRegistry.AddLast (context); if (_waitQueue.Count == 0) { _contextQueue.Enqueue (context); } else { var ares = _waitQueue.Dequeue (); ares.Complete (context, false); } return true; } } internal void UnregisterContext (HttpListenerContext context) { lock (_contextRegistrySync) _contextRegistry.Remove (context); } #endregion #region Public Methods /// /// Shuts down the listener immediately. /// public void Abort () { if (_disposed) return; lock (_contextRegistrySync) { if (_disposed) return; close (true); } } /// /// Begins getting an incoming request asynchronously. /// /// /// /// This asynchronous operation must be completed by calling /// the EndGetContext method. /// /// /// Typically, the EndGetContext method is called by /// . /// /// /// /// 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 . /// /// /// /// This listener has no URI prefix on which listens. /// /// /// -or- /// /// /// This listener has not been started or is currently stopped. /// /// /// /// This method is canceled. /// /// /// This listener has been closed. /// public IAsyncResult BeginGetContext (AsyncCallback callback, object state) { if (_disposed) throw new ObjectDisposedException (_objectName); if (_prefixes.Count == 0) { var msg = "The listener has no URI prefix on which listens."; throw new InvalidOperationException (msg); } if (!_listening) { var msg = "The listener has not been started."; throw new InvalidOperationException (msg); } return beginGetContext (callback, state); } /// /// Shuts down the listener. /// public void Close () { if (_disposed) return; lock (_contextRegistrySync) { 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 instance obtained by calling /// the BeginGetContext method. /// /// /// is . /// /// /// was not obtained by calling /// the BeginGetContext method. /// /// /// This method was already called for . /// /// /// This method is canceled. /// /// /// This listener has been closed. /// public HttpListenerContext EndGetContext (IAsyncResult asyncResult) { if (_disposed) throw new ObjectDisposedException (_objectName); if (asyncResult == null) throw new ArgumentNullException ("asyncResult"); var ares = asyncResult as HttpListenerAsyncResult; if (ares == null) { var msg = "A wrong IAsyncResult instance."; throw new ArgumentException (msg, "asyncResult"); } lock (ares.SyncRoot) { if (ares.EndCalled) { var msg = "This IAsyncResult instance cannot be reused."; throw new InvalidOperationException (msg); } ares.EndCalled = true; } if (!ares.IsCompleted) ares.AsyncWaitHandle.WaitOne (); return ares.Context; } /// /// 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 has not been started or is currently stopped. /// /// /// /// This method is canceled. /// /// /// This listener has been closed. /// public HttpListenerContext GetContext () { if (_disposed) throw new ObjectDisposedException (_objectName); if (_prefixes.Count == 0) { var msg = "The listener has no URI prefix on which listens."; throw new InvalidOperationException (msg); } if (!_listening) { var msg = "The listener has not been started."; throw new InvalidOperationException (msg); } var ares = beginGetContext (null, null); ares.EndCalled = true; if (!ares.IsCompleted) ares.AsyncWaitHandle.WaitOne (); return ares.Context; } /// /// Starts receiving incoming requests. /// /// /// This listener has been closed. /// public void Start () { if (_disposed) throw new ObjectDisposedException (_objectName); lock (_contextRegistrySync) { if (_disposed) throw new ObjectDisposedException (_objectName); if (_listening) return; EndPointManager.AddListener (this); _listening = true; } } /// /// Stops receiving incoming requests. /// /// /// This listener has been closed. /// public void Stop () { if (_disposed) throw new ObjectDisposedException (_objectName); lock (_contextRegistrySync) { if (!_listening) return; _listening = false; cleanupContextQueue (false); cleanupContextRegistry (); var msg = "The listener is stopped."; cleanupWaitQueue (msg); EndPointManager.RemoveListener (this); } } #endregion #region Explicit Interface Implementations /// /// Releases all resources used by the listener. /// void IDisposable.Dispose () { if (_disposed) return; lock (_contextRegistrySync) { if (_disposed) return; close (true); } } #endregion } }