|
5 | 5 | using System.Diagnostics.CodeAnalysis; |
6 | 6 | using System.IO; |
7 | 7 | using System.Linq; |
| 8 | +using System.Net; |
8 | 9 | using System.Net.Http; |
9 | 10 | using System.Net.Mime; |
| 11 | +using System.Net.Sockets; |
10 | 12 | using System.Text; |
11 | 13 | using System.Text.Json.Nodes; |
12 | 14 | using System.Threading; |
@@ -51,6 +53,57 @@ public OpenApiKernelPluginFactoryTests() |
51 | 53 | this._openApiDocument = ResourcePluginsProvider.LoadFromResource("documentV2_0.json"); |
52 | 54 | } |
53 | 55 |
|
| 56 | + [Fact] |
| 57 | + public async Task ItDoesNotFollowRedirectsByDefaultAsync() |
| 58 | + { |
| 59 | + // Arrange |
| 60 | + await using var server = new RedirectLoopbackServer(); |
| 61 | + var serverUrl = server.BaseUri.AbsoluteUri; |
| 62 | + |
| 63 | + var operation = new RestApiOperation( |
| 64 | + id: "redirect", |
| 65 | + servers: [new RestApiServer(serverUrl)], |
| 66 | + path: "start", |
| 67 | + method: HttpMethod.Get, |
| 68 | + description: "redirect test", |
| 69 | + parameters: [], |
| 70 | + responses: new Dictionary<string, RestApiExpectedResponse>(), |
| 71 | + securityRequirements: []); |
| 72 | + var specification = new RestApiSpecification(new RestApiInfo(), null, [operation]); |
| 73 | + var executionParameters = new OpenApiFunctionExecutionParameters |
| 74 | + { |
| 75 | + ServerUrlValidationOptions = new RestApiOperationServerUrlValidationOptions |
| 76 | + { |
| 77 | + AllowedBaseUrls = [new Uri(serverUrl)] |
| 78 | + } |
| 79 | + }; |
| 80 | + var plugin = OpenApiKernelPluginFactory.CreateFromOpenApi("redirectPlugin", specification, executionParameters); |
| 81 | + var kernel = new Kernel(); |
| 82 | + |
| 83 | + // Act |
| 84 | + await Assert.ThrowsAsync<HttpOperationException>(() => kernel.InvokeAsync(plugin["redirect"])); |
| 85 | + |
| 86 | + // Assert |
| 87 | + Assert.False(server.RedirectTargetContacted); |
| 88 | + } |
| 89 | + |
| 90 | + [Fact] |
| 91 | + public async Task ItDoesNotFollowRedirectsWhenLoadingOpenApiDocumentsByDefaultAsync() |
| 92 | + { |
| 93 | + // Arrange |
| 94 | + await using var server = new RedirectLoopbackServer(); |
| 95 | + |
| 96 | + // Act |
| 97 | + await Assert.ThrowsAsync<HttpOperationException>(() => |
| 98 | + OpenApiKernelPluginFactory.CreateFromOpenApiAsync( |
| 99 | + "redirectedDocumentPlugin", |
| 100 | + new Uri(server.BaseUri, "start"), |
| 101 | + this._executionParameters)); |
| 102 | + |
| 103 | + // Assert |
| 104 | + Assert.False(server.RedirectTargetContacted); |
| 105 | + } |
| 106 | + |
54 | 107 | [Fact] |
55 | 108 | public async Task ItCanIncludeOpenApiOperationParameterTypesIntoFunctionParametersViewAsync() |
56 | 109 | { |
@@ -771,6 +824,87 @@ private static void AssertPayloadParameters(KernelPlugin plugin, string function |
771 | 824 | Assert.Equal("content_type", function.Metadata.Parameters[1].Name); |
772 | 825 | } |
773 | 826 |
|
| 827 | + private sealed class RedirectLoopbackServer : IAsyncDisposable |
| 828 | + { |
| 829 | + private readonly TcpListener _listener; |
| 830 | + private readonly CancellationTokenSource _cancellationTokenSource = new(); |
| 831 | + private readonly Task _serverTask; |
| 832 | + private int _redirectTargetContacted; |
| 833 | + |
| 834 | + public RedirectLoopbackServer() |
| 835 | + { |
| 836 | + this._listener = new TcpListener(IPAddress.Loopback, 0); |
| 837 | + this._listener.Start(); |
| 838 | + var endpoint = (IPEndPoint)this._listener.LocalEndpoint; |
| 839 | + this.BaseUri = new Uri($"http://{endpoint.Address}:{endpoint.Port}/"); |
| 840 | + this._serverTask = this.RunAsync(); |
| 841 | + } |
| 842 | + |
| 843 | + public Uri BaseUri { get; } |
| 844 | + |
| 845 | + public bool RedirectTargetContacted => Volatile.Read(ref this._redirectTargetContacted) != 0; |
| 846 | + |
| 847 | + public async ValueTask DisposeAsync() |
| 848 | + { |
| 849 | + this._cancellationTokenSource.Cancel(); |
| 850 | + this._listener.Stop(); |
| 851 | + await this._serverTask.ConfigureAwait(false); |
| 852 | + this._cancellationTokenSource.Dispose(); |
| 853 | + } |
| 854 | + |
| 855 | + private async Task RunAsync() |
| 856 | + { |
| 857 | + while (!this._cancellationTokenSource.IsCancellationRequested) |
| 858 | + { |
| 859 | + TcpClient client; |
| 860 | + try |
| 861 | + { |
| 862 | + client = await this._listener.AcceptTcpClientAsync(this._cancellationTokenSource.Token).ConfigureAwait(false); |
| 863 | + } |
| 864 | + catch (OperationCanceledException) |
| 865 | + { |
| 866 | + break; |
| 867 | + } |
| 868 | + catch (SocketException) when (this._cancellationTokenSource.IsCancellationRequested) |
| 869 | + { |
| 870 | + break; |
| 871 | + } |
| 872 | + |
| 873 | + using (client) |
| 874 | + { |
| 875 | + await this.HandleRequestAsync(client, this._cancellationTokenSource.Token).ConfigureAwait(false); |
| 876 | + } |
| 877 | + } |
| 878 | + } |
| 879 | + |
| 880 | + private async Task HandleRequestAsync(TcpClient client, CancellationToken cancellationToken) |
| 881 | + { |
| 882 | + var stream = client.GetStream(); |
| 883 | + using var reader = new StreamReader(stream, Encoding.ASCII, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true); |
| 884 | + var requestLine = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); |
| 885 | + string? header; |
| 886 | + do |
| 887 | + { |
| 888 | + header = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); |
| 889 | + } |
| 890 | + while (!string.IsNullOrEmpty(header)); |
| 891 | + |
| 892 | + var requestTarget = requestLine?.Split(' ')[1]; |
| 893 | + string response; |
| 894 | + if (requestTarget == "/start") |
| 895 | + { |
| 896 | + response = $"HTTP/1.1 302 Found\r\nLocation: {new Uri(this.BaseUri, "target")}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; |
| 897 | + } |
| 898 | + else |
| 899 | + { |
| 900 | + Interlocked.Exchange(ref this._redirectTargetContacted, 1); |
| 901 | + response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}"; |
| 902 | + } |
| 903 | + |
| 904 | + await stream.WriteAsync(Encoding.ASCII.GetBytes(response), cancellationToken).ConfigureAwait(false); |
| 905 | + } |
| 906 | + } |
| 907 | + |
774 | 908 | private sealed class FakePlugin |
775 | 909 | { |
776 | 910 | public string? ParameterValueFakeMethodCalledWith { get; private set; } |
|
0 commit comments