-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(neon_http_client): Add AuthorizationThrottlingInterceptor
Signed-off-by: provokateurin <[email protected]>
- Loading branch information
1 parent
5471fae
commit 114487b
Showing
5 changed files
with
323 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
.../packages/neon_http_client/lib/src/interceptors/authorization_throttling_interceptor.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import 'package:http/http.dart'; | ||
import 'package:meta/meta.dart'; | ||
import 'package:neon_http_client/src/interceptors/http_interceptor.dart'; | ||
import 'package:nextcloud/nextcloud.dart'; | ||
|
||
@internal | ||
final class AuthorizationThrottlingInterceptor implements HttpInterceptor { | ||
AuthorizationThrottlingInterceptor({ | ||
required this.baseURL, | ||
}); | ||
|
||
final Uri baseURL; | ||
var _blocked = false; | ||
|
||
bool _matchesBaseURL(Uri uri) => uri.toString().startsWith(baseURL.toString()); | ||
|
||
@override | ||
bool shouldInterceptRequest(BaseRequest request) { | ||
if (!_matchesBaseURL(request.url)) { | ||
return false; | ||
} | ||
|
||
final authorization = request.headers['authorization']; | ||
return authorization != null && authorization.isNotEmpty && _blocked; | ||
} | ||
|
||
@override | ||
Never interceptRequest({required BaseRequest request}) { | ||
assert( | ||
shouldInterceptRequest(request), | ||
'Request should not be intercepted.', | ||
); | ||
|
||
throw DynamiteStatusCodeException(Response('', 401)); | ||
} | ||
|
||
@override | ||
bool shouldInterceptResponse(StreamedResponse response) { | ||
final request = response.request; | ||
if (request == null) { | ||
return false; | ||
} | ||
|
||
if (!_matchesBaseURL(request.url)) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
@override | ||
StreamedResponse interceptResponse({required StreamedResponse response, required Uri url}) { | ||
assert( | ||
shouldInterceptResponse(response), | ||
'Response should not be intercepted.', | ||
); | ||
|
||
final authorization = response.request!.headers['authorization']; | ||
if (authorization != null && authorization.isNotEmpty && response.statusCode == 401) { | ||
_blocked = true; | ||
} else if (response.statusCode == 200 && response.request!.url.path.endsWith('/index.php/login/v2/poll')) { | ||
_blocked = false; | ||
} | ||
|
||
return response; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
233 changes: 233 additions & 0 deletions
233
...ackages/neon_http_client/test/interceptors/authorization_throttling_interceptor_test.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
import 'package:http/http.dart'; | ||
import 'package:neon_http_client/src/interceptors/authorization_throttling_interceptor.dart'; | ||
import 'package:nextcloud/nextcloud.dart'; | ||
import 'package:test/test.dart'; | ||
|
||
void main() { | ||
final baseURL = Uri.parse('https://cloud.example.com:8443/nextcloud'); | ||
|
||
group(AuthorizationThrottlingInterceptor, () { | ||
test('blocks after unauthorized response', () async { | ||
final interceptor = AuthorizationThrottlingInterceptor(baseURL: baseURL); | ||
|
||
final authorizedRequest = Request('GET', baseURL); | ||
authorizedRequest.headers['authorization'] = 'test'; | ||
final authorizedResponse = StreamedResponse( | ||
const Stream.empty(), | ||
401, | ||
request: authorizedRequest, | ||
); | ||
|
||
expect( | ||
interceptor.shouldInterceptRequest(authorizedRequest), | ||
isFalse, | ||
); | ||
|
||
expect( | ||
interceptor.shouldInterceptResponse(authorizedResponse), | ||
isTrue, | ||
); | ||
expect( | ||
interceptor.interceptResponse(response: authorizedResponse, url: authorizedRequest.url), | ||
isA<StreamedResponse>(), | ||
); | ||
|
||
expect( | ||
interceptor.shouldInterceptRequest(authorizedRequest), | ||
isTrue, | ||
); | ||
await expectLater( | ||
() async => interceptor.interceptRequest(request: authorizedRequest), | ||
throwsA(isA<DynamiteStatusCodeException>().having((e) => e.response.statusCode, 'response.statusCode', 401)), | ||
); | ||
}); | ||
|
||
test('unblocks after successful poll', () async { | ||
final interceptor = AuthorizationThrottlingInterceptor(baseURL: baseURL); | ||
|
||
final authorizedRequest = Request('GET', baseURL); | ||
authorizedRequest.headers['authorization'] = 'test'; | ||
final authorizedResponse = StreamedResponse( | ||
const Stream.empty(), | ||
401, | ||
request: authorizedRequest, | ||
); | ||
|
||
final pollRequest = Request( | ||
'POST', | ||
Uri.parse('$baseURL/index.php/login/v2/poll'), | ||
); | ||
final pollResponse = StreamedResponse( | ||
const Stream.empty(), | ||
200, | ||
request: pollRequest, | ||
); | ||
|
||
expect( | ||
interceptor.shouldInterceptRequest(authorizedRequest), | ||
isFalse, | ||
); | ||
|
||
expect( | ||
interceptor.shouldInterceptResponse(authorizedResponse), | ||
isTrue, | ||
); | ||
expect( | ||
interceptor.interceptResponse(response: authorizedResponse, url: authorizedRequest.url), | ||
isA<StreamedResponse>(), | ||
); | ||
|
||
expect( | ||
interceptor.shouldInterceptRequest(authorizedRequest), | ||
isTrue, | ||
); | ||
await expectLater( | ||
() async => interceptor.interceptRequest(request: authorizedRequest), | ||
throwsA(isA<DynamiteStatusCodeException>().having((e) => e.response.statusCode, 'response.statusCode', 401)), | ||
); | ||
|
||
expect( | ||
interceptor.shouldInterceptRequest(pollRequest), | ||
isFalse, | ||
); | ||
expect( | ||
interceptor.shouldInterceptResponse(pollResponse), | ||
isTrue, | ||
); | ||
expect( | ||
interceptor.interceptResponse(response: pollResponse, url: pollRequest.url), | ||
isA<StreamedResponse>(), | ||
); | ||
|
||
expect( | ||
interceptor.shouldInterceptRequest(authorizedRequest), | ||
isFalse, | ||
); | ||
}); | ||
|
||
test('never blocks requests not matching baseURL', () async { | ||
final interceptor = AuthorizationThrottlingInterceptor(baseURL: baseURL); | ||
|
||
final otherServerRequest = Request('GET', Uri.parse('http://example.com')); | ||
otherServerRequest.headers['authorization'] = 'test'; | ||
final otherServerResponse = StreamedResponse( | ||
const Stream.empty(), | ||
401, | ||
request: otherServerRequest, | ||
); | ||
|
||
final correctServerRequest = Request('GET', baseURL); | ||
correctServerRequest.headers['authorization'] = 'test'; | ||
final correctServerResponse = StreamedResponse( | ||
const Stream.empty(), | ||
401, | ||
request: correctServerRequest, | ||
); | ||
|
||
expect( | ||
interceptor.shouldInterceptRequest(otherServerRequest), | ||
isFalse, | ||
); | ||
expect( | ||
interceptor.shouldInterceptResponse(otherServerResponse), | ||
isFalse, | ||
); | ||
|
||
expect( | ||
interceptor.shouldInterceptRequest(correctServerRequest), | ||
isFalse, | ||
); | ||
expect( | ||
interceptor.shouldInterceptResponse(correctServerResponse), | ||
isTrue, | ||
); | ||
expect( | ||
interceptor.interceptResponse(response: correctServerResponse, url: correctServerRequest.url), | ||
isA<StreamedResponse>(), | ||
); | ||
expect( | ||
interceptor.shouldInterceptRequest(correctServerRequest), | ||
isTrue, | ||
); | ||
await expectLater( | ||
() async => interceptor.interceptRequest(request: correctServerRequest), | ||
throwsA(isA<DynamiteStatusCodeException>().having((e) => e.response.statusCode, 'response.statusCode', 401)), | ||
); | ||
|
||
expect( | ||
interceptor.shouldInterceptRequest(otherServerRequest), | ||
isFalse, | ||
); | ||
expect( | ||
interceptor.shouldInterceptResponse(otherServerResponse), | ||
isFalse, | ||
); | ||
}); | ||
|
||
test('never blocks requests without authorization', () async { | ||
final interceptor = AuthorizationThrottlingInterceptor(baseURL: baseURL); | ||
|
||
final unauthorizedRequest = Request('GET', baseURL); | ||
final unauthorizedResponse = StreamedResponse( | ||
const Stream.empty(), | ||
401, | ||
request: unauthorizedRequest, | ||
); | ||
|
||
final authorizedRequest = Request('GET', baseURL); | ||
authorizedRequest.headers['authorization'] = 'test'; | ||
final authorizedResponse = StreamedResponse( | ||
const Stream.empty(), | ||
401, | ||
request: authorizedRequest, | ||
); | ||
|
||
expect( | ||
interceptor.shouldInterceptRequest(unauthorizedRequest), | ||
isFalse, | ||
); | ||
expect( | ||
interceptor.shouldInterceptResponse(unauthorizedResponse), | ||
isTrue, | ||
); | ||
expect( | ||
interceptor.interceptResponse(response: unauthorizedResponse, url: unauthorizedRequest.url), | ||
isA<StreamedResponse>(), | ||
); | ||
|
||
expect( | ||
interceptor.shouldInterceptRequest(authorizedRequest), | ||
isFalse, | ||
); | ||
expect( | ||
interceptor.shouldInterceptResponse(authorizedResponse), | ||
isTrue, | ||
); | ||
expect( | ||
interceptor.interceptResponse(response: authorizedResponse, url: authorizedRequest.url), | ||
isA<StreamedResponse>(), | ||
); | ||
expect( | ||
interceptor.shouldInterceptRequest(authorizedRequest), | ||
isTrue, | ||
); | ||
await expectLater( | ||
() async => interceptor.interceptRequest(request: authorizedRequest), | ||
throwsA(isA<DynamiteStatusCodeException>().having((e) => e.response.statusCode, 'response.statusCode', 401)), | ||
); | ||
|
||
expect( | ||
interceptor.shouldInterceptRequest(unauthorizedRequest), | ||
isFalse, | ||
); | ||
expect( | ||
interceptor.shouldInterceptResponse(unauthorizedResponse), | ||
isTrue, | ||
); | ||
expect( | ||
interceptor.interceptResponse(response: unauthorizedResponse, url: unauthorizedRequest.url), | ||
isA<StreamedResponse>(), | ||
); | ||
}); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters