Beispiel #1
0
        public bool TakeMessageHeaders(ReadOnlySequence <byte> buffer, out SequencePosition consumed, out SequencePosition examined)
        {
            // Make sure the buffer is limited
            bool overLength = false;

            if (buffer.Length >= _remainingRequestHeadersBytesAllowed)
            {
                buffer = buffer.Slice(buffer.Start, _remainingRequestHeadersBytesAllowed);

                // If we sliced it means the current buffer bigger than what we're
                // allowed to look at
                overLength = true;
            }

            var result = _parser.ParseHeaders(new Proto1ParsingHandler(this), buffer, out consumed, out examined, out var consumedBytes);

            _remainingRequestHeadersBytesAllowed -= consumedBytes;

            if (!result && overLength)
            {
                BadProtoRequestException.Throw(RequestRejectionReason.HeadersExceedMaxTotalSize);
            }
            if (result)
            {
                TimeoutControl.CancelTimeout();
            }

            return(result);
        }
        private int CalculateChunkSize(int extraHexDigit, int currentParsedSize)
        {
            try
            {
                checked
                {
                    if (extraHexDigit >= '0' && extraHexDigit <= '9')
                    {
                        return(currentParsedSize * 0x10 + (extraHexDigit - '0'));
                    }
                    else if (extraHexDigit >= 'A' && extraHexDigit <= 'F')
                    {
                        return(currentParsedSize * 0x10 + (extraHexDigit - ('A' - 10)));
                    }
                    else if (extraHexDigit >= 'a' && extraHexDigit <= 'f')
                    {
                        return(currentParsedSize * 0x10 + (extraHexDigit - ('a' - 10)));
                    }
                }
            }
            catch (OverflowException ex)
            {
                throw new IOException(CoreStrings.BadRequest_BadChunkSizeData, ex);
            }

            BadProtoRequestException.Throw(RequestRejectionReason.BadChunkSizeData);
            return(-1); // can't happen, but compiler complains
        }
 protected override void OnReadStarting()
 {
     if (_contentLength > _context.MaxRequestBodySize)
     {
         BadProtoRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge);
     }
 }
        private void ParseChunkedSuffix(ReadOnlySequence <byte> buffer, out SequencePosition consumed, out SequencePosition examined)
        {
            consumed = buffer.Start;
            examined = buffer.Start;

            if (buffer.Length < 2)
            {
                examined = buffer.End;
                return;
            }

            var suffixBuffer = buffer.Slice(0, 2);
            var suffixSpan   = suffixBuffer.ToSpan();

            // Advance examined before possibly throwing, so we don't risk examining less than the previous call to ParseChunkedSuffix.
            examined = suffixBuffer.End;

            if (suffixSpan[0] == '\r' && suffixSpan[1] == '\n')
            {
                consumed = suffixBuffer.End;
                AddAndCheckConsumedBytes(2);
                _mode = Mode.Prefix;
            }
            else
            {
                BadProtoRequestException.Throw(RequestRejectionReason.BadChunkSuffix);
            }
        }
Beispiel #5
0
        internal void EnsureHostHeaderExists()
        {
            // https://tools.ietf.org/html/rfc7230#section-5.4
            // A server MUST respond with a 400 (Bad Request) status code to any
            // HTTP/1.1 request message that lacks a Host header field and to any
            // request message that contains more than one Host header field or a
            // Host header field with an invalid field-value.

            var hostCount = ProtoRequestHeaders.HostCount;
            var hostText  = ProtoRequestHeaders.HeaderHost.ToString();

            if (hostCount <= 0)
            {
                if (_httpVersion == Proto.ProtoVersion.Proto10)
                {
                    return;
                }
                BadProtoRequestException.Throw(RequestRejectionReason.MissingHostHeader);
            }
            else if (hostCount > 1)
            {
                BadProtoRequestException.Throw(RequestRejectionReason.MultipleHostHeaders);
            }
            else if (_requestTargetForm != ProtoRequestTarget.OriginForm)
            {
                // Tail call
                ValidateNonOriginHostHeader(hostText);
            }
            else if (!ProtoUtilities.IsHostHeaderValid(hostText))
            {
                BadProtoRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
            }
        }
