Skip to content

Commit 1e417f8

Browse files
Extends Proxy to support other URLs. Closes #54 (#87)
1 parent 9c3dbd6 commit 1e417f8

File tree

8 files changed

+99
-68
lines changed

8 files changed

+99
-68
lines changed

msgraph-developer-proxy/ChaosEngine.cs

Lines changed: 74 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@
22
// Licensed under the MIT License.
33

44
using System.Net;
5-
using System.Security.Cryptography.X509Certificates;
65
using System.Text.Json;
76
using System.Text.RegularExpressions;
87
using Titanium.Web.Proxy;
98
using Titanium.Web.Proxy.EventArguments;
109
using Titanium.Web.Proxy.Helpers;
1110
using Titanium.Web.Proxy.Http;
1211
using Titanium.Web.Proxy.Models;
13-
using Titanium.Web.Proxy.Network;
1412

1513
namespace Microsoft.Graph.DeveloperProxy {
1614
internal enum FailMode {
@@ -79,6 +77,11 @@ public class ChaosEngine {
7977
private ExplicitProxyEndPoint? _explicitEndPoint;
8078
private readonly Dictionary<string, DateTime> _throttledRequests;
8179
private readonly ConsoleColor _color;
80+
// lists of URLs to watch, used for intercepting requests
81+
private List<Regex> urlsToWatch = new List<Regex>();
82+
// lists of hosts to watch extracted from urlsToWatch,
83+
// used for deciding which URLs to decrypt for further inspection
84+
private List<Regex> hostsToWatch = new List<Regex>();
8285

8386
public ChaosEngine(ProxyConfiguration config) {
8487
_config = config ?? throw new ArgumentNullException(nameof(config));
@@ -96,7 +99,13 @@ public ChaosEngine(ProxyConfiguration config) {
9699
}
97100

98101
public async Task Run(CancellationToken? cancellationToken) {
99-
Console.WriteLine($"Configuring proxy for cloud {_config.Cloud} - {_config.HostName}");
102+
if (!_config.UrlsToWatch.Any()) {
103+
Console.WriteLine("No URLs to watch configured. Please add URLs to watch in the appsettings.json config file.");
104+
return;
105+
}
106+
107+
LoadUrlsToWatch();
108+
100109
_proxyServer = new ProxyServer();
101110

102111
_proxyServer.CertificateManager.CertificateStorage = new CertificateDiskCache();
@@ -146,6 +155,36 @@ public async Task Run(CancellationToken? cancellationToken) {
146155
while (_proxyServer.ProxyRunning) { Thread.Sleep(10); }
147156
}
148157

158+
// Convert strings from config to regexes.
159+
// From the list of URLs, extract host names and convert them to regexes.
160+
// We need this because before we decrypt a request, we only have access
161+
// to the host name, not the full URL.
162+
private void LoadUrlsToWatch() {
163+
foreach (var urlToWatch in _config.UrlsToWatch) {
164+
// add the full URL
165+
var urlToWatchRegexString = Regex.Escape(urlToWatch).Replace("\\*", ".*");
166+
urlsToWatch.Add(new Regex(urlToWatchRegexString, RegexOptions.Compiled | RegexOptions.IgnoreCase));
167+
168+
// extract host from the URL
169+
var hostToWatch = "";
170+
if (urlToWatch.Contains("://")) {
171+
// if the URL contains a protocol, extract the host from the URL
172+
hostToWatch = urlToWatch.Split("://")[1].Substring(0, urlToWatch.Split("://")[1].IndexOf("/"));
173+
}
174+
else {
175+
// if the URL doesn't contain a protocol,
176+
// we assume the whole URL is a host name
177+
hostToWatch = urlToWatch;
178+
}
179+
180+
var hostToWatchRegexString = Regex.Escape(hostToWatch).Replace("\\*", ".*");
181+
// don't add the same host twice
182+
if (!hostsToWatch.Any(h => h.ToString() == hostToWatchRegexString)) {
183+
hostsToWatch.Add(new Regex(hostToWatchRegexString, RegexOptions.Compiled | RegexOptions.IgnoreCase));
184+
}
185+
}
186+
}
187+
149188
private void Console_CancelKeyPress(object? sender, ConsoleCancelEventArgs e) {
150189
StopProxy();
151190
}
@@ -207,10 +246,8 @@ private FailMode ShouldFail(Request r) {
207246
}
208247

209248
async Task OnBeforeTunnelConnectRequest(object sender, TunnelConnectSessionEventArgs e) {
210-
string hostname = e.HttpClient.Request.RequestUri.Host;
211-
212249
// Ensures that only the targeted Https domains are proxyied
213-
if (!hostname.Contains(_config.HostName)) {
250+
if (!ShouldDecryptRequest(e.HttpClient.Request.RequestUri.Host)) {
214251
e.DecryptSsl = false;
215252
}
216253
}
@@ -231,14 +268,14 @@ async Task OnRequest(object sender, SessionEventArgs e) {
231268
e.UserData = e.HttpClient.Request;
232269
}
233270

234-
// Chaos happens only for graph requests which are not OPTIONS
235-
if (method is not "OPTIONS" && e.HttpClient.Request.RequestUri.Host.Contains(_config.HostName)) {
236-
Console.WriteLine($"saw a graph request: {e.HttpClient.Request.Method} {e.HttpClient.Request.RequestUriString}");
237-
HandleGraphRequest(e);
271+
// Chaos happens only for requests which are not OPTIONS
272+
if (method is not "OPTIONS" && ShouldWatchRequest(e.HttpClient.Request.Url)) {
273+
Console.WriteLine($"saw a request: {e.HttpClient.Request.Method} {e.HttpClient.Request.Url}");
274+
HandleRequest(e);
238275
}
239276
}
240277

241-
private void HandleGraphRequest(SessionEventArgs e) {
278+
private void HandleRequest(SessionEventArgs e) {
242279
var responseComponents = ResponseComponents.Build();
243280
var matchingResponse = GetMatchingMockResponse(e.HttpClient.Request);
244281
if (matchingResponse is not null) {
@@ -254,12 +291,13 @@ private void HandleGraphRequest(SessionEventArgs e) {
254291
}
255292

256293
if (failMode == FailMode.PassThru && _config.FailureRate != 100) {
257-
Console.WriteLine($"\tPassed through {e.HttpClient.Request.RequestUri.AbsolutePath}");
294+
Console.WriteLine($"\tPassed through {e.HttpClient.Request.Url}");
258295
return;
259296
}
260297

261298
FailResponse(e, responseComponents, failMode);
262-
if (!IsSdkRequest(e.HttpClient.Request)) {
299+
if (IsGraphRequest(e.HttpClient.Request) &&
300+
!IsSdkRequest(e.HttpClient.Request)) {
263301
Console.ForegroundColor = ConsoleColor.Green;
264302
Console.Error.WriteLine($"\tTIP: {BuildUseSdkMessage(e.HttpClient.Request)}");
265303
Console.ForegroundColor = _color;
@@ -289,8 +327,14 @@ private static bool IsSdkRequest(Request request) {
289327
return request.Headers.HeaderExists("SdkVersion");
290328
}
291329

330+
private static bool IsGraphRequest(Request request) {
331+
return request.RequestUri.Host.Contains("graph", StringComparison.OrdinalIgnoreCase);
332+
}
333+
292334
private static bool WarnNoSelect(Request request) {
293-
return request.Method == "GET" && !request.Url.Contains("$select", StringComparison.OrdinalIgnoreCase);
335+
return IsGraphRequest(request) &&
336+
request.Method == "GET" &&
337+
!request.Url.Contains("$select", StringComparison.OrdinalIgnoreCase);
294338
}
295339

296340
private static string GetMoveToSdkUrl(Request request) {
@@ -343,28 +387,36 @@ private static void ProcessMockResponse(SessionEventArgs e, ResponseComponents r
343387
}
344388
}
345389

390+
private bool ShouldDecryptRequest(string hostName) {
391+
return hostsToWatch.Any(h => h.IsMatch(hostName));
392+
}
393+
394+
private bool ShouldWatchRequest(string requestUrl) {
395+
return urlsToWatch.Any(u => u.IsMatch(requestUrl));
396+
}
397+
346398
private ProxyMockResponse? GetMatchingMockResponse(Request request) {
347399
if (_config.NoMocks ||
348400
_config.Responses is null ||
349401
!_config.Responses.Any()) {
350402
return null;
351403
}
352404

353-
var mockResponse = _config.Responses.FirstOrDefault(r => {
354-
if (r.Method != request.Method) return false;
355-
if (r.Url == request.RequestUri.AbsolutePath) {
405+
var mockResponse = _config.Responses.FirstOrDefault(mockResponse => {
406+
if (mockResponse.Method != request.Method) return false;
407+
if (mockResponse.Url == request.Url) {
356408
return true;
357409
}
358410

359411
// check if the URL contains a wildcard
360412
// if it doesn't, it's not a match for the current request for sure
361-
if (!r.Url.Contains('*')) {
413+
if (!mockResponse.Url.Contains('*')) {
362414
return false;
363415
}
364416

365417
// turn mock URL with wildcard into a regex and match against the request URL
366-
var urlRegex = Regex.Escape(r.Url).Replace("\\*", ".*");
367-
return Regex.IsMatch(request.RequestUri.AbsolutePath, urlRegex);
418+
var mockResponseUrlRegex = Regex.Escape(mockResponse.Url).Replace("\\*", ".*");
419+
return Regex.IsMatch(request.Url, mockResponseUrlRegex);
368420
});
369421
return mockResponse;
370422
}
@@ -393,11 +445,11 @@ private void UpdateProxyResponse(SessionEventArgs e, ResponseComponents response
393445
})
394446
);
395447
}
396-
Console.WriteLine($"\t{(matchingResponse is not null ? "Mocked" : "Failed")} {e.HttpClient.Request.RequestUri.AbsolutePath} with {responseComponents.ErrorStatus}");
448+
Console.WriteLine($"\t{(matchingResponse is not null ? "Mocked" : "Failed")} {e.HttpClient.Request.Url} with {responseComponents.ErrorStatus}");
397449
e.GenericResponse(responseComponents.Body ?? string.Empty, responseComponents.ErrorStatus, responseComponents.Headers);
398450
}
399451

400-
private string BuildApiErrorMessage(Request r) => $"Some error was generated by the proxy. {(IsSdkRequest(r) ? "" : BuildUseSdkMessage(r))}";
452+
private string BuildApiErrorMessage(Request r) => $"Some error was generated by the proxy. {(IsGraphRequest(r) ? (IsSdkRequest(r) ? "" : BuildUseSdkMessage(r)) : "")}";
401453

402454
private string BuildThrottleKey(Request r) => $"{r.Method}-{r.Url}";
403455

msgraph-developer-proxy/Properties/launchSettings.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@
1010
"commandLineArgs": "--port 8080 --failure-rate 100 --no-mocks",
1111
"hotReloadEnabled": true
1212
},
13-
"All chaos with mock responses - China Cloud": {
14-
"commandName": "Project",
15-
"commandLineArgs": "--port 8080 --failure-rate 100 --no-mocks --cloud china",
16-
"hotReloadEnabled": true
17-
},
1813
"High chaos only 429 503 and no mocks": {
1914
"commandName": "Project",
2015
"commandLineArgs": "--port 8080 --failure-rate 95 -a 429 503 --no-mocks",

msgraph-developer-proxy/ProxyCommandHandler.cs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,21 @@
44
using Microsoft.Extensions.Configuration;
55
using System.CommandLine;
66
using System.CommandLine.Invocation;
7-
using System.Text.Json;
87

98
namespace Microsoft.Graph.DeveloperProxy {
109
public class ProxyCommandHandler : ICommandHandler {
1110
public Option<int> Port { get; set; }
1211
public Option<int> Rate { get; set; }
1312
public Option<bool> DisableMocks { get; set; }
14-
public Option<string> Cloud { get; set; }
1513
public Option<IEnumerable<int>> AllowedErrors { get; }
1614

17-
public ProxyCommandHandler(Option<int> port, Option<int> rate, Option<bool> disableMocks, Option<string> cloud, Option<IEnumerable<int>> allowedErrors) {
15+
public ProxyCommandHandler(Option<int> port, Option<int> rate, Option<bool> disableMocks, Option<IEnumerable<int>> allowedErrors) {
1816
Port = port ?? throw new ArgumentNullException(nameof(port));
1917
Rate = rate ?? throw new ArgumentNullException(nameof(rate));
2018
DisableMocks = disableMocks ?? throw new ArgumentNullException(nameof(disableMocks));
21-
Cloud = cloud ?? throw new ArgumentNullException(nameof(cloud));
2219
AllowedErrors = allowedErrors ?? throw new ArgumentNullException(nameof(allowedErrors));
2320
}
2421

25-
2622
public int Invoke(InvocationContext context) {
2723
return InvokeAsync(context).GetAwaiter().GetResult();
2824
}
@@ -31,15 +27,12 @@ public async Task<int> InvokeAsync(InvocationContext context) {
3127
int port = context.ParseResult.GetValueForOption(Port);
3228
int failureRate = context.ParseResult.GetValueForOption(Rate);
3329
bool disableMocks = context.ParseResult.GetValueForOption(DisableMocks);
34-
string? cloud = context.ParseResult.GetValueForOption(Cloud);
3530
IEnumerable<int> allowedErrors = context.ParseResult.GetValueForOption(AllowedErrors) ?? Enumerable.Empty<int>();
3631
CancellationToken? cancellationToken = (CancellationToken?)context.BindingContext.GetService(typeof(CancellationToken?));
3732
Configuration.Port = port;
3833
Configuration.FailureRate = failureRate;
3934
Configuration.NoMocks = disableMocks;
4035
Configuration.AllowedErrors = allowedErrors;
41-
if (cloud is not null)
42-
Configuration.Cloud = cloud;
4336

4437
var newReleaseInfo = await UpdateNotification.CheckForNewVersion();
4538
if (newReleaseInfo != null) {
@@ -56,7 +49,7 @@ public async Task<int> InvokeAsync(InvocationContext context) {
5649
return 0;
5750
}
5851
catch (Exception ex) {
59-
Console.Error.WriteLine("An error occured while running the Developer Proxy");
52+
Console.Error.WriteLine("An error occurred while running the Developer Proxy");
6053
Console.Error.WriteLine(ex.Message.ToString());
6154
Console.Error.WriteLine(ex.StackTrace?.ToString());
6255
var inner = ex.InnerException;

msgraph-developer-proxy/ProxyConfiguration.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,11 @@ public class ProxyConfiguration : IDisposable
1313
public int FailureRate { get; set; } = 50;
1414
[JsonPropertyName("noMocks")]
1515
public bool NoMocks { get; set; } = false;
16-
[JsonPropertyName("cloud")]
17-
public string Cloud { get; set; } = "global";
18-
[JsonPropertyName("cloudHosts")]
19-
public Dictionary<string, string> CloudHosts { get; set; } = new();
16+
[JsonPropertyName("urlsToWatch")]
17+
public string[] UrlsToWatch { get; set; } = Array.Empty<string>();
2018
[JsonPropertyName("allowedErrors")]
2119
public IEnumerable<int> AllowedErrors { get; set; } = Array.Empty<int>();
2220

23-
public string HostName => CloudHosts.ContainsKey(Cloud) ? CloudHosts[Cloud] : throw new ArgumentOutOfRangeException(nameof(Cloud), InvalidCloudMessage);
24-
25-
private string InvalidCloudMessage => $"The value provided for the cloud: {Cloud} is not valid, current valid values are: {string.Join(", ", CloudHosts.Keys.ToArray())}.";
26-
2721
[JsonPropertyName("responses")]
2822
public IEnumerable<ProxyMockResponse> Responses { get; set; } = Array.Empty<ProxyMockResponse>();
2923

msgraph-developer-proxy/ProxyHost.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,6 @@ public RootCommand GetRootCommand() {
2929
noMocksOptions.ArgumentHelpName = "no mocks";
3030
noMocksOptions.SetDefaultValue(false);
3131

32-
var cloudOption = new Option<string>("--cloud", "Set the target cloud to proxy requests for");
33-
cloudOption.AddAlias("-c");
34-
cloudOption.ArgumentHelpName = "cloud";
35-
cloudOption.SetDefaultValue("global");
36-
3732
var allowedErrorsOption = new Option<IEnumerable<int>>("--allowed-errors", "List of errors that the developer proxy may produce");
3833
allowedErrorsOption.AddAlias("-a");
3934
allowedErrorsOption.ArgumentHelpName = "allowed errors";
@@ -45,11 +40,10 @@ public RootCommand GetRootCommand() {
4540
portOption,
4641
rateOption,
4742
noMocksOptions,
48-
cloudOption,
4943
allowedErrorsOption
5044
};
5145
command.Description = "HTTP proxy to create random failures for calls to Microsoft Graph";
52-
command.Handler = new ProxyCommandHandler(portOption, rateOption, noMocksOptions, cloudOption, allowedErrorsOption);
46+
command.Handler = new ProxyCommandHandler(portOption, rateOption, noMocksOptions, allowedErrorsOption);
5347

5448
return command;
5549
}

msgraph-developer-proxy/UpdateNotification.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
using System;
5-
using System.Collections.Generic;
64
using System.Diagnostics;
7-
using System.Linq;
85
using System.Net.Http.Headers;
96
using System.Reflection;
10-
using System.Text;
117
using System.Text.Json;
128
using System.Text.Json.Serialization;
13-
using System.Threading.Tasks;
149

1510
namespace Microsoft.Graph.DeveloperProxy {
1611
internal class ReleaseInfo {
Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
{
2-
"cloudHosts": {
3-
"global": "graph.microsoft.com",
4-
"usGov": "graph.microsoft.us",
5-
"dod": "dod-graph.microsoft.us",
6-
"china": "microsoftgraph.chinacloudapi.cn"
7-
}
2+
"urlsToWatch": [
3+
"https://graph.microsoft.com/v1.0/*",
4+
"https://graph.microsoft.com/beta/*",
5+
"https://graph.microsoft.us/v1.0/*",
6+
"https://graph.microsoft.us/beta/*",
7+
"https://dod-graph.microsoft.us/v1.0/*",
8+
"https://dod-graph.microsoft.us/beta/*",
9+
"https://microsoftgraph.chinacloudapi.cn/v1.0/*",
10+
"https://microsoftgraph.chinacloudapi.cn/beta/*",
11+
"https://*.sharepoint.*/*_api/*",
12+
"https://*.sharepoint.*/*_vti_bin/*",
13+
"https://*.sharepoint-df.*/*_api/*",
14+
"https://*.sharepoint-df.*/*_vti_bin/*"
15+
]
816
}

msgraph-developer-proxy/responses.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"responses": [
33
{
4-
"url": "/v1.0/me",
4+
"url": "https://graph.microsoft.com/v1.0/me",
55
"responseBody": {
66
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
77
"businessPhones": [
@@ -20,13 +20,13 @@
2020
}
2121
},
2222
{
23-
"url": "/v1.0/me",
23+
"url": "https://graph.microsoft.com/v1.0/me",
2424
"method": "PATCH",
2525
"responseBody": {},
2626
"responseCode": 204
2727
},
2828
{
29-
"url": "/v1.0/users/48d31887-5fad-4d73-a9f5-3c356e68a038",
29+
"url": "https://graph.microsoft.com/v1.0/users/48d31887-5fad-4d73-a9f5-3c356e68a038",
3030
"responseBody": {
3131
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
3232
"businessPhones": [
@@ -45,18 +45,18 @@
4545
}
4646
},
4747
{
48-
"url": "/v1.0/me/photo",
48+
"url": "https://graph.microsoft.com/v1.0/me/photo",
4949
"responseCode": 404
5050
},
5151
{
52-
"url": "/v1.0/users/*/photo/$value",
52+
"url": "https://graph.microsoft.com/v1.0/users/*/photo/$value",
5353
"responseBody": "@picture.jpg",
5454
"responseHeaders": {
5555
"content-type": "image/jpeg"
5656
}
5757
},
5858
{
59-
"url": "/v1.0/users/*",
59+
"url": "https://graph.microsoft.com/v1.0/users/*",
6060
"responseBody": {
6161
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
6262
"businessPhones": [

0 commit comments

Comments
 (0)