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); } }
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); } }
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); } }
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); }
protected void AddAndCheckConsumedBytes(long consumedBytes) { _consumedBytes += consumedBytes; if (_consumedBytes > _context.MaxRequestBodySize) { BadProtoRequestException.Throw(RequestRejectionReason.RequestBodyTooLarge); } }
private static long ParseContentLength(string value) { if (!HeaderUtilities.TryParseNonNegativeInt64(value, out var parsed)) { BadProtoRequestException.Throw(RequestRejectionReason.InvalidContentLength, value); } return(parsed); }
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); } }
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; }
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); }
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; }
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; }
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); }
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); } }
public virtual void ConnectionBadRequest(string connectionId, BadProtoRequestException ex) { _connectionBadRequest(_logger, connectionId, ex.Message, ex); }
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); }