Improved the validation code of the Confluence search tool

This commit is contained in:
Thorsten Sommer committed 2026-09-23 16:40:58 +02:00
1 parent 6afd724002
commit 637fe5d3e7
1 file changed
+11 -13
@@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using AIStudio.Provider; using AIStudio.Provider;
@@ -87,7 +88,7 @@ public sealed class ConfluenceSearchTool(WebPageRetrievalService webPageRetrieva
}); });
if (!ToolSettingsValueParser.TryReadBoundedOptionalPositiveInt(settingsValues, TIMEOUT_SECONDS_SETTING, MAX_TIMEOUT_SECONDS, if (!ToolSettingsValueParser.TryReadBoundedOptionalPositiveInt(settingsValues, TIMEOUT_SECONDS_SETTING, MAX_TIMEOUT_SECONDS,
TB("The setting '{0}' must be a positive integer."), TB("The setting '{0}' must not exceed {1}."), out _, out var timeoutError)) TB("The setting '{0}' must be a positive integer."), TB("The setting '{0}' must be less than or equal to {1}."), out _, out var timeoutError))
return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState { IsConfigured = false, Message = timeoutError }); return Task.FromResult<ToolConfigurationState?>(new ToolConfigurationState { IsConfigured = false, Message = timeoutError });
return Task.FromResult<ToolConfigurationState?>(null); return Task.FromResult<ToolConfigurationState?>(null);
@@ -124,11 +125,8 @@ public sealed class ConfluenceSearchTool(WebPageRetrievalService webPageRetrieva
throw new ArgumentException($"Argument 'spaceKey' must not exceed {MAX_SPACE_KEY_CHARACTERS} characters or contain control characters."); throw new ArgumentException($"Argument 'spaceKey' must not exceed {MAX_SPACE_KEY_CHARACTERS} characters or contain control characters.");
} }
var timeoutSeconds = ToolSettingsValueParser.ReadOptionalPositiveInt(context.SettingsValues, TIMEOUT_SECONDS_SETTING) ?? DEFAULT_TIMEOUT_SECONDS; var timeoutSeconds = Math.Min(ToolSettingsValueParser.ReadOptionalPositiveInt(context.SettingsValues, TIMEOUT_SECONDS_SETTING) ?? DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS);
if (timeoutSeconds > MAX_TIMEOUT_SECONDS) var searchUrl = BuildSearchUrl(baseUrl, query, spaceKey);
throw new InvalidOperationException(TB("The Confluence search timeout exceeds its allowed limit."));
var searchUrl = BuildSearchUrl(baseUrl!, query, spaceKey);
RetrievedWebPage retrievedPage; RetrievedWebPage retrievedPage;
try try
{ {
@@ -137,11 +135,11 @@ public sealed class ConfluenceSearchTool(WebPageRetrievalService webPageRetrieva
TimeoutSeconds = timeoutSeconds, TimeoutSeconds = timeoutSeconds,
ProviderConfidence = context.ProviderConfidence, ProviderConfidence = context.ProviderConfidence,
UseOsSso = true, UseOsSso = true,
IsPrivateHostAllowed = host => IsWikiHost(baseUrl!, host), IsPrivateHostAllowed = host => IsWikiHost(baseUrl, host),
// Checked before every redirect is followed, so the query never reaches a host // Checked before every redirect is followed, so the query never reaches a host
// outside the wiki: // outside the wiki:
IsTargetAllowed = target => IsWithinWiki(baseUrl!, target), IsTargetAllowed = target => IsWithinWiki(baseUrl, target),
}, token); }, token);
} }
catch (WebPageAccessBlockedException exception) when (exception.Reason is WebPageAccessBlockReason.TARGET_NOT_ALLOWED) catch (WebPageAccessBlockedException exception) when (exception.Reason is WebPageAccessBlockReason.TARGET_NOT_ALLOWED)
@@ -154,7 +152,7 @@ public sealed class ConfluenceSearchTool(WebPageRetrievalService webPageRetrieva
} }
var page = retrievedPage.Page; var page = retrievedPage.Page;
if (!IsWithinWiki(baseUrl!, page.FinalUrl)) if (!IsWithinWiki(baseUrl, page.FinalUrl))
throw new InvalidOperationException(TB("Confluence redirected the search outside the configured wiki.")); throw new InvalidOperationException(TB("Confluence redirected the search outside the configured wiki."));
if (IsLoginPage(page.FinalUrl)) if (IsLoginPage(page.FinalUrl))
@@ -202,14 +200,14 @@ public sealed class ConfluenceSearchTool(WebPageRetrievalService webPageRetrieva
url.AbsolutePath.EndsWith("/login.action", StringComparison.OrdinalIgnoreCase) || url.AbsolutePath.EndsWith("/login.action", StringComparison.OrdinalIgnoreCase) ||
url.Query.Contains("os_destination=", StringComparison.OrdinalIgnoreCase); url.Query.Contains("os_destination=", StringComparison.OrdinalIgnoreCase);
internal static bool TryParseBaseUrl(string? value, out Uri? baseUrl) internal static bool TryParseBaseUrl(string? value, [NotNullWhen(true)] out Uri? baseUrl)
{ {
baseUrl = null; baseUrl = null;
if (!Uri.TryCreate(value?.Trim(), UriKind.Absolute, out var uri) || if (!Uri.TryCreate(value?.Trim(), UriKind.Absolute, out var uri) ||
uri.Scheme is not "https" || uri.Scheme is not "https" ||
!string.IsNullOrEmpty(uri.UserInfo) || !string.IsNullOrWhiteSpace(uri.UserInfo) ||
!string.IsNullOrEmpty(uri.Query) || !string.IsNullOrWhiteSpace(uri.Query) ||
!string.IsNullOrEmpty(uri.Fragment)) !string.IsNullOrWhiteSpace(uri.Fragment))
return false; return false;
baseUrl = new Uri(uri.AbsoluteUri.TrimEnd('/') + '/'); baseUrl = new Uri(uri.AbsoluteUri.TrimEnd('/') + '/');