internal static int GetEntityTagLength(string input, int startIndex, out EntityTagHeaderValue parsedValue) { Contract.Requires(startIndex >= 0); parsedValue = null; if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return(0); } // Caller must remove leading whitespaces. If not, we'll return 0. bool isWeak = false; int current = startIndex; char firstChar = input[startIndex]; if (firstChar == '*') { // We have '*' value, indicating "any" ETag. parsedValue = Any; current++; } else { // The RFC defines 'W/' as prefix, but we'll be flexible and also accept lower-case 'w'. if ((firstChar == 'W') || (firstChar == 'w')) { current++; // We need at least 3 more chars: the '/' character followed by two quotes. if ((current + 2 >= input.Length) || (input[current] != '/')) { return(0); } isWeak = true; current++; // we have a weak-entity tag. current = current + HttpRuleParser.GetWhitespaceLength(input, current); } int tagStartIndex = current; int tagLength = 0; if (HttpRuleParser.GetQuotedStringLength(input, current, out tagLength) != HttpParseResult.Parsed) { return(0); } parsedValue = new EntityTagHeaderValue(); if (tagLength == input.Length) { // Most of the time we'll have strong ETags without leading/trailing whitespaces. Contract.Assert(startIndex == 0); Contract.Assert(!isWeak); parsedValue.tag = input; parsedValue.isWeak = false; } else { parsedValue.tag = input.Substring(tagStartIndex, tagLength); parsedValue.isWeak = isWeak; } current = current + tagLength; } current = current + HttpRuleParser.GetWhitespaceLength(input, current); return(current - startIndex); }
internal static int GetContentRangeLength(string input, int startIndex, out object parsedValue) { Debug.Assert(startIndex >= 0); parsedValue = null; if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return(0); } // Parse the unit string: <unit> in '<unit> <from>-<to>/<length>' int unitLength = HttpRuleParser.GetTokenLength(input, startIndex); if (unitLength == 0) { return(0); } string unit = input.Substring(startIndex, unitLength); int current = startIndex + unitLength; int separatorLength = HttpRuleParser.GetWhitespaceLength(input, current); if (separatorLength == 0) { return(0); } current = current + separatorLength; if (current == input.Length) { return(0); } // Read range values <from> and <to> in '<unit> <from>-<to>/<length>' int fromStartIndex = current; int fromLength = 0; int toStartIndex = 0; int toLength = 0; if (!TryGetRangeLength(input, ref current, out fromLength, out toStartIndex, out toLength)) { return(0); } // After the range is read we expect the length separator '/' if ((current == input.Length) || (input[current] != '/')) { return(0); } current++; // Skip '/' separator current = current + HttpRuleParser.GetWhitespaceLength(input, current); if (current == input.Length) { return(0); } // We may not have a length (e.g. 'bytes 1-2/*'). But if we do, parse the length now. int lengthStartIndex = current; int lengthLength = 0; if (!TryGetLengthLength(input, ref current, out lengthLength)) { return(0); } if (!TryCreateContentRange(input, unit, fromStartIndex, fromLength, toStartIndex, toLength, lengthStartIndex, lengthLength, out parsedValue)) { return(0); } return(current - startIndex); }
internal static int GetViaLength(string input, int startIndex, out object parsedValue) { Contract.Requires(startIndex >= 0); parsedValue = null; if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return 0; } // Read <protocolName> and <protocolVersion> in '[<protocolName>/]<protocolVersion> <receivedBy> [<comment>]' string protocolName = null; string protocolVersion = null; int current = GetProtocolEndIndex(input, startIndex, out protocolName, out protocolVersion); // If we reached the end of the string after reading protocolName/Version we return (we expect at least // <receivedBy> to follow). If reading protocolName/Version read 0 bytes, we return. if ((current == startIndex) || (current == input.Length)) { return 0; } Debug.Assert(protocolVersion != null); // Read <receivedBy> in '[<protocolName>/]<protocolVersion> <receivedBy> [<comment>]' string receivedBy = null; int receivedByLength = HttpRuleParser.GetHostLength(input, current, true, out receivedBy); if (receivedByLength == 0) { return 0; } current = current + receivedByLength; current = current + HttpRuleParser.GetWhitespaceLength(input, current); string comment = null; if ((current < input.Length) && (input[current] == '(')) { // We have a <comment> in '[<protocolName>/]<protocolVersion> <receivedBy> [<comment>]' int commentLength = 0; if (HttpRuleParser.GetCommentLength(input, current, out commentLength) != HttpParseResult.Parsed) { return 0; // We found a '(' character but it wasn't a valid comment. Abort. } comment = input.Substring(current, commentLength); current = current + commentLength; current = current + HttpRuleParser.GetWhitespaceLength(input, current); } ViaHeaderValue result = new ViaHeaderValue(); result._protocolVersion = protocolVersion; result._protocolName = protocolName; result._receivedBy = receivedBy; result._comment = comment; parsedValue = result; return current - startIndex; }
private static int GetProtocolEndIndex(string input, int startIndex, out string?protocolName, out string?protocolVersion) { // We have a string of the form '[<protocolName>/]<protocolVersion> <receivedBy> [<comment>]'. The first // token may either be the protocol name or protocol version. We'll only find out after reading the token // and by looking at the following character: If it is a '/' we just parsed the protocol name, otherwise // the protocol version. protocolName = null; protocolVersion = null; int current = startIndex; int protocolVersionOrNameLength = HttpRuleParser.GetTokenLength(input, current); if (protocolVersionOrNameLength == 0) { return(0); } current = startIndex + protocolVersionOrNameLength; int whitespaceLength = HttpRuleParser.GetWhitespaceLength(input, current); current = current + whitespaceLength; if (current == input.Length) { return(0); } if (input[current] == '/') { // We parsed the protocol name protocolName = input.Substring(startIndex, protocolVersionOrNameLength); current++; // skip the '/' delimiter current = current + HttpRuleParser.GetWhitespaceLength(input, current); protocolVersionOrNameLength = HttpRuleParser.GetTokenLength(input, current); if (protocolVersionOrNameLength == 0) { return(0); // We have a string "<token>/" followed by non-token chars. This is invalid. } protocolVersion = input.Substring(current, protocolVersionOrNameLength); current = current + protocolVersionOrNameLength; whitespaceLength = HttpRuleParser.GetWhitespaceLength(input, current); current = current + whitespaceLength; } else { protocolVersion = input.Substring(startIndex, protocolVersionOrNameLength); } if (whitespaceLength == 0) { return(0); // We were able to parse [<protocolName>/]<protocolVersion> but it wasn't followed by a WS } return(current); }
internal static int GetRetryConditionLength(string?input, int startIndex, out object?parsedValue) { Debug.Assert(startIndex >= 0); parsedValue = null; if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return(0); } int current = startIndex; // Caller must remove leading whitespace. DateTimeOffset date = DateTimeOffset.MinValue; int deltaSeconds = -1; // use -1 to indicate that the value was not set. 'delta' values are always >=0 // We either have a timespan or a date/time value. Determine which one we have by looking at the first char. // If it is a number, we have a timespan, otherwise we assume we have a date. char firstChar = input[current]; if ((firstChar >= '0') && (firstChar <= '9')) { int deltaStartIndex = current; int deltaLength = HttpRuleParser.GetNumberLength(input, current, false); // The value must be in the range 0..2^31 if ((deltaLength == 0) || (deltaLength > HttpRuleParser.MaxInt32Digits)) { return(0); } current = current + deltaLength; current = current + HttpRuleParser.GetWhitespaceLength(input, current); // RetryConditionHeaderValue only allows 1 value. There must be no delimiter/other chars after 'delta' if (current != input.Length) { return(0); } if (!HeaderUtilities.TryParseInt32(input, deltaStartIndex, deltaLength, out deltaSeconds)) { return(0); // int.TryParse() may return 'false' if the value has 10 digits and is > Int32.MaxValue. } } else { if (!HttpDateParser.TryParse(input.AsSpan(current), out date)) { return(0); } // If we got a valid date, then the parser consumed the whole string (incl. trailing whitespace). current = input.Length; } RetryConditionHeaderValue result = new RetryConditionHeaderValue(); if (deltaSeconds == -1) // we didn't change delta, so we must have found a date. { result._date = date; } else { result._delta = new TimeSpan(0, 0, deltaSeconds); } parsedValue = result; return(current - startIndex); }
internal static int GetRangeItemLength(string input, int startIndex, out RangeItemHeaderValue parsedValue) { Contract.Requires(startIndex >= 0); // This parser parses number ranges: e.g. '1-2', '1-', '-2'. parsedValue = null; if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return(0); } // Caller must remove leading whitespaces. If not, we'll return 0. int current = startIndex; // Try parse the first value of a value pair. int fromStartIndex = current; int fromLength = HttpRuleParser.GetNumberLength(input, current, false); if (fromLength > HttpRuleParser.MaxInt64Digits) { return(0); } current = current + fromLength; current = current + HttpRuleParser.GetWhitespaceLength(input, current); // Afer the first value, the '-' character must follow. if ((current == input.Length) || (input[current] != '-')) { // We need a '-' character otherwise this can't be a valid range. return(0); } current++; // skip the '-' character current = current + HttpRuleParser.GetWhitespaceLength(input, current); int toStartIndex = current; int toLength = 0; // If we didn't reach the end of the string, try parse the second value of the range. if (current < input.Length) { toLength = HttpRuleParser.GetNumberLength(input, current, false); if (toLength > HttpRuleParser.MaxInt64Digits) { return(0); } current = current + toLength; current = current + HttpRuleParser.GetWhitespaceLength(input, current); } if ((fromLength == 0) && (toLength == 0)) { return(0); // At least one value must be provided in order to be a valid range. } // Try convert first value to int64 long from = 0; if ((fromLength > 0) && !HeaderUtilities.TryParseInt64(input.Substring(fromStartIndex, fromLength), out from)) { return(0); } // Try convert second value to int64 long to = 0; if ((toLength > 0) && !HeaderUtilities.TryParseInt64(input.Substring(toStartIndex, toLength), out to)) { return(0); } // 'from' must not be greater than 'to' if ((fromLength > 0) && (toLength > 0) && (from > to)) { return(0); } parsedValue = new RangeItemHeaderValue((fromLength == 0 ? (long?)null : (long?)from), (toLength == 0 ? (long?)null : (long?)to)); return(current - startIndex); }
internal static int GetAuthenticationLength(string?input, int startIndex, out object?parsedValue) { Debug.Assert(startIndex >= 0); parsedValue = null; if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return(0); } // Parse the scheme string: <scheme> in '<scheme> <parameter>' int schemeLength = HttpRuleParser.GetTokenLength(input, startIndex); if (schemeLength == 0) { return(0); } var result = new AuthenticationHeaderValue(); string?targetScheme = null; switch (schemeLength) { // Avoid allocating a scheme string for the most common cases. case 5: targetScheme = "Basic"; break; case 6: targetScheme = "Digest"; break; case 4: targetScheme = "NTLM"; break; case 9: targetScheme = "Negotiate"; break; } result._scheme = targetScheme != null && string.CompareOrdinal(input, startIndex, targetScheme, 0, schemeLength) == 0 ? targetScheme : input.Substring(startIndex, schemeLength); int current = startIndex + schemeLength; int whitespaceLength = HttpRuleParser.GetWhitespaceLength(input, current); current = current + whitespaceLength; if ((current == input.Length) || (input[current] == ',')) { // If we only have a scheme followed by whitespace, we're done. parsedValue = result; return(current - startIndex); } // We need at least one space between the scheme and parameters. If there is no whitespace, then we must // have reached the end of the string (i.e. scheme-only string). if (whitespaceLength == 0) { return(0); } // If we get here, we have a <scheme> followed by a whitespace. Now we expect the following: // '<scheme> <blob>[,<name>=<value>]*[, <otherscheme>...]*': <blob> potentially contains one // or more '=' characters, optionally followed by additional name/value pairs, optionally followed by // other schemes. <blob> may be a quoted string. // We look at the value after ',': if it is <token>=<value> then we have a parameter for <scheme>. // If we have either a <token>-only or <token><whitespace><blob> then we have another scheme. int parameterStartIndex = current; int parameterEndIndex = current; if (!TrySkipFirstBlob(input, ref current, ref parameterEndIndex)) { return(0); } if (current < input.Length) { if (!TryGetParametersEndIndex(input, ref current, ref parameterEndIndex)) { return(0); } } result._parameter = input.Substring(parameterStartIndex, parameterEndIndex - parameterStartIndex + 1); parsedValue = result; return(current - startIndex); }
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax|None}; httponly private static int GetSetCookieLength(StringSegment input, int startIndex, out SetCookieHeaderValue?parsedValue) { Contract.Requires(startIndex >= 0); var offset = startIndex; parsedValue = null; if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length)) { return(0); } var result = new SetCookieHeaderValue(); // The caller should have already consumed any leading whitespace, commas, etc.. // Name=value; // Name var itemLength = HttpRuleParser.GetTokenLength(input, offset); if (itemLength == 0) { return(0); } result._name = input.Subsegment(offset, itemLength); offset += itemLength; // = (no spaces) if (!ReadEqualsSign(input, ref offset)) { return(0); } // value or "quoted value" // The value may be empty result._value = CookieHeaderParserShared.GetCookieValue(input, ref offset); // *(';' SP cookie-av) while (offset < input.Length) { if (input[offset] == ',') { // Divider between headers break; } if (input[offset] != ';') { // Expecting a ';' between parameters return(0); } offset++; offset += HttpRuleParser.GetWhitespaceLength(input, offset); // cookie-av = expires-av / max-age-av / domain-av / path-av / secure-av / samesite-av / httponly-av / extension-av itemLength = HttpRuleParser.GetTokenLength(input, offset); if (itemLength == 0) { // Trailing ';' or leading into garbage. Let the next parser fail. break; } var token = input.Subsegment(offset, itemLength); offset += itemLength; // expires-av = "Expires=" sane-cookie-date if (StringSegment.Equals(token, ExpiresToken, StringComparison.OrdinalIgnoreCase)) { // = (no spaces) if (!ReadEqualsSign(input, ref offset)) { return(0); } // We don't want to include comma, becouse date may contain it (eg. Sun, 06 Nov...) var dateString = ReadToSemicolonOrEnd(input, ref offset, includeComma: false); DateTimeOffset expirationDate; if (!HttpRuleParser.TryStringToDate(dateString, out expirationDate)) { // Invalid expiration date, abort return(0); } result.Expires = expirationDate; } // max-age-av = "Max-Age=" non-zero-digit *DIGIT else if (StringSegment.Equals(token, MaxAgeToken, StringComparison.OrdinalIgnoreCase)) { // = (no spaces) if (!ReadEqualsSign(input, ref offset)) { return(0); } itemLength = HttpRuleParser.GetNumberLength(input, offset, allowDecimal: false); if (itemLength == 0) { return(0); } var numberString = input.Subsegment(offset, itemLength); long maxAge; if (!HeaderUtilities.TryParseNonNegativeInt64(numberString, out maxAge)) { // Invalid expiration date, abort return(0); } result.MaxAge = TimeSpan.FromSeconds(maxAge); offset += itemLength; } // domain-av = "Domain=" domain-value // domain-value = <subdomain> ; defined in [RFC1034], Section 3.5, as enhanced by [RFC1123], Section 2.1 else if (StringSegment.Equals(token, DomainToken, StringComparison.OrdinalIgnoreCase)) { // = (no spaces) if (!ReadEqualsSign(input, ref offset)) { return(0); } // We don't do any detailed validation on the domain. result.Domain = ReadToSemicolonOrEnd(input, ref offset); } // path-av = "Path=" path-value // path-value = <any CHAR except CTLs or ";"> else if (StringSegment.Equals(token, PathToken, StringComparison.OrdinalIgnoreCase)) { // = (no spaces) if (!ReadEqualsSign(input, ref offset)) { return(0); } // We don't do any detailed validation on the path. result.Path = ReadToSemicolonOrEnd(input, ref offset); } // secure-av = "Secure" else if (StringSegment.Equals(token, SecureToken, StringComparison.OrdinalIgnoreCase)) { result.Secure = true; } // samesite-av = "SameSite=" samesite-value // samesite-value = "Strict" / "Lax" / "None" else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase)) { if (!ReadEqualsSign(input, ref offset)) { result.SameSite = SameSiteMode.Unspecified; } else { var enforcementMode = ReadToSemicolonOrEnd(input, ref offset); if (StringSegment.Equals(enforcementMode, SameSiteStrictToken, StringComparison.OrdinalIgnoreCase)) { result.SameSite = SameSiteMode.Strict; } else if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase)) { result.SameSite = SameSiteMode.Lax; } else if (StringSegment.Equals(enforcementMode, SameSiteNoneToken, StringComparison.OrdinalIgnoreCase)) { result.SameSite = SameSiteMode.None; } else { result.SameSite = SameSiteMode.Unspecified; } } } // httponly-av = "HttpOnly" else if (StringSegment.Equals(token, HttpOnlyToken, StringComparison.OrdinalIgnoreCase)) { result.HttpOnly = true; } // extension-av = <any CHAR except CTLs or ";"> else { var tokenStart = offset - itemLength; ReadToSemicolonOrEnd(input, ref offset, includeComma: true); result.Extensions.Add(input.Subsegment(tokenStart, offset - tokenStart)); } } parsedValue = result; return(offset - startIndex); }
internal static int GetAuthenticationLength(string input, int startIndex, out object parsedValue) { Contract.Requires(startIndex >= 0); parsedValue = null; if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return(0); } // Parse the scheme string: <scheme> in '<scheme> <parameter>' int schemeLength = HttpRuleParser.GetTokenLength(input, startIndex); if (schemeLength == 0) { return(0); } AuthenticationHeaderValue result = new AuthenticationHeaderValue(); result._scheme = input.Substring(startIndex, schemeLength); int current = startIndex + schemeLength; int whitespaceLength = HttpRuleParser.GetWhitespaceLength(input, current); current = current + whitespaceLength; if ((current == input.Length) || (input[current] == ',')) { // If we only have a scheme followed by whitespace, we're done. parsedValue = result; return(current - startIndex); } // We need at least one space between the scheme and parameters. If there are no whitespace, then we must // have reached the end of the string (i.e. scheme-only string). if (whitespaceLength == 0) { return(0); } // If we get here, we have a <scheme> followed by a whitespace. Now we expect the following: // '<scheme> <blob>[,<name>=<value>]*[, <otherscheme>...]*': <blob> potentially contains one // or more '=' characters, optionally followed by additional name/value pairs, optionally followed by // other schemes. <blob> may be a quoted string. // We look at the value after ',': if it is <token>=<value> then we have a parameter for <scheme>. // If we have either a <token>-only or <token><whitespace><blob> then we have another scheme. int parameterStartIndex = current; int parameterEndIndex = current; if (!TrySkipFirstBlob(input, ref current, ref parameterEndIndex)) { return(0); } if (current < input.Length) { if (!TryGetParametersEndIndex(input, ref current, ref parameterEndIndex)) { return(0); } } result._parameter = input.Substring(parameterStartIndex, parameterEndIndex - parameterStartIndex + 1); parsedValue = result; return(current - startIndex); }
internal static int GetNextNonEmptyOrWhitespaceIndex( string input, int startIndex, bool skipEmptyValues, out bool separatorFound) { separatorFound = false; int index1 = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); if (index1 == input.Length || input[index1] != ',') { return(index1); } separatorFound = true; int startIndex1 = index1 + 1; int index2 = startIndex1 + HttpRuleParser.GetWhitespaceLength(input, startIndex1); int startIndex2; if (skipEmptyValues) { for (; index2 < input.Length && input[index2] == ','; index2 = startIndex2 + HttpRuleParser.GetWhitespaceLength(input, startIndex2)) { startIndex2 = index2 + 1; } } return(index2); }
private static int GetContentRangeLength(StringSegment input, int startIndex, out ContentRangeHeaderValue?parsedValue) { Contract.Requires(startIndex >= 0); parsedValue = null; if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return(0); } // Parse the unit string: <unit> in '<unit> <from>-<to>/<length>' var unitLength = HttpRuleParser.GetTokenLength(input, startIndex); if (unitLength == 0) { return(0); } var unit = input.Subsegment(startIndex, unitLength); var current = startIndex + unitLength; var separatorLength = HttpRuleParser.GetWhitespaceLength(input, current); if (separatorLength == 0) { return(0); } current = current + separatorLength; if (current == input.Length) { return(0); } // Read range values <from> and <to> in '<unit> <from>-<to>/<length>' var fromStartIndex = current; if (!TryGetRangeLength(input, ref current, out var fromLength, out var toStartIndex, out var toLength)) { return(0); } // After the range is read we expect the length separator '/' if ((current == input.Length) || (input[current] != '/')) { return(0); } current++; // Skip '/' separator current = current + HttpRuleParser.GetWhitespaceLength(input, current); if (current == input.Length) { return(0); } // We may not have a length (e.g. 'bytes 1-2/*'). But if we do, parse the length now. var lengthStartIndex = current; if (!TryGetLengthLength(input, ref current, out var lengthLength)) { return(0); } if (!TryCreateContentRange(input, unit, fromStartIndex, fromLength, toStartIndex, toLength, lengthStartIndex, lengthLength, out parsedValue)) { return(0); } return(current - startIndex); }