/// <summary> /// Instantiates a new <see cref="Enumerator"/> instance for <paramref name="tokenizer"/>. /// </summary> /// <param name="tokenizer">The containing <see cref="TrimmingTokenizer"/>.</param> public Enumerator(TrimmingTokenizer tokenizer) { _tokenizer = tokenizer; _count = 0; _enumerator = tokenizer._tokenizer.GetEnumerator(); _remainder = StringSegment.Empty; }
// ??? Should we continue to use NameValueCollection here and as the base for ParameterCollection? /// <summary> /// Parses the 'text' of a slash command or 'subtext' of an outgoing WebHook of the form /// '<c>action param1; param2=value2; param3=value 3; param4="quoted value4"; ...</c>'. /// Parameter values containing semi-colons can either escape the semi-colon using a backslash character, /// i.e '\;', or quote the value using single quotes or double quotes. /// </summary> /// <example> /// An example of an outgoing WebHook or slash command using this format is /// <c>/appointment add title=doctor visit; time=Feb 3 2016 2 PM; location=Children's Hospital</c> /// where '/appointment' is the trigger word or slash command, 'add' is the action and title, time, and location /// are parameters. /// </example> /// <param name="text">The 'text' of a slash command or 'subtext' of an outgoing WebHook.</param> public static KeyValuePair <string, NameValueCollection> ParseActionWithParameters(string text) { var actionValue = ParseActionWithValue(text); var parameters = new ParameterCollection(); var result = new KeyValuePair <string, NameValueCollection>(actionValue.Key, parameters); var encodedSeparators = EncodeNonSeparatorCharacters(actionValue.Value); var keyValuePairs = new TrimmingTokenizer(encodedSeparators, ParameterSeparator); foreach (var keyValuePair in keyValuePairs) { var parameter = new TrimmingTokenizer(keyValuePair, EqualSeparator, 2).ToArray(); var name = parameter[0].Value; ValidateParameterName(name); // Unquote and convert parameter value var value = string.Empty; if (parameter.Length > 1) { value = parameter[1].Value; value = value.Trim(new[] { '\'' }).Trim('"'); value = value.Replace("\\\0", ";"); } parameters.Add(name, value); } return(result); }
private async Task <IActionResult> VerifyContentDistributionRequest(WebSubSubscription subscription, HttpRequest request) { if (subscription.State != WebSubSubscriptionState.SubscribeValidated) { return(new NotFoundResult()); } if (String.IsNullOrWhiteSpace(subscription.Secret)) { return(null); } string signatureHeader = GetRequestHeader(request, WebSubConstants.SIGNATURE_HEADER_NAME, out IActionResult verificationResult); if (verificationResult != null) { return(verificationResult); } TrimmingTokenizer tokens = new TrimmingTokenizer(signatureHeader, _pairSeparators); if (tokens.Count != 2) { return(HandleInvalidSignatureHeader()); } TrimmingTokenizer.Enumerator tokensEnumerator = tokens.GetEnumerator(); tokensEnumerator.MoveNext(); StringSegment signatureHeaderKey = tokensEnumerator.Current; tokensEnumerator.MoveNext(); byte[] signatureHeaderExpectedHash = FromHex(tokensEnumerator.Current.Value, WebSubConstants.SIGNATURE_HEADER_NAME); if (signatureHeaderExpectedHash == null) { return(CreateBadHexEncodingResult(WebSubConstants.SIGNATURE_HEADER_NAME)); } byte[] payloadActualHash = await ComputeRequestBodyHashAsync(request, signatureHeaderKey, Encoding.UTF8.GetBytes(subscription.Secret)); if (payloadActualHash == null) { return(HandleInvalidSignatureHeader()); } if (!SecretEqual(signatureHeaderExpectedHash, payloadActualHash)) { return(CreateBadSignatureResult(WebSubConstants.SIGNATURE_HEADER_NAME)); } return(null); }
// Header contains a comma-separated collection of key / value pairs. Get all values for the "v1" key. private IEnumerable <StringSegment> GetSignatures(string header) { var pairs = new TrimmingTokenizer(header, CommaSeparator); foreach (var pair in pairs) { var keyValuePair = new TrimmingTokenizer(pair, EqualSeparator, maxCount: 2); var enumerator = keyValuePair.GetEnumerator(); enumerator.MoveNext(); if (StringSegment.Equals(enumerator.Current, StripeConstants.SignatureKey, StringComparison.Ordinal)) { enumerator.MoveNext(); yield return(enumerator.Current); } } }
// Header contains a comma-separated collection of key / value pairs. Get the value for the "t" key. private StringSegment GetTimestamp(string header) { var pairs = new TrimmingTokenizer(header, CommaSeparator); foreach (var pair in pairs) { var keyValuePair = new TrimmingTokenizer(pair, EqualSeparator, maxCount: 2); var enumerator = keyValuePair.GetEnumerator(); enumerator.MoveNext(); if (StringSegment.Equals(enumerator.Current, StripeConstants.TimestampKey, StringComparison.Ordinal)) { enumerator.MoveNext(); return(enumerator.Current); } } return(StringSegment.Empty); }
/// <summary> /// Parses the 'text' of a slash command or 'subtext' of an outgoing WebHook of the form '<c>action value</c>'. /// </summary> /// <example> /// An example of an outgoing WebHook or slash command using this format is /// <c>'/assistant query what's the weather?'</c> where '/assistant' is the trigger word or slash command, /// 'query' is the action and 'what's the weather?' is the value. /// </example> /// <param name="text">The 'text' of a slash command or 'subtext' of an outgoing WebHook.</param> public static KeyValuePair <string, string> ParseActionWithValue(string text) { if (string.IsNullOrEmpty(text)) { return(new KeyValuePair <string, string>(string.Empty, string.Empty)); } var values = new TrimmingTokenizer(text, LwsSeparator, 2).ToArray(); if (values.Length == 0) { return(new KeyValuePair <string, string>(string.Empty, string.Empty)); } var result = new KeyValuePair <string, string>( values[0].Value, values.Length > 1 ? values[1].Value : string.Empty); return(result); }
/// <summary> /// Parses the 'text' of a slash command or 'subtext' of an outgoing WebHook of the form '<c>action value</c>'. /// </summary> /// <example> /// An example of an outgoing WebHook or slash command using this format is /// <c>'/assistant query what's the weather?'</c> where '/assistant' is the trigger word or slash command, /// 'query' is the action and 'what's the weather?' is the value. /// </example> /// <param name="text">The 'text' of a slash command or 'subtext' of an outgoing WebHook.</param> /// <returns> /// A <see cref="KeyValuePair{TKey, TValue}"/> mapping the action to its value. Both properties of the /// <see cref="KeyValuePair{TKey, TValue}"/> will be <see cref="StringSegment.Empty"/> when the /// <paramref name="text"/> does not contain an action. The <see cref="KeyValuePair{TKey, TValue}.Value"/> will /// be <see cref="StringSegment.Empty"/> when the <paramref name="text"/> contains no interior spaces i.e. the /// action has no value. /// </returns> public static KeyValuePair <StringSegment, StringSegment> ParseActionWithValue(string text) { if (string.IsNullOrEmpty(text)) { return(new KeyValuePair <StringSegment, StringSegment>(StringSegment.Empty, StringSegment.Empty)); } var values = new TrimmingTokenizer(text, LwsSeparator, maxCount: 2); var enumerator = values.GetEnumerator(); if (!enumerator.MoveNext()) { return(new KeyValuePair <StringSegment, StringSegment>(StringSegment.Empty, StringSegment.Empty)); } var action = enumerator.Current; var value = enumerator.MoveNext() ? enumerator.Current : StringSegment.Empty; return(new KeyValuePair <StringSegment, StringSegment>(action, value)); }
/// <inheritdoc /> public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (next == null) { throw new ArgumentNullException(nameof(next)); } var request = context.HttpContext.Request; if (HttpMethods.IsPost(request.Method)) { // 1. Confirm a secure connection. var errorResult = EnsureSecureConnection(ReceiverName, context.HttpContext.Request); if (errorResult != null) { context.Result = errorResult; return; } // 2. Get the expected hash from the signature header. var header = GetRequestHeader(request, ZoomConstants.SignatureHeaderName, out errorResult); if (errorResult != null) { context.Result = errorResult; return; } var values = new TrimmingTokenizer(header, PairSeparators); var enumerator = values.GetEnumerator(); enumerator.MoveNext(); var headerKey = enumerator.Current; if (values.Count != 2 || !StringSegment.Equals( headerKey, ZoomConstants.SignatureHeaderKey, StringComparison.OrdinalIgnoreCase)) { Logger.LogWarning( 0, $"Invalid '{ZoomConstants.SignatureHeaderName}' header value. Expecting a value of " + $"'{ZoomConstants.SignatureHeaderKey}=<value>'."); var message = string.Format( CultureInfo.CurrentCulture, Resources.SignatureFilter_BadHeaderValue, ZoomConstants.SignatureHeaderName, ZoomConstants.SignatureHeaderKey, "<value>"); errorResult = new BadRequestObjectResult(message); context.Result = errorResult; return; } enumerator.MoveNext(); var headerValue = enumerator.Current.Value; var expectedHash = FromHex(headerValue, ZoomConstants.SignatureHeaderName); if (expectedHash == null) { context.Result = CreateBadHexEncodingResult(ZoomConstants.SignatureHeaderKey); return; } // 3. Get the configured secret key. var secretKey = GetSecretKey(ReceiverName, context.RouteData, ZoomConstants.SecretKeyMinLength); if (secretKey == null) { context.Result = new NotFoundResult(); return; } var secret = Encoding.UTF8.GetBytes(secretKey); // 4. Get the actual hash of the request body. var actualHash = await ComputeRequestBodySha1HashAsync(request, secret); // 5. Verify that the actual hash matches the expected hash. if (!SecretEqual(expectedHash, actualHash)) { // Log about the issue and short-circuit remainder of the pipeline. errorResult = CreateBadSignatureResult(ZoomConstants.SignatureHeaderName); context.Result = errorResult; return; } } await next(); }
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (next == null) { throw new ArgumentNullException(nameof(next)); } if (!this.IsRequestApplicable(context.RouteData) && HttpMethods.IsPost(context.HttpContext.Request.Method)) { await next(); return; } IActionResult ErrorResult = base.EnsureSecureConnection(this.ReceiverName, context.HttpContext.Request); if (ErrorResult != null) { context.Result = ErrorResult; return; } string Header = base.GetRequestHeader(context.HttpContext.Request, BitbucketConstants.SignatureHeaderName, out ErrorResult); if (ErrorResult != null) { context.Result = ErrorResult; return; } TrimmingTokenizer Values = new TrimmingTokenizer(Header, SEPARATORS); TrimmingTokenizer.Enumerator Enumerator = Values.GetEnumerator(); Enumerator.MoveNext(); StringSegment HeaderKey = Enumerator.Current; if (Values.Count != 2 || !StringSegment.Equals(HeaderKey, BitbucketConstants.SignatureHeaderKey, StringComparison.OrdinalIgnoreCase)) { string ErrorMessage = string.Format(CultureInfo.CurrentCulture, Resources.SignatureFilter_BadHeaderValue, BitbucketConstants.SignatureHeaderName, BitbucketConstants.SignatureHeaderKey, "<value>"); base.Logger.LogError(1, ErrorMessage); context.Result = new BadRequestObjectResult(ErrorMessage); return; } Enumerator.MoveNext(); string HeaderValue = Enumerator.Current.Value; byte[] ExpectedHash = base.FromHex(HeaderValue, BitbucketConstants.SignatureHeaderName); if (ExpectedHash == null) { context.Result = base.CreateBadHexEncodingResult(BitbucketConstants.SignatureHeaderName); return; } byte[] Secret = this.GetSecret(this.ReceiverName, context.RouteData); if (Secret == null) { context.Result = new NotFoundResult(); return; } byte[] ActualHash = await base.ComputeRequestBodySha256HashAsync(context.HttpContext.Request, Secret); if (!BitbucketVerifySignatureFilter.SecretEqual(ExpectedHash, ActualHash)) { context.Result = base.CreateBadSignatureResult(BitbucketConstants.SignatureHeaderName); return; } await next(); }
/// <summary> /// Gets the set of tuples mapping application keys to secret keys. The secret keys are used to verify the /// signature of an incoming Pusher WebHook request. /// </summary> /// <param name="request">The current <see cref="HttpRequest"/>.</param> /// <param name="routeData"> /// The <see cref="RouteData"/> for this request. A (potentially empty) ID value in this data allows /// <see cref="PusherVerifySignatureFilter"/> to support multiple senders with individual configurations. /// </param> /// <returns></returns> protected virtual async Task <IDictionary <string, string> > GetSecretLookupTable( HttpRequest request, RouteData routeData) { if (routeData == null) { throw new ArgumentNullException(nameof(routeData)); } // 1. Check if we've already found the configured lookup table for this id. routeData.TryGetReceiverId(out var id); id = id ?? string.Empty; if (_lookupTables.TryGetValue(id, out var lookupTable)) { // Success. return(lookupTable); } // 2. Get the configuration value. var secretKeyPairs = await GetReceiverConfig( request, routeData, ReceiverName, PusherConstants.SecretKeyMinLength, PusherConstants.SecretKeyMaxLength); if (secretKeyPairs == null) { // Missing a configuration value for this id. return(null); } // 3. Parse the configuration value as application key / secret key pairs.; lookupTable = new Dictionary <string, string>(StringComparer.Ordinal); foreach (var secretKeyPair in new TrimmingTokenizer(secretKeyPairs, BetweenPairSeparators)) { var partTokenizer = new TrimmingTokenizer(secretKeyPair, PairSeparators); if (partTokenizer.Count != 2) { // Corrupted configuration value. Logger.LogCritical( 1, "Could not find a valid configuration for the '{ReceiverName}' WebHook receiver and " + "instance '{Id}'. The configuration value must be a comma-separated list of segments, each " + "of the form '<appKey1>_<secretKey1>; <appKey2>_<secretKey2>'.", ReceiverName, id); var message = string.Format( CultureInfo.CurrentCulture, Resources.SignatureFilter_BadSecret, ReceiverName, id); throw new InvalidOperationException(Resources.SignatureFilter_BadSecret); } // Empty or duplicate application keys are fine; will be ignored during lookups or merged when added to // the table (keeping the last value). var enumerator = partTokenizer.GetEnumerator(); enumerator.MoveNext(); var applicationKey = enumerator.Current.Value; enumerator.MoveNext(); var secretKey = enumerator.Current.Value; lookupTable[applicationKey] = secretKey; } if (lookupTable.Count == 0) { // Because SecretKeyMinLength is non-zero, this corner case may be impossible. No harm however. Logger.LogCritical( 2, "Could not find a valid configuration for the '{ReceiverName}' WebHook receiver and instance " + "'{Id}'. To receive '{ReceiverName}' WebHook requests for instance '{Id}', please add a " + "non-empty configuration value.", ReceiverName, id, ReceiverName, id); var message = string.Format( CultureInfo.CurrentCulture, Resources.SignatureFilter_NoSecrets, ReceiverName, id); throw new InvalidOperationException(message); } // 4. Add the new lookup table to the dictionary. Parsing was successful. _lookupTables.TryAdd(id, lookupTable); return(lookupTable); }
private IActionResult ValidateHeader(string header) { var hasTimestamp = false; var hasSignature = false; var pairs = new TrimmingTokenizer(header, CommaSeparator); foreach (var pair in pairs) { var keyValuePair = new TrimmingTokenizer(pair, EqualSeparator, maxCount: 2); if (keyValuePair.Count != 2) { // Header is not formatted correctly. Logger.LogWarning( 0, $"The '{StripeConstants.SignatureHeaderName}' header value is invalid. '{{InvalidPair}}' " + "should be a 'key=value' pair.", pair); var message = string.Format( CultureInfo.CurrentCulture, Resources.SignatureFilter_InvalidHeaderFormat, StripeConstants.SignatureHeaderName); return(new BadRequestObjectResult(message)); } var enumerator = keyValuePair.GetEnumerator(); enumerator.MoveNext(); var key = enumerator.Current; if (StringSegment.Equals(key, StripeConstants.SignatureKey, StringComparison.Ordinal)) { enumerator.MoveNext(); hasSignature = !StringSegment.IsNullOrEmpty(enumerator.Current); } else if (StringSegment.Equals(key, StripeConstants.TimestampKey, StringComparison.Ordinal)) { enumerator.MoveNext(); hasTimestamp = !StringSegment.IsNullOrEmpty(enumerator.Current); } } if (!hasSignature) { Logger.LogWarning( 1, $"The '{StripeConstants.SignatureHeaderName}' header value is invalid. Does not contain a " + $"timestamp ('{StripeConstants.SignatureKey}') value."); } if (!hasTimestamp) { Logger.LogWarning( 2, $"The '{StripeConstants.SignatureHeaderName}' header value is invalid. Does not contain a " + $"signature ('{StripeConstants.TimestampKey}') value."); } if (!hasSignature || !hasTimestamp) { var message = string.Format( CultureInfo.CurrentCulture, Resources.SignatureFilter_HeaderMissingValue, StripeConstants.SignatureHeaderName, StripeConstants.TimestampKey, StripeConstants.SignatureKey); return(new BadRequestObjectResult(message)); } // Success return(null); }
/// <inheritdoc /> public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (next == null) { throw new ArgumentNullException(nameof(next)); } var routeData = context.RouteData; var request = context.HttpContext.Request; if (routeData.TryGetReceiverName(out var receiverName) && IsApplicable(receiverName) && HttpMethods.IsPost(request.Method)) { // 1. Get the expected hash from the signature header. var header = GetRequestHeader(request, GitHubConstants.SignatureHeaderName, out var errorResult); if (errorResult != null) { context.Result = errorResult; return; } var values = new TrimmingTokenizer(header, PairSeparators); var enumerator = values.GetEnumerator(); enumerator.MoveNext(); var headerKey = enumerator.Current; if (values.Count != 2 || !StringSegment.Equals( headerKey, GitHubConstants.SignatureHeaderKey, StringComparison.OrdinalIgnoreCase)) { Logger.LogError( 1, "Invalid '{HeaderName}' header value. Expecting a value of '{Key}={Value}'.", GitHubConstants.SignatureHeaderName, GitHubConstants.SignatureHeaderKey, "<value>"); var message = string.Format( CultureInfo.CurrentCulture, Resources.SignatureFilter_BadHeaderValue, GitHubConstants.SignatureHeaderName, GitHubConstants.SignatureHeaderKey, "<value>"); errorResult = WebHookResultUtilities.CreateErrorResult(message); context.Result = errorResult; return; } enumerator.MoveNext(); var headerValue = enumerator.Current.Value; var expectedHash = GetDecodedHash(headerValue, GitHubConstants.SignatureHeaderName, out errorResult); if (errorResult != null) { context.Result = errorResult; return; } // 2. Get the configured secret key. var secretKey = await GetReceiverConfig( request, routeData, ReceiverName, GitHubConstants.SecretKeyMinLength, GitHubConstants.SecretKeyMaxLength); if (secretKey == null) { context.Result = new NotFoundResult(); return; } var secret = Encoding.UTF8.GetBytes(secretKey); // 3. Get the actual hash of the request body. var actualHash = await GetRequestBodyHash_SHA1(request, secret); // 4. Verify that the actual hash matches the expected hash. if (!SecretEqual(expectedHash, actualHash)) { // Log about the issue and short-circuit remainder of the pipeline. errorResult = CreateBadSignatureResult(receiverName, GitHubConstants.SignatureHeaderName); context.Result = errorResult; return; } } await next(); }
internal static IDictionary <string, string> ReadSettings(IConfiguration configuration, ILogger logger) { // All keys are lowercased before additions or lookups. var settings = new Dictionary <string, string>(StringComparer.Ordinal); foreach (var setting in configuration.AsEnumerable()) { var key = setting.Key; if (key.Length > WebHookConstants.ReceiverConfigurationKeyPrefix.Length && key.StartsWith(WebHookConstants.ReceiverConfigurationKeyPrefix, StringComparison.OrdinalIgnoreCase)) { // Extract configuration (again, likely receiver) name var configurationName = key.Substring(WebHookConstants.ReceiverConfigurationKeyPrefix.Length); // Parse values foreach (var segment in new TrimmingTokenizer(setting.Value, BetweenPairSeparators)) { var values = new TrimmingTokenizer(segment, PairSeparators); if (values.Count == 1) { var enumerator = values.GetEnumerator(); enumerator.MoveNext(); var value = enumerator.Current.Value; AddKey(settings, logger, configurationName, string.Empty, value); } else if (values.Count == 2) { var enumerator = values.GetEnumerator(); enumerator.MoveNext(); var id = enumerator.Current.Value; enumerator.MoveNext(); var value = enumerator.Current.Value; AddKey(settings, logger, configurationName, id, value); } else { logger.LogError( 0, "The '{Key}' application setting must have a comma-separated value of one or more " + "secrets of the form <secret> or <id>=<secret>.", key); var message = string.Format(CultureInfo.CurrentCulture, Resources.Config_BadValue, key); throw new InvalidOperationException(message); } } } } if (settings.Count == 0) { var format = WebHookConstants.ReceiverConfigurationKeyPrefix + "<receiver>"; logger.LogError( 1, "Did not find any applications settings of the form '{Format}'. To receive WebHooks, please add " + "corresponding applications settings.", format); } return(settings); }