/// <summary> /// Modifies the <see cref="UseGzip"/> option in this response object and returns the same object.</summary> /// <param name="option"> /// The new value for the <see cref="UseGzip"/> option.</param> public HttpResponseContent Set(UseGzipOption option) { UseGzip = option; return this; }
private bool outputResponse(HttpResponse response, HttpStatusCode status, HttpResponseHeaders headers, Stream contentStream, UseGzipOption useGzipOption, HttpRequest originalRequest) { // IMPORTANT: Do not access properties of ‘response’. Use the other parameters instead, which contain copies. // The request handler can re-use HttpResponse objects between requests. Thus the properties of ‘response’ would // be accessed simultaneously from multiple instances of this method running in separate threads. // Additionally, HttpResponse is a MarshalByRefObject but HttpResponseHeaders is not, so ‘response’ // may be a transparent proxy, in which case ‘response.Headers’ would retrieve a serialized copy. // ‘response’ is ONLY used to pass it to _server.ResponseExceptionHandler(). Socket.NoDelay = false; try { bool gzipRequested = false; if (originalRequest.Headers.AcceptEncoding != null) foreach (HttpContentEncoding hce in originalRequest.Headers.AcceptEncoding) gzipRequested = gzipRequested || (hce == HttpContentEncoding.Gzip); bool contentLengthKnown = false; long contentLength = 0; // Find out if we know the content length if (contentStream == null) { contentLength = 0; contentLengthKnown = true; } else if (headers.ContentLength != null) { contentLength = headers.ContentLength.Value; contentLengthKnown = true; } else { // See if we can deduce the content length from the stream try { if (contentStream.CanSeek) { contentLength = contentStream.Length; contentLengthKnown = true; } } catch (NotSupportedException) { } } bool useKeepAlive = originalRequest.HttpVersion == HttpProtocolVersion.Http11 && originalRequest.Headers.Connection.HasFlag(HttpConnection.KeepAlive) && !headers.Connection.HasFlag(HttpConnection.Close); headers.Connection = useKeepAlive ? HttpConnection.KeepAlive : HttpConnection.Close; headers.ContentLength = null; // Special cases: status codes that may not have a body if (!status.MayHaveBody()) { if (contentStream != null) throw new InvalidOperationException("A response with the {0} status cannot have a body (GetContentStream must be null or return null).".Fmt(status)); if (headers.ContentType != null) throw new InvalidOperationException("A response with the {0} status cannot have a Content-Type header.".Fmt(status)); sendHeaders(status, headers); return useKeepAlive; } // Special case: empty body if (contentLengthKnown && contentLength == 0) { headers.ContentLength = 0; sendHeaders(status, headers); return useKeepAlive; } // If no Content-Type is given, use default headers.ContentType = headers.ContentType ?? _server.Options.DefaultContentType; // If we know the content length and the stream can seek, then we can support Ranges - but it's not worth it for less than 16 KB if (originalRequest.HttpVersion == HttpProtocolVersion.Http11 && contentLengthKnown && contentLength > 16 * 1024 && status == HttpStatusCode._200_OK && contentStream.CanSeek) { headers.AcceptRanges = HttpAcceptRanges.Bytes; // If the client requested a range, then serve it if (status == HttpStatusCode._200_OK && originalRequest.Headers.Range != null) { // Construct a canonical set of satisfiable ranges var ranges = new SortedList<long, long>(); foreach (var r in originalRequest.Headers.Range) { long rFrom = r.From == null || r.From.Value < 0 ? 0 : r.From.Value; long rTo = r.To == null || r.To.Value >= contentLength ? contentLength - 1 : r.To.Value; if (ranges.ContainsKey(rFrom)) ranges[rFrom] = Math.Max(ranges[rFrom], rTo); else ranges.Add(rFrom, rTo); } // If one of the ranges spans the complete file, don't bother with ranges if (!ranges.ContainsKey(0) || ranges[0] < contentLength - 1) { // Make a copy of this so that we can modify Ranges while iterating over it var rangeFroms = new List<long>(ranges.Keys); long prevFrom = 0; bool havePrevFrom = false; foreach (long from in rangeFroms) { if (!havePrevFrom) { prevFrom = from; havePrevFrom = true; } else if (ranges[prevFrom] >= from) { ranges[prevFrom] = Math.Max(ranges[prevFrom], ranges[from]); ranges.Remove(from); } } // Note that "ContentLength" here refers to the total size of the file. // The functions ServeSingleRange() and ServeRanges() will automatically // set a Content-Length header that specifies the size of just the range(s). // Also note that if Ranges.Count is 0, we want to fall through and handle the request without ranges if (ranges.Count == 1) { var range = ranges.First(); serveSingleRange(headers, contentStream, originalRequest, range.Key, range.Value, contentLength); return useKeepAlive; } else if (ranges.Count > 1) { serveRanges(headers, contentStream, originalRequest, ranges, contentLength); return useKeepAlive; } } } } bool useGzip = useGzipOption != UseGzipOption.DontUseGzip && gzipRequested && !(contentLengthKnown && contentLength <= 1024) && originalRequest.HttpVersion == HttpProtocolVersion.Http11; if (useGzip && useGzipOption == UseGzipOption.AutoDetect && contentLengthKnown && contentLength >= _server.Options.GzipAutodetectThreshold && contentStream.CanSeek) { try { contentStream.Seek((contentLength - _server.Options.GzipAutodetectThreshold) / 2, SeekOrigin.Begin); byte[] buf = new byte[_server.Options.GzipAutodetectThreshold]; contentStream.Read(buf, 0, _server.Options.GzipAutodetectThreshold); using (var ms = new MemoryStream()) { using (var gzTester = new GZipOutputStream(ms)) { gzTester.SetLevel(1); gzTester.Write(buf, 0, _server.Options.GzipAutodetectThreshold); } if (ms.ToArray().Length >= 0.99 * _server.Options.GzipAutodetectThreshold) useGzip = false; } contentStream.Seek(0, SeekOrigin.Begin); } catch { } } headers.ContentEncoding = useGzip ? HttpContentEncoding.Gzip : HttpContentEncoding.Identity; // If we know the content length and it is smaller than the in-memory gzip threshold, gzip and output everything now if (useGzip && contentLengthKnown && contentLength < _server.Options.GzipInMemoryUpToSize) { // In this case, do all the gzipping before sending the headers. // After all we want to include the new (compressed) Content-Length. MemoryStream ms = new MemoryStream(); GZipOutputStream gz = new GZipOutputStream(ms); gz.SetLevel(1); byte[] contentReadBuffer = new byte[65536]; int bytes = contentStream.Read(contentReadBuffer, 0, 65536); while (bytes > 0) { gz.Write(contentReadBuffer, 0, bytes); bytes = contentStream.Read(contentReadBuffer, 0, 65536); } gz.Close(); byte[] resultBuffer = ms.ToArray(); headers.ContentLength = resultBuffer.Length; sendHeaders(status, headers); if (originalRequest.Method == HttpMethod.Head) return useKeepAlive; _stream.Write(resultBuffer); return useKeepAlive; } Stream output; if (useGzip && !useKeepAlive) { // In this case, send the headers first, then instantiate the GZipStream. // Otherwise we run the risk that the GzipStream might write to the socket before the headers are sent. // Also note that we are not sending a Content-Length header; even if we know the content length // of the uncompressed file, we cannot predict the length of the compressed output yet sendHeaders(status, headers); if (originalRequest.Method == HttpMethod.Head) return useKeepAlive; var str = new GZipOutputStream(new DoNotCloseStream(_stream)); str.SetLevel(1); output = str; } else if (useGzip) { // In this case, combine Gzip with chunked Transfer-Encoding. No Content-Length header headers.TransferEncoding = HttpTransferEncoding.Chunked; sendHeaders(status, headers); if (originalRequest.Method == HttpMethod.Head) return useKeepAlive; var str = new GZipOutputStream(new ChunkedEncodingStream(_stream, leaveInnerOpen: true)); str.SetLevel(1); output = str; } else if (useKeepAlive && !contentLengthKnown) { // Use chunked encoding without Gzip headers.TransferEncoding = HttpTransferEncoding.Chunked; sendHeaders(status, headers); if (originalRequest.Method == HttpMethod.Head) return useKeepAlive; output = new ChunkedEncodingStream(_stream, leaveInnerOpen: true); } else { // No Gzip, no chunked, but if we know the content length, supply it // (if we don't, then we're not using keep-alive here) if (contentLengthKnown) headers.ContentLength = contentLength; sendHeaders(status, headers); if (originalRequest.Method == HttpMethod.Head) return useKeepAlive; // We need DoNotCloseStream here because the later code needs to be able to // close ‘output’ in case it’s a Gzip and/or Chunked stream; however, we don’t // want to close the socket because it might be a keep-alive connection. output = new DoNotCloseStream(_stream); } // Finally output the actual content byte[] buffer = new byte[65536]; int bufferSize = buffer.Length; int bytesRead; while (true) { // There are no “valid” exceptions that may originate from the content stream, so the “Error500” setting // actually propagates everything. if (_server.PropagateExceptions) bytesRead = contentStream.Read(buffer, 0, bufferSize); else try { bytesRead = contentStream.Read(buffer, 0, bufferSize); } catch (Exception e) { if (!(e is SocketException) && _server.Options.OutputExceptionInformation) output.Write((headers.ContentType.StartsWith("text/html") ? exceptionToHtml(e) : exceptionToPlaintext(e)).ToUtf8()); var handler = _server.ResponseExceptionHandler; if (handler != null) handler(originalRequest, e, response); output.Close(); return false; } if (bytesRead == 0) break; // Performance optimisation: If we’re at the end of a body of known length, cause // the last bit to be sent to the socket without the Nagle delay try { if (contentStream.CanSeek && contentStream.Position == contentStream.Length) Socket.NoDelay = true; } catch { } output.Write(buffer, 0, bytesRead); } // Important: If we are using Gzip and/or Chunked encoding, this causes the relevant // streams to output the last bytes. output.Close(); // Now re-enable the Nagle algorithm in case this is a keep-alive connection. Socket.NoDelay = false; return useKeepAlive; } finally { try { if (originalRequest.FileUploads != null) foreach (var fileUpload in originalRequest.FileUploads.Values.Where(fu => fu.LocalFilename != null && !fu.LocalFileMoved)) File.Delete(fileUpload.LocalFilename); } catch (Exception) { } } }
/// <summary> /// Modifies the <see cref="UseGzip"/> option in this response object and returns the same object.</summary> /// <param name="option"> /// The new value for the <see cref="UseGzip"/> option.</param> public HttpResponseContent Set(UseGzipOption option) { UseGzip = option; return(this); }