From 4e4199116a1b0aa51154278ef99183f1648c404d Mon Sep 17 00:00:00 2001 From: provokateurin Date: Sat, 31 Aug 2024 12:21:04 +0200 Subject: [PATCH] feat(neon_http_client): Add AuthenticationInterceptor Signed-off-by: provokateurin --- .../authentication_interceptor.dart | 37 +++++ .../lib/src/neon_http_client.dart | 11 +- .../authentication_interceptor_test.dart | 137 ++++++++++++++++++ .../test/neon_http_client_test.dart | 4 +- 4 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 packages/neon_framework/packages/neon_http_client/lib/src/interceptors/authentication_interceptor.dart create mode 100644 packages/neon_framework/packages/neon_http_client/test/interceptors/authentication_interceptor_test.dart diff --git a/packages/neon_framework/packages/neon_http_client/lib/src/interceptors/authentication_interceptor.dart b/packages/neon_framework/packages/neon_http_client/lib/src/interceptors/authentication_interceptor.dart new file mode 100644 index 00000000000..9532d59037e --- /dev/null +++ b/packages/neon_framework/packages/neon_http_client/lib/src/interceptors/authentication_interceptor.dart @@ -0,0 +1,37 @@ +import 'dart:async'; + +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 AuthenticationInterceptor implements HttpInterceptor { + var _blocked = false; + + @override + bool shouldInterceptRequest(BaseRequest request) { + return (request.headers['authorization'] ?? '').isNotEmpty && _blocked; + } + + @override + FutureOr interceptRequest({required BaseRequest request}) { + throw DynamiteStatusCodeException(Response('', 401)); + } + + @override + bool shouldInterceptResponse(StreamedResponse response) { + if ((response.request?.headers['authorization'] ?? '').isEmpty) { + return false; + } + + return response.statusCode == 401 || (response.request?.url.path.endsWith('/index.php/login/v2/poll') ?? false); + } + + @override + FutureOr interceptResponse({required StreamedResponse response, required Uri url}) { + _blocked = response.statusCode == 401; + + return response; + } +} diff --git a/packages/neon_framework/packages/neon_http_client/lib/src/neon_http_client.dart b/packages/neon_framework/packages/neon_http_client/lib/src/neon_http_client.dart index 734f06675af..fc303ebad74 100644 --- a/packages/neon_framework/packages/neon_http_client/lib/src/neon_http_client.dart +++ b/packages/neon_framework/packages/neon_http_client/lib/src/neon_http_client.dart @@ -4,6 +4,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:cookie_store/cookie_store.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; +import 'package:neon_http_client/src/interceptors/authentication_interceptor.dart'; import 'package:neon_http_client/src/interceptors/interceptors.dart'; import 'package:neon_http_client/src/utils/utils.dart'; import 'package:universal_io/io.dart'; @@ -64,9 +65,13 @@ final class NeonHttpClient with http.BaseClient { ); } - builder.add( - CSRFInterceptor(client: baseClient, baseURL: baseURL), - ); + builder + ..add( + CSRFInterceptor(client: baseClient, baseURL: baseURL), + ) + ..add( + AuthenticationInterceptor(), + ); }); return NeonHttpClient._( diff --git a/packages/neon_framework/packages/neon_http_client/test/interceptors/authentication_interceptor_test.dart b/packages/neon_framework/packages/neon_http_client/test/interceptors/authentication_interceptor_test.dart new file mode 100644 index 00000000000..e6af63776dc --- /dev/null +++ b/packages/neon_framework/packages/neon_http_client/test/interceptors/authentication_interceptor_test.dart @@ -0,0 +1,137 @@ +import 'package:http/http.dart'; +import 'package:neon_http_client/src/interceptors/authentication_interceptor.dart'; +import 'package:nextcloud/nextcloud.dart'; +import 'package:test/test.dart'; + +void main() { + group(AuthenticationInterceptor, () { + test('blocks after unauthenticated response', () async { + final interceptor = AuthenticationInterceptor(); + + final authorizedRequest = Request('GET', Uri()); + 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(), + ); + + expect( + interceptor.shouldInterceptRequest(authorizedRequest), + isTrue, + ); + await expectLater( + () async => interceptor.interceptRequest(request: authorizedRequest), + throwsA(isA().having((e) => e.response.statusCode, 'response.statusCode', 401)), + ); + }); + + test('unblocks after successful poll', () async { + final interceptor = AuthenticationInterceptor(); + + final authorizedRequest = Request('GET', Uri()); + authorizedRequest.headers['authorization'] = 'test'; + final authorizedResponse = StreamedResponse( + const Stream.empty(), + 401, + request: authorizedRequest, + ); + + expect( + interceptor.interceptResponse(response: authorizedResponse, url: authorizedRequest.url), + isA(), + ); + expect( + interceptor.shouldInterceptRequest(authorizedRequest), + isTrue, + ); + + final pollRequest = Request( + 'POST', + Uri.parse('https://cloud.example.com:8443/nextcloud/index.php/login/v2/poll'), + ); + final pollResponse = StreamedResponse( + const Stream.empty(), + 200, + request: pollRequest, + ); + + expect( + interceptor.interceptResponse(response: pollResponse, url: pollRequest.url), + isA(), + ); + expect( + interceptor.shouldInterceptRequest(authorizedRequest), + isFalse, + ); + }); + + test('never blocks requests without authorization', () async { + final interceptor = AuthenticationInterceptor(); + + final unauthorizedRequest = Request('GET', Uri()); + final unauthorizedResponse = StreamedResponse( + const Stream.empty(), + 401, + request: unauthorizedRequest, + ); + + expect( + interceptor.shouldInterceptRequest(unauthorizedRequest), + isFalse, + ); + expect( + interceptor.shouldInterceptResponse(unauthorizedResponse), + isFalse, + ); + + final authorizedRequest = Request('GET', Uri()); + 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: unauthorizedResponse, url: unauthorizedRequest.url), + isA(), + ); + expect( + interceptor.shouldInterceptRequest(authorizedRequest), + isTrue, + ); + + expect( + interceptor.shouldInterceptRequest(unauthorizedRequest), + isFalse, + ); + expect( + interceptor.shouldInterceptResponse(unauthorizedResponse), + isFalse, + ); + }); + }); +} diff --git a/packages/neon_framework/packages/neon_http_client/test/neon_http_client_test.dart b/packages/neon_framework/packages/neon_http_client/test/neon_http_client_test.dart index eea4741e586..6e0eb9bb085 100644 --- a/packages/neon_framework/packages/neon_http_client/test/neon_http_client_test.dart +++ b/packages/neon_framework/packages/neon_http_client/test/neon_http_client_test.dart @@ -73,8 +73,8 @@ void main() { ); expect( - client.interceptors.last, - isA(), + client.interceptors.indexWhere((i) => i is CSRFInterceptor), + greaterThan(client.interceptors.indexWhere((i) => i is CookieStoreInterceptor)), ); });