22// Licensed under the MIT License.
33
44using System . Net ;
5- using System . Security . Cryptography . X509Certificates ;
65using System . Text . Json ;
76using System . Text . RegularExpressions ;
87using Titanium . Web . Proxy ;
98using Titanium . Web . Proxy . EventArguments ;
109using Titanium . Web . Proxy . Helpers ;
1110using Titanium . Web . Proxy . Http ;
1211using Titanium . Web . Proxy . Models ;
13- using Titanium . Web . Proxy . Network ;
1412
1513namespace 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 ( $ "\t Passed through { e . HttpClient . Request . RequestUri . AbsolutePath } ") ;
294+ Console . WriteLine ( $ "\t Passed 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 ( $ "\t TIP: { 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
0 commit comments