Skip to content

Commit 1746534

Browse files
Adds M365 mock response plugin (#555)
Fixes bug in MockGeneratorPlugin Fixes bug in GraphMockResponsePlugin Fixes bug in GraphRandomErrorPlugin Makes MockResponsePlugin extensible Fixes bug in merging CORS headers
1 parent 21da697 commit 1746534

File tree

7 files changed

+158
-19
lines changed

7 files changed

+158
-19
lines changed

dev-proxy-abstractions/GraphBatchResponsePayload.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public class GraphBatchResponsePayloadResponse
2020
[JsonPropertyName("body")]
2121
public dynamic? Body { get; set; }
2222
[JsonPropertyName("headers")]
23-
public List<MockResponseHeader>? Headers { get; set; }
23+
public Dictionary<string, string>? Headers { get; set; }
2424
}
2525

2626
public class GraphBatchResponsePayloadResponseBody

dev-proxy-abstractions/ProxyUtils.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,8 @@ public static void MergeHeaders(IList<MockResponseHeader> allHeaders, IList<Mock
309309
var existingHeader = allHeaders.FirstOrDefault(h => h.Name.Equals(header.Name, StringComparison.OrdinalIgnoreCase));
310310
if (existingHeader is not null)
311311
{
312-
if (header.Name.Equals("Access-Control-Expose-Headers", StringComparison.OrdinalIgnoreCase))
312+
if (header.Name.Equals("Access-Control-Expose-Headers", StringComparison.OrdinalIgnoreCase) ||
313+
header.Name.Equals("Access-Control-Allow-Headers", StringComparison.OrdinalIgnoreCase))
313314
{
314315
var existingValues = existingHeader.Value.Split(',').Select(v => v.Trim());
315316
var newValues = header.Value.Split(',').Select(v => v.Trim());

dev-proxy-plugins/MockResponses/GraphMockResponsePlugin.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ protected override async Task OnRequest(object? sender, ProxyRequestArgs e)
5858
{
5959
Id = request.Id,
6060
Status = (int)HttpStatusCode.BadGateway,
61-
Headers = headers.ToList(),
61+
Headers = headers.ToDictionary(h => h.Name, h => h.Value),
6262
Body = new GraphBatchResponsePayloadResponseBody
6363
{
6464
Error = new GraphBatchResponsePayloadResponseBodyError
@@ -82,11 +82,7 @@ protected override async Task OnRequest(object? sender, ProxyRequestArgs e)
8282

8383
if (mockResponse.Response?.Headers is not null)
8484
{
85-
// add all the mocked headers into the response we want
86-
foreach (var header in mockResponse.Response.Headers)
87-
{
88-
headers.Add(header);
89-
}
85+
ProxyUtils.MergeHeaders(headers, mockResponse.Response.Headers);
9086
}
9187

9288
// default the content type to application/json unless set in the mock response
@@ -127,7 +123,7 @@ protected override async Task OnRequest(object? sender, ProxyRequestArgs e)
127123
{
128124
Id = request.Id,
129125
Status = (int)statusCode,
130-
Headers = headers.ToList(),
126+
Headers = headers.ToDictionary(h => h.Name, h => h.Value),
131127
Body = body
132128
};
133129

@@ -144,7 +140,9 @@ protected override async Task OnRequest(object? sender, ProxyRequestArgs e)
144140
{
145141
Responses = responses.ToArray()
146142
};
147-
e.Session.GenericResponse(JsonSerializer.Serialize(batchResponse), HttpStatusCode.OK, batchHeaders.Select(h => new HttpHeader(h.Name, h.Value)));
143+
var batchResponseString = JsonSerializer.Serialize(batchResponse);
144+
ProcessMockResponse(ref batchResponseString, batchHeaders, e, null);
145+
e.Session.GenericResponse(batchResponseString ?? string.Empty, HttpStatusCode.OK, batchHeaders.Select(h => new HttpHeader(h.Name, h.Value)));
148146
_logger?.LogRequest([$"200 {e.Session.HttpClient.Request.RequestUri}"], MessageType.Mocked, new LoggingContext(e.Session));
149147
e.ResponseState.HasBeenSet = true;
150148
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System.Text;
5+
using System.Text.Json;
6+
using System.Text.Json.Serialization;
7+
using System.Text.RegularExpressions;
8+
using System.Web;
9+
using Microsoft.DevProxy.Abstractions;
10+
11+
namespace Microsoft.DevProxy.Plugins.MockResponses;
12+
13+
class IdToken {
14+
[JsonPropertyName("aud")]
15+
public string? Aud { get; set; }
16+
[JsonPropertyName("iss")]
17+
public string? Iss { get; set; }
18+
[JsonPropertyName("iat")]
19+
public int? Iat { get; set; }
20+
[JsonPropertyName("nbf")]
21+
public int? Nbf { get; set; }
22+
[JsonPropertyName("exp")]
23+
public int? Exp { get; set; }
24+
[JsonPropertyName("name")]
25+
public string? Name { get; set; }
26+
[JsonPropertyName("nonce")]
27+
public string? Nonce { get; set; }
28+
[JsonPropertyName("oid")]
29+
public string? Oid { get; set; }
30+
[JsonPropertyName("preferred_username")]
31+
public string? PreferredUsername { get; set; }
32+
[JsonPropertyName("rh")]
33+
public string? Rh { get; set; }
34+
[JsonPropertyName("sub")]
35+
public string? Sub { get; set; }
36+
[JsonPropertyName("tid")]
37+
public string? Tid { get; set; }
38+
[JsonPropertyName("uti")]
39+
public string? Uti { get; set; }
40+
[JsonPropertyName("ver")]
41+
public string? Ver { get; set; }
42+
}
43+
44+
public class M365MockResponsePlugin : GraphMockResponsePlugin
45+
{
46+
private string? lastNonce;
47+
public override string Name => nameof(M365MockResponsePlugin);
48+
49+
protected override void ProcessMockResponse(ref byte[] body, IList<MockResponseHeader> headers, ProxyRequestArgs e, MockResponse? matchingResponse)
50+
{
51+
base.ProcessMockResponse(ref body, headers, e, matchingResponse);
52+
53+
var bodyString = Encoding.UTF8.GetString(body);
54+
var changed = false;
55+
56+
StoreLastNonce(e);
57+
UpdateMsalState(ref bodyString, e, ref changed);
58+
UpdateIdToken(ref bodyString, e, ref changed);
59+
60+
if (changed)
61+
{
62+
body = Encoding.UTF8.GetBytes(bodyString);
63+
}
64+
}
65+
66+
private void StoreLastNonce(ProxyRequestArgs e)
67+
{
68+
if (e.Session.HttpClient.Request.RequestUri.Query.Contains("nonce="))
69+
{
70+
var queryString = HttpUtility.ParseQueryString(e.Session.HttpClient.Request.RequestUri.Query);
71+
lastNonce = queryString["nonce"];
72+
}
73+
}
74+
75+
private void UpdateIdToken(ref string body, ProxyRequestArgs e, ref bool changed)
76+
{
77+
if (!body.Contains("id_token\":\"@dynamic") ||
78+
string.IsNullOrEmpty(lastNonce))
79+
{
80+
return;
81+
}
82+
83+
var idTokenRegex = new Regex("id_token\":\"([^\"]+)\"");
84+
85+
var idToken = idTokenRegex.Match(body).Groups[1].Value;
86+
idToken = idToken.Replace("@dynamic.", "");
87+
var tokenChunks = idToken.Split('.');
88+
// base64 decode the second chunk from the array
89+
// before decoding, we need to pad the base64 to a multiple of 4
90+
// or Convert.FromBase64String will throw an exception
91+
var decodedToken = Encoding.UTF8.GetString(Convert.FromBase64String(PadBase64(tokenChunks[1])));
92+
var token = JsonSerializer.Deserialize<IdToken>(decodedToken);
93+
if (token is null)
94+
{
95+
return;
96+
}
97+
98+
token.Nonce = lastNonce;
99+
100+
tokenChunks[1] = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(token)));
101+
body = idTokenRegex.Replace(body, $"id_token\":\"{string.Join('.', tokenChunks)}\"");
102+
changed = true;
103+
}
104+
105+
private string PadBase64(string base64)
106+
{
107+
var padding = new string('=', (4 - base64.Length % 4) % 4);
108+
return base64 + padding;
109+
}
110+
111+
private void UpdateMsalState(ref string body, ProxyRequestArgs e, ref bool changed)
112+
{
113+
if (!body.Contains("state=@dynamic") ||
114+
!e.Session.HttpClient.Request.RequestUri.Query.Contains("state="))
115+
{
116+
return;
117+
}
118+
119+
var queryString = HttpUtility.ParseQueryString(e.Session.HttpClient.Request.RequestUri.Query);
120+
var msalState = queryString["state"];
121+
body = body.Replace("state=@dynamic", $"state={msalState}");
122+
changed = true;
123+
}
124+
}

dev-proxy-plugins/MockResponses/MockResponsePlugin.cs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using Titanium.Web.Proxy.Http;
1313
using Titanium.Web.Proxy.Models;
1414
using Microsoft.DevProxy.Plugins.Behavior;
15+
using System.Text;
1516

1617
namespace Microsoft.DevProxy.Plugins.MockResponses;
1718

@@ -115,12 +116,12 @@ protected virtual Task OnRequest(object? sender, ProxyRequestArgs e)
115116
var matchingResponse = GetMatchingMockResponse(request);
116117
if (matchingResponse is not null)
117118
{
118-
ProcessMockResponse(e, matchingResponse);
119+
ProcessMockResponseInternal(e, matchingResponse);
119120
state.HasBeenSet = true;
120121
}
121122
else if (_configuration.BlockUnmockedRequests)
122123
{
123-
ProcessMockResponse(e, new MockResponse
124+
ProcessMockResponseInternal(e, new MockResponse
124125
{
125126
Request = new()
126127
{
@@ -202,7 +203,23 @@ private bool IsNthRequest(MockResponse mockResponse)
202203
return mockResponse.Request.Nth == nth;
203204
}
204205

205-
private void ProcessMockResponse(ProxyRequestArgs e, MockResponse matchingResponse)
206+
protected virtual void ProcessMockResponse(ref byte[] body, IList<MockResponseHeader> headers, ProxyRequestArgs e, MockResponse? matchingResponse)
207+
{
208+
}
209+
210+
protected virtual void ProcessMockResponse(ref string? body, IList<MockResponseHeader> headers, ProxyRequestArgs e, MockResponse? matchingResponse)
211+
{
212+
if (string.IsNullOrEmpty(body))
213+
{
214+
return;
215+
}
216+
217+
var bytes = Encoding.UTF8.GetBytes(body);
218+
ProcessMockResponse(ref bytes, headers, e, matchingResponse);
219+
body = Encoding.UTF8.GetString(bytes);
220+
}
221+
222+
private void ProcessMockResponseInternal(ProxyRequestArgs e, MockResponse matchingResponse)
206223
{
207224
string? body = null;
208225
string requestId = Guid.NewGuid().ToString();
@@ -216,10 +233,7 @@ private void ProcessMockResponse(ProxyRequestArgs e, MockResponse matchingRespon
216233

217234
if (matchingResponse.Response?.Headers is not null)
218235
{
219-
foreach (var header in matchingResponse.Response.Headers)
220-
{
221-
headers.Add(header);
222-
}
236+
ProxyUtils.MergeHeaders(headers, matchingResponse.Response.Headers);
223237
}
224238

225239
// default the content type to application/json unless set in the mock response
@@ -254,6 +268,7 @@ private void ProcessMockResponse(ProxyRequestArgs e, MockResponse matchingRespon
254268
else
255269
{
256270
var bodyBytes = File.ReadAllBytes(filePath);
271+
ProcessMockResponse(ref bodyBytes, headers, e, matchingResponse);
257272
e.Session.GenericResponse(bodyBytes, statusCode, headers.Select(h => new HttpHeader(h.Name, h.Value)));
258273
_logger?.LogRequest([$"{matchingResponse.Response.StatusCode ?? 200} {matchingResponse.Request?.Url}"], MessageType.Mocked, new LoggingContext(e.Session));
259274
return;
@@ -264,6 +279,7 @@ private void ProcessMockResponse(ProxyRequestArgs e, MockResponse matchingRespon
264279
body = bodyString;
265280
}
266281
}
282+
ProcessMockResponse(ref body, headers, e, matchingResponse);
267283
e.Session.GenericResponse(body ?? string.Empty, statusCode, headers.Select(h => new HttpHeader(h.Name, h.Value)));
268284

269285
_logger?.LogRequest([$"{matchingResponse.Response?.StatusCode ?? 200} {matchingResponse.Request?.Url}"], MessageType.Mocked, new LoggingContext(e.Session));

dev-proxy-plugins/RandomErrors/GraphRandomErrorPlugin.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ private void FailBatch(ProxyRequestArgs e)
149149
var requestUrl = ProxyUtils.GetAbsoluteRequestUrlFromBatch(e.Session.HttpClient.Request.RequestUri, request.Url);
150150
var throttledRequests = e.GlobalData[RetryAfterPlugin.ThrottledRequestsKey] as List<ThrottlerInfo>;
151151
throttledRequests?.Add(new ThrottlerInfo(GraphUtils.BuildThrottleKey(requestUrl), ShouldThrottle, retryAfterDate));
152-
response.Headers = new List<MockResponseHeader> { new("Retry-After", retryAfterInSeconds.ToString()) };
152+
response.Headers = new Dictionary<string, string> { { "Retry-After", retryAfterInSeconds.ToString() } };
153153
}
154154

155155
responses.Add(response);

dev-proxy-plugins/RequestLogs/MockGeneratorPlugin.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ request.Context is null ||
136136
// assume body is binary
137137
try
138138
{
139-
var filename = $"response-{DateTime.Now:yyyyMMddHHmmss}.bin";
139+
var filename = $"response-{Guid.NewGuid()}.bin";
140140
_logger?.LogDebug("Reading response body as bytes...");
141141
var body = await session.GetResponseBody();
142142
_logger?.LogDebug($"Writing response body to {filename}...");

0 commit comments

Comments
 (0)