Example #1
0
 /// <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;
 }
Example #2
0
        // ??? 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);
        }
Example #3
0
        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);
        }
Example #6
0
        /// <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);
        }
Example #7
0
        /// <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));
        }
Example #8
0
        /// <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();
        }
Example #9
0
        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);
        }