1919using System . Security . Claims ;
2020using System . Text . Json . Serialization ;
2121using System . Text . RegularExpressions ;
22+ using System . Web ;
2223using Titanium . Web . Proxy . EventArguments ;
2324using Titanium . Web . Proxy . Http ;
2425using Titanium . Web . Proxy . Models ;
@@ -246,6 +247,7 @@ private async Task SetupOpenIdConnectConfigurationAsync()
246247 } ) ;
247248
248249 var requestUrlWithoutQuery = request . RequestUri . GetLeftPart ( UriPartial . Path ) ;
250+ var requestQuery = request . RequestUri . Query ;
249251 var parameters = new Dictionary < string , string > ( ) ;
250252 var action = Configuration . Actions . FirstOrDefault ( action =>
251253 {
@@ -256,33 +258,94 @@ private async Task SetupOpenIdConnectConfigurationAsync()
256258
257259 var absoluteActionUrl = ( Configuration . BaseUrl + action . Url ) . Replace ( "//" , "/" , 8 ) ;
258260
259- if ( absoluteActionUrl == requestUrlWithoutQuery )
261+ // split action URL into path and query parts
262+ string actionPath ;
263+ string ? actionQuery = null ;
264+ var queryIndex = absoluteActionUrl . IndexOf ( '?' , StringComparison . OrdinalIgnoreCase ) ;
265+ if ( queryIndex >= 0 )
260266 {
261- return true ;
267+ actionPath = absoluteActionUrl [ ..queryIndex ] ;
268+ actionQuery = absoluteActionUrl [ queryIndex ..] ;
269+ }
270+ else
271+ {
272+ actionPath = absoluteActionUrl ;
262273 }
263274
264- // check if the action contains parameters
265- // if it doesn't, it's not a match for the current request for sure
266- if ( ! absoluteActionUrl . Contains ( '{' , StringComparison . OrdinalIgnoreCase ) )
275+ // match the path part
276+ var pathMatched = false ;
277+ if ( actionPath == requestUrlWithoutQuery )
267278 {
268- return false ;
279+ pathMatched = true ;
280+ }
281+ else if ( actionPath . Contains ( '{' , StringComparison . OrdinalIgnoreCase ) )
282+ {
283+ var pathRegex = Regex . Replace ( Regex . Escape ( actionPath ) . Replace ( "\\ {" , "{" , StringComparison . OrdinalIgnoreCase ) , "({[^}]+})" , parameterMatchEvaluator ) ;
284+ var pathMatch = Regex . Match ( requestUrlWithoutQuery , pathRegex ) ;
285+ if ( pathMatch . Success )
286+ {
287+ pathMatched = true ;
288+ foreach ( var groupName in pathMatch . Groups . Keys )
289+ {
290+ if ( groupName == "0" )
291+ {
292+ continue ;
293+ }
294+ parameters [ groupName ] = Uri . UnescapeDataString ( pathMatch . Groups [ groupName ] . Value ) ;
295+ }
296+ }
269297 }
270298
271- // convert parameters into named regex groups
272- var urlRegex = Regex . Replace ( Regex . Escape ( absoluteActionUrl ) . Replace ( "\\ {" , "{" , StringComparison . OrdinalIgnoreCase ) , "({[^}]+})" , parameterMatchEvaluator ) ;
273- var match = Regex . Match ( requestUrlWithoutQuery , urlRegex ) ;
274- if ( ! match . Success )
299+ if ( ! pathMatched )
275300 {
276301 return false ;
277302 }
278303
279- foreach ( var groupName in match . Groups . Keys )
304+ // if the action has no query string definition, it's a match
305+ if ( string . IsNullOrEmpty ( actionQuery ) )
306+ {
307+ return true ;
308+ }
309+
310+ // match query string parameters individually (order-independent)
311+ var actionQueryParams = HttpUtility . ParseQueryString ( actionQuery . TrimStart ( '?' ) ) ;
312+ var requestQueryParams = HttpUtility . ParseQueryString ( requestQuery . TrimStart ( '?' ) ) ;
313+
314+ foreach ( string ? key in actionQueryParams . AllKeys )
280315 {
281- if ( groupName == "0" )
316+ if ( key is null )
282317 {
283318 continue ;
284319 }
285- parameters . Add ( groupName , Uri . UnescapeDataString ( match . Groups [ groupName ] . Value ) ) ;
320+
321+ // a key can be declared multiple times in the action (e.g. ?id={id1}&id={id2})
322+ // and supplied multiple times in the request (e.g. ?id=1&id=2); pair them by position
323+ var actionValues = actionQueryParams . GetValues ( key ) ?? [ ] ;
324+ var requestValues = requestQueryParams . GetValues ( key ) ;
325+
326+ if ( requestValues is null )
327+ {
328+ return false ;
329+ }
330+
331+ for ( var i = 0 ; i < actionValues . Length ; i ++ )
332+ {
333+ // check if the declared value is a parameter pattern like {param}
334+ var paramMatch = Regex . Match ( actionValues [ i ] , "^{([^}]+)}$" ) ;
335+ if ( paramMatch . Success )
336+ {
337+ // bind the i-th declared placeholder to the i-th supplied value
338+ if ( i < requestValues . Length )
339+ {
340+ var paramName = paramMatch . Groups [ 1 ] . Value . Replace ( '-' , '_' ) ;
341+ parameters [ paramName ] = requestValues [ i ] ?? string . Empty ;
342+ }
343+ }
344+ else if ( ! requestValues . Contains ( actionValues [ i ] , StringComparer . Ordinal ) )
345+ {
346+ return false ;
347+ }
348+ }
286349 }
287350 return true ;
288351 } ) ;
@@ -541,8 +604,8 @@ private void GetOne(SessionEventArgs e, CrudApiAction action, IDictionary<string
541604 {
542605 try
543606 {
544- var item = _data ? . SelectToken ( ReplaceParams ( action . Query , parameters ) ) ;
545- if ( item is null )
607+ if ( ! TryReplaceParams ( action . Query , parameters , out var query ) ||
608+ _data ? . SelectToken ( query ) is not JToken item )
546609 {
547610 SendNotFoundResponse ( e ) ;
548611 Logger . LogRequest ( $ "404 { action . Url } ", MessageType . Mocked , new LoggingContext ( e ) ) ;
@@ -563,7 +626,15 @@ private void GetMany(SessionEventArgs e, CrudApiAction action, IDictionary<strin
563626 {
564627 try
565628 {
566- var items = ( _data ? . SelectTokens ( ReplaceParams ( action . Query , parameters ) ) ) ?? [ ] ;
629+ // an action may reference more indexed values than were supplied
630+ // (e.g. {id2} with a single id); treat that as no matches
631+ if ( ! TryReplaceParams ( action . Query , parameters , out var query ) )
632+ {
633+ SendJsonResponse ( "[]" , HttpStatusCode . OK , e ) ;
634+ Logger . LogRequest ( $ "200 { action . Url } ", MessageType . Mocked , new LoggingContext ( e ) ) ;
635+ return ;
636+ }
637+ var items = _data ? . SelectTokens ( query ) ?? [ ] ;
567638 SendJsonResponse ( JsonConvert . SerializeObject ( items , Formatting . Indented ) , HttpStatusCode . OK , e ) ;
568639 Logger . LogRequest ( $ "200 { action . Url } ", MessageType . Mocked , new LoggingContext ( e ) ) ;
569640 }
@@ -594,8 +665,8 @@ private void Merge(SessionEventArgs e, CrudApiAction action, IDictionary<string,
594665 {
595666 try
596667 {
597- var item = _data ? . SelectToken ( ReplaceParams ( action . Query , parameters ) ) ;
598- if ( item is null )
668+ if ( ! TryReplaceParams ( action . Query , parameters , out var query ) ||
669+ _data ? . SelectToken ( query ) is not JToken item )
599670 {
600671 SendNotFoundResponse ( e ) ;
601672 Logger . LogRequest ( $ "404 { action . Url } ", MessageType . Mocked , new LoggingContext ( e ) ) ;
@@ -617,8 +688,8 @@ private void Update(SessionEventArgs e, CrudApiAction action, IDictionary<string
617688 {
618689 try
619690 {
620- var item = _data ? . SelectToken ( ReplaceParams ( action . Query , parameters ) ) ;
621- if ( item is null )
691+ if ( ! TryReplaceParams ( action . Query , parameters , out var query ) ||
692+ _data ? . SelectToken ( query ) is not JToken item )
622693 {
623694 SendNotFoundResponse ( e ) ;
624695 Logger . LogRequest ( $ "404 { action . Url } ", MessageType . Mocked , new LoggingContext ( e ) ) ;
@@ -640,8 +711,8 @@ private void Delete(SessionEventArgs e, CrudApiAction action, IDictionary<string
640711 {
641712 try
642713 {
643- var item = _data ? . SelectToken ( ReplaceParams ( action . Query , parameters ) ) ;
644- if ( item is null )
714+ if ( ! TryReplaceParams ( action . Query , parameters , out var query ) ||
715+ _data ? . SelectToken ( query ) is not JToken item )
645716 {
646717 SendNotFoundResponse ( e ) ;
647718 Logger . LogRequest ( $ "404 { action . Url } ", MessageType . Mocked , new LoggingContext ( e ) ) ;
@@ -676,17 +747,19 @@ private static bool HasPermission(string permission, string permissionString)
676747 return permissions . Contains ( permission , StringComparer . OrdinalIgnoreCase ) ;
677748 }
678749
679- private static string ReplaceParams ( string query , IDictionary < string , string > parameters )
750+ private static bool TryReplaceParams ( string query , IDictionary < string , string > parameters , out string result )
680751 {
681- var result = Regex . Replace ( query , "{([^}]+)}" , new MatchEvaluator ( m =>
752+ result = Regex . Replace ( query , "{([^}]+)}" , new MatchEvaluator ( m =>
682753 {
683754 return $ "{{{m.Groups[1].Value.Replace('-', '_')}}}";
684755 } ) ) ;
685756 foreach ( var param in parameters )
686757 {
687758 result = result . Replace ( $ "{{{param.Key}}}", param . Value , StringComparison . OrdinalIgnoreCase ) ;
688759 }
689- return result ;
760+ // report unresolved placeholders (e.g. an action referencing {id2} when only one
761+ // value was supplied) so callers can treat it as no match instead of failing
762+ return ! Regex . IsMatch ( result , "{[^}]+}" ) ;
690763 }
691764
692765 protected override void Dispose ( bool disposing )
0 commit comments