2
2
// Licensed under the MIT License.
3
3
4
4
using System . Net ;
5
- using System . Security . Cryptography . X509Certificates ;
6
5
using System . Text . Json ;
7
6
using System . Text . RegularExpressions ;
8
7
using Titanium . Web . Proxy ;
9
8
using Titanium . Web . Proxy . EventArguments ;
10
9
using Titanium . Web . Proxy . Helpers ;
11
10
using Titanium . Web . Proxy . Http ;
12
11
using Titanium . Web . Proxy . Models ;
13
- using Titanium . Web . Proxy . Network ;
14
12
15
13
namespace Microsoft . Graph . DeveloperProxy {
16
14
internal enum FailMode {
@@ -79,6 +77,11 @@ public class ChaosEngine {
79
77
private ExplicitProxyEndPoint ? _explicitEndPoint ;
80
78
private readonly Dictionary < string , DateTime > _throttledRequests ;
81
79
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 > ( ) ;
82
85
83
86
public ChaosEngine ( ProxyConfiguration config ) {
84
87
_config = config ?? throw new ArgumentNullException ( nameof ( config ) ) ;
@@ -96,7 +99,13 @@ public ChaosEngine(ProxyConfiguration config) {
96
99
}
97
100
98
101
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
+
100
109
_proxyServer = new ProxyServer ( ) ;
101
110
102
111
_proxyServer . CertificateManager . CertificateStorage = new CertificateDiskCache ( ) ;
@@ -146,6 +155,36 @@ public async Task Run(CancellationToken? cancellationToken) {
146
155
while ( _proxyServer . ProxyRunning ) { Thread . Sleep ( 10 ) ; }
147
156
}
148
157
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
+
149
188
private void Console_CancelKeyPress ( object ? sender , ConsoleCancelEventArgs e ) {
150
189
StopProxy ( ) ;
151
190
}
@@ -207,10 +246,8 @@ private FailMode ShouldFail(Request r) {
207
246
}
208
247
209
248
async Task OnBeforeTunnelConnectRequest ( object sender , TunnelConnectSessionEventArgs e ) {
210
- string hostname = e . HttpClient . Request . RequestUri . Host ;
211
-
212
249
// Ensures that only the targeted Https domains are proxyied
213
- if ( ! hostname . Contains ( _config . HostName ) ) {
250
+ if ( ! ShouldDecryptRequest ( e . HttpClient . Request . RequestUri . Host ) ) {
214
251
e . DecryptSsl = false ;
215
252
}
216
253
}
@@ -231,14 +268,14 @@ async Task OnRequest(object sender, SessionEventArgs e) {
231
268
e . UserData = e . HttpClient . Request ;
232
269
}
233
270
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 ) ;
238
275
}
239
276
}
240
277
241
- private void HandleGraphRequest ( SessionEventArgs e ) {
278
+ private void HandleRequest ( SessionEventArgs e ) {
242
279
var responseComponents = ResponseComponents . Build ( ) ;
243
280
var matchingResponse = GetMatchingMockResponse ( e . HttpClient . Request ) ;
244
281
if ( matchingResponse is not null ) {
@@ -254,12 +291,13 @@ private void HandleGraphRequest(SessionEventArgs e) {
254
291
}
255
292
256
293
if ( failMode == FailMode . PassThru && _config . FailureRate != 100 ) {
257
- Console . WriteLine ( $ "\t Passed through { e . HttpClient . Request . RequestUri . AbsolutePath } ") ;
294
+ Console . WriteLine ( $ "\t Passed through { e . HttpClient . Request . Url } ") ;
258
295
return ;
259
296
}
260
297
261
298
FailResponse ( e , responseComponents , failMode ) ;
262
- if ( ! IsSdkRequest ( e . HttpClient . Request ) ) {
299
+ if ( IsGraphRequest ( e . HttpClient . Request ) &&
300
+ ! IsSdkRequest ( e . HttpClient . Request ) ) {
263
301
Console . ForegroundColor = ConsoleColor . Green ;
264
302
Console . Error . WriteLine ( $ "\t TIP: { BuildUseSdkMessage ( e . HttpClient . Request ) } ") ;
265
303
Console . ForegroundColor = _color ;
@@ -289,8 +327,14 @@ private static bool IsSdkRequest(Request request) {
289
327
return request . Headers . HeaderExists ( "SdkVersion" ) ;
290
328
}
291
329
330
+ private static bool IsGraphRequest ( Request request ) {
331
+ return request . RequestUri . Host . Contains ( "graph" , StringComparison . OrdinalIgnoreCase ) ;
332
+ }
333
+
292
334
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 ) ;
294
338
}
295
339
296
340
private static string GetMoveToSdkUrl ( Request request ) {
@@ -343,28 +387,36 @@ private static void ProcessMockResponse(SessionEventArgs e, ResponseComponents r
343
387
}
344
388
}
345
389
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
+
346
398
private ProxyMockResponse ? GetMatchingMockResponse ( Request request ) {
347
399
if ( _config . NoMocks ||
348
400
_config . Responses is null ||
349
401
! _config . Responses . Any ( ) ) {
350
402
return null ;
351
403
}
352
404
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 ) {
356
408
return true ;
357
409
}
358
410
359
411
// check if the URL contains a wildcard
360
412
// 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 ( '*' ) ) {
362
414
return false ;
363
415
}
364
416
365
417
// 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 ) ;
368
420
} ) ;
369
421
return mockResponse ;
370
422
}
@@ -393,11 +445,11 @@ private void UpdateProxyResponse(SessionEventArgs e, ResponseComponents response
393
445
} )
394
446
) ;
395
447
}
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 } ") ;
397
449
e . GenericResponse ( responseComponents . Body ?? string . Empty , responseComponents . ErrorStatus , responseComponents . Headers ) ;
398
450
}
399
451
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 ) ) : "" ) } ";
401
453
402
454
private string BuildThrottleKey ( Request r ) => $ "{ r . Method } -{ r . Url } ";
403
455
0 commit comments