Skip to content

.Net: Updates to HttpPlugin #11437

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions dotnet/src/Plugins/Plugins.Core/HttpPlugin.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Net.Http;
using System.Threading;
Expand Down Expand Up @@ -36,6 +38,15 @@ public HttpPlugin() : this(null)
public HttpPlugin(HttpClient? client = null) =>
this._client = client ?? HttpClientProvider.GetHttpClient();

/// <summary>
/// List of allowed domains to download from.
/// </summary>
public IEnumerable<string>? AllowedDomains
{
get => this._allowedDomains;
set => this._allowedDomains = value is null ? null : new HashSet<string>(value, StringComparer.OrdinalIgnoreCase);
}

/// <summary>
/// Sends an HTTP GET request to the specified URI and returns the response body as a string.
/// </summary>
Expand Down Expand Up @@ -88,17 +99,38 @@ public Task<string> DeleteAsync(
CancellationToken cancellationToken = default) =>
this.SendRequestAsync(uri, HttpMethod.Delete, requestContent: null, cancellationToken);

#region private
private HashSet<string>? _allowedDomains;

/// <summary>
/// If a list of allowed domains has been provided, the host of the provided uri is checked
/// to verify it is in the allowed domain list.
/// </summary>
private bool IsUriAllowed(Uri uri)
{
Verify.NotNull(uri);

return this._allowedDomains is null || this._allowedDomains.Contains(uri.Host);
}

/// <summary>Sends an HTTP request and returns the response content as a string.</summary>
/// <param name="uri">The URI of the request.</param>
/// <param name="uriStr">The URI of the request.</param>
/// <param name="method">The HTTP method for the request.</param>
/// <param name="requestContent">Optional request content.</param>
/// <param name="cancellationToken">The token to use to request cancellation.</param>
private async Task<string> SendRequestAsync(string uri, HttpMethod method, HttpContent? requestContent, CancellationToken cancellationToken)
private async Task<string> SendRequestAsync(string uriStr, HttpMethod method, HttpContent? requestContent, CancellationToken cancellationToken)
{
var uri = new Uri(uriStr);
if (!this.IsUriAllowed(uri))
{
throw new InvalidOperationException("Sending requests to the provided location is not allowed.");
}

using var request = new HttpRequestMessage(method, uri) { Content = requestContent };
request.Headers.Add("User-Agent", HttpHeaderConstant.Values.UserAgent);
request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(HttpPlugin)));
using var response = await this._client.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false);
return await response.Content.ReadAsStringWithExceptionMappingAsync(cancellationToken).ConfigureAwait(false);
}
#endregion
}
19 changes: 19 additions & 0 deletions dotnet/src/Plugins/Plugins.UnitTests/Core/HttpPluginTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,25 @@ public async Task ItCanDeleteAsync()
this.VerifyMock(mockHandler, HttpMethod.Delete);
}

[Fact]
public async Task ItThrowsInvalidOperationExceptionForInvalidDomainAsync()
{
// Arrange
var mockHandler = this.CreateMock();
using var client = new HttpClient(mockHandler.Object);
var plugin = new HttpPlugin(client)
{
AllowedDomains = ["www.example.com"]
};
var invalidUri = "http://www.notexample.com";

// Act & Assert
await Assert.ThrowsAsync<InvalidOperationException>(async () => await plugin.GetAsync(invalidUri));
await Assert.ThrowsAsync<InvalidOperationException>(async () => await plugin.PostAsync(invalidUri, this._content));
await Assert.ThrowsAsync<InvalidOperationException>(async () => await plugin.PutAsync(invalidUri, this._content));
await Assert.ThrowsAsync<InvalidOperationException>(async () => await plugin.DeleteAsync(invalidUri));
}

private Mock<HttpMessageHandler> CreateMock()
{
var mockHandler = new Mock<HttpMessageHandler>();
Expand Down
Loading