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 | }
|
---|