Beispiel #6
0
        private void ValidateNonOriginHostHeader(string hostText)
        {
            if (_requestTargetForm == ProtoRequestTarget.AuthorityForm)
            {
                if (hostText != RawTarget)
                {
                    BadProtoRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
                }
            }
            else if (_requestTargetForm == ProtoRequestTarget.AbsoluteForm)
            {
                // If the target URI includes an authority component, then a
                // client MUST send a field - value for Host that is identical to that
                // authority component, excluding any userinfo subcomponent and its "@"
                // delimiter.

                // System.Uri doesn't not tell us if the port was in the original string or not.
                // When IsDefaultPort = true, we will allow Host: with or without the default port
                if (hostText != _absoluteRequestTarget.Authority)
                {
                    if (!_absoluteRequestTarget.IsDefaultPort ||
                        hostText != _absoluteRequestTarget.Authority + ":" + _absoluteRequestTarget.Port.ToString(CultureInfo.InvariantCulture))
                    {
                        BadProtoRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
                    }
                }
            }

            if (!ProtoUtilities.IsHostHeaderValid(hostText))
            {
                BadProtoRequestException.Throw(RequestRejectionReason.InvalidHostHeader, hostText);
            }
        }
Beispiel #7
0
 protected override void OnReadStarting()
 {
     // Note ContentLength or MaxRequestBodySize may be null
     if (_context.RequestHeaders.ContentLength > _context.MaxRequestBodySize)
     {
         BadProtoRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge);
     }
 }
        private void ParseChunkedPrefix(ReadOnlySequence <byte> buffer, out SequencePosition consumed, out SequencePosition examined)
        {
            consumed = buffer.Start;
            examined = buffer.Start;
            var reader = new SequenceReader <byte>(buffer);

            if (!reader.TryRead(out var ch1) || !reader.TryRead(out var ch2))
            {
                examined = reader.Position;
                return;
            }

            // Advance examined before possibly throwing, so we don't risk examining less than the previous call to ParseChunkedPrefix.
            examined = reader.Position;

            var chunkSize = CalculateChunkSize(ch1, 0);

            ch1 = ch2;

            while (reader.Consumed < MaxChunkPrefixBytes)
            {
                if (ch1 == ';')
                {
                    consumed = reader.Position;
                    examined = reader.Position;

                    AddAndCheckConsumedBytes(reader.Consumed);
                    _inputLength = chunkSize;
                    _mode        = Mode.Extension;
                    return;
                }

                if (!reader.TryRead(out ch2))
                {
                    examined = reader.Position;
                    return;
                }

                // Advance examined before possibly throwing, so we don't risk examining less than the previous call to ParseChunkedPrefix.
                examined = reader.Position;

                if (ch1 == '\r' && ch2 == '\n')
                {
                    consumed = reader.Position;

                    AddAndCheckConsumedBytes(reader.Consumed);
                    _inputLength = chunkSize;
                    _mode        = chunkSize > 0 ? Mode.Data : Mode.Trailer;
                    return;
                }

                chunkSize = CalculateChunkSize(ch1, chunkSize);
                ch1       = ch2;
            }

            // At this point, 10 bytes have been consumed which is enough to parse the max value "7FFFFFFF\r\n".
            BadProtoRequestException.Throw(RequestRejectionReason.BadChunkSizeData);
        }
Beispiel #9
0
        protected void AddAndCheckConsumedBytes(long consumedBytes)
        {
            _consumedBytes += consumedBytes;

            if (_consumedBytes > _context.MaxRequestBodySize)
            {
                BadProtoRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge);
            }
        }
Beispiel #10
0
        private static long ParseContentLength(string value)
        {
            if (!HeaderUtilities.TryParseNonNegativeInt64(value, out var parsed))
            {
                BadProtoRequestException.Throw(RequestRejectionReason.InvalidContentLength, value);
            }

            return(parsed);
        }
Beispiel #11
0
        protected void CheckCompletedReadResult(ReadResult result)
        {
            if (result.IsCompleted)
            {
                // OnInputOrOutputCompleted() is an idempotent method that closes the connection. Sometimes
                // input completion is observed here before the Input.OnWriterCompleted() callback is fired,
                // so we call OnInputOrOutputCompleted() now to prevent a race in our tests where a 400
                // response is written after observing the unexpected end of request content instead of just
                // closing the connection without a response as expected.
                _context.OnInputOrOutputCompleted();

                BadProtoRequestException.Throw(RequestRejectionReason.UnexpectedEndOfRequestContent);
            }
        }
