| 1 | // Licensed to the .NET Foundation under one or more agreements.
|
|---|
| 2 | // The .NET Foundation licenses this file to you under the MIT license.
|
|---|
| 3 |
|
|---|
| 4 | // ReSharper disable RedundantUsingDirective
|
|---|
| 5 | // ReSharper disable RedundantUnsafeContext
|
|---|
| 6 |
|
|---|
| 7 | using System;
|
|---|
| 8 | using System.Collections;
|
|---|
| 9 | using System.Diagnostics.CodeAnalysis;
|
|---|
| 10 | using System.Net;
|
|---|
| 11 | using System.Security.Authentication.ExtendedProtection;
|
|---|
| 12 | using System.Text;
|
|---|
| 13 | using System.Threading.Tasks;
|
|---|
| 14 |
|
|---|
| 15 | namespace SpaceWizards.HttpListener
|
|---|
| 16 | {
|
|---|
| 17 | public sealed unsafe partial class HttpListener : IDisposable
|
|---|
| 18 | {
|
|---|
| 19 | public delegate ExtendedProtectionPolicy ExtendedProtectionSelector(HttpListenerRequest request);
|
|---|
| 20 |
|
|---|
| 21 | private readonly object _internalLock;
|
|---|
| 22 | private volatile State _state; // _state is set only within lock blocks, but often read outside locks.
|
|---|
| 23 | private readonly HttpListenerPrefixCollection _prefixes;
|
|---|
| 24 | internal Hashtable _uriPrefixes = new Hashtable();
|
|---|
| 25 | private bool _ignoreWriteExceptions;
|
|---|
| 26 | private readonly ServiceNameStore _defaultServiceNames;
|
|---|
| 27 | private readonly HttpListenerTimeoutManager _timeoutManager;
|
|---|
| 28 | private ExtendedProtectionPolicy _extendedProtectionPolicy;
|
|---|
| 29 | private AuthenticationSchemeSelector? _authenticationDelegate;
|
|---|
| 30 | private AuthenticationSchemes _authenticationScheme = AuthenticationSchemes.Anonymous;
|
|---|
| 31 | private ExtendedProtectionSelector? _extendedProtectionSelectorDelegate;
|
|---|
| 32 | private string? _realm;
|
|---|
| 33 |
|
|---|
| 34 | internal ICollection PrefixCollection => _uriPrefixes.Keys;
|
|---|
| 35 |
|
|---|
| 36 | public HttpListener()
|
|---|
| 37 | {
|
|---|
| 38 | _state = State.Stopped;
|
|---|
| 39 | _internalLock = new object();
|
|---|
| 40 | _defaultServiceNames = new ServiceNameStore();
|
|---|
| 41 |
|
|---|
| 42 | _timeoutManager = new HttpListenerTimeoutManager(this);
|
|---|
| 43 | _prefixes = new HttpListenerPrefixCollection(this);
|
|---|
| 44 |
|
|---|
| 45 | // default: no CBT checks on any platform (appcompat reasons); applies also to PolicyEnforcement
|
|---|
| 46 | // config element
|
|---|
| 47 | _extendedProtectionPolicy = new ExtendedProtectionPolicy(PolicyEnforcement.Never);
|
|---|
| 48 | }
|
|---|
| 49 |
|
|---|
| 50 | public AuthenticationSchemeSelector? AuthenticationSchemeSelectorDelegate
|
|---|
| 51 | {
|
|---|
| 52 | get => _authenticationDelegate;
|
|---|
| 53 | set
|
|---|
| 54 | {
|
|---|
| 55 | CheckDisposed();
|
|---|
| 56 | _authenticationDelegate = value;
|
|---|
| 57 | }
|
|---|
| 58 | }
|
|---|
| 59 |
|
|---|
| 60 | #if !UNITY_NETFRAMEWORK
|
|---|
| 61 | [DisallowNull]
|
|---|
| 62 | #endif
|
|---|
| 63 | public ExtendedProtectionSelector? ExtendedProtectionSelectorDelegate
|
|---|
| 64 | {
|
|---|
| 65 | get => _extendedProtectionSelectorDelegate;
|
|---|
| 66 | set
|
|---|
| 67 | {
|
|---|
| 68 | CheckDisposed();
|
|---|
| 69 | if (value == null)
|
|---|
| 70 | {
|
|---|
| 71 | throw new ArgumentNullException(nameof(value));
|
|---|
| 72 | }
|
|---|
| 73 |
|
|---|
| 74 | _extendedProtectionSelectorDelegate = value;
|
|---|
| 75 | }
|
|---|
| 76 | }
|
|---|
| 77 |
|
|---|
| 78 | public AuthenticationSchemes AuthenticationSchemes
|
|---|
| 79 | {
|
|---|
| 80 | get => _authenticationScheme;
|
|---|
| 81 | set
|
|---|
| 82 | {
|
|---|
| 83 | CheckDisposed();
|
|---|
| 84 | _authenticationScheme = value;
|
|---|
| 85 | }
|
|---|
| 86 | }
|
|---|
| 87 |
|
|---|
| 88 | public ExtendedProtectionPolicy ExtendedProtectionPolicy
|
|---|
| 89 | {
|
|---|
| 90 | get => _extendedProtectionPolicy;
|
|---|
| 91 | set
|
|---|
| 92 | {
|
|---|
| 93 | CheckDisposed();
|
|---|
| 94 | if (value == null)
|
|---|
| 95 | {
|
|---|
| 96 | throw new ArgumentNullException(nameof(value));
|
|---|
| 97 | }
|
|---|
| 98 | if (value.CustomChannelBinding != null)
|
|---|
| 99 | {
|
|---|
| 100 | throw new ArgumentException(SR.net_listener_cannot_set_custom_cbt, nameof(value));
|
|---|
| 101 | }
|
|---|
| 102 |
|
|---|
| 103 | _extendedProtectionPolicy = value;
|
|---|
| 104 | }
|
|---|
| 105 | }
|
|---|
| 106 |
|
|---|
| 107 | public ServiceNameCollection DefaultServiceNames => _defaultServiceNames.ServiceNames;
|
|---|
| 108 |
|
|---|
| 109 | public HttpListenerPrefixCollection Prefixes
|
|---|
| 110 | {
|
|---|
| 111 | get
|
|---|
| 112 | {
|
|---|
| 113 | CheckDisposed();
|
|---|
| 114 | return _prefixes;
|
|---|
| 115 | }
|
|---|
| 116 | }
|
|---|
| 117 |
|
|---|
| 118 | internal void AddPrefix(string uriPrefix)
|
|---|
| 119 | {
|
|---|
| 120 | string? registeredPrefix = null;
|
|---|
| 121 | try
|
|---|
| 122 | {
|
|---|
| 123 | if (uriPrefix == null)
|
|---|
| 124 | {
|
|---|
| 125 | throw new ArgumentNullException(nameof(uriPrefix));
|
|---|
| 126 | }
|
|---|
| 127 | CheckDisposed();
|
|---|
| 128 | int i;
|
|---|
| 129 | if (string.Compare(uriPrefix, 0, "http://", 0, 7, StringComparison.OrdinalIgnoreCase) == 0)
|
|---|
| 130 | {
|
|---|
| 131 | i = 7;
|
|---|
| 132 | }
|
|---|
| 133 | else if (string.Compare(uriPrefix, 0, "https://", 0, 8, StringComparison.OrdinalIgnoreCase) == 0)
|
|---|
| 134 | {
|
|---|
| 135 | i = 8;
|
|---|
| 136 | }
|
|---|
| 137 | else
|
|---|
| 138 | {
|
|---|
| 139 | throw new ArgumentException(SR.net_listener_scheme, nameof(uriPrefix));
|
|---|
| 140 | }
|
|---|
| 141 |
|
|---|
| 142 | int j = ServiceNameStore.FindEndOfHostname(uriPrefix, i);
|
|---|
| 143 | if (i == j)
|
|---|
| 144 | {
|
|---|
| 145 | throw new ArgumentException(SR.net_listener_host, nameof(uriPrefix));
|
|---|
| 146 | }
|
|---|
| 147 | if (uriPrefix[uriPrefix.Length - 1] != '/')
|
|---|
| 148 | {
|
|---|
| 149 | throw new ArgumentException(SR.net_listener_slash, nameof(uriPrefix));
|
|---|
| 150 | }
|
|---|
| 151 | StringBuilder registeredPrefixBuilder = new StringBuilder();
|
|---|
| 152 | if (uriPrefix[j] == ':')
|
|---|
| 153 | {
|
|---|
| 154 | registeredPrefixBuilder.Append(uriPrefix);
|
|---|
| 155 | }
|
|---|
| 156 | else
|
|---|
| 157 | {
|
|---|
| 158 | registeredPrefixBuilder.Append(uriPrefix, 0, j);
|
|---|
| 159 | registeredPrefixBuilder.Append(i == 7 ? ":80" : ":443");
|
|---|
| 160 | registeredPrefixBuilder.Append(uriPrefix, j, uriPrefix.Length - j);
|
|---|
| 161 | }
|
|---|
| 162 | for (i = 0; registeredPrefixBuilder[i] != ':'; i++)
|
|---|
| 163 | {
|
|---|
| 164 | registeredPrefixBuilder[i] = (char)CaseInsensitiveAscii.AsciiToLower[(byte)registeredPrefixBuilder[i]];
|
|---|
| 165 | }
|
|---|
| 166 | registeredPrefix = registeredPrefixBuilder.ToString();
|
|---|
| 167 | if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"mapped uriPrefix: {uriPrefix} to registeredPrefix: {registeredPrefix}");
|
|---|
| 168 | if (_state == State.Started)
|
|---|
| 169 | {
|
|---|
| 170 | AddPrefixCore(registeredPrefix);
|
|---|
| 171 | }
|
|---|
| 172 | _uriPrefixes[uriPrefix] = registeredPrefix;
|
|---|
| 173 | _defaultServiceNames.Add(uriPrefix);
|
|---|
| 174 | }
|
|---|
| 175 | catch (Exception exception)
|
|---|
| 176 | {
|
|---|
| 177 | if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, exception);
|
|---|
| 178 | throw;
|
|---|
| 179 | }
|
|---|
| 180 | }
|
|---|
| 181 |
|
|---|
| 182 | internal bool ContainsPrefix(string uriPrefix) => _uriPrefixes.Contains(uriPrefix);
|
|---|
| 183 |
|
|---|
| 184 | internal bool RemovePrefix(string uriPrefix)
|
|---|
| 185 | {
|
|---|
| 186 | try
|
|---|
| 187 | {
|
|---|
| 188 | CheckDisposed();
|
|---|
| 189 | if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"uriPrefix: {uriPrefix}");
|
|---|
| 190 | if (uriPrefix == null)
|
|---|
| 191 | {
|
|---|
| 192 | throw new ArgumentNullException(nameof(uriPrefix));
|
|---|
| 193 | }
|
|---|
| 194 |
|
|---|
| 195 | if (!_uriPrefixes.Contains(uriPrefix))
|
|---|
| 196 | {
|
|---|
| 197 | return false;
|
|---|
| 198 | }
|
|---|
| 199 |
|
|---|
| 200 | if (_state == State.Started)
|
|---|
| 201 | {
|
|---|
| 202 | RemovePrefixCore((string)_uriPrefixes[uriPrefix]!);
|
|---|
| 203 | }
|
|---|
| 204 |
|
|---|
| 205 | _uriPrefixes.Remove(uriPrefix);
|
|---|
| 206 | _defaultServiceNames.Remove(uriPrefix);
|
|---|
| 207 | }
|
|---|
| 208 | catch (Exception exception)
|
|---|
| 209 | {
|
|---|
| 210 | if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, exception);
|
|---|
| 211 | throw;
|
|---|
| 212 | }
|
|---|
| 213 | return true;
|
|---|
| 214 | }
|
|---|
| 215 |
|
|---|
| 216 | internal void RemoveAll(bool clear)
|
|---|
| 217 | {
|
|---|
| 218 | CheckDisposed();
|
|---|
| 219 | // go through the uri list and unregister for each one of them
|
|---|
| 220 | if (_uriPrefixes.Count > 0)
|
|---|
| 221 | {
|
|---|
| 222 | if (_state == State.Started)
|
|---|
| 223 | {
|
|---|
| 224 | foreach (string registeredPrefix in _uriPrefixes.Values)
|
|---|
| 225 | {
|
|---|
| 226 | RemovePrefixCore(registeredPrefix);
|
|---|
| 227 | }
|
|---|
| 228 | }
|
|---|
| 229 |
|
|---|
| 230 | if (clear)
|
|---|
| 231 | {
|
|---|
| 232 | _uriPrefixes.Clear();
|
|---|
| 233 | _defaultServiceNames.Clear();
|
|---|
| 234 | }
|
|---|
| 235 | }
|
|---|
| 236 | }
|
|---|
| 237 |
|
|---|
| 238 | public string? Realm
|
|---|
| 239 | {
|
|---|
| 240 | get => _realm;
|
|---|
| 241 | set
|
|---|
| 242 | {
|
|---|
| 243 | CheckDisposed();
|
|---|
| 244 | _realm = value;
|
|---|
| 245 | }
|
|---|
| 246 | }
|
|---|
| 247 |
|
|---|
| 248 | public bool IsListening => _state == State.Started;
|
|---|
| 249 |
|
|---|
| 250 | public bool IgnoreWriteExceptions
|
|---|
| 251 | {
|
|---|
| 252 | get => _ignoreWriteExceptions;
|
|---|
| 253 | set
|
|---|
| 254 | {
|
|---|
| 255 | CheckDisposed();
|
|---|
| 256 | _ignoreWriteExceptions = value;
|
|---|
| 257 | }
|
|---|
| 258 | }
|
|---|
| 259 |
|
|---|
| 260 | public Task<HttpListenerContext> GetContextAsync()
|
|---|
| 261 | {
|
|---|
| 262 | return Task.Factory.FromAsync(
|
|---|
| 263 | (callback, state) => ((HttpListener)state!).BeginGetContext(callback, state),
|
|---|
| 264 | iar => ((HttpListener)iar!.AsyncState!).EndGetContext(iar),
|
|---|
| 265 | this);
|
|---|
| 266 | }
|
|---|
| 267 |
|
|---|
| 268 | public void Close()
|
|---|
| 269 | {
|
|---|
| 270 | try
|
|---|
| 271 | {
|
|---|
| 272 | if (NetEventSource.Log.IsEnabled()) NetEventSource.Info("HttpListenerRequest::Close()");
|
|---|
| 273 | ((IDisposable)this).Dispose();
|
|---|
| 274 | }
|
|---|
| 275 | catch (Exception exception)
|
|---|
| 276 | {
|
|---|
| 277 | if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"Close {exception}");
|
|---|
| 278 | throw;
|
|---|
| 279 | }
|
|---|
| 280 | }
|
|---|
| 281 |
|
|---|
| 282 | internal void CheckDisposed()
|
|---|
| 283 | {
|
|---|
| 284 | if (_state == State.Closed)
|
|---|
| 285 | {
|
|---|
| 286 | throw new ObjectDisposedException(GetType().FullName);
|
|---|
| 287 | }
|
|---|
| 288 | }
|
|---|
| 289 |
|
|---|
| 290 | private enum State
|
|---|
| 291 | {
|
|---|
| 292 | Stopped,
|
|---|
| 293 | Started,
|
|---|
| 294 | Closed,
|
|---|
| 295 | }
|
|---|
| 296 |
|
|---|
| 297 | void IDisposable.Dispose() => Dispose();
|
|---|
| 298 | }
|
|---|
| 299 | }
|
|---|