@@ -17,10 +17,12 @@ const localizeTimestamp =
1717const serviceName = 'customs' ;
1818
1919class CustomsClient {
20- constructor ( url , log , error , statsd ) {
20+ constructor ( url , log , error , statsd , rateLimit ) {
2121 this . log = log ;
2222 this . error = error ;
2323 this . statsd = statsd ;
24+ this . rateLimit = rateLimit ;
25+
2426 const customsHttpAgentConfig = config . get ( 'customsHttpAgent' ) ;
2527
2628 if ( url !== 'none' ) {
@@ -87,6 +89,14 @@ class CustomsClient {
8789 }
8890
8991 async check ( request , email , action ) {
92+ const checked = await this . checkV2 ( request , 'check' , action , {
93+ ip : request ?. app ?. clientAddress ,
94+ email,
95+ } ) ;
96+ if ( checked ) {
97+ return ;
98+ }
99+
90100 const result = await this . makeRequest ( '/check' , {
91101 ...this . sanitizePayload ( {
92102 ip : request . app . clientAddress ,
@@ -108,6 +118,14 @@ class CustomsClient {
108118 }
109119
110120 async checkAuthenticated ( request , uid , action ) {
121+ const checked = await this . checkV2 ( request , 'checkAuthenticated' , action , {
122+ ip : request ?. app ?. clientAddress ,
123+ uid,
124+ } ) ;
125+ if ( checked ) {
126+ return ;
127+ }
128+
111129 const result = await this . makeRequest ( '/checkAuthenticated' , {
112130 ...this . sanitizePayload ( {
113131 action,
@@ -121,6 +139,13 @@ class CustomsClient {
121139 }
122140
123141 async checkIpOnly ( request , action ) {
142+ const checked = await this . checkV2 ( request , 'checkIpOnly' , action , {
143+ ip : request ?. app ?. clientAddress ,
144+ } ) ;
145+ if ( checked ) {
146+ return ;
147+ }
148+
124149 const result = await this . makeRequest ( '/checkIpOnly' , {
125150 ...this . sanitizePayload ( {
126151 action,
@@ -257,6 +282,76 @@ class CustomsClient {
257282 } ) ;
258283 }
259284 }
285+
286+ // #region Customs V2
287+
288+ /**
289+ * Version 2 Customs Approach
290+ * =======================================================================================
291+ * This uses a library provided by libs and works directly with Redis to make rate limiting
292+ * decisions. The previous customs check to see if there is 'new' configuration for the
293+ * customs action being checked. If there is, we will call into this code instead of calling
294+ * the legacy customs service.
295+ */
296+ async checkV2 ( request , type , action , opts ) {
297+ // Short circuit if rate limit wasn't provided.
298+ if ( this . rateLimit == null ) {
299+ return false ;
300+ }
301+
302+ // Fallback to the legacy customs service approach, if v2 action isn't configured
303+ const actionConfigured = this . rateLimit . supportsAction ( action ) ;
304+ if ( ! actionConfigured ) {
305+ this . statsd ?. increment ( `${ serviceName } .check.v1` , [ `action:${ action } ` ] ) ;
306+ return false ;
307+ }
308+
309+ // Otherwise, call the new nx lib instead of the legacy service
310+ this . statsd ?. increment ( `${ serviceName } .check.v2` , [ `action:${ action } ` ] ) ;
311+ const result = await this . rateLimit . check ( action , opts ) ;
312+
313+ // If statsd was provided, record metrics
314+ this . statsd ?. increment ( `${ serviceName } .request.v2.${ type } ` , {
315+ action,
316+ block : result != null ,
317+ blockReason : result ?. reason || '' ,
318+ } ) ;
319+
320+
321+ // If no result, we exit. Check essentially passes.
322+ if ( result == null ) {
323+ return true ;
324+ }
325+
326+ // We use the rate limiter to allow X number unblock attempts per day. Once
327+ // unblock attempts have been exhausted, the user cannot request an unblock
328+ // code and must wait until the unblockEmail ban duration has expired. Similar
329+ // logic existed in the old customs server, but these sorts of decisions are
330+ // actually domain of the service using customs and not customs itself, so
331+ // this is the revised approach.
332+ let canUnblock = false ;
333+ const { email } = opts ;
334+ if ( email ) {
335+ const unblockResult = await this . rateLimit . check ( 'unblockEmail' , {
336+ email,
337+ } ) ;
338+ canUnblock = unblockResult == null ;
339+ }
340+
341+ request . emitMetricsEvent ( 'customs.blocked' ) ;
342+ const retryAfterLocalized = localizeTimestamp . format (
343+ Date . now ( ) + result . retryAfter * 1000 ,
344+ request . headers [ 'accept-language' ]
345+ ) ;
346+
347+ throw this . error . tooManyRequests (
348+ result . retryAfter ,
349+ retryAfterLocalized ,
350+ canUnblock
351+ ) ;
352+
353+ }
354+ // #endregion
260355}
261356
262357module . exports = CustomsClient ;
0 commit comments