Beispiel #12
0
        private void OnAuthorityFormTarget(ProtoMethod method, Span <byte> target)
        {
            _requestTargetForm = ProtoRequestTarget.AuthorityForm;

            // This is not complete validation. It is just a quick scan for invalid characters
            // but doesn't check that the target fully matches the URI spec.
            if (ProtoCharacters.ContainsInvalidAuthorityChar(target))
            {
                ThrowRequestTargetRejected(target);
            }

            // The authority-form of request-target is only used for CONNECT
            // requests (https://tools.ietf.org/html/rfc7231#section-4.3.6).
            if (method != ProtoMethod.Connect)
            {
                BadProtoRequestException.Throw(RequestRejectionReason.ConnectMethodRequired);
            }

            // When making a CONNECT request to establish a tunnel through one or
            // more proxies, a client MUST send only the target URI's authority
            // component (excluding any userinfo and its "@" delimiter) as the
            // request-target.For example,
            //
            //  CONNECT www.example.com:80 HTTP/1.1
            //
            // Allowed characters in the 'host + port' section of authority.
            // See https://tools.ietf.org/html/rfc3986#section-3.2

            var previousValue = _parsedRawTarget;

            if (ServerOptions.DisableStringReuse ||
                previousValue == null || previousValue.Length != target.Length ||
                !StringUtilities.BytesOrdinalEqualsStringAndAscii(previousValue, target))
            {
                // The previous string does not match what the bytes would convert to,
                // so we will need to generate a new string.
                RawTarget = _parsedRawTarget = target.GetAsciiStringNonNullCharacters();
            }
            else
            {
                // Reuse previous value
                RawTarget = _parsedRawTarget;
            }

            Path        = string.Empty;
            QueryString = string.Empty;
            // Clear parsedData for path and queryString as we won't check it if we come via this path again,
            // an setting to null is fast as it doesn't need to use a GC write barrier.
            _parsedPath = _parsedQueryString = null;
        }
Beispiel #13
0
        private unsafe void AppendUnknownHeaders(Span <byte> name, string valueString)
        {
            string key = new string('\0', name.Length);

            fixed(byte *pKeyBytes = name)
            fixed(char *keyBuffer = key)
            {
                if (!StringUtilities.TryGetAsciiString(pKeyBytes, keyBuffer, name.Length))
                {
                    BadProtoRequestException.Throw(RequestRejectionReason.InvalidCharactersInHeaderName);
                }
            }

            Unknown.TryGetValue(key, out var existing);
            Unknown[key] = AppendValue(existing, valueString);
        }
Beispiel #14
0
        private void AppendContentLength(Span <byte> value)
        {
            if (_contentLength.HasValue)
            {
                BadProtoRequestException.Throw(RequestRejectionReason.MultipleContentLengths);
            }

            if (!Utf8Parser.TryParse(value, out long parsed, out var consumed) ||
                parsed < 0 ||
                consumed != value.Length)
            {
                BadProtoRequestException.Throw(RequestRejectionReason.InvalidContentLength, value.GetAsciiOrUTF8StringNonNullCharacters());
            }

            _contentLength = parsed;
        }
Beispiel #15
0
        private void OnAsteriskFormTarget(ProtoMethod method)
        {
            _requestTargetForm = ProtoRequestTarget.AsteriskForm;

            // The asterisk-form of request-target is only used for a server-wide
            // OPTIONS request (https://tools.ietf.org/html/rfc7231#section-4.3.7).
            if (method != ProtoMethod.Options)
            {
                BadProtoRequestException.Throw(RequestRejectionReason.OptionsMethodRequired);
            }

            RawTarget   = Asterisk;
            Path        = string.Empty;
            QueryString = string.Empty;
            // Clear parsedData as we won't check it if we come via this path again,
            // an setting to null is fast as it doesn't need to use a GC write barrier.
            _parsedRawTarget = _parsedPath = _parsedQueryString = null;
        }
