Skip to content
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

Fix GZip handling for requests #4165

Merged
merged 6 commits into from
Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Azure.Sdk.Tools.TestProxy.Common;
using Xunit;

namespace Azure.Sdk.Tools.TestProxy.Tests
Expand Down Expand Up @@ -217,5 +218,35 @@ public async Task TestPlaybackSetsRetryAfterToZero()
await testRecordingHandler.HandlePlaybackRequest(recordingId, request, response);
Assert.False(response.Headers.ContainsKey("Retry-After"));
}

[Fact]
public async Task TestPlaybackWithGZippedContentPlayback()
{
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
var httpContext = new DefaultHttpContext();
var body = "{\"x-recording-file\":\"Test.RecordEntries/request_response_with_gzipped_content.json\"}";
httpContext.Request.Body = TestHelpers.GenerateStreamRequestBody(body);
httpContext.Request.ContentLength = body.Length;

var controller = new Playback(testRecordingHandler, new NullLoggerFactory())
{
ControllerContext = new ControllerContext()
{
HttpContext = httpContext
}
};
await controller.Start();

var recordingId = httpContext.Response.Headers["x-recording-id"].ToString();
Assert.NotNull(recordingId);
Assert.True(testRecordingHandler.PlaybackSessions.ContainsKey(recordingId));
var entry = testRecordingHandler.PlaybackSessions[recordingId].Session.Entries[0];
HttpRequest request = TestHelpers.CreateRequestFromEntry(entry);

// compress the body to simulate what the request coming from the library will look like
request.Body = new MemoryStream(GZipUtilities.CompressBody(BinaryData.FromStream(request.Body).ToArray(), request.Headers));
HttpResponse response = new DefaultHttpContext().Response;
await testRecordingHandler.HandlePlaybackRequest(recordingId, request, response);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
Expand Down Expand Up @@ -632,7 +633,7 @@ public void TestCreateUpstreamRequestIncludesExpectedHeaders(params string[] inc
var recordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
var upstreamRequestContext = GenerateHttpRequestContext(incomingHeaders);

var output = recordingHandler.CreateUpstreamRequest(upstreamRequestContext.Request, new byte[] { });
var output = recordingHandler.CreateUpstreamRequest(upstreamRequestContext.Request);

// iterate across the set we know about and confirm that GenerateUpstreamRequest worked properly!
var setOfHeaders = GenerateHeaderValuesTuples(incomingHeaders);
Expand Down Expand Up @@ -684,7 +685,7 @@ public async Task TestRecordMaintainsUpstreamOverrideHostHeader(string upstreamH

httpContext.Request.Method = "GET";

var upstreamRequest = testRecordingHandler.CreateUpstreamRequest(httpContext.Request, new byte[] { });
var upstreamRequest = testRecordingHandler.CreateUpstreamRequest(httpContext.Request);

if (!String.IsNullOrWhiteSpace(upstreamHostHeaderValue))
{
Expand All @@ -696,6 +697,52 @@ public async Task TestRecordMaintainsUpstreamOverrideHostHeader(string upstreamH
}
}

[Fact]
public async Task TestRecordWithGZippedContent()
{
var httpContext = new DefaultHttpContext();
var bodyBytes = Encoding.UTF8.GetBytes("{\"hello\":\"world\"}");
var mockClient = new HttpClient(new MockHttpHandler(bodyBytes, "application/json", "gzip"));
var path = Directory.GetCurrentDirectory();
var recordingHandler = new RecordingHandler(path)
{
RedirectableClient = mockClient,
RedirectlessClient = mockClient
};

var relativePath = "recordings/gzip";
var fullPathToRecording = Path.Combine(path, relativePath) + ".json";

await recordingHandler.StartRecordingAsync(relativePath, httpContext.Response);

var recordingId = httpContext.Response.Headers["x-recording-id"].ToString();

httpContext.Request.ContentType = "application/json";
httpContext.Request.Headers["Content-Encoding"] = "gzip";
httpContext.Request.ContentLength = 0;
httpContext.Request.Headers["x-recording-id"] = recordingId;
httpContext.Request.Headers["x-recording-upstream-base-uri"] = "http://example.org";
httpContext.Request.Method = "GET";
httpContext.Request.Body = new MemoryStream(GZipUtilities.CompressBody(bodyBytes, httpContext.Request.Headers));

await recordingHandler.HandleRecordRequestAsync(recordingId, httpContext.Request, httpContext.Response);
recordingHandler.StopRecording(recordingId);

try
{
using var fileStream = File.Open(fullPathToRecording, FileMode.Open);
using var doc = JsonDocument.Parse(fileStream);
var record = RecordSession.Deserialize(doc.RootElement);
var entry = record.Entries.First();
Assert.Equal("{\"hello\":\"world\"}", Encoding.UTF8.GetString(entry.Request.Body));
Assert.Equal("{\"hello\":\"world\"}", Encoding.UTF8.GetString(entry.Response.Body));
}
finally
{
File.Delete(fullPathToRecording);
}
}

#region SetRecordingOptions
[Theory]
[InlineData("{ \"HandleRedirects\": \"true\"}", true)]
Expand Down Expand Up @@ -979,17 +1026,38 @@ public IgnoreOnLinuxFact()
internal class MockHttpHandler : HttpMessageHandler
{
public const string DefaultResponse = "default response";
private readonly byte[] _responseContent;
private readonly string _contentType;
private readonly string _contentEncoding;

public MockHttpHandler()
public MockHttpHandler(byte[] responseContent = default, string contentType = default, string contentEncoding = default)
{
_responseContent = responseContent ?? Encoding.UTF8.GetBytes(DefaultResponse);
_contentType = contentType ?? "application/json";
_contentEncoding = contentEncoding;
}

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(HttpStatusCode.OK);

// we need to set the content before the content headers as otherwise they will be cleared out after setting content.
if (_contentEncoding == "gzip")
{
response.Content = new ByteArrayContent(GZipUtilities.CompressBodyCore(_responseContent));
}
else
{
response.Content = new ByteArrayContent(_responseContent);
}

return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
response.Content.Headers.ContentType = new MediaTypeHeaderValue(_contentType);
if (_contentEncoding != null)
{
Content = new StringContent(DefaultResponse)
});
response.Content.Headers.ContentEncoding.Add(_contentEncoding);
}

return Task.FromResult(response);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"Entries": [
{
"RequestUri": "https://fakeazsdktestaccount.table.core.windows.net/Tables",
"RequestMethod": "POST",
"RequestHeaders": {
"Accept": "application/json;odata=minimalmetadata",
"Accept-Encoding": "gzip, deflate",
"Authorization": "Sanitized",
"Connection": "keep-alive",
"Content-Length": "34",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
"DataServiceVersion": "3.0",
"Date": "Tue, 18 May 2021 23:27:42 GMT",
"User-Agent": "azsdk-python-data-tables/12.0.0b7 Python/3.8.6 (Windows-10-10.0.19041-SP0)",
"x-ms-client-request-id": "a4c24b7a-b830-11eb-a05e-10e7c6392c5a",
"x-ms-date": "Tue, 18 May 2021 23:27:42 GMT",
"x-ms-version": "2019-02-02"
},
"RequestBody": "{\u0022TableName\u0022: \u0022listtable09bf2a3d\u0022}",
scbedd marked this conversation as resolved.
Show resolved Hide resolved
"StatusCode": 201,
"ResponseHeaders": {
"Cache-Control": "no-cache",
"Content-Type": "application/json",
"Content-Encoding": "gzip",
"Date": "Tue, 18 May 2021 23:27:43 GMT",
"Retry-After": "10",
"Location": "https://fakeazsdktestaccount.table.core.windows.net/Tables(\u0027listtable09bf2a3d\u0027)",
"Server": [
"Windows-Azure-Table/1.0",
"Microsoft-HTTPAPI/2.0"
],
"Transfer-Encoding": "chunked",
"X-Content-Type-Options": "nosniff",
"x-ms-client-request-id": "a4c24b7a-b830-11eb-a05e-10e7c6392c5a",
"x-ms-request-id": "d2270777-c002-0072-313d-4ce19f000000",
"x-ms-version": "2019-02-02"
},
"ResponseBody": {
"odata.metadata": "https://fakeazsdktestaccount.table.core.windows.net/$metadata#Tables/@Element",
"TableName": "listtable09bf2a3d",
"connectionString": null
}
}
],
"Variables": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http.Headers;
using Microsoft.AspNetCore.Http;

namespace Azure.Sdk.Tools.TestProxy.Common
{
/// <summary>
/// Utility methods to compress and decompress content to/from GZip.
/// </summary>
public static class GZipUtilities
{
private const string Gzip = "gzip";
private const string ContentEncoding = "Content-Encoding";

public static byte[] CompressBody(byte[] incomingBody, IDictionary<string, string[]> headers)
{
if (headers.TryGetValue(ContentEncoding, out var values) && values.Contains(Gzip))
{
return CompressBodyCore(incomingBody);
}

return incomingBody;
}

public static byte[] CompressBody(byte[] incomingBody, IHeaderDictionary headers)
{
if (headers.TryGetValue(ContentEncoding, out var values) && values.Contains(Gzip))
{
return CompressBodyCore(incomingBody);
}

return incomingBody;
}

public static byte[] CompressBodyCore(byte[] body)
{
using (var uncompressedStream = new MemoryStream(body))
using (var resultStream = new MemoryStream())
{
using (var compressedStream = new GZipStream(resultStream, CompressionMode.Compress))
{
uncompressedStream.CopyTo(compressedStream);
scbedd marked this conversation as resolved.
Show resolved Hide resolved
}

return resultStream.ToArray();
}
}

public static byte[] DecompressBody(MemoryStream incomingBody, HttpContentHeaders headers)
{
if (headers.TryGetValues(ContentEncoding, out var values) && values.Contains(Gzip))
{
return DecompressBodyCore(incomingBody);
}

return incomingBody.ToArray();
}

public static byte[] DecompressBody(byte[] incomingBody, IHeaderDictionary headers)
{
if (headers.TryGetValue(ContentEncoding, out var values) && values.Contains(Gzip))
{
return DecompressBodyCore(new MemoryStream(incomingBody));
}

return incomingBody;
}

private static byte[] DecompressBodyCore(MemoryStream stream)
{
using var uncompressedStream = new GZipStream(stream, CompressionMode.Decompress);
using var resultStream = new MemoryStream();
uncompressedStream.CopyTo(resultStream);
return resultStream.ToArray();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"profiles": {
"Azure.Sdk.Tools.TestProxy": {
"commandName": "Project",
"commandLineArgs": "--help",
scbedd marked this conversation as resolved.
Show resolved Hide resolved
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"Logging__LogLevel__Microsoft": "Information"
Expand Down
Loading