55using Microsoft . Graph . DeveloperProxy . Abstractions ;
66using System . Net ;
77using System . Text . Json ;
8- using System . Text . Json . Serialization ;
98using System . Text . RegularExpressions ;
109using Titanium . Web . Proxy . Http ;
1110using Titanium . Web . Proxy . Models ;
@@ -27,46 +26,14 @@ public class RateLimitConfiguration {
2726public class RateLimitingPlugin : BaseProxyPlugin {
2827 public override string Name => nameof ( RateLimitingPlugin ) ;
2928 private readonly RateLimitConfiguration _configuration = new ( ) ;
30- private readonly Dictionary < string , DateTime > _throttledRequests = new ( ) ;
3129 // initial values so that we know when we intercept the
3230 // first request and can set the initial values
3331 private int _resourcesRemaining = - 1 ;
3432 private DateTime _resetTime = DateTime . MinValue ;
3533
36- private bool ShouldForceThrottle ( ProxyRequestArgs e ) {
37- var r = e . Session . HttpClient . Request ;
38- string key = BuildThrottleKey ( r ) ;
39- if ( _throttledRequests . TryGetValue ( key , out DateTime retryAfterDate ) ) {
40- if ( retryAfterDate > DateTime . Now ) {
41- _logger ? . LogRequest ( new [ ] { $ "Calling { r . Url } again before waiting for the Retry-After period.", "Request will be throttled" } , MessageType . Failed , new LoggingContext ( e . Session ) ) ;
42- // update the retryAfterDate to extend the throttling window to ensure that brute forcing won't succeed.
43- _throttledRequests [ key ] = retryAfterDate . AddSeconds ( _configuration . RetryAfterSeconds ) ;
44- return true ;
45- }
46- else {
47- // clean up expired throttled request and ensure that this request is passed through.
48- _throttledRequests . Remove ( key ) ;
49- return false ;
50- }
51- }
52-
53- return false ;
54- }
55-
56- private void ForceThrottleResponse ( ProxyRequestArgs e ) => UpdateProxyResponse ( e , HttpStatusCode . TooManyRequests ) ;
57-
58- private bool ShouldThrottle ( ProxyRequestArgs e ) {
59- if ( _resourcesRemaining > 0 ) {
60- return false ;
61- }
62-
63- var r = e . Session . HttpClient . Request ;
64- string key = BuildThrottleKey ( r ) ;
65-
66- _logger ? . LogRequest ( new [ ] { $ "Exceeded resource limit when calling { r . Url } .", "Request will be throttled" } , MessageType . Failed , new LoggingContext ( e . Session ) ) ;
67- // update the retryAfterDate to extend the throttling window to ensure that brute forcing won't succeed.
68- _throttledRequests [ key ] = DateTime . Now . AddSeconds ( _configuration . RetryAfterSeconds ) ;
69- return true ;
34+ private ThrottlingInfo ShouldThrottle ( Request request , string throttlingKey ) {
35+ var throttleKeyForRequest = BuildThrottleKey ( request ) ;
36+ return new ThrottlingInfo ( throttleKeyForRequest == throttlingKey ? _configuration . RetryAfterSeconds : 0 , _configuration . HeaderRetryAfter ) ;
7037 }
7138
7239 private void ThrottleResponse ( ProxyRequestArgs e ) => UpdateProxyResponse ( e , HttpStatusCode . TooManyRequests ) ;
@@ -75,24 +42,31 @@ private void UpdateProxyResponse(ProxyHttpEventArgsBase e, HttpStatusCode errorS
7542 var headers = new List < HttpHeader > ( ) ;
7643 var body = string . Empty ;
7744 var request = e . Session . HttpClient . Request ;
45+ var response = e . Session . HttpClient . Response ;
7846
79- // override the response body and headers for the error response
80- if ( errorStatus != HttpStatusCode . OK &&
81- ProxyUtils . IsGraphRequest ( request ) ) {
82- string requestId = Guid . NewGuid ( ) . ToString ( ) ;
83- string requestDate = DateTime . Now . ToString ( ) ;
84- headers . AddRange ( ProxyUtils . BuildGraphResponseHeaders ( request , requestId , requestDate ) ) ;
85-
86- body = JsonSerializer . Serialize ( new GraphErrorResponseBody (
87- new GraphErrorResponseError {
88- Code = new Regex ( "([A-Z])" ) . Replace ( errorStatus . ToString ( ) , m => { return $ " { m . Groups [ 1 ] } "; } ) . Trim ( ) ,
89- Message = BuildApiErrorMessage ( request ) ,
90- InnerError = new GraphErrorResponseInnerError {
91- RequestId = requestId ,
92- Date = requestDate
93- }
94- } )
95- ) ;
47+ // resources exceeded
48+ if ( errorStatus == HttpStatusCode . TooManyRequests ) {
49+ if ( ProxyUtils . IsGraphRequest ( request ) ) {
50+ string requestId = Guid . NewGuid ( ) . ToString ( ) ;
51+ string requestDate = DateTime . Now . ToString ( ) ;
52+ headers . AddRange ( ProxyUtils . BuildGraphResponseHeaders ( request , requestId , requestDate ) ) ;
53+
54+ body = JsonSerializer . Serialize ( new GraphErrorResponseBody (
55+ new GraphErrorResponseError {
56+ Code = new Regex ( "([A-Z])" ) . Replace ( errorStatus . ToString ( ) , m => { return $ " { m . Groups [ 1 ] } "; } ) . Trim ( ) ,
57+ Message = BuildApiErrorMessage ( request ) ,
58+ InnerError = new GraphErrorResponseInnerError {
59+ RequestId = requestId ,
60+ Date = requestDate
61+ }
62+ } )
63+ ) ;
64+ }
65+
66+ headers . Add ( new HttpHeader ( _configuration . HeaderRetryAfter , _configuration . RetryAfterSeconds . ToString ( ) ) ) ;
67+
68+ e . Session . GenericResponse ( body ?? string . Empty , errorStatus , headers ) ;
69+ return ;
9670 }
9771
9872 // add rate limiting headers if reached the threshold percentage
@@ -102,24 +76,51 @@ private void UpdateProxyResponse(ProxyHttpEventArgsBase e, HttpStatusCode errorS
10276 new HttpHeader ( _configuration . HeaderRemaining , _resourcesRemaining . ToString ( ) ) ,
10377 new HttpHeader ( _configuration . HeaderReset , ( _resetTime - DateTime . Now ) . TotalSeconds . ToString ( "N0" ) ) // drop decimals
10478 } ) ;
105- }
10679
107- // send an error response if we are (forced) throttling
108- if ( errorStatus == HttpStatusCode . TooManyRequests ) {
109- headers . Add ( new HttpHeader ( _configuration . HeaderRetryAfter , _configuration . RetryAfterSeconds . ToString ( ) ) ) ;
80+ // make rate limiting information available for CORS requests
81+ if ( request . Headers . FirstOrDefault ( ( h ) => h . Name . Equals ( "Origin" , StringComparison . OrdinalIgnoreCase ) ) is not null ) {
82+ if ( ! response . Headers . HeaderExists ( "Access-Control-Allow-Origin" ) ) {
83+ headers . Add ( new HttpHeader ( "Access-Control-Allow-Origin" , "*" ) ) ;
84+ }
85+ var exposeHeadersHeader = response . Headers . FirstOrDefault ( ( h ) => h . Name . Equals ( "Access-Control-Expose-Headers" , StringComparison . OrdinalIgnoreCase ) ) ;
86+ var headerValue = "" ;
87+ if ( exposeHeadersHeader is null ) {
88+ headerValue = $ "{ _configuration . HeaderLimit } , { _configuration . HeaderRemaining } , { _configuration . HeaderReset } , { _configuration . HeaderRetryAfter } ";
89+ }
90+ else {
91+ headerValue = exposeHeadersHeader . Value ;
92+ if ( ! headerValue . Contains ( _configuration . HeaderLimit ) ) {
93+ headerValue += $ ", { _configuration . HeaderLimit } ";
94+ }
95+ if ( ! headerValue . Contains ( _configuration . HeaderRemaining ) ) {
96+ headerValue += $ ", { _configuration . HeaderRemaining } ";
97+ }
98+ if ( ! headerValue . Contains ( _configuration . HeaderReset ) ) {
99+ headerValue += $ ", { _configuration . HeaderReset } ";
100+ }
101+ if ( ! headerValue . Contains ( _configuration . HeaderRetryAfter ) ) {
102+ headerValue += $ ", { _configuration . HeaderRetryAfter } ";
103+ }
104+ response . Headers . RemoveHeader ( "Access-Control-Expose-Headers" ) ;
105+ }
110106
111- e . Session . GenericResponse ( body ?? string . Empty , errorStatus , headers ) ;
112- return ;
107+ headers . Add ( new HttpHeader ( "Access-Control-Expose-Headers" , headerValue ) ) ;
108+ }
113109 }
114110
115- if ( errorStatus == HttpStatusCode . OK ) {
116- // add headers to the original API response
117- e . Session . HttpClient . Response . Headers . AddHeaders ( headers ) ;
118- }
111+ // add headers to the original API response
112+ e . Session . HttpClient . Response . Headers . AddHeaders ( headers ) ;
119113 }
120114 private static string BuildApiErrorMessage ( Request r ) => $ "Some error was generated by the proxy. { ( ProxyUtils . IsGraphRequest ( r ) ? ProxyUtils . IsSdkRequest ( r ) ? "" : String . Join ( ' ' , MessageUtils . BuildUseSdkForErrorsMessage ( r ) ) : "" ) } ";
121115
122- private string BuildThrottleKey ( Request r ) => $ "{ r . Method } -{ r . Url } ";
116+ private string BuildThrottleKey ( Request r ) {
117+ if ( ProxyUtils . IsGraphRequest ( r ) ) {
118+ return GraphUtils . BuildThrottleKey ( r ) ;
119+ }
120+ else {
121+ return r . RequestUri . Host ;
122+ }
123+ }
123124
124125 public override void Register ( IPluginEvents pluginEvents ,
125126 IProxyContext context ,
@@ -134,8 +135,6 @@ public override void Register(IPluginEvents pluginEvents,
134135
135136 // add rate limiting headers to the response from the API
136137 private async Task OnResponse ( object ? sender , ProxyResponseArgs e ) {
137- var session = e . Session ;
138- var state = e . ResponseState ;
139138 if ( _urlsToWatch is null ||
140139 ! e . HasRequestUrlMatch ( _urlsToWatch ) ) {
141140 return ;
@@ -169,44 +168,18 @@ _urlsToWatch is null ||
169168
170169 // subtract the cost of the request
171170 _resourcesRemaining -= _configuration . CostPerRequest ;
172- // avoid communicating negative values
173171 if ( _resourcesRemaining < 0 ) {
174- _resourcesRemaining = 0 ;
175- }
172+ var request = e . Session . HttpClient . Request ;
173+
174+ _logger ? . LogRequest ( new [ ] { $ "Exceeded resource limit when calling { request . Url } .", "Request will be throttled" } , MessageType . Failed , new LoggingContext ( e . Session ) ) ;
175+ e . ThrottledRequests . Add ( new ThrottlerInfo (
176+ BuildThrottleKey ( request ) ,
177+ ShouldThrottle ,
178+ DateTime . Now . AddSeconds ( _configuration . RetryAfterSeconds )
179+ ) ) ;
176180
177- if ( ShouldForceThrottle ( e ) ) {
178- ForceThrottleResponse ( e ) ;
179- state . HasBeenSet = true ;
180- }
181- else if ( ShouldThrottle ( e ) ) {
182181 ThrottleResponse ( e ) ;
183182 state . HasBeenSet = true ;
184183 }
185184 }
186185}
187-
188-
189- internal class GraphErrorResponseBody {
190- [ JsonPropertyName ( "error" ) ]
191- public GraphErrorResponseError Error { get ; set ; }
192-
193- public GraphErrorResponseBody ( GraphErrorResponseError error ) {
194- Error = error ;
195- }
196- }
197-
198- internal class GraphErrorResponseError {
199- [ JsonPropertyName ( "code" ) ]
200- public string Code { get ; set ; } = string . Empty ;
201- [ JsonPropertyName ( "message" ) ]
202- public string Message { get ; set ; } = string . Empty ;
203- [ JsonPropertyName ( "innerError" ) ]
204- public GraphErrorResponseInnerError ? InnerError { get ; set ; }
205- }
206-
207- internal class GraphErrorResponseInnerError {
208- [ JsonPropertyName ( "request-id" ) ]
209- public string RequestId { get ; set ; } = string . Empty ;
210- [ JsonPropertyName ( "date" ) ]
211- public string Date { get ; set ; } = string . Empty ;
212- }
0 commit comments