| 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 | //
|
|---|
| 5 | // System.Net.HttpListenerResponse
|
|---|
| 6 | //
|
|---|
| 7 | // Author:
|
|---|
| 8 | // Gonzalo Paniagua Javier (gonzalo@novell.com)
|
|---|
| 9 | //
|
|---|
| 10 | // Copyright (c) 2005 Novell, Inc. (http://www.novell.com)
|
|---|
| 11 | //
|
|---|
| 12 | // Permission is hereby granted, free of charge, to any person obtaining
|
|---|
| 13 | // a copy of this software and associated documentation files (the
|
|---|
| 14 | // "Software"), to deal in the Software without restriction, including
|
|---|
| 15 | // without limitation the rights to use, copy, modify, merge, publish,
|
|---|
| 16 | // distribute, sublicense, and/or sell copies of the Software, and to
|
|---|
| 17 | // permit persons to whom the Software is furnished to do so, subject to
|
|---|
| 18 | // the following conditions:
|
|---|
| 19 | //
|
|---|
| 20 | // The above copyright notice and this permission notice shall be
|
|---|
| 21 | // included in all copies or substantial portions of the Software.
|
|---|
| 22 | //
|
|---|
| 23 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|---|
| 24 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|---|
| 25 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|---|
| 26 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|---|
| 27 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|---|
| 28 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|---|
| 29 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|---|
| 30 | //
|
|---|
| 31 |
|
|---|
| 32 | // ReSharper disable RedundantExtendsListEntry
|
|---|
| 33 |
|
|---|
| 34 | using System;
|
|---|
| 35 | using System.Globalization;
|
|---|
| 36 | using System.IO;
|
|---|
| 37 | using System.Net;
|
|---|
| 38 | using System.Text;
|
|---|
| 39 |
|
|---|
| 40 | namespace SpaceWizards.HttpListener
|
|---|
| 41 | {
|
|---|
| 42 | public sealed partial class HttpListenerResponse : IDisposable
|
|---|
| 43 | {
|
|---|
| 44 | private long _contentLength;
|
|---|
| 45 | private Version _version = HttpVersion.Version11;
|
|---|
| 46 | private int _statusCode = 200;
|
|---|
| 47 | internal object _headersLock = new object();
|
|---|
| 48 | private bool _forceCloseChunked;
|
|---|
| 49 |
|
|---|
| 50 | internal HttpListenerResponse(HttpListenerContext context)
|
|---|
| 51 | {
|
|---|
| 52 | _httpContext = context;
|
|---|
| 53 | }
|
|---|
| 54 |
|
|---|
| 55 | internal bool ForceCloseChunked => _forceCloseChunked;
|
|---|
| 56 |
|
|---|
| 57 | private void EnsureResponseStream()
|
|---|
| 58 | {
|
|---|
| 59 | if (_responseStream == null)
|
|---|
| 60 | {
|
|---|
| 61 | _responseStream = _httpContext!.Connection.GetResponseStream();
|
|---|
| 62 | }
|
|---|
| 63 | }
|
|---|
| 64 |
|
|---|
| 65 | public Version ProtocolVersion
|
|---|
| 66 | {
|
|---|
| 67 | get => _version;
|
|---|
| 68 | set
|
|---|
| 69 | {
|
|---|
| 70 | CheckDisposed();
|
|---|
| 71 | if (value == null)
|
|---|
| 72 | {
|
|---|
| 73 | throw new ArgumentNullException(nameof(value));
|
|---|
| 74 | }
|
|---|
| 75 | if (value.Major != 1 || (value.Minor != 0 && value.Minor != 1))
|
|---|
| 76 | {
|
|---|
| 77 | throw new ArgumentException(SR.net_wrongversion, nameof(value));
|
|---|
| 78 | }
|
|---|
| 79 |
|
|---|
| 80 | _version = new Version(value.Major, value.Minor); // match Windows behavior, trimming to just Major.Minor
|
|---|
| 81 | }
|
|---|
| 82 | }
|
|---|
| 83 |
|
|---|
| 84 | public int StatusCode
|
|---|
| 85 | {
|
|---|
| 86 | get => _statusCode;
|
|---|
| 87 | set
|
|---|
| 88 | {
|
|---|
| 89 | CheckDisposed();
|
|---|
| 90 |
|
|---|
| 91 | if (value < 100 || value > 999)
|
|---|
| 92 | throw new ProtocolViolationException(SR.net_invalidstatus);
|
|---|
| 93 |
|
|---|
| 94 | _statusCode = value;
|
|---|
| 95 | }
|
|---|
| 96 | }
|
|---|
| 97 |
|
|---|
| 98 | private void Dispose() => Close(true);
|
|---|
| 99 |
|
|---|
| 100 | public void Close()
|
|---|
| 101 | {
|
|---|
| 102 | if (Disposed)
|
|---|
| 103 | return;
|
|---|
| 104 |
|
|---|
| 105 | Close(false);
|
|---|
| 106 | }
|
|---|
| 107 |
|
|---|
| 108 | public void Abort()
|
|---|
| 109 | {
|
|---|
| 110 | if (Disposed)
|
|---|
| 111 | return;
|
|---|
| 112 |
|
|---|
| 113 | Close(true);
|
|---|
| 114 | }
|
|---|
| 115 |
|
|---|
| 116 | private void Close(bool force)
|
|---|
| 117 | {
|
|---|
| 118 | Disposed = true;
|
|---|
| 119 | _httpContext!.Connection.Close(force);
|
|---|
| 120 | }
|
|---|
| 121 |
|
|---|
| 122 | public void Close(byte[] responseEntity, bool willBlock)
|
|---|
| 123 | {
|
|---|
| 124 | CheckDisposed();
|
|---|
| 125 |
|
|---|
| 126 | if (responseEntity == null)
|
|---|
| 127 | {
|
|---|
| 128 | throw new ArgumentNullException(nameof(responseEntity));
|
|---|
| 129 | }
|
|---|
| 130 |
|
|---|
| 131 | if (!SentHeaders && _boundaryType != BoundaryType.Chunked)
|
|---|
| 132 | {
|
|---|
| 133 | ContentLength64 = responseEntity.Length;
|
|---|
| 134 | }
|
|---|
| 135 |
|
|---|
| 136 | if (willBlock)
|
|---|
| 137 | {
|
|---|
| 138 | try
|
|---|
| 139 | {
|
|---|
| 140 | OutputStream.Write(responseEntity, 0, responseEntity.Length);
|
|---|
| 141 | }
|
|---|
| 142 | finally
|
|---|
| 143 | {
|
|---|
| 144 | Close(false);
|
|---|
| 145 | }
|
|---|
| 146 | }
|
|---|
| 147 | else
|
|---|
| 148 | {
|
|---|
| 149 | OutputStream.BeginWrite(responseEntity, 0, responseEntity.Length, iar =>
|
|---|
| 150 | {
|
|---|
| 151 | var thisRef = (HttpListenerResponse)iar.AsyncState!;
|
|---|
| 152 | try
|
|---|
| 153 | {
|
|---|
| 154 | thisRef.OutputStream.EndWrite(iar);
|
|---|
| 155 | }
|
|---|
| 156 | finally
|
|---|
| 157 | {
|
|---|
| 158 | thisRef.Close(false);
|
|---|
| 159 | }
|
|---|
| 160 | }, this);
|
|---|
| 161 | }
|
|---|
| 162 | }
|
|---|
| 163 |
|
|---|
| 164 | public void CopyFrom(HttpListenerResponse templateResponse)
|
|---|
| 165 | {
|
|---|
| 166 | _webHeaders.Clear();
|
|---|
| 167 | _webHeaders.Add(templateResponse._webHeaders);
|
|---|
| 168 | _contentLength = templateResponse._contentLength;
|
|---|
| 169 | _statusCode = templateResponse._statusCode;
|
|---|
| 170 | _statusDescription = templateResponse._statusDescription;
|
|---|
| 171 | _keepAlive = templateResponse._keepAlive;
|
|---|
| 172 | _version = templateResponse._version;
|
|---|
| 173 | }
|
|---|
| 174 |
|
|---|
| 175 | internal void SendHeaders(bool closing, MemoryStream ms, bool isWebSocketHandshake = false)
|
|---|
| 176 | {
|
|---|
| 177 | if (!isWebSocketHandshake)
|
|---|
| 178 | {
|
|---|
| 179 | if (_webHeaders[HttpKnownHeaderNames.Server] == null)
|
|---|
| 180 | {
|
|---|
| 181 | _webHeaders.Set(HttpKnownHeaderNames.Server, HttpHeaderStrings.NetCoreServerName);
|
|---|
| 182 | }
|
|---|
| 183 |
|
|---|
| 184 | if (_webHeaders[HttpKnownHeaderNames.Date] == null)
|
|---|
| 185 | {
|
|---|
| 186 | _webHeaders.Set(HttpKnownHeaderNames.Date, DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture));
|
|---|
| 187 | }
|
|---|
| 188 |
|
|---|
| 189 | if (_boundaryType == BoundaryType.None)
|
|---|
| 190 | {
|
|---|
| 191 | if (HttpListenerRequest.ProtocolVersion <= HttpVersion.Version10)
|
|---|
| 192 | {
|
|---|
| 193 | _keepAlive = false;
|
|---|
| 194 | }
|
|---|
| 195 | else
|
|---|
| 196 | {
|
|---|
| 197 | _boundaryType = BoundaryType.Chunked;
|
|---|
| 198 | }
|
|---|
| 199 |
|
|---|
| 200 | if (CanSendResponseBody(_httpContext!.Response.StatusCode))
|
|---|
| 201 | {
|
|---|
| 202 | _contentLength = -1;
|
|---|
| 203 | }
|
|---|
| 204 | else
|
|---|
| 205 | {
|
|---|
| 206 | _boundaryType = BoundaryType.ContentLength;
|
|---|
| 207 | _contentLength = 0;
|
|---|
| 208 | }
|
|---|
| 209 | }
|
|---|
| 210 |
|
|---|
| 211 | if (_boundaryType != BoundaryType.Chunked)
|
|---|
| 212 | {
|
|---|
| 213 | if (_boundaryType != BoundaryType.ContentLength && closing)
|
|---|
| 214 | {
|
|---|
| 215 | _contentLength = CanSendResponseBody(_httpContext!.Response.StatusCode) ? -1 : 0;
|
|---|
| 216 | }
|
|---|
| 217 |
|
|---|
| 218 | if (_boundaryType == BoundaryType.ContentLength)
|
|---|
| 219 | {
|
|---|
| 220 | _webHeaders.Set(HttpKnownHeaderNames.ContentLength, _contentLength.ToString("D", CultureInfo.InvariantCulture));
|
|---|
| 221 | }
|
|---|
| 222 | }
|
|---|
| 223 |
|
|---|
| 224 | /* Apache forces closing the connection for these status codes:
|
|---|
| 225 | * HttpStatusCode.BadRequest 400
|
|---|
| 226 | * HttpStatusCode.RequestTimeout 408
|
|---|
| 227 | * HttpStatusCode.LengthRequired 411
|
|---|
| 228 | * HttpStatusCode.RequestEntityTooLarge 413
|
|---|
| 229 | * HttpStatusCode.RequestUriTooLong 414
|
|---|
| 230 | * HttpStatusCode.InternalServerError 500
|
|---|
| 231 | * HttpStatusCode.ServiceUnavailable 503
|
|---|
| 232 | */
|
|---|
| 233 | bool conn_close = (_statusCode == (int)HttpStatusCode.BadRequest || _statusCode == (int)HttpStatusCode.RequestTimeout
|
|---|
| 234 | || _statusCode == (int)HttpStatusCode.LengthRequired || _statusCode == (int)HttpStatusCode.RequestEntityTooLarge
|
|---|
| 235 | || _statusCode == (int)HttpStatusCode.RequestUriTooLong || _statusCode == (int)HttpStatusCode.InternalServerError
|
|---|
| 236 | || _statusCode == (int)HttpStatusCode.ServiceUnavailable);
|
|---|
| 237 |
|
|---|
| 238 | if (!conn_close)
|
|---|
| 239 | {
|
|---|
| 240 | conn_close = !_httpContext!.Request.KeepAlive;
|
|---|
| 241 | }
|
|---|
| 242 |
|
|---|
| 243 | // They sent both KeepAlive: true and Connection: close
|
|---|
| 244 | if (!_keepAlive || conn_close)
|
|---|
| 245 | {
|
|---|
| 246 | _webHeaders.Set(HttpKnownHeaderNames.Connection, HttpHeaderStrings.Close);
|
|---|
| 247 | conn_close = true;
|
|---|
| 248 | }
|
|---|
| 249 |
|
|---|
| 250 | if (SendChunked)
|
|---|
| 251 | {
|
|---|
| 252 | _webHeaders.Set(HttpKnownHeaderNames.TransferEncoding, HttpHeaderStrings.Chunked);
|
|---|
| 253 | }
|
|---|
| 254 |
|
|---|
| 255 | int reuses = _httpContext!.Connection.Reuses;
|
|---|
| 256 | if (reuses >= 100)
|
|---|
| 257 | {
|
|---|
| 258 | _forceCloseChunked = true;
|
|---|
| 259 | if (!conn_close)
|
|---|
| 260 | {
|
|---|
| 261 | _webHeaders.Set(HttpKnownHeaderNames.Connection, HttpHeaderStrings.Close);
|
|---|
| 262 | conn_close = true;
|
|---|
| 263 | }
|
|---|
| 264 | }
|
|---|
| 265 |
|
|---|
| 266 | if (HttpListenerRequest.ProtocolVersion <= HttpVersion.Version10)
|
|---|
| 267 | {
|
|---|
| 268 | if (_keepAlive)
|
|---|
| 269 | {
|
|---|
| 270 | Headers[HttpResponseHeader.KeepAlive] = "true";
|
|---|
| 271 | }
|
|---|
| 272 |
|
|---|
| 273 | if (!conn_close)
|
|---|
| 274 | {
|
|---|
| 275 | _webHeaders.Set(HttpKnownHeaderNames.Connection, HttpHeaderStrings.KeepAlive);
|
|---|
| 276 | }
|
|---|
| 277 | }
|
|---|
| 278 |
|
|---|
| 279 | ComputeCookies();
|
|---|
| 280 | }
|
|---|
| 281 |
|
|---|
| 282 | Encoding encoding = Encoding.Default;
|
|---|
| 283 | StreamWriter writer = new StreamWriter(ms, encoding, 256);
|
|---|
| 284 | writer.Write("HTTP/1.1 {0} ", _statusCode); // "1.1" matches Windows implementation, which ignores the response version
|
|---|
| 285 | writer.Flush();
|
|---|
| 286 | byte[] statusDescriptionBytes = WebHeaderEncoding.GetBytes(StatusDescription);
|
|---|
| 287 | ms.Write(statusDescriptionBytes, 0, statusDescriptionBytes.Length);
|
|---|
| 288 | writer.Write("\r\n");
|
|---|
| 289 |
|
|---|
| 290 | writer.Write(FormatHeaders(_webHeaders));
|
|---|
| 291 | writer.Flush();
|
|---|
| 292 | #if UNITY_NETFRAMEWORK
|
|---|
| 293 | int preamble = encoding.GetPreamble().Length;
|
|---|
| 294 | #else
|
|---|
| 295 | int preamble = encoding.Preamble.Length;
|
|---|
| 296 | #endif
|
|---|
| 297 | EnsureResponseStream();
|
|---|
| 298 |
|
|---|
| 299 | /* Assumes that the ms was at position 0 */
|
|---|
| 300 | ms.Position = preamble;
|
|---|
| 301 | SentHeaders = !isWebSocketHandshake;
|
|---|
| 302 | }
|
|---|
| 303 |
|
|---|
| 304 | private static bool HeaderCanHaveEmptyValue(string name) =>
|
|---|
| 305 | !string.Equals(name, HttpKnownHeaderNames.Location, StringComparison.OrdinalIgnoreCase);
|
|---|
| 306 |
|
|---|
| 307 | private static string FormatHeaders(WebHeaderCollection headers)
|
|---|
| 308 | {
|
|---|
| 309 | var sb = new StringBuilder();
|
|---|
| 310 |
|
|---|
| 311 | for (int i = 0; i < headers.Count; i++)
|
|---|
| 312 | {
|
|---|
| 313 | string key = headers.GetKey(i);
|
|---|
| 314 | string[] values = headers.GetValues(i)!;
|
|---|
| 315 |
|
|---|
| 316 | int startingLength = sb.Length;
|
|---|
| 317 |
|
|---|
| 318 | sb.Append(key).Append(": ");
|
|---|
| 319 | bool anyValues = false;
|
|---|
| 320 | for (int j = 0; j < values.Length; j++)
|
|---|
| 321 | {
|
|---|
| 322 | string value = values[j];
|
|---|
| 323 | if (!string.IsNullOrWhiteSpace(value))
|
|---|
| 324 | {
|
|---|
| 325 | if (anyValues)
|
|---|
| 326 | {
|
|---|
| 327 | if (key.Equals(HttpKnownHeaderNames.SetCookie, StringComparison.OrdinalIgnoreCase) ||
|
|---|
| 328 | key.Equals(HttpKnownHeaderNames.SetCookie2, StringComparison.OrdinalIgnoreCase))
|
|---|
| 329 | {
|
|---|
| 330 | sb.Append("\r\n").Append(key).Append(": ");
|
|---|
| 331 | }
|
|---|
| 332 | else
|
|---|
| 333 | {
|
|---|
| 334 | sb.Append(", ");
|
|---|
| 335 | }
|
|---|
| 336 | }
|
|---|
| 337 | sb.Append(value);
|
|---|
| 338 | anyValues = true;
|
|---|
| 339 | }
|
|---|
| 340 | }
|
|---|
| 341 |
|
|---|
| 342 | if (anyValues || HeaderCanHaveEmptyValue(key))
|
|---|
| 343 | {
|
|---|
| 344 | // Complete the header
|
|---|
| 345 | sb.Append("\r\n");
|
|---|
| 346 | }
|
|---|
| 347 | else
|
|---|
| 348 | {
|
|---|
| 349 | // Empty header; remove it.
|
|---|
| 350 | sb.Length = startingLength;
|
|---|
| 351 | }
|
|---|
| 352 | }
|
|---|
| 353 |
|
|---|
| 354 | return sb.Append("\r\n").ToString();
|
|---|
| 355 | }
|
|---|
| 356 |
|
|---|
| 357 | private bool Disposed { get; set; }
|
|---|
| 358 | internal bool SentHeaders { get; set; }
|
|---|
| 359 | }
|
|---|
| 360 | }
|
|---|