Beispiel #16
0
        public bool TakeStartLine(ReadOnlySequence <byte> buffer, out SequencePosition consumed, out SequencePosition examined)
        {
            var overLength = false;

            if (buffer.Length >= ServerOptions.Limits.MaxRequestLineSize)
            {
                buffer     = buffer.Slice(buffer.Start, ServerOptions.Limits.MaxRequestLineSize);
                overLength = true;
            }

            var result = _parser.ParseRequestLine(new Proto1ParsingHandler(this), buffer, out consumed, out examined);

            if (!result && overLength)
            {
                BadProtoRequestException.Throw(RequestRejectionReason.RequestLineTooLong);
            }

            return(result);
        }
        private async Task PumpAsync()
        {
            Debug.Assert(!RequestUpgrade, "Upgraded connections should never use this code path!");

            Exception error = null;

            try
            {
                var awaitable = _context.Input.ReadAsync();

                if (!awaitable.IsCompleted)
                {
                    TryProduceContinue();
                }

                while (true)
                {
                    var result = await awaitable;

                    if (_context.RequestTimedOut)
                    {
                        BadProtoRequestException.Throw(RequestRejectionReason.RequestBodyTimeout);
                    }

                    var readableBuffer = result.Buffer;
                    var consumed       = readableBuffer.Start;
                    var examined       = readableBuffer.Start;

                    try
                    {
                        if (_canceled)
                        {
                            break;
                        }

                        if (!readableBuffer.IsEmpty)
                        {
                            bool done;
                            done = Read(readableBuffer, _requestBodyPipe.Writer, out consumed, out examined);

                            await _requestBodyPipe.Writer.FlushAsync();

                            if (done)
                            {
                                break;
                            }
                        }

                        // Read() will have already have greedily consumed the entire request body if able.
                        CheckCompletedReadResult(result);
                    }
                    finally
                    {
                        _context.Input.AdvanceTo(consumed, examined);
                    }

                    awaitable = _context.Input.ReadAsync();
                }
            }
            catch (Exception ex)
            {
                error = ex;
            }
            finally
            {
                _requestBodyPipe.Writer.Complete(error);
            }
        }
        public override async ValueTask <ReadResult> ReadAsync(CancellationToken cancellationToken = default)
        {
            ThrowIfCompleted();

            if (_isReading)
            {
                throw new InvalidOperationException("Reading is already in progress.");
            }

            if (_readCompleted)
            {
                _isReading = true;
                return(_readResult);
            }

            TryStart();

            // The while(true) loop is required because the Proto1 connection calls CancelPendingRead to unblock
            // the call to StartTimingReadAsync to check if the request timed out.
            // However, if the user called CancelPendingRead, we want that to return a canceled ReadResult
            // We internally track an int for that.
            while (true)
            {
                // The issue is that TryRead can get a canceled read result
                // which is unknown to StartTimingReadAsync.
                if (_context.RequestTimedOut)
                {
                    BadProtoRequestException.Throw(RequestRejectionReason.RequestBodyTimeout);
                }

                try
                {
                    var readAwaitable = _context.Input.ReadAsync(cancellationToken);

                    _isReading  = true;
                    _readResult = await StartTimingReadAsync(readAwaitable, cancellationToken);
                }
                catch (ConnectionAbortedException ex)
                {
                    throw new TaskCanceledException("The request was aborted", ex);
                }

                if (_context.RequestTimedOut)
                {
                    BadProtoRequestException.Throw(RequestRejectionReason.RequestBodyTimeout);
                }

                // Make sure to handle when this is canceled here.
                if (_readResult.IsCanceled)
                {
                    if (Interlocked.Exchange(ref _userCanceled, 0) == 1)
                    {
                        // Ignore the readResult if it wasn't by the user.
                        break;
                    }
                    else
                    {
                        // Reset the timing read here for the next call to read.
                        StopTimingRead(0);
                        continue;
                    }
                }

                var readableBuffer       = _readResult.Buffer;
                var readableBufferLength = readableBuffer.Length;
                StopTimingRead(readableBufferLength);

                CheckCompletedReadResult(_readResult);

                if (readableBufferLength > 0)
                {
                    CreateReadResultFromConnectionReadResult();

                    break;
                }
            }

            return(_readResult);
        }
