/// <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;
 }
Example #2
0
            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) { }
                }
            }
Example #3
0
 /// <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);
 }