Beispiel #19
0
        protected override bool TryParseRequest(ReadResult result, out bool endConnection)
        {
            var examined = result.Buffer.End;
            var consumed = result.Buffer.End;

            try
            {
                ParseRequest(result.Buffer, out consumed, out examined);
            }
            catch (InvalidOperationException)
            {
                if (_requestProcessingStatus == RequestProcessingStatus.ParsingHeaders)
                {
                    BadProtoRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders);
                }
                throw;
            }
            finally
            {
                Input.AdvanceTo(consumed, examined);
            }

            if (result.IsCompleted)
            {
                switch (_requestProcessingStatus)
                {
                case RequestProcessingStatus.RequestPending:
                    endConnection = true;
                    return(true);

                case RequestProcessingStatus.ParsingRequestLine:
                    BadProtoRequestException.Throw(RequestRejectionReason.InvalidRequestLine);
                    break;

                case RequestProcessingStatus.ParsingHeaders:
                    BadProtoRequestException.Throw(RequestRejectionReason.MalformedRequestInvalidHeaders);
                    break;
                }
            }
            else if (!_keepAlive && _requestProcessingStatus == RequestProcessingStatus.RequestPending)
            {
                // Stop the request processing loop if the server is shutting down or there was a keep-alive timeout
                // and there is no ongoing request.
                endConnection = true;
                return(true);
            }
            else if (RequestTimedOut)
            {
                // In this case, there is an ongoing request but the start line/header parsing has timed out, so send
                // a 408 response.
                BadProtoRequestException.Throw(RequestRejectionReason.RequestHeadersTimeout);
            }

            endConnection = false;
            if (_requestProcessingStatus == RequestProcessingStatus.AppStarted)
            {
                EnsureHostHeaderExists();
                return(true);
            }
            else
            {
                return(false);
            }
        }
Beispiel #20
0
 public virtual void ConnectionBadRequest(string connectionId, BadProtoRequestException ex)
 {
     _connectionBadRequest(_logger, connectionId, ex.Message, ex);
 }
Beispiel #21
0
        public static MessageBody For(
            ProtoVersion httpVersion,
            ProtoRequestHeaders headers,
            Proto1Connection context)
        {
            // see also http://tools.ietf.org/html/rfc2616#section-4.4
            var keepAlive = httpVersion != ProtoVersion.Proto10;

            var upgrade = false;

            if (headers.HasConnection)
            {
                var connectionOptions = ProtoHeaders.ParseConnection(headers.HeaderConnection);

                upgrade   = (connectionOptions & ConnectionOptions.Upgrade) == ConnectionOptions.Upgrade;
                keepAlive = (connectionOptions & ConnectionOptions.KeepAlive) == ConnectionOptions.KeepAlive;
            }

            if (upgrade)
            {
                if (headers.HeaderTransferEncoding.Count > 0 || (headers.ContentLength.HasValue && headers.ContentLength.Value != 0))
                {
                    BadProtoRequestException.Throw(RequestRejectionReason.UpgradeRequestCannotHavePayload);
                }

                return(new Proto1UpgradeMessageBody(context));
            }

            if (headers.HasTransferEncoding)
            {
                var transferEncoding = headers.HeaderTransferEncoding;
                var transferCoding   = ProtoHeaders.GetFinalTransferCoding(transferEncoding);

                // https://tools.ietf.org/html/rfc7230#section-3.3.3
                // If a Transfer-Encoding header field
                // is present in a request and the chunked transfer coding is not
                // the final encoding, the message body length cannot be determined
                // reliably; the server MUST respond with the 400 (Bad Request)
                // status code and then close the connection.
                if (transferCoding != TransferCoding.Chunked)
                {
                    BadProtoRequestException.Throw(RequestRejectionReason.FinalTransferCodingNotChunked, transferEncoding);
                }

                // TODO may push more into the wrapper rather than just calling into the message body
                // NBD for now.
                return(new Proto1ChunkedEncodingMessageBody(keepAlive, context));
            }

            if (headers.ContentLength.HasValue)
            {
                var contentLength = headers.ContentLength.Value;

                if (contentLength == 0)
                {
                    return(keepAlive ? MessageBody.ZeroContentLengthKeepAlive : MessageBody.ZeroContentLengthClose);
                }

                return(new Proto1ContentLengthMessageBody(keepAlive, contentLength, context));
            }

            // If we got here, request contains no Content-Length or Transfer-Encoding header.
            // Reject with 411 Length Required.
            if (context.Method == ProtoMethod.Post || context.Method == ProtoMethod.Put)
            {
                var requestRejectionReason = httpVersion == ProtoVersion.Proto11 ? RequestRejectionReason.LengthRequired : RequestRejectionReason.LengthRequiredProto10;
                BadProtoRequestException.Throw(requestRejectionReason, context.Method);
            }

            return(keepAlive ? MessageBody.ZeroContentLengthKeepAlive : MessageBody.ZeroContentLengthClose);
        }