From ea4a30ca00c4184b4ef05baf93420c1c53122402 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Thu, 23 Jun 2022 11:48:32 +0200 Subject: [PATCH 1/4] feat(http): [WIP] work on middleware feature --- .../example/http_client_fastapi_example.dart | 386 ++++++++++++++++++ .../example/http_client_test.dart | 41 ++ .../interfaces/authenticated_client.dart | 87 ++++ .../interfaces/authentication_client.dart | 197 +++++++++ .../header_authentication_client.dart | 1 + .../interfaces/oauth2_client.dart | 7 +- .../authentication/refresh_token_client.dart | 86 +++- .../lib/src/implemented_base_client.dart | 31 ++ .../lib/src/implemented_client.dart | 86 ++++ .../lib/src/mixins/body_transformer.dart | 83 ++++ .../lib/src/mixins/headers_transformer.dart | 131 ++++++ .../lib/src/mixins/oauth2_transformer.dart | 33 ++ .../lib/src/mixins/request_transformer.dart | 27 ++ .../lib/src/rest_client.dart | 166 +++++++- 14 files changed, 1349 insertions(+), 13 deletions(-) create mode 100644 packages/wyatt_http_client/example/http_client_fastapi_example.dart create mode 100644 packages/wyatt_http_client/example/http_client_test.dart create mode 100644 packages/wyatt_http_client/lib/src/authentication/interfaces/authenticated_client.dart create mode 100644 packages/wyatt_http_client/lib/src/implemented_base_client.dart create mode 100644 packages/wyatt_http_client/lib/src/implemented_client.dart create mode 100644 packages/wyatt_http_client/lib/src/mixins/body_transformer.dart create mode 100644 packages/wyatt_http_client/lib/src/mixins/headers_transformer.dart create mode 100644 packages/wyatt_http_client/lib/src/mixins/oauth2_transformer.dart create mode 100644 packages/wyatt_http_client/lib/src/mixins/request_transformer.dart diff --git a/packages/wyatt_http_client/example/http_client_fastapi_example.dart b/packages/wyatt_http_client/example/http_client_fastapi_example.dart new file mode 100644 index 00000000..21c13b42 --- /dev/null +++ b/packages/wyatt_http_client/example/http_client_fastapi_example.dart @@ -0,0 +1,386 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +import 'package:wyatt_http_client/src/authentication/refresh_token_client.dart'; +import 'package:wyatt_http_client/src/rest_client.dart'; +import 'package:wyatt_http_client/src/utils/protocols.dart'; + +enum EmailVerificationAction { + signUp, + resetPassword, + changeEmail; + + String toSnakeCase() { + return name.splitMapJoin( + RegExp('[A-Z]'), + onMatch: (m) => '_${m[0]?.toLowerCase()}', + onNonMatch: (n) => n, + ); + } + + factory EmailVerificationAction.fromString(String str) { + return EmailVerificationAction.values.firstWhere( + (EmailVerificationAction element) => element.toSnakeCase() == str, + ); + } +} + +class VerifyCode { + final String email; + final String verificationCode; + final EmailVerificationAction action; + VerifyCode({ + required this.email, + required this.verificationCode, + required this.action, + }); + + VerifyCode copyWith({ + String? email, + String? verificationCode, + EmailVerificationAction? action, + }) { + return VerifyCode( + email: email ?? this.email, + verificationCode: verificationCode ?? this.verificationCode, + action: action ?? this.action, + ); + } + + Map toMap() { + return { + 'email': email, + 'verification_code': verificationCode, + 'action': action.toSnakeCase(), + }; + } + + factory VerifyCode.fromMap(Map map) { + return VerifyCode( + email: map['email'] as String, + verificationCode: map['verification_code'] as String, + action: EmailVerificationAction.fromString(map['action'] as String), + ); + } + + String toJson() => json.encode(toMap()); + + factory VerifyCode.fromJson(String source) => + VerifyCode.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'VerifyCode(email: $email, verificationCode: $verificationCode, action: $action)'; +} + +class Account { + final String email; + final String? sessionId; + Account({ + required this.email, + this.sessionId, + }); + + Account copyWith({ + String? email, + String? sessionId, + }) { + return Account( + email: email ?? this.email, + sessionId: sessionId ?? this.sessionId, + ); + } + + Map toMap() { + return { + 'email': email, + 'session_id': sessionId, + }; + } + + factory Account.fromMap(Map map) { + return Account( + email: map['email'] as String, + sessionId: map['session_id'] != null ? map['session_id'] as String : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory Account.fromJson(String source) => + Account.fromMap(json.decode(source) as Map); + + @override + String toString() => 'Account(email: $email, sessionId: $sessionId)'; +} + +class SignUp { + final String sessionId; + final String password; + SignUp({ + required this.sessionId, + required this.password, + }); + + SignUp copyWith({ + String? sessionId, + String? password, + }) { + return SignUp( + sessionId: sessionId ?? this.sessionId, + password: password ?? this.password, + ); + } + + Map toMap() { + return { + 'session_id': sessionId, + 'password': password, + }; + } + + factory SignUp.fromMap(Map map) { + return SignUp( + sessionId: map['session_id'] as String, + password: map['password'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory SignUp.fromJson(String source) => + SignUp.fromMap(json.decode(source) as Map); + + @override + String toString() => 'SignUp(sessionId: $sessionId, password: $password)'; +} + +class TokenSuccess { + final String accessToken; + final String refreshToken; + final Account account; + TokenSuccess({ + required this.accessToken, + required this.refreshToken, + required this.account, + }); + + TokenSuccess copyWith({ + String? accessToken, + String? refreshToken, + Account? account, + }) { + return TokenSuccess( + accessToken: accessToken ?? this.accessToken, + refreshToken: refreshToken ?? this.refreshToken, + account: account ?? this.account, + ); + } + + Map toMap() { + return { + 'access_token': accessToken, + 'refresh_token': refreshToken, + 'account': account.toMap(), + }; + } + + factory TokenSuccess.fromMap(Map map) { + return TokenSuccess( + accessToken: map['access_token'] as String, + refreshToken: map['refresh_token'] as String, + account: Account.fromMap(map['account'] as Map), + ); + } + + String toJson() => json.encode(toMap()); + + factory TokenSuccess.fromJson(String source) => + TokenSuccess.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'TokenSuccess(accessToken: $accessToken, refreshToken: $refreshToken, account: $account)'; +} + +class Login { + final String email; + final String password; + Login({ + required this.email, + required this.password, + }); + + Login copyWith({ + String? email, + String? password, + }) { + return Login( + email: email ?? this.email, + password: password ?? this.password, + ); + } + + Map toMap() { + return { + 'email': email, + 'password': password, + }; + } + + factory Login.fromMap(Map map) { + return Login( + email: map['email'] as String, + password: map['password'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory Login.fromJson(String source) => + Login.fromMap(json.decode(source) as Map); + + @override + String toString() => 'Login(email: $email, password: $password)'; +} + +class FastAPI { + final String baseUrl; + final RefreshTokenClient client; + final int apiVersion; + + FastAPI({ + this.baseUrl = 'localhost:80', + RefreshTokenClient? client, + this.apiVersion = 1, + }) : client = client ?? + RefreshTokenClient( + authorizationEndpoint: '', + tokenEndpoint: '', + accessTokenParser: (body) => body['access_token']! as String, + refreshTokenParser: (body) => body['refresh_token']! as String, + inner: RestClient( + protocol: Protocols.http, + authority: baseUrl, + ), + ); + + String get apiPath => '/api/v$apiVersion'; + + Future sendSignUpCode(String email) async { + final r = await client.post( + Uri.parse('$apiPath/auth/send-sign-up-code'), + body: { + 'email': email, + }, + ); + if (r.statusCode != 201) { + throw Exception('Invalid reponse: ${r.statusCode}'); + } + } + + Future verifyCode(VerifyCode verifyCode) async { + final r = await client.post( + Uri.parse('$apiPath/auth/verify-code'), + body: verifyCode.toMap(), + ); + if (r.statusCode != 202) { + throw Exception('Invalid reponse: ${r.statusCode}'); + } else { + return Account.fromMap( + (jsonDecode(r.body) as Map)['account'] + as Map, + ); + } + } + + Future signUp(SignUp signUp) async { + final r = await client.post( + Uri.parse('$apiPath/auth/sign-up'), + body: signUp.toMap(), + ); + if (r.statusCode != 201) { + throw Exception('Invalid reponse: ${r.statusCode}'); + } else { + return Account.fromJson(r.body); + } + } + + Future signInWithPassword(Login login) async { + final r = await client.authorize(login.toMap()); + return TokenSuccess.fromJson(r.body); + } + + Future refresh() async { + final r = await client.refresh(); + return TokenSuccess.fromJson(r?.body ?? ''); + } + + Future> getAccountList() async { + final r = await client.get( + Uri.parse('$apiPath/account'), + ); + if (r.statusCode != 200) { + throw Exception('Invalid reponse: ${r.statusCode}'); + } else { + final list = (jsonDecode(r.body) as Map)['founds'] + as List>; + final result = []; + for (final element in list) { + result.add(Account.fromMap(element)); + } + return result; + } + } +} + +void main(List args) async { + final api = FastAPI( + client: RefreshTokenClient( + authorizationEndpoint: '/api/v1/auth/sign-in-with-password', + tokenEndpoint: '/api/v1/auth/refresh', + accessTokenParser: (body) => body['access_token']! as String, + refreshTokenParser: (body) => body['refresh_token']! as String, + inner: RestClient( + protocol: Protocols.http, + authority: 'localhost:80', + ), + ), + ); + + // await api.sendSignUpCode('git@pcl.ovh'); + // final verifiedAccount = await api.verifyCode( + // VerifyCode( + // email: 'git@pcl.ovh', + // verificationCode: '000000000', + // action: EmailVerificationAction.signUp, + // ), + // ); + // print(verifiedAccount); + // final registeredAccount = await api.signUp( + // SignUp(sessionId: verifiedAccount.sessionId ?? '', password: 'password'), + // ); + // print(registeredAccount); + final signedInAccount = await api.signInWithPassword( + Login(email: 'git@pcl.ovh', password: 'password'), + ); + // print(signedInAccount); + final accountList = await api.getAccountList(); + print(accountList); +} diff --git a/packages/wyatt_http_client/example/http_client_test.dart b/packages/wyatt_http_client/example/http_client_test.dart new file mode 100644 index 00000000..36a66c15 --- /dev/null +++ b/packages/wyatt_http_client/example/http_client_test.dart @@ -0,0 +1,41 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + + +Future main(List args) async { + // final client = Oauth2Client( + // accessToken: 'test-token', + // inner: RestClient(protocol: Protocols.http, authority: 'localhost:80'), + // ); + // final client = RestClient( + // protocol: Protocols.http, + // authority: 'localhost:80', + // inner: Oauth2Client( + // authorizationEndpoint: '/api/v1/account/test', + // tokenEndpoint: '/api/v1/account/test', + // accessToken: 'test-token', + // refreshToken: 'refresh-token', + // ), + // ); + // final client = RestClient(protocol: Protocols.http, authority: 'localhost:80'); + // final client =AwesomeRestClient(protocol: Protocols.http, authority: 'localhost:80'); + // var r = await client.post( + // Uri.parse('/api/v1/account/test'), + // body: { + // 'email': 'test@test.fr', + // }, + // ); +} diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/authenticated_client.dart b/packages/wyatt_http_client/lib/src/authentication/interfaces/authenticated_client.dart new file mode 100644 index 00000000..0c8cc7f1 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/authentication/interfaces/authenticated_client.dart @@ -0,0 +1,87 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/implemented_base_client.dart'; + +abstract class AuthenticatedClient implements ImplementedBaseClient { + final Client _inner; + + AuthenticatedClient({ + Client? inner, + }) : _inner = inner ?? Client(); + + @override + void close() => _inner.close(); + + @override + Future head(Uri url, {Map? headers}) => + _inner.head(url, headers: headers); + + @override + Future get(Uri url, {Map? headers}) => + _inner.get(url, headers: headers); + + @override + Future post( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.post(url, headers: headers, body: body, encoding: encoding); + + @override + Future put( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.put(url, headers: headers, body: body, encoding: encoding); + + @override + Future patch( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.patch(url, headers: headers, body: body, encoding: encoding); + + @override + Future delete( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.delete(url, headers: headers, body: body, encoding: encoding); + + @override + Future read(Uri url, {Map? headers}) => + _inner.read(url, headers: headers); + + @override + Future readBytes(Uri url, {Map? headers}) => + _inner.readBytes(url, headers: headers); + + @override + Future send(BaseRequest request) => _inner.send(request); +} diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/authentication_client.dart b/packages/wyatt_http_client/lib/src/authentication/interfaces/authentication_client.dart index 9e956216..94c844d4 100644 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/authentication_client.dart +++ b/packages/wyatt_http_client/lib/src/authentication/interfaces/authentication_client.dart @@ -14,8 +14,161 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'dart:convert'; +import 'dart:typed_data'; + import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/mixins/body_transformer.dart'; +import 'package:wyatt_http_client/src/mixins/headers_transformer.dart'; import 'package:wyatt_http_client/src/rest_client.dart'; +import 'package:wyatt_http_client/src/utils/protocols.dart'; +import 'package:wyatt_http_client/src/utils/utils.dart'; + +class AwesomeClient extends BaseClient { + final Client _inner; + + AwesomeClient({ + Client? inner, + }) : _inner = inner ?? Client(); + + @override + Future send(BaseRequest request) { + return _inner.send(request); + } +} + +class AwesomeRestClient extends AwesomeClient + with HeadersTransformer, BodyTransformer { + final Protocols protocol; + final String? authority; + + AwesomeRestClient({ + this.protocol = Protocols.https, + this.authority = '', + super.inner, + }); + + @override + Object? bodyMutator(Object? body) { + print('bodyMutator: Json encoding'); + return jsonEncode(body); + } + + @override + Map? headersMutator( + Object? body, + Map? headers, + ) { + print('headerMutator: Json encoding'); + final mutation = { + 'content-type': 'application/json; charset=utf-8', + }; + if (headers != null) { + headers.addAll(mutation); + return headers; + } else { + return mutation; + } + } + + @override + BaseRequest requestMutator(BaseRequest request) { + print('requestMutator: scheme + authority'); + final Uri uri = Uri.parse('${protocol.scheme}$authority${request.url}'); + return Utils.copyRequestWith(request, url: uri); + } +} + +class AwesomeUrlClient extends AuthenticatedClient { + AwesomeUrlClient(super.inner); +} + +class AwesomeOauth2Client extends AuthenticatedClient with HeadersTransformer { + AwesomeOauth2Client(super.inner); + + @override + Map? headersMutator( + Object? body, + Map? headers, + ) { + print('headersMutator: Token manager'); + final mutation = { + 'authorization': 'Bearer TOKEN', + }; + if (headers != null) { + headers.addAll(mutation); + return headers; + } else { + return mutation; + } + } +} + +abstract class AuthenticatedClient implements Client { + final Client _inner; + + Client get inner => _inner; + + AuthenticatedClient(BaseClient? inner) : _inner = inner ?? RestClient(); + + @override + void close() => _inner.close(); + + @override + Future head(Uri url, {Map? headers}) => + _inner.head(url, headers: headers); + + @override + Future get(Uri url, {Map? headers}) => + _inner.get(url, headers: headers); + + @override + Future post( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.post(url, headers: headers, body: body, encoding: encoding); + + @override + Future put( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.put(url, headers: headers, body: body, encoding: encoding); + + @override + Future patch( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.patch(url, headers: headers, body: body, encoding: encoding); + + @override + Future delete( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.delete(url, headers: headers, body: body, encoding: encoding); + + @override + Future read(Uri url, {Map? headers}) => + _inner.read(url, headers: headers); + + @override + Future readBytes(Uri url, {Map? headers}) => + _inner.readBytes(url, headers: headers); + + @override + Future send(BaseRequest request) => _inner.send(request); +} abstract class AuthenticationClient extends BaseClient { final BaseClient _inner; @@ -24,6 +177,50 @@ abstract class AuthenticationClient extends BaseClient { AuthenticationClient(BaseClient? inner) : _inner = inner ?? RestClient(); + @override + Future head(Uri url, {Map? headers}) => + _inner.head(url, headers: headers); + + @override + Future get(Uri url, {Map? headers}) => + _inner.get(url, headers: headers); + + @override + Future post( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.post(url, headers: headers, body: body, encoding: encoding); + + @override + Future put( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.put(url, headers: headers, body: body, encoding: encoding); + + @override + Future patch( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.patch(url, headers: headers, body: body, encoding: encoding); + + @override + Future delete( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.delete(url, headers: headers, body: body, encoding: encoding); + @override Future send(BaseRequest request) { return _inner.send(request); diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/header_authentication_client.dart b/packages/wyatt_http_client/lib/src/authentication/interfaces/header_authentication_client.dart index c206748f..ef8aae8d 100644 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/header_authentication_client.dart +++ b/packages/wyatt_http_client/lib/src/authentication/interfaces/header_authentication_client.dart @@ -31,6 +31,7 @@ abstract class HeaderAuthenticationClient extends AuthenticationClient { final newHeader = modifyHeader(Map.from(request.headers), request); request.headers.clear(); request.headers.addAll(newHeader); + print(newHeader); return super.send(request); } } diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart b/packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart index 1da70e99..5704788c 100644 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart +++ b/packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'package:http/http.dart'; import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart'; typedef TokenParser = String Function(Map); @@ -21,14 +22,14 @@ typedef TokenParser = String Function(Map); abstract class Oauth2Client extends HeaderAuthenticationClient { Oauth2Client(super.inner); - Future refresh() { + Future refresh() { return Future.value(); } - Future authorize( + Future authorize( Map body, { Map? headers, }) { - return Future.value(); + return Future.value(); } } diff --git a/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart b/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart index 67d6a1d0..01201966 100644 --- a/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart +++ b/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart @@ -23,6 +23,78 @@ import 'package:wyatt_http_client/src/utils/header_keys.dart'; import 'package:wyatt_http_client/src/utils/http_status.dart'; import 'package:wyatt_http_client/src/utils/utils.dart'; +// class Oauth2Client extends ImplementedBaseClient with RequestTransformer { +// final String authorizationEndpoint; +// final String tokenEndpoint; +// String? accessToken; +// String? refreshToken; +// String? tokenToUse; + +// Oauth2Client({ +// required this.authorizationEndpoint, +// required this.tokenEndpoint, +// this.accessToken, +// this.refreshToken, +// super.inner, +// }) : tokenToUse = accessToken; + +// @override +// BaseRequest requestMutator(BaseRequest request) { +// print('Oauth2Client::requestMutator -> add authorization: $accessToken'); +// final headers = request.headers; +// final mutation = { +// 'Authorization': 'Bearer $tokenToUse', +// }; +// if (tokenToUse?.isNotEmpty ?? false) { +// headers.addAll(mutation); +// return Utils.copyRequestWith(request, headers: headers); +// } +// return request; +// } + +// Future refresh() async { +// if (refreshToken?.isNotEmpty ?? false) { +// tokenToUse = refreshToken; + +// final response = await get( +// Uri.parse(tokenEndpoint), +// ); + +// if (response.statusCode == HttpStatus.ok) { +// // final body = json.decode(response.body) as Map; +// // accessToken = accessTokenParser(body); +// print('Oauth2Client::refresh -> ok'); +// } +// return response; +// } +// return null; +// } + +// @override +// Map? headersMutator( +// Object? body, +// Map? headers, +// ) { +// print( +// 'Oauth2Client::headersMutator -> add authorization: $accessToken', +// ); +// final mutation = { +// 'Authorization': 'Bearer $accessToken', +// }; +// if (accessToken.isNotEmpty) { +// if (headers != null) { +// headers.addAll(mutation); +// return headers; +// } else { +// return mutation; +// } +// } else { +// return headers; +// } +// } + +// } + class RefreshTokenClient extends Oauth2Client { final String authorizationEndpoint; final String tokenEndpoint; @@ -50,6 +122,8 @@ class RefreshTokenClient extends Oauth2Client { Map header, [ BaseRequest? request, ]) { + print('accessToken $accessToken'); + print('request $request'); if (accessToken != null && request != null) { header[authenticationHeader] = '$authenticationMethod $accessToken'; return header; @@ -58,13 +132,13 @@ class RefreshTokenClient extends Oauth2Client { } @override - Future authorize( + Future authorize( Map body, { Map? headers, }) async { final response = await inner.post( Uri.parse(authorizationEndpoint), - body: jsonEncode(body), + body: body, headers: headers, ); @@ -80,10 +154,11 @@ class RefreshTokenClient extends Oauth2Client { this.refreshToken = refreshToken; } } + return response; } @override - Future refresh() async { + Future refresh() async { if (refreshToken != null) { final Map header = { authenticationHeader: '$authenticationHeader $refreshToken', @@ -98,11 +173,16 @@ class RefreshTokenClient extends Oauth2Client { final body = json.decode(response.body) as Map; accessToken = accessTokenParser(body); } + return response; } + return null; } @override Future send(BaseRequest request) async { + final newHeader = modifyHeader(Map.from(request.headers), request); + request.headers.clear(); + request.headers.addAll(newHeader); final response = await super.send(request); if (response.statusCode == HttpStatus.unauthorized) { diff --git a/packages/wyatt_http_client/lib/src/implemented_base_client.dart b/packages/wyatt_http_client/lib/src/implemented_base_client.dart new file mode 100644 index 00000000..e42ed84f --- /dev/null +++ b/packages/wyatt_http_client/lib/src/implemented_base_client.dart @@ -0,0 +1,31 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:http/http.dart'; + +class ImplementedBaseClient extends BaseClient { + final Client inner; + + ImplementedBaseClient({ + Client? inner, + }) : inner = inner ?? Client(); + + @override + Future send(BaseRequest request) { + return inner.send(request); + } + +} diff --git a/packages/wyatt_http_client/lib/src/implemented_client.dart b/packages/wyatt_http_client/lib/src/implemented_client.dart new file mode 100644 index 00000000..a9790c33 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/implemented_client.dart @@ -0,0 +1,86 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:http/http.dart'; + +abstract class ImplementedClient extends BaseClient { + final Client _inner; + + ImplementedClient({ + Client? inner, + }) : _inner = inner ?? Client(); + + @override + void close() => _inner.close(); + + @override + Future head(Uri url, {Map? headers}) => + _inner.head(url, headers: headers); + + @override + Future get(Uri url, {Map? headers}) => + _inner.get(url, headers: headers); + + @override + Future post( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.post(url, headers: headers, body: body, encoding: encoding); + + @override + Future put( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.put(url, headers: headers, body: body, encoding: encoding); + + @override + Future patch( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.patch(url, headers: headers, body: body, encoding: encoding); + + @override + Future delete( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _inner.delete(url, headers: headers, body: body, encoding: encoding); + + @override + Future read(Uri url, {Map? headers}) => + _inner.read(url, headers: headers); + + @override + Future readBytes(Uri url, {Map? headers}) => + _inner.readBytes(url, headers: headers); + + @override + Future send(BaseRequest request) => _inner.send(request); +} diff --git a/packages/wyatt_http_client/lib/src/mixins/body_transformer.dart b/packages/wyatt_http_client/lib/src/mixins/body_transformer.dart new file mode 100644 index 00000000..b5931d48 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/mixins/body_transformer.dart @@ -0,0 +1,83 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:convert'; + +import 'package:http/http.dart'; + +mixin BodyTransformer on Client { + Object? bodyMutator(Object? body); + + @override + Future post( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.post( + url, + headers: headers, + body: bodyMutator(body), + encoding: encoding, + ); + } + + @override + Future put( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.put( + url, + headers: headers, + body: bodyMutator(body), + encoding: encoding, + ); + } + + @override + Future patch( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.patch( + url, + headers: headers, + body: bodyMutator(body), + encoding: encoding, + ); + } + + @override + Future delete( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.delete( + url, + headers: headers, + body: bodyMutator(body), + encoding: encoding, + ); + } +} diff --git a/packages/wyatt_http_client/lib/src/mixins/headers_transformer.dart b/packages/wyatt_http_client/lib/src/mixins/headers_transformer.dart new file mode 100644 index 00000000..42cc270b --- /dev/null +++ b/packages/wyatt_http_client/lib/src/mixins/headers_transformer.dart @@ -0,0 +1,131 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:http/http.dart'; + +mixin HeadersTransformer on Client { + Map? headersMutator( + Object? body, + Map? headers, + ); + + @override + Future head( + Uri url, { + Map? headers, + }) { + return super.head( + url, + headers: headersMutator(null, headers), + ); + } + + @override + Future get( + Uri url, { + Map? headers, + }) { + return super.get( + url, + headers: headersMutator(null, headers), + ); + } + + @override + Future post( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.post( + url, + headers: headersMutator(body, headers), + body: body, + encoding: encoding, + ); + } + + @override + Future put( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.put( + url, + headers: headersMutator(body, headers), + body: body, + encoding: encoding, + ); + } + + @override + Future patch( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.patch( + url, + headers: headersMutator(body, headers), + body: body, + encoding: encoding, + ); + } + + @override + Future delete( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) { + return super.delete( + url, + headers: headersMutator(body, headers), + body: body, + encoding: encoding, + ); + } + + @override + Future read( + Uri url, { + Map? headers, + }) { + return super.read( + url, + headers: headersMutator(null, headers), + ); + } + + @override + Future readBytes( + Uri url, { + Map? headers, + }) { + return super.readBytes( + url, + headers: headersMutator(null, headers), + ); + } +} diff --git a/packages/wyatt_http_client/lib/src/mixins/oauth2_transformer.dart b/packages/wyatt_http_client/lib/src/mixins/oauth2_transformer.dart new file mode 100644 index 00000000..a1abf27a --- /dev/null +++ b/packages/wyatt_http_client/lib/src/mixins/oauth2_transformer.dart @@ -0,0 +1,33 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/implemented_base_client.dart'; + +mixin Oauth2Transformer on ImplementedBaseClient { + late final String authorizationEndpoint; + late final String tokenEndpoint; + String? accessToken; + String? refreshToken; + + BaseRequest requestAuthenticator(BaseRequest request); + + @override + Future send(BaseRequest request) { + final req = requestAuthenticator(request); + return super.send(req); + } +} diff --git a/packages/wyatt_http_client/lib/src/mixins/request_transformer.dart b/packages/wyatt_http_client/lib/src/mixins/request_transformer.dart new file mode 100644 index 00000000..24db0982 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/mixins/request_transformer.dart @@ -0,0 +1,27 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/implemented_base_client.dart'; + +mixin RequestTransformer on ImplementedBaseClient { + BaseRequest requestMutator(BaseRequest request); + + @override + Future send(BaseRequest request) { + return super.send(requestMutator(request)); + } +} diff --git a/packages/wyatt_http_client/lib/src/rest_client.dart b/packages/wyatt_http_client/lib/src/rest_client.dart index cde6f3d6..1925eef1 100644 --- a/packages/wyatt_http_client/lib/src/rest_client.dart +++ b/packages/wyatt_http_client/lib/src/rest_client.dart @@ -14,25 +14,177 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'dart:convert'; + import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/implemented_base_client.dart'; +import 'package:wyatt_http_client/src/mixins/body_transformer.dart'; +import 'package:wyatt_http_client/src/mixins/headers_transformer.dart'; +import 'package:wyatt_http_client/src/mixins/request_transformer.dart'; import 'package:wyatt_http_client/src/utils/protocols.dart'; import 'package:wyatt_http_client/src/utils/utils.dart'; -class RestClient extends BaseClient { +class RestClient extends ImplementedBaseClient + with BodyTransformer, HeadersTransformer, RequestTransformer { final Protocols protocol; final String? authority; - final Client _inner; - RestClient({ this.protocol = Protocols.https, this.authority = '', - Client? inner, - }) : _inner = inner ?? Client(); + super.inner, + }); @override - Future send(BaseRequest request) { + Object? bodyMutator(Object? body) { + print( + 'RestClient::bodyMutator -> encode in json if body is Map: ${body is Map}', + ); + if (body is Map) { + return jsonEncode(body); + } + return body; + } + + @override + Map? headersMutator( + Object? body, + Map? headers, + ) { + print( + 'RestClient::headersMutator -> add json content-type if body is Map: ${body is Map}', + ); + final mutation = { + 'content-type': 'application/json; charset=utf-8', + }; + if (body is Map) { + if (headers != null) { + headers.addAll(mutation); + return headers; + } else { + return mutation; + } + } + return headers; + } + + @override + BaseRequest requestMutator(BaseRequest request) { + print( + 'RestClient::requestMutator -> add prefix path: ${protocol.scheme}$authority', + ); final Uri uri = Uri.parse('${protocol.scheme}$authority${request.url}'); - return _inner.send(Utils.copyRequestWith(request, url: uri)); + return Utils.copyRequestWith(request, url: uri); } } + +// class RestClient extends BaseClient { +// final Protocols protocol; +// final String? authority; + +// final Client _inner; + +// RestClient({ +// this.protocol = Protocols.https, +// this.authority = '', +// Client? inner, +// }) : _inner = inner ?? Client(); + +// String? forceJson(Object? body) { +// String? b; +// if (body != null && body is Map) { +// b = jsonEncode(body); +// } +// return b; +// } + +// Map? forceJsonHeader( +// Object? body, Map? headers,) { +// final Map h = headers ?? {}; +// if (body != null && body is Map) { +// h['Content-Type'] = 'application/json'; +// } +// return h; +// } + +// // @override +// // Future post( +// // Uri url, { +// // Map? headers, +// // Object? body, +// // Encoding? encoding, +// // }) { +// // final b = forceJson(body) ?? body; +// // final h = forceJsonHeader(body, headers) ?? headers; +// // print(b); +// // print(h); +// // return super.post( +// // url, +// // headers: h, +// // body: b, +// // encoding: encoding, +// // ); +// // } + +// @override +// Future put( +// Uri url, { +// Map? headers, +// Object? body, +// Encoding? encoding, +// }) { +// final b = forceJson(body) ?? body; +// final h = forceJsonHeader(body, headers) ?? headers; +// return super.put( +// url, +// headers: h, +// body: b, +// encoding: encoding, +// ); +// } + +// @override +// Future patch( +// Uri url, { +// Map? headers, +// Object? body, +// Encoding? encoding, +// }) { +// final b = forceJson(body) ?? body; +// final h = forceJsonHeader(body, headers) ?? headers; +// return super.patch( +// url, +// headers: h, +// body: b, +// encoding: encoding, +// ); +// } + +// @override +// Future delete( +// Uri url, { +// Map? headers, +// Object? body, +// Encoding? encoding, +// }) { +// final b = forceJson(body) ?? body; +// final h = forceJsonHeader(body, headers) ?? headers; +// return super.delete( +// url, +// headers: h, +// body: b, +// encoding: encoding, +// ); +// } + +// @override +// Future send(BaseRequest request) { +// final Uri uri = Uri.parse('${protocol.scheme}$authority${request.url}'); +// return _inner.send( +// Utils.copyRequestWith( +// request, +// url: uri, +// ), +// ); +// } +// } From 58d3b70bee405c820cdd46183eea6faa69247020 Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Thu, 23 Jun 2022 17:07:04 +0200 Subject: [PATCH 2/4] feat(http): [WIP] implements middleware system --- .../wyatt_http_client/example/pipeline.dart | 149 ++++++++++++++++++ .../wyatt_http_client/lib/src/middleware.dart | 77 +++++++++ .../lib/src/middleware_client.dart | 117 ++++++++++++++ .../middlewares/body_to_json_middleware.dart | 52 ++++++ .../src/middlewares/default_middleware.dart | 26 +++ .../middlewares/refresh_token_middleware.dart | 72 +++++++++ .../middlewares/simple_logger_middleware.dart | 47 ++++++ .../middlewares/uri_prefix_middleware.dart | 42 +++++ .../lib/src/models/middleware_request.dart | 89 +++++++++++ .../lib/src/models/middleware_response.dart | 37 +++++ .../lib/src/models/unfreezed_request.dart | 55 +++++++ .../wyatt_http_client/lib/src/pipeline.dart | 57 +++++++ 12 files changed, 820 insertions(+) create mode 100644 packages/wyatt_http_client/example/pipeline.dart create mode 100644 packages/wyatt_http_client/lib/src/middleware.dart create mode 100644 packages/wyatt_http_client/lib/src/middleware_client.dart create mode 100644 packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart create mode 100644 packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart create mode 100644 packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart create mode 100644 packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart create mode 100644 packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart create mode 100644 packages/wyatt_http_client/lib/src/models/middleware_request.dart create mode 100644 packages/wyatt_http_client/lib/src/models/middleware_response.dart create mode 100644 packages/wyatt_http_client/lib/src/models/unfreezed_request.dart create mode 100644 packages/wyatt_http_client/lib/src/pipeline.dart diff --git a/packages/wyatt_http_client/example/pipeline.dart b/packages/wyatt_http_client/example/pipeline.dart new file mode 100644 index 00000000..1ee6cade --- /dev/null +++ b/packages/wyatt_http_client/example/pipeline.dart @@ -0,0 +1,149 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:wyatt_http_client/src/middleware_client.dart'; +import 'package:wyatt_http_client/src/middlewares/body_to_json_middleware.dart'; +import 'package:wyatt_http_client/src/middlewares/refresh_token_middleware.dart'; +import 'package:wyatt_http_client/src/middlewares/simple_logger_middleware.dart'; +import 'package:wyatt_http_client/src/middlewares/uri_prefix_middleware.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; +import 'package:wyatt_http_client/src/utils/protocols.dart'; + +// class RequestMutatorMiddleware implements Middleware { +// @override +// Middleware? parent; + +// @override +// Middleware? child; + +// RequestMutatorMiddleware({ +// this.parent, +// this.child, +// }); + +// @override +// BaseRequest onRequest(BaseRequest request) { +// print('RequestMutator::OnRequest: ${request.method} -> ${request.url}'); +// return child?.onRequest(request) ?? request; +// } + +// @override +// BaseResponse onResponse(BaseResponse response) { +// final res = child?.onResponse(response) ?? response; +// print( +// 'RequestMutator::OnResponse: ${res.statusCode} -> ${res.contentLength} bytes', +// ); +// return res; +// } +// } + +// typedef Middleware = Handler Function(Handler innerHandler); + +// Middleware createMiddleware({ +// FutureOr Function(Request)? requestHandler, +// FutureOr Function(Response)? responseHandler, +// FutureOr Function(Object error, StackTrace)? errorHandler, +// }) { +// requestHandler ??= (request) => null; +// responseHandler ??= (response) => response; + +// FutureOr Function(Object, StackTrace)? onError; +// if (errorHandler != null) { +// onError = (error, stackTrace) { +// if (error is Exception) throw error; +// return errorHandler(error, stackTrace); +// }; +// } + +// return (Handler innerHandler) { +// return (request) { +// return Future.sync(() => requestHandler!(request)).then((response) { +// if (response != null) return response; + +// return Future.sync(() => innerHandler(request)) +// .then((response) => responseHandler!(response), onError: onError); +// }); +// }; +// }; +// } + +// extension MiddlewareX on Middleware { +// Middleware addMiddleware(Middleware other) => +// (Handler handler) => this(other(handler)); +// Handler addHandler(Handler handler) => this(handler); +// } + +// typedef Handler = FutureOr Function(Request request); + +// final headerMutator = createMiddleware( +// responseHandler: (response) { +// print(response.headers); +// return response; +// },); + +// class Pipeline { +// const Pipeline(); + +// Pipeline addMiddleware(Middleware middleware) => +// _Pipeline(middleware, addHandler); + +// Handler addHandler(Handler handler) => handler; + +// Middleware get middleware => addHandler; +// } + +// class _Pipeline extends Pipeline { +// final Middleware _middleware; +// final Middleware _parent; + +// _Pipeline(this._middleware, this._parent); + +// @override +// Handler addHandler(Handler handler) => _parent(_middleware(handler)); +// } + +Future main(List args) async { + final Pipeline pipeline1 = Pipeline() + .addMiddleware(SimpleLoggerMiddleware()) + .addMiddleware( + UriPrefixMiddleware( + protocol: Protocols.http, + authority: 'localhost:80', + ), + ) + .addMiddleware(BodyToJsonMiddleware()); + + final Pipeline pipeline2 = Pipeline().addMiddleware( + RefreshTokenMiddleware( + authorizationEndpoint: + 'http://localhost:80/api/v1/account/test?action=authorize', + tokenEndpoint: 'http://localhost:80/api/v1/account/test?action=refresh', + innerClientMiddlewares: pipeline1.middleware, + ), + ); + + final Pipeline pipeline = pipeline1 + pipeline2; + + print(pipeline.getLogic()); + final client = MiddlewareClient(pipeline); + final r = await client.post( + Uri.parse('/api/v1/account/test'), + body: { + 'email': 'test@test.fr', + }, + ); +} diff --git a/packages/wyatt_http_client/lib/src/middleware.dart b/packages/wyatt_http_client/lib/src/middleware.dart new file mode 100644 index 00000000..1c36442c --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middleware.dart @@ -0,0 +1,77 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/middleware_client.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/models/middleware_response.dart'; + +class Middleware { + Middleware? child; + + MiddlewareClient? _client; + + Middleware({ + this.child, + }); + + Middleware._({ + this.child, + MiddlewareClient? client, + }) : _client = client; + + String getName() => 'Middleware'; + + void setClient(MiddlewareClient? client) { + _client = client; + child?.setClient(client); + } + + Client? getClient() { + return _client?.inner; + } + + Middleware deepCopy() { + if (child != null) { + return Middleware._(child: child?.deepCopy(), client: _client); + } else { + return Middleware._(client: _client); + } + } + + void addChild(Middleware middleware) { + if (child != null) { + child?.addChild(middleware); + } else { + child = middleware; + } + } + + MiddlewareRequest onRequest( + MiddlewareRequest request, + ) { + return child?.onRequest(request) ?? request; + } + + MiddlewareResponse onResponse(MiddlewareResponse response) { + return child?.onResponse(response) ?? response; + } + + @override + String toString() { + return getName(); + } +} diff --git a/packages/wyatt_http_client/lib/src/middleware_client.dart b/packages/wyatt_http_client/lib/src/middleware_client.dart new file mode 100644 index 00000000..ce02e8da --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middleware_client.dart @@ -0,0 +1,117 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/models/middleware_response.dart'; +import 'package:wyatt_http_client/src/models/unfreezed_request.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; + +class MiddlewareClient extends BaseClient { + final Client inner; + final Middleware middleware; + final Pipeline pipeline; + + MiddlewareClient( + this.pipeline, { + Middleware? middleware, + Client? inner, + }) : inner = inner ?? Client(), + middleware = middleware ?? pipeline.middleware { + this.middleware.setClient(this); + } + + @override + Future head(Uri url, {Map? headers}) => + _sendUnstreamed('HEAD', url, headers); + + @override + Future get(Uri url, {Map? headers}) => + _sendUnstreamed('GET', url, headers); + + @override + Future post( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _sendUnstreamed('POST', url, headers, body, encoding); + + @override + Future put( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _sendUnstreamed('PUT', url, headers, body, encoding); + + @override + Future patch( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _sendUnstreamed('PATCH', url, headers, body, encoding); + + @override + Future delete( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _sendUnstreamed('DELETE', url, headers, body, encoding); + + @override + Future send(BaseRequest request) { + return inner.send(request); + } + + Future _sendUnstreamed( + String method, + Uri url, + Map? headers, [ + Object? body, + Encoding? encoding, + ]) async { + final modifiedRequest = middleware.onRequest( + MiddlewareRequest( + unfreezedRequest: UnfreezedRequest( + method: method, + url: url, + headers: headers, + body: body, + encoding: encoding, + ), + httpRequest: Request(method, url), + ), + ); + + final res = await Response.fromStream( + await send(modifiedRequest.httpRequest), + ); + final response = + middleware.onResponse(MiddlewareResponse(httpResponse: res)); + + return response.httpResponse as Response; + } +} diff --git a/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart new file mode 100644 index 00000000..99cc7bc8 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart @@ -0,0 +1,52 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:convert'; + +import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; + +class BodyToJsonMiddleware extends Middleware { + BodyToJsonMiddleware({ + super.child, + }); + + @override + String getName() => 'BodyToJsonMiddleware'; + + @override + MiddlewareRequest onRequest(MiddlewareRequest request) { + print( + 'BodyToJson::OnRequest: transforms body in json if Map then update ' + 'headers with right content-type', + ); + var newReq = request.unfreezedRequest; + final mutation = { + 'content-type': 'application/json; charset=utf-8', + }; + if (newReq.body is Map) { + Map? headers = newReq.headers; + if (headers != null) { + headers.addAll(mutation); + } else { + headers = mutation; + } + newReq = newReq.copyWith(body: jsonEncode(newReq.body), headers: headers); + request.updateUnfreezedRequest(newReq); + } + return super.onRequest(request); + } +} diff --git a/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart new file mode 100644 index 00000000..7acd70a0 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart @@ -0,0 +1,26 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:wyatt_http_client/src/middleware.dart'; + +class DefaultMiddleware extends Middleware { + DefaultMiddleware({ + super.child, + }); + + @override + String getName() => 'DefaultMiddleware'; +} diff --git a/packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart new file mode 100644 index 00000000..7394acb0 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart @@ -0,0 +1,72 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/middleware_client.dart'; +import 'package:wyatt_http_client/src/middlewares/default_middleware.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; + +class RefreshTokenMiddleware extends Middleware { + final String authorizationEndpoint; + final String tokenEndpoint; + + String? accessToken; + String? refreshToken; + + Middleware innerClientMiddlewares; + + RefreshTokenMiddleware({ + required this.authorizationEndpoint, + required this.tokenEndpoint, + Middleware? innerClientMiddlewares, + super.child, + }) : innerClientMiddlewares = innerClientMiddlewares ?? DefaultMiddleware(); + + @override + String getName() => 'RefreshTokenMiddleware'; + + @override + MiddlewareRequest onRequest(MiddlewareRequest request) { + print( + 'RefreshToken::OnRequest: accessToken: $accessToken', + ); + if (accessToken == null) { + // Refresh token + final pipeline = Pipeline().addMiddleware(innerClientMiddlewares); + print(pipeline.getLogic()); + final client = MiddlewareClient( + pipeline, + inner: getClient(), + ); + final _ = client.post(Uri.parse(tokenEndpoint)); + } + var newReq = request.unfreezedRequest; + final mutation = { + 'authorization': accessToken ?? '', + }; + Map? headers = newReq.headers; + if (headers != null) { + headers.addAll(mutation); + } else { + headers = mutation; + } + newReq = newReq.copyWith(headers: headers); + request.updateUnfreezedRequest(newReq); + + return super.onRequest(request); + } +} diff --git a/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart new file mode 100644 index 00000000..d3c26aa0 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart @@ -0,0 +1,47 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/models/middleware_response.dart'; + +class SimpleLoggerMiddleware extends Middleware { + SimpleLoggerMiddleware({ + super.child, + }); + + @override + String getName() => 'SimpleLoggerMiddleware'; + + @override + MiddlewareRequest onRequest(MiddlewareRequest request) { + print( + 'Logger::OnRequest: ${request.httpRequest.method} ' + '${request.httpRequest.url}', + ); + return super.onRequest(request); + } + + @override + MiddlewareResponse onResponse(MiddlewareResponse response) { + final res = super.onResponse(response); + print( + 'Logger::OnResponse: ${res.httpResponse.statusCode} -> ' + 'received ${res.httpResponse.contentLength} bytes', + ); + return res; + } +} diff --git a/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart new file mode 100644 index 00000000..9a8ec2dc --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart @@ -0,0 +1,42 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/utils/protocols.dart'; + +class UriPrefixMiddleware extends Middleware { + final Protocols protocol; + final String? authority; + + UriPrefixMiddleware({ + required this.protocol, + required this.authority, + super.child, + }); + + @override + String getName() => 'UriPrefixMiddleware'; + + @override + MiddlewareRequest onRequest(MiddlewareRequest request) { + final Uri uri = + Uri.parse('${protocol.scheme}$authority${request.httpRequest.url}'); + print('UriPrefix::OnRequest: ${request.httpRequest.url} -> $uri'); + request.updateHttpRequest(url: uri); + return super.onRequest(request); + } +} diff --git a/packages/wyatt_http_client/lib/src/models/middleware_request.dart b/packages/wyatt_http_client/lib/src/models/middleware_request.dart new file mode 100644 index 00000000..b55481ff --- /dev/null +++ b/packages/wyatt_http_client/lib/src/models/middleware_request.dart @@ -0,0 +1,89 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/models/unfreezed_request.dart'; +import 'package:wyatt_http_client/src/utils/utils.dart'; + +class MiddlewareRequest { + UnfreezedRequest unfreezedRequest; + Request httpRequest; + + MiddlewareRequest({ + required this.unfreezedRequest, + required this.httpRequest, + }); + + MiddlewareRequest copyWith({ + UnfreezedRequest? unfreezedRequest, + Request? httpRequest, + }) { + return MiddlewareRequest( + unfreezedRequest: unfreezedRequest ?? this.unfreezedRequest, + httpRequest: httpRequest ?? this.httpRequest, + ); + } + + void updateUnfreezedRequest(UnfreezedRequest unfreezedRequest) { + final request = httpRequest; + if (unfreezedRequest.headers != null) { + request.headers.addAll(unfreezedRequest.headers!); + } + if (unfreezedRequest.encoding != null) { + request.encoding = unfreezedRequest.encoding!; + } + if (unfreezedRequest.body != null) { + final body = unfreezedRequest.body; + if (body is String) { + request.body = body; + } else if (body is List) { + request.bodyBytes = body.cast(); + } else if (body is Map) { + request.bodyFields = body.cast(); + } else { + throw ArgumentError('Invalid request body "$body".'); + } + } + this.unfreezedRequest = unfreezedRequest; + httpRequest = request; + } + + void updateHttpRequest({ + String? method, + Uri? url, + Map? headers, + int? maxRedirects, + bool? followRedirects, + bool? persistentConnection, + String? body, + }) { + httpRequest = Utils.copyRequestWith( + httpRequest, + method: method, + url: url, + headers: headers, + maxRedirects: maxRedirects, + followRedirects: followRedirects, + persistentConnection: persistentConnection, + body: body, + ) as Request; + } + + @override + String toString() => 'MiddlewareRequest(unfreezedRequest: ' + '$unfreezedRequest, httpRequest: $httpRequest)'; +} diff --git a/packages/wyatt_http_client/lib/src/models/middleware_response.dart b/packages/wyatt_http_client/lib/src/models/middleware_response.dart new file mode 100644 index 00000000..5e67ae74 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/models/middleware_response.dart @@ -0,0 +1,37 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:http/http.dart'; + +class MiddlewareResponse { + BaseResponse httpResponse; + + MiddlewareResponse({ + required this.httpResponse, + }); + + MiddlewareResponse copyWith({ + BaseResponse? httpResponse, + }) { + return MiddlewareResponse( + httpResponse: httpResponse ?? this.httpResponse, + ); + } + + @override + String toString() => 'MiddlewareResponse(httpResponse: $httpResponse)'; +} diff --git a/packages/wyatt_http_client/lib/src/models/unfreezed_request.dart b/packages/wyatt_http_client/lib/src/models/unfreezed_request.dart new file mode 100644 index 00000000..9aa9790f --- /dev/null +++ b/packages/wyatt_http_client/lib/src/models/unfreezed_request.dart @@ -0,0 +1,55 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:convert'; + +class UnfreezedRequest { + final String method; + final Uri url; + final Map? headers; + final Object? body; + final Encoding? encoding; + + UnfreezedRequest({ + required this.method, + required this.url, + this.headers, + this.body, + this.encoding, + }); + + UnfreezedRequest copyWith({ + String? method, + Uri? url, + Map? headers, + Object? body, + Encoding? encoding, + }) { + return UnfreezedRequest( + method: method ?? this.method, + url: url ?? this.url, + headers: headers ?? this.headers, + body: body ?? this.body, + encoding: encoding ?? this.encoding, + ); + } + + @override + String toString() { + return 'UnfreezedRequest(method: $method, url: $url, headers: ' + '$headers, body: $body, encoding: $encoding)'; + } +} diff --git a/packages/wyatt_http_client/lib/src/pipeline.dart b/packages/wyatt_http_client/lib/src/pipeline.dart new file mode 100644 index 00000000..479c1f1b --- /dev/null +++ b/packages/wyatt_http_client/lib/src/pipeline.dart @@ -0,0 +1,57 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/middlewares/default_middleware.dart'; + +class Pipeline { + final Middleware _middleware; + + Pipeline() : _middleware = DefaultMiddleware(); + + Pipeline addMiddleware(Middleware middleware) { + _middleware.addChild(middleware); + return this; + } + + Middleware get middleware { + return _middleware; + } + + Pipeline operator +(Pipeline other) { + final copy = _middleware.deepCopy()..addChild(other.middleware); + return Pipeline()..addMiddleware(copy); + } + + String getLogic() { + final req = []; + final res = []; + Middleware? m = _middleware; + while (m != null) { + if (m is! DefaultMiddleware) { + req.add('$m'); + res.insert(0, '$m'); + } + m = m.child; + } + return '[Req] -> ${req.join(' -> ')}\n[Res] -> ${res.join(' -> ')}'; + } + + @override + String toString() { + return middleware.toString(); + } +} From 14cb3485e4c19e94e5c2074b7c2179cf5628274a Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Fri, 24 Jun 2022 10:47:28 +0200 Subject: [PATCH 3/4] feat(http): implements doublelinked list for middlewares --- .../example/http_client_fastapi_example.dart | 77 ++++--- .../wyatt_http_client/example/pipeline.dart | 25 +-- .../wyatt_http_client/lib/src/middleware.dart | 55 +---- .../lib/src/middleware_client.dart | 61 +++--- .../lib/src/middleware_node.dart | 103 +++++++++ .../middlewares/body_to_json_middleware.dart | 14 +- .../src/middlewares/default_middleware.dart | 6 +- .../middlewares/refresh_token_middleware.dart | 201 +++++++++++++++--- .../middlewares/simple_logger_middleware.dart | 23 +- .../middlewares/uri_prefix_middleware.dart | 9 +- .../lib/src/models/middleware_context.dart | 48 +++++ .../lib/src/models/middleware_request.dart | 12 +- .../lib/src/models/middleware_response.dart | 19 +- .../wyatt_http_client/lib/src/pipeline.dart | 132 ++++++++++-- .../lib/src/utils/http_methods.dart | 28 +++ .../lib/src/utils/http_status.dart | 174 +++++++++------ 16 files changed, 720 insertions(+), 267 deletions(-) create mode 100644 packages/wyatt_http_client/lib/src/middleware_node.dart create mode 100644 packages/wyatt_http_client/lib/src/models/middleware_context.dart create mode 100644 packages/wyatt_http_client/lib/src/utils/http_methods.dart diff --git a/packages/wyatt_http_client/example/http_client_fastapi_example.dart b/packages/wyatt_http_client/example/http_client_fastapi_example.dart index 21c13b42..93e40d3e 100644 --- a/packages/wyatt_http_client/example/http_client_fastapi_example.dart +++ b/packages/wyatt_http_client/example/http_client_fastapi_example.dart @@ -17,8 +17,13 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; -import 'package:wyatt_http_client/src/authentication/refresh_token_client.dart'; -import 'package:wyatt_http_client/src/rest_client.dart'; +import 'package:wyatt_http_client/src/middleware_client.dart'; +import 'package:wyatt_http_client/src/middlewares/body_to_json_middleware.dart'; +import 'package:wyatt_http_client/src/middlewares/refresh_token_middleware.dart'; +import 'package:wyatt_http_client/src/middlewares/simple_logger_middleware.dart'; +import 'package:wyatt_http_client/src/middlewares/uri_prefix_middleware.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; +import 'package:wyatt_http_client/src/utils/http_status.dart'; import 'package:wyatt_http_client/src/utils/protocols.dart'; enum EmailVerificationAction { @@ -262,24 +267,14 @@ class Login { class FastAPI { final String baseUrl; - final RefreshTokenClient client; + final MiddlewareClient client; final int apiVersion; FastAPI({ this.baseUrl = 'localhost:80', - RefreshTokenClient? client, + MiddlewareClient? client, this.apiVersion = 1, - }) : client = client ?? - RefreshTokenClient( - authorizationEndpoint: '', - tokenEndpoint: '', - accessTokenParser: (body) => body['access_token']! as String, - refreshTokenParser: (body) => body['refresh_token']! as String, - inner: RestClient( - protocol: Protocols.http, - authority: baseUrl, - ), - ); + }) : client = client ?? MiddlewareClient(); String get apiPath => '/api/v$apiVersion'; @@ -323,14 +318,21 @@ class FastAPI { } Future signInWithPassword(Login login) async { - final r = await client.authorize(login.toMap()); - return TokenSuccess.fromJson(r.body); + final r = await client.post( + Uri.parse('$apiPath/auth/sign-in-with-password'), + body: login.toMap(), + ); + if (r.statusCode != 200) { + throw Exception('Invalid reponse: ${r.statusCode}'); + } else { + return TokenSuccess.fromJson(r.body); + } } - Future refresh() async { - final r = await client.refresh(); - return TokenSuccess.fromJson(r?.body ?? ''); - } + // Future refresh() async { + // final r = await client.refresh(); + // return TokenSuccess.fromJson(r?.body ?? ''); + // } Future> getAccountList() async { final r = await client.get( @@ -351,17 +353,30 @@ class FastAPI { } void main(List args) async { + final Pipeline pipeline = Pipeline() + .addMiddleware(SimpleLoggerMiddleware()) + .addMiddleware( + UriPrefixMiddleware( + protocol: Protocols.http, + authority: 'localhost:80', + ), + ) + .addMiddleware(BodyToJsonMiddleware()) + .addMiddleware( + RefreshTokenMiddleware( + authorizationEndpoint: '/api/v1/auth/sign-in-with-password', + tokenEndpoint: '/api/v1/auth/refresh', + accessTokenParser: (body) => body['access_token']! as String, + refreshTokenParser: (body) => body['refresh_token']! as String, + unauthorized: HttpStatus.forbidden, + ), + ); + + print(pipeline.getLogic()); + final client = MiddlewareClient(pipeline: pipeline); + final api = FastAPI( - client: RefreshTokenClient( - authorizationEndpoint: '/api/v1/auth/sign-in-with-password', - tokenEndpoint: '/api/v1/auth/refresh', - accessTokenParser: (body) => body['access_token']! as String, - refreshTokenParser: (body) => body['refresh_token']! as String, - inner: RestClient( - protocol: Protocols.http, - authority: 'localhost:80', - ), - ), + client: client, ); // await api.sendSignUpCode('git@pcl.ovh'); diff --git a/packages/wyatt_http_client/example/pipeline.dart b/packages/wyatt_http_client/example/pipeline.dart index 1ee6cade..4b9fa9fe 100644 --- a/packages/wyatt_http_client/example/pipeline.dart +++ b/packages/wyatt_http_client/example/pipeline.dart @@ -117,7 +117,7 @@ import 'package:wyatt_http_client/src/utils/protocols.dart'; // } Future main(List args) async { - final Pipeline pipeline1 = Pipeline() + final Pipeline pipeline = Pipeline() .addMiddleware(SimpleLoggerMiddleware()) .addMiddleware( UriPrefixMiddleware( @@ -125,21 +125,18 @@ Future main(List args) async { authority: 'localhost:80', ), ) - .addMiddleware(BodyToJsonMiddleware()); - - final Pipeline pipeline2 = Pipeline().addMiddleware( - RefreshTokenMiddleware( - authorizationEndpoint: - 'http://localhost:80/api/v1/account/test?action=authorize', - tokenEndpoint: 'http://localhost:80/api/v1/account/test?action=refresh', - innerClientMiddlewares: pipeline1.middleware, - ), - ); - - final Pipeline pipeline = pipeline1 + pipeline2; + .addMiddleware(BodyToJsonMiddleware()) + .addMiddleware( + RefreshTokenMiddleware( + authorizationEndpoint: '/api/v1/account/test?action=authorize', + tokenEndpoint: '/api/v1/account/test?action=refresh', + accessTokenParser: (body) => body['access_token']! as String, + refreshTokenParser: (body) => body['refresh_token']! as String, + ), + ); print(pipeline.getLogic()); - final client = MiddlewareClient(pipeline); + final client = MiddlewareClient(pipeline: pipeline); final r = await client.post( Uri.parse('/api/v1/account/test'), body: { diff --git a/packages/wyatt_http_client/lib/src/middleware.dart b/packages/wyatt_http_client/lib/src/middleware.dart index 1c36442c..4295a278 100644 --- a/packages/wyatt_http_client/lib/src/middleware.dart +++ b/packages/wyatt_http_client/lib/src/middleware.dart @@ -14,60 +14,27 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/middleware_client.dart'; -import 'package:wyatt_http_client/src/models/middleware_request.dart'; -import 'package:wyatt_http_client/src/models/middleware_response.dart'; +part of 'pipeline.dart'; class Middleware { - Middleware? child; - + /// The http [MiddlewareClient] used by this [Middleware] MiddlewareClient? _client; - Middleware({ - this.child, - }); + String getName() => 'MiddlewareNode'; - Middleware._({ - this.child, - MiddlewareClient? client, - }) : _client = client; + // ignore: avoid_setters_without_getters + set httpClient(MiddlewareClient? client) => _client = client; - String getName() => 'Middleware'; + Client? get client => _client?.inner; - void setClient(MiddlewareClient? client) { - _client = client; - child?.setClient(client); - } - - Client? getClient() { - return _client?.inner; - } - - Middleware deepCopy() { - if (child != null) { - return Middleware._(child: child?.deepCopy(), client: _client); - } else { - return Middleware._(client: _client); - } - } - - void addChild(Middleware middleware) { - if (child != null) { - child?.addChild(middleware); - } else { - child = middleware; - } - } - - MiddlewareRequest onRequest( + Future onRequest( MiddlewareRequest request, - ) { - return child?.onRequest(request) ?? request; + ) async { + return request; } - MiddlewareResponse onResponse(MiddlewareResponse response) { - return child?.onResponse(response) ?? response; + Future onResponse(MiddlewareResponse response) async { + return response; } @override diff --git a/packages/wyatt_http_client/lib/src/middleware_client.dart b/packages/wyatt_http_client/lib/src/middleware_client.dart index ce02e8da..e6b0dd43 100644 --- a/packages/wyatt_http_client/lib/src/middleware_client.dart +++ b/packages/wyatt_http_client/lib/src/middleware_client.dart @@ -17,33 +17,32 @@ import 'dart:convert'; import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/models/middleware_context.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; import 'package:wyatt_http_client/src/models/middleware_response.dart'; import 'package:wyatt_http_client/src/models/unfreezed_request.dart'; import 'package:wyatt_http_client/src/pipeline.dart'; +import 'package:wyatt_http_client/src/utils/http_methods.dart'; class MiddlewareClient extends BaseClient { final Client inner; - final Middleware middleware; final Pipeline pipeline; - MiddlewareClient( - this.pipeline, { - Middleware? middleware, + MiddlewareClient({ + Pipeline? pipeline, Client? inner, - }) : inner = inner ?? Client(), - middleware = middleware ?? pipeline.middleware { - this.middleware.setClient(this); + }) : pipeline = pipeline ?? Pipeline(), + inner = inner ?? Client() { + this.pipeline.setClient(this); } @override Future head(Uri url, {Map? headers}) => - _sendUnstreamed('HEAD', url, headers); + _sendUnstreamed(HttpMethods.head.method, url, headers); @override Future get(Uri url, {Map? headers}) => - _sendUnstreamed('GET', url, headers); + _sendUnstreamed(HttpMethods.get.method, url, headers); @override Future post( @@ -52,7 +51,7 @@ class MiddlewareClient extends BaseClient { Object? body, Encoding? encoding, }) => - _sendUnstreamed('POST', url, headers, body, encoding); + _sendUnstreamed(HttpMethods.post.method, url, headers, body, encoding); @override Future put( @@ -61,7 +60,7 @@ class MiddlewareClient extends BaseClient { Object? body, Encoding? encoding, }) => - _sendUnstreamed('PUT', url, headers, body, encoding); + _sendUnstreamed(HttpMethods.put.method, url, headers, body, encoding); @override Future patch( @@ -70,7 +69,7 @@ class MiddlewareClient extends BaseClient { Object? body, Encoding? encoding, }) => - _sendUnstreamed('PATCH', url, headers, body, encoding); + _sendUnstreamed(HttpMethods.patch.method, url, headers, body, encoding); @override Future delete( @@ -79,7 +78,7 @@ class MiddlewareClient extends BaseClient { Object? body, Encoding? encoding, }) => - _sendUnstreamed('DELETE', url, headers, body, encoding); + _sendUnstreamed(HttpMethods.delete.method, url, headers, body, encoding); @override Future send(BaseRequest request) { @@ -93,24 +92,34 @@ class MiddlewareClient extends BaseClient { Object? body, Encoding? encoding, ]) async { - final modifiedRequest = middleware.onRequest( - MiddlewareRequest( - unfreezedRequest: UnfreezedRequest( - method: method, - url: url, - headers: headers, - body: body, - encoding: encoding, - ), - httpRequest: Request(method, url), + final originalRequest = MiddlewareRequest( + unfreezedRequest: UnfreezedRequest( + method: method, + url: url, + headers: headers, + body: body, + encoding: encoding, ), + httpRequest: Request(method, url), + context: MiddlewareContext(pipeline: pipeline), + ); + final modifiedRequest = await pipeline.onRequest( + originalRequest.copyWith(), ); final res = await Response.fromStream( await send(modifiedRequest.httpRequest), ); - final response = - middleware.onResponse(MiddlewareResponse(httpResponse: res)); + final response = await pipeline.onResponse( + MiddlewareResponse( + httpResponse: res, + middlewareRequest: modifiedRequest, + context: MiddlewareContext( + pipeline: pipeline, + originalRequest: originalRequest, + ), + ), + ); return response.httpResponse as Response; } diff --git a/packages/wyatt_http_client/lib/src/middleware_node.dart b/packages/wyatt_http_client/lib/src/middleware_node.dart new file mode 100644 index 00000000..32697e40 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middleware_node.dart @@ -0,0 +1,103 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +part of 'pipeline.dart'; + +class MiddlewareNode { + final Pipeline pipeline; + + Middleware? middleware; + + late MiddlewareNode _parent; + late MiddlewareNode _child; + final bool _isEnd; + + /// Reference to the previous [MiddlewareNode] in the [Pipeline] + MiddlewareNode get parent => _parent; + + /// Reference to the next [MiddlewareNode] in the [Pipeline] + MiddlewareNode get child => _child; + + /// Whether this is the begin [MiddlewareNode] + bool get isBegin => _parent == this; + + /// Whether this is the end [MiddlewareNode] + bool get isEnd => _child == this; + + /// Whether this is the first [MiddlewareNode] + bool get isFirst => !isBegin && _parent == pipeline.begin; + + /// Whether this is the last [MiddlewareNode] + bool get isLast => !isEnd && _child == pipeline.end; + + MiddlewareNode._( + this.pipeline, + this.middleware, { + MiddlewareNode? parent, + MiddlewareNode? child, + }) : _isEnd = false { + _parent = parent ?? this; + _child = child ?? this; + } + + MiddlewareNode._end(this.pipeline) : _isEnd = true { + _child = this; + } + + MiddlewareNode._begin(this.pipeline) : _isEnd = true { + _parent = this; + } + + /// Creates a new [MiddlewareNode] right **before** this in [pipeline] + MiddlewareNode insertBefore(Middleware middleware) { + if (isBegin) { + throw StateError( + 'A MiddlewareNode cannot be inserted ' + 'before begin MiddlewareNode', + ); + } + final newMiddlewareNode = + MiddlewareNode._(pipeline, middleware, parent: _parent, child: this); + _parent._child = newMiddlewareNode; + _parent = newMiddlewareNode; + pipeline._length++; + return newMiddlewareNode; + } + + /// Creates a new [MiddlewareNode] right **after** this in [pipeline] + MiddlewareNode insertAfter(Middleware middleware) { + if (isEnd) { + throw StateError( + 'A MiddlewareNode cannot be inserted ' + 'after end MiddlewareNode', + ); + } + final newMiddlewareNode = + MiddlewareNode._(pipeline, middleware, parent: this, child: _child); + _child._parent = newMiddlewareNode; + _child = newMiddlewareNode; + pipeline._length++; + return newMiddlewareNode; + } + + MiddlewareNode remove() { + if (_isEnd) throw StateError('Cannot remove end MiddlewareNode'); + _child._parent = _parent; + _parent._child = _child; + pipeline._length--; + return child; + } +} diff --git a/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart index 99cc7bc8..af124dc3 100644 --- a/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart @@ -16,21 +16,17 @@ import 'dart:convert'; -import 'package:wyatt_http_client/src/middleware.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; class BodyToJsonMiddleware extends Middleware { - BodyToJsonMiddleware({ - super.child, - }); + @override + String getName() => 'BodyToJson'; @override - String getName() => 'BodyToJsonMiddleware'; - - @override - MiddlewareRequest onRequest(MiddlewareRequest request) { + Future onRequest(MiddlewareRequest request) { print( - 'BodyToJson::OnRequest: transforms body in json if Map then update ' + '${getName()}::OnRequest: transforms body in json if Map then update ' 'headers with right content-type', ); var newReq = request.unfreezedRequest; diff --git a/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart index 7acd70a0..67c65728 100644 --- a/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart @@ -14,13 +14,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; class DefaultMiddleware extends Middleware { - DefaultMiddleware({ - super.child, - }); - @override String getName() => 'DefaultMiddleware'; } diff --git a/packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart index 7394acb0..02391f61 100644 --- a/packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart @@ -14,59 +14,200 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:wyatt_http_client/src/middleware.dart'; +import 'dart:convert'; + +import 'package:http/http.dart'; import 'package:wyatt_http_client/src/middleware_client.dart'; -import 'package:wyatt_http_client/src/middlewares/default_middleware.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/models/middleware_response.dart'; import 'package:wyatt_http_client/src/pipeline.dart'; +import 'package:wyatt_http_client/src/utils/authentication_methods.dart'; +import 'package:wyatt_http_client/src/utils/header_keys.dart'; +import 'package:wyatt_http_client/src/utils/http_status.dart'; + +typedef TokenParser = String Function(Map); class RefreshTokenMiddleware extends Middleware { final String authorizationEndpoint; final String tokenEndpoint; String? accessToken; + final TokenParser accessTokenParser; String? refreshToken; + final TokenParser refreshTokenParser; - Middleware innerClientMiddlewares; + final String authenticationHeader; + final String authenticationMethod; + final HttpStatus unauthorized; + final int maxRetries; RefreshTokenMiddleware({ required this.authorizationEndpoint, required this.tokenEndpoint, - Middleware? innerClientMiddlewares, - super.child, - }) : innerClientMiddlewares = innerClientMiddlewares ?? DefaultMiddleware(); + required this.accessTokenParser, + required this.refreshTokenParser, + this.authenticationHeader = HeaderKeys.authorization, + this.authenticationMethod = AuthenticationMethods.bearer, + this.unauthorized = HttpStatus.unauthorized, + this.maxRetries = 3, + }); @override - String getName() => 'RefreshTokenMiddleware'; + String getName() => 'RefreshToken'; @override - MiddlewareRequest onRequest(MiddlewareRequest request) { + Future onRequest(MiddlewareRequest request) async { print( - 'RefreshToken::OnRequest: accessToken: $accessToken', + '${getName()}::OnRequest: accessToken: $accessToken', ); - if (accessToken == null) { - // Refresh token - final pipeline = Pipeline().addMiddleware(innerClientMiddlewares); - print(pipeline.getLogic()); - final client = MiddlewareClient( - pipeline, - inner: getClient(), + if (request.context.originalRequest?.unfreezedRequest.url == + Uri.parse(authorizationEndpoint)) { + return super.onRequest(request); + } + if (accessToken != null) { + // Modify header with accessToken + var newReq = request.unfreezedRequest; + final mutation = { + authenticationHeader: '$authenticationMethod $accessToken', + }; + Map? headers = newReq.headers; + if (headers != null) { + headers.addAll(mutation); + } else { + headers = mutation; + } + newReq = newReq.copyWith(headers: headers); + request.updateUnfreezedRequest(newReq); + return super.onRequest(request); + } + if (refreshToken != null) { + // Refresh accessToken with refreshToken before perform request + final subPipeline = request.context.pipeline.fromUntil(this); + final httpClient = MiddlewareClient( + pipeline: subPipeline, + inner: client, ); - final _ = client.post(Uri.parse(tokenEndpoint)); - } - var newReq = request.unfreezedRequest; - final mutation = { - 'authorization': accessToken ?? '', - }; - Map? headers = newReq.headers; - if (headers != null) { - headers.addAll(mutation); - } else { - headers = mutation; - } - newReq = newReq.copyWith(headers: headers); - request.updateUnfreezedRequest(newReq); + final Map headers = { + authenticationHeader: '$authenticationHeader $refreshToken', + }; + final response = + await httpClient.get(Uri.parse(tokenEndpoint), headers: headers); + final status = HttpStatus.from(response.statusCode); + if (status.isSuccess()) { + final body = jsonDecode(response.body) as Map; + accessToken = accessTokenParser(body); + // Then modify current request with accessToken + var newReq = request.unfreezedRequest; + final mutation = { + authenticationHeader: '$authenticationMethod $accessToken', + }; + Map? headers = newReq.headers; + if (headers != null) { + headers.addAll(mutation); + } else { + headers = mutation; + } + newReq = newReq.copyWith(headers: headers); + request.updateUnfreezedRequest(newReq); + } else { + // Retry + int retries = 0; + while (retries < maxRetries) { + final Map headers = { + authenticationHeader: '$authenticationHeader $refreshToken', + }; + final response = + await httpClient.get(Uri.parse(tokenEndpoint), headers: headers); + final status = HttpStatus.from(response.statusCode); + if (status.isSuccess()) { + final body = jsonDecode(response.body) as Map; + accessToken = accessTokenParser(body); + + // Then modify current request with accessToken + var newReq = request.unfreezedRequest; + final mutation = { + authenticationHeader: '$authenticationMethod $accessToken', + }; + Map? headers = newReq.headers; + if (headers != null) { + headers.addAll(mutation); + } else { + headers = mutation; + } + newReq = newReq.copyWith(headers: headers); + request.updateUnfreezedRequest(newReq); + break; + } + retries++; + } + } + return super.onRequest(request); + } + // Pass return super.onRequest(request); } + + @override + Future onResponse(MiddlewareResponse response) async { + final res = await super.onResponse(response); + final status = HttpStatus.from(res.httpResponse.statusCode); + if (res.context.originalRequest?.unfreezedRequest.url == + Uri.parse(authorizationEndpoint)) { + if (status.isSuccess()) { + final body = jsonDecode((res.httpResponse as Response).body) as Map; + final accessToken = accessTokenParser(body); + final refreshToken = refreshTokenParser(body); + + if (accessToken.isNotEmpty) { + this.accessToken = accessToken; + } + if (refreshToken.isNotEmpty) { + this.refreshToken = refreshToken; + } + } + return res; + } + if (status == unauthorized) { + print( + '${getName()}::OnResponse: $unauthorized', + ); + // Refresh token then retry + final subPipeline = res.context.pipeline.fromUntil(this); + final httpClient = MiddlewareClient( + pipeline: subPipeline, + inner: client, + ); + final Map headers = { + authenticationHeader: '$authenticationHeader $refreshToken', + }; + final response = + await httpClient.get(Uri.parse(tokenEndpoint), headers: headers); + final refreshstatus = HttpStatus.from(response.statusCode); + if (refreshstatus.isSuccess()) { + print( + '${getName()}::OnResponse: refresh successfuly', + ); + final body = jsonDecode(response.body) as Map; + accessToken = accessTokenParser(body); + + // Then modify current request with accessToken + final midReq = res.middlewareRequest; + final newReq = midReq.httpRequest; + final mutation = { + authenticationHeader: '$authenticationMethod $accessToken', + }; + Map? headers = newReq.headers; + if (headers != null) { + headers.addAll(mutation); + } else { + headers = mutation; + } + midReq.updateHttpRequest(headers: headers); + final newRes = await httpClient.send(midReq.httpRequest); + return res.copyWith(httpResponse: res as Response); + } + } + return res; + } } diff --git a/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart index d3c26aa0..6118e03b 100644 --- a/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart @@ -14,32 +14,29 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:wyatt_http_client/src/middleware.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; import 'package:wyatt_http_client/src/models/middleware_response.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; class SimpleLoggerMiddleware extends Middleware { - SimpleLoggerMiddleware({ - super.child, - }); + @override + String getName() => 'SimpleLogger'; @override - String getName() => 'SimpleLoggerMiddleware'; - - @override - MiddlewareRequest onRequest(MiddlewareRequest request) { + Future onRequest(MiddlewareRequest request) { print( - 'Logger::OnRequest: ${request.httpRequest.method} ' - '${request.httpRequest.url}', + '${getName()}::OnRequest: ${request.httpRequest.method} ' + '${request.httpRequest.url}\n${request.unfreezedRequest.headers}' + '\n>> ${request.unfreezedRequest.body}', ); return super.onRequest(request); } @override - MiddlewareResponse onResponse(MiddlewareResponse response) { - final res = super.onResponse(response); + Future onResponse(MiddlewareResponse response) async { + final res = await super.onResponse(response); print( - 'Logger::OnResponse: ${res.httpResponse.statusCode} -> ' + '${getName()}::OnResponse: ${res.httpResponse.statusCode} -> ' 'received ${res.httpResponse.contentLength} bytes', ); return res; diff --git a/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart index 9a8ec2dc..b83e995f 100644 --- a/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart @@ -14,8 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:wyatt_http_client/src/middleware.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; import 'package:wyatt_http_client/src/utils/protocols.dart'; class UriPrefixMiddleware extends Middleware { @@ -25,17 +25,16 @@ class UriPrefixMiddleware extends Middleware { UriPrefixMiddleware({ required this.protocol, required this.authority, - super.child, }); @override - String getName() => 'UriPrefixMiddleware'; + String getName() => 'UriPrefix'; @override - MiddlewareRequest onRequest(MiddlewareRequest request) { + Future onRequest(MiddlewareRequest request) { final Uri uri = Uri.parse('${protocol.scheme}$authority${request.httpRequest.url}'); - print('UriPrefix::OnRequest: ${request.httpRequest.url} -> $uri'); + print('${getName()}::OnRequest: ${request.httpRequest.url} -> $uri'); request.updateHttpRequest(url: uri); return super.onRequest(request); } diff --git a/packages/wyatt_http_client/lib/src/models/middleware_context.dart b/packages/wyatt_http_client/lib/src/models/middleware_context.dart new file mode 100644 index 00000000..e00f6395 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/models/middleware_context.dart @@ -0,0 +1,48 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/models/middleware_response.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; + +class MiddlewareContext { + Pipeline pipeline; + MiddlewareRequest? originalRequest; + MiddlewareResponse? originalResponse; + + MiddlewareContext({ + required this.pipeline, + this.originalRequest, + this.originalResponse, + }); + + MiddlewareContext copyWith({ + Pipeline? pipeline, + MiddlewareRequest? originalRequest, + MiddlewareResponse? originalResponse, + }) { + return MiddlewareContext( + pipeline: pipeline ?? this.pipeline, + originalRequest: originalRequest ?? this.originalRequest, + originalResponse: originalResponse ?? this.originalResponse, + ); + } + + @override + String toString() => 'MiddlewareContext(pipeline: $pipeline, ' + 'originalRequest: $originalRequest, originalResponse: $originalResponse)'; +} diff --git a/packages/wyatt_http_client/lib/src/models/middleware_request.dart b/packages/wyatt_http_client/lib/src/models/middleware_request.dart index b55481ff..b1355774 100644 --- a/packages/wyatt_http_client/lib/src/models/middleware_request.dart +++ b/packages/wyatt_http_client/lib/src/models/middleware_request.dart @@ -16,25 +16,33 @@ // along with this program. If not, see . import 'package:http/http.dart'; + +import 'package:wyatt_http_client/src/models/middleware_context.dart'; import 'package:wyatt_http_client/src/models/unfreezed_request.dart'; import 'package:wyatt_http_client/src/utils/utils.dart'; class MiddlewareRequest { UnfreezedRequest unfreezedRequest; Request httpRequest; + MiddlewareContext context; MiddlewareRequest({ required this.unfreezedRequest, required this.httpRequest, - }); + required this.context, + }) { + context = context.copyWith(originalRequest: this); + } MiddlewareRequest copyWith({ UnfreezedRequest? unfreezedRequest, Request? httpRequest, + MiddlewareContext? context, }) { return MiddlewareRequest( unfreezedRequest: unfreezedRequest ?? this.unfreezedRequest, httpRequest: httpRequest ?? this.httpRequest, + context: context ?? this.context, ); } @@ -85,5 +93,5 @@ class MiddlewareRequest { @override String toString() => 'MiddlewareRequest(unfreezedRequest: ' - '$unfreezedRequest, httpRequest: $httpRequest)'; + '$unfreezedRequest, httpRequest: $httpRequest, context: $context)'; } diff --git a/packages/wyatt_http_client/lib/src/models/middleware_response.dart b/packages/wyatt_http_client/lib/src/models/middleware_response.dart index 5e67ae74..0645e776 100644 --- a/packages/wyatt_http_client/lib/src/models/middleware_response.dart +++ b/packages/wyatt_http_client/lib/src/models/middleware_response.dart @@ -16,22 +16,35 @@ // along with this program. If not, see . import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/models/middleware_context.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; class MiddlewareResponse { BaseResponse httpResponse; - + MiddlewareRequest middlewareRequest; + MiddlewareContext context; + MiddlewareResponse({ required this.httpResponse, - }); + required this.middlewareRequest, + required this.context, + }) { + context = context.copyWith(originalResponse: this); + } MiddlewareResponse copyWith({ BaseResponse? httpResponse, + MiddlewareRequest? middlewareRequest, + MiddlewareContext? context, }) { return MiddlewareResponse( httpResponse: httpResponse ?? this.httpResponse, + middlewareRequest: middlewareRequest ?? this.middlewareRequest, + context: context ?? this.context, ); } @override - String toString() => 'MiddlewareResponse(httpResponse: $httpResponse)'; + String toString() => 'MiddlewareResponse(httpResponse: $httpResponse, ' + 'middlewareRequest: $middlewareRequest, context: $context)'; } diff --git a/packages/wyatt_http_client/lib/src/pipeline.dart b/packages/wyatt_http_client/lib/src/pipeline.dart index 479c1f1b..0d2b1477 100644 --- a/packages/wyatt_http_client/lib/src/pipeline.dart +++ b/packages/wyatt_http_client/lib/src/pipeline.dart @@ -14,44 +14,140 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:wyatt_http_client/src/middleware.dart'; -import 'package:wyatt_http_client/src/middlewares/default_middleware.dart'; +import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/middleware_client.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/models/middleware_response.dart'; + +part 'middleware.dart'; +part 'middleware_node.dart'; class Pipeline { - final Middleware _middleware; + int _length = 0; + late final MiddlewareNode begin; + late final MiddlewareNode end; - Pipeline() : _middleware = DefaultMiddleware(); + MiddlewareNode get first => begin.child; + MiddlewareNode get last => end.parent; + bool get isEmpty => _length == 0; + bool get isNotEmpty => !isEmpty; + int get length => _length; + + Pipeline() { + _initialize(); + } + + Pipeline.fromIterable(Iterable list) { + _initialize(); + MiddlewareNode parent = begin; + for (final element in list) { + if (element != null) { + parent = parent.insertAfter(element); + } + } + } + + Pipeline.from(Pipeline pipeline) + : this.fromIterable(pipeline.middlewares.map((node) => node.middleware)); + + void _initialize() { + _length = 0; + begin = MiddlewareNode._begin(this); + end = MiddlewareNode._end(this); + begin._child = end; + end._parent = begin; + } + + Iterable get middlewares sync* { + for (var middleware = first; + middleware != end; + middleware = middleware.child) { + yield middleware; + } + } + + int indexOf(MiddlewareNode node) { + int i = -1; + int j = -1; + for (final element in middlewares) { + j++; + if (element == node) { + i = j; + continue; + } + } + return i; + } + + int indexOfChild(Middleware middleware) { + int i = -1; + int j = -1; + for (final element in middlewares) { + j++; + if (element.middleware == middleware) { + i = j; + continue; + } + } + return i; + } + + /// Create new [Pipeline] from first [Middleware] to the specified one. + Pipeline fromUntil(Middleware middleware, {bool include = false}) { + final nodes = []; + for (final element in middlewares) { + if (element.middleware != middleware) { + nodes.add(element.middleware); + } + if (element.middleware == middleware) { + if (include) { + nodes.add(element.middleware); + } + break; + } + } + return Pipeline.fromIterable(nodes); + } Pipeline addMiddleware(Middleware middleware) { - _middleware.addChild(middleware); + last.insertAfter(middleware); return this; } - Middleware get middleware { - return _middleware; + void setClient(MiddlewareClient? client) { + for (var node = first; node != end; node = node.child) { + node.middleware?.httpClient = client; + } } - Pipeline operator +(Pipeline other) { - final copy = _middleware.deepCopy()..addChild(other.middleware); - return Pipeline()..addMiddleware(copy); + Future onRequest(MiddlewareRequest request) async { + MiddlewareRequest req = request; + for (var node = first; node != end; node = node.child) { + req = await node.middleware?.onRequest(req) ?? req; + } + return req; + } + + Future onResponse(MiddlewareResponse response) async { + MiddlewareResponse res = response; + for (var node = last; node != begin; node = node.parent) { + res = await node.middleware?.onResponse(res) ?? res; + } + return res; } String getLogic() { final req = []; final res = []; - Middleware? m = _middleware; - while (m != null) { - if (m is! DefaultMiddleware) { - req.add('$m'); - res.insert(0, '$m'); - } - m = m.child; + for (final m in middlewares) { + req.add('${m.middleware}'); + res.insert(0, '${m.middleware}'); } return '[Req] -> ${req.join(' -> ')}\n[Res] -> ${res.join(' -> ')}'; } @override String toString() { - return middleware.toString(); + return getLogic(); } } diff --git a/packages/wyatt_http_client/lib/src/utils/http_methods.dart b/packages/wyatt_http_client/lib/src/utils/http_methods.dart new file mode 100644 index 00000000..49087965 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/utils/http_methods.dart @@ -0,0 +1,28 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +enum HttpMethods { + head('HEAD'), + get('GET'), + post('POST'), + put('PUT'), + patch('PATCH'), + delete('DELETE'); + + final String method; + + const HttpMethods(this.method); +} diff --git a/packages/wyatt_http_client/lib/src/utils/http_status.dart b/packages/wyatt_http_client/lib/src/utils/http_status.dart index 931c3587..c5715c70 100644 --- a/packages/wyatt_http_client/lib/src/utils/http_status.dart +++ b/packages/wyatt_http_client/lib/src/utils/http_status.dart @@ -1,83 +1,123 @@ // Copyright (C) 2022 WYATT GROUP // Please see the AUTHORS file for details. -// +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with this program. If not, see . /// Status codes for HTTP responses. Extracted from dart:io -abstract class HttpStatus { - static const int continue_ = 100; - static const int switchingProtocols = 101; - static const int processing = 102; - static const int ok = 200; - static const int created = 201; - static const int accepted = 202; - static const int nonAuthoritativeInformation = 203; - static const int noContent = 204; - static const int resetContent = 205; - static const int partialContent = 206; - static const int multiStatus = 207; - static const int alreadyReported = 208; - static const int imUsed = 226; - static const int multipleChoices = 300; - static const int movedPermanently = 301; - static const int found = 302; - static const int movedTemporarily = 302; // Common alias for found. - static const int seeOther = 303; - static const int notModified = 304; - static const int useProxy = 305; - static const int temporaryRedirect = 307; - static const int permanentRedirect = 308; - static const int badRequest = 400; - static const int unauthorized = 401; - static const int paymentRequired = 402; - static const int forbidden = 403; - static const int notFound = 404; - static const int methodNotAllowed = 405; - static const int notAcceptable = 406; - static const int proxyAuthenticationRequired = 407; - static const int requestTimeout = 408; - static const int conflict = 409; - static const int gone = 410; - static const int lengthRequired = 411; - static const int preconditionFailed = 412; - static const int requestEntityTooLarge = 413; - static const int requestUriTooLong = 414; - static const int unsupportedMediaType = 415; - static const int requestedRangeNotSatisfiable = 416; - static const int expectationFailed = 417; - static const int misdirectedRequest = 421; - static const int unprocessableEntity = 422; - static const int locked = 423; - static const int failedDependency = 424; - static const int upgradeRequired = 426; - static const int preconditionRequired = 428; - static const int tooManyRequests = 429; - static const int requestHeaderFieldsTooLarge = 431; - static const int connectionClosedWithoutResponse = 444; - static const int unavailableForLegalReasons = 451; - static const int clientClosedRequest = 499; - static const int internalServerError = 500; - static const int notImplemented = 501; - static const int badGateway = 502; - static const int serviceUnavailable = 503; - static const int gatewayTimeout = 504; - static const int httpVersionNotSupported = 505; - static const int variantAlsoNegotiates = 506; - static const int insufficientStorage = 507; - static const int loopDetected = 508; - static const int notExtended = 510; - static const int networkAuthenticationRequired = 511; +enum HttpStatus { + continue_(100), + switchingProtocols(101), + processing(102), + ok(200), + created(201), + accepted(202), + nonAuthoritativeInformation(203), + noContent(204), + resetContent(205), + partialContent(206), + multiStatus(207), + alreadyReported(208), + imUsed(226), + multipleChoices(300), + movedPermanently(301), + found(302), + movedTemporarily(302), // Common alias for found. + seeOther(303), + notModified(304), + useProxy(305), + temporaryRedirect(307), + permanentRedirect(308), + badRequest(400), + unauthorized(401), + paymentRequired(402), + forbidden(403), + notFound(404), + methodNotAllowed(405), + notAcceptable(406), + proxyAuthenticationRequired(407), + requestTimeout(408), + conflict(409), + gone(410), + lengthRequired(411), + preconditionFailed(412), + requestEntityTooLarge(413), + requestUriTooLong(414), + unsupportedMediaType(415), + requestedRangeNotSatisfiable(416), + expectationFailed(417), + misdirectedRequest(421), + unprocessableEntity(422), + locked(423), + failedDependency(424), + upgradeRequired(426), + preconditionRequired(428), + tooManyRequests(429), + requestHeaderFieldsTooLarge(431), + connectionClosedWithoutResponse(444), + unavailableForLegalReasons(451), + clientClosedRequest(499), + internalServerError(500), + notImplemented(501), + badGateway(502), + serviceUnavailable(503), + gatewayTimeout(504), + httpVersionNotSupported(505), + variantAlsoNegotiates(506), + insufficientStorage(507), + loopDetected(508), + notExtended(510), + networkAuthenticationRequired(511), // Client generated status code. - static const int networkConnectTimeoutError = 599; + networkConnectTimeoutError(599); + + final int statusCode; + + const HttpStatus(this.statusCode); + + bool equals(Object other) { + if (other is HttpStatus) { + return statusCode == other.statusCode; + } + if (other is int) { + return statusCode == other; + } + return false; + } + + bool isInfo() { + return statusCode >= 100 && statusCode < 200; + } + + bool isSuccess() { + return statusCode >= 200 && statusCode < 300; + } + + bool isRedirection() { + return statusCode >= 300 && statusCode < 400; + } + + bool isClientError() { + return statusCode >= 400 && statusCode < 500; + } + + bool isServerError() { + return statusCode >= 500 && statusCode < 600; + } + + factory HttpStatus.from(int status) { + return HttpStatus.values + .firstWhere((element) => element.statusCode == status); + } + } From 81367e5f3af1251f8355f209c23d6e6f9e1a178f Mon Sep 17 00:00:00 2001 From: Hugo Pointcheval Date: Fri, 24 Jun 2022 15:58:48 +0200 Subject: [PATCH 4/4] feat(http): add new middleware feature --- packages/wyatt_http_client/README.md | 187 ++++++++++++-- .../example/http_client_example.dart | 124 ++++++---- .../example/http_client_fastapi_example.dart | 35 ++- .../example/http_client_test.dart | 41 --- .../wyatt_http_client/example/pipeline.dart | 36 ++- .../basic_authentication_client.dart | 67 ----- .../bearer_authentication_client.dart | 63 ----- .../digest_authentication_client.dart | 75 ------ .../interfaces/authenticated_client.dart | 87 ------- .../interfaces/authentication_client.dart | 234 ------------------ .../header_authentication_client.dart | 37 --- .../authentication/refresh_token_client.dart | 195 --------------- .../lib/src/implemented_client.dart | 86 ------- .../wyatt_http_client/lib/src/middleware.dart | 46 ++-- .../lib/src/middleware_client.dart | 33 +-- .../lib/src/middleware_node.dart | 103 -------- .../access_token_auth_middleware.dart} | 21 +- .../middlewares/basic_auth_middleware.dart | 59 +++++ .../middlewares/body_to_json_middleware.dart | 31 +-- .../src/middlewares/default_middleware.dart | 4 +- .../middlewares/digest_auth_middleware.dart | 92 +++++++ .../middlewares.dart} | 29 +-- .../refresh_token_auth_middleware.dart | 191 ++++++++++++++ .../middlewares/refresh_token_middleware.dart | 213 ---------------- .../middlewares/simple_logger_middleware.dart | 34 ++- .../unsafe_auth_middleware.dart} | 41 +-- .../middlewares/uri_prefix_middleware.dart | 23 +- .../lib/src/mixins/body_transformer.dart | 83 ------- .../lib/src/mixins/headers_transformer.dart | 131 ---------- .../lib/src/mixins/oauth2_transformer.dart | 33 --- .../lib/src/models/middleware_context.dart | 18 +- .../lib/src/models/middleware_request.dart | 83 +++---- .../lib/src/models/middleware_response.dart | 32 +-- .../models.dart} | 21 +- .../wyatt_http_client/lib/src/pipeline.dart | 166 +++++-------- .../lib/src/rest_client.dart | 190 -------------- .../lib/src/utils/convert.dart | 11 + .../oauth2_client.dart => utils/delay.dart} | 31 +-- .../lib/src/utils/protocols.dart | 1 - .../lib/src/utils/request_utils.dart | 89 +++++++ .../lib/src/utils/utils.dart | 86 +------ .../lib/wyatt_http_client.dart | 13 +- 42 files changed, 1025 insertions(+), 2150 deletions(-) delete mode 100644 packages/wyatt_http_client/example/http_client_test.dart delete mode 100644 packages/wyatt_http_client/lib/src/authentication/basic_authentication_client.dart delete mode 100644 packages/wyatt_http_client/lib/src/authentication/bearer_authentication_client.dart delete mode 100644 packages/wyatt_http_client/lib/src/authentication/digest_authentication_client.dart delete mode 100644 packages/wyatt_http_client/lib/src/authentication/interfaces/authenticated_client.dart delete mode 100644 packages/wyatt_http_client/lib/src/authentication/interfaces/authentication_client.dart delete mode 100644 packages/wyatt_http_client/lib/src/authentication/interfaces/header_authentication_client.dart delete mode 100644 packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart delete mode 100644 packages/wyatt_http_client/lib/src/implemented_client.dart delete mode 100644 packages/wyatt_http_client/lib/src/middleware_node.dart rename packages/wyatt_http_client/lib/src/{implemented_base_client.dart => middlewares/access_token_auth_middleware.dart} (69%) create mode 100644 packages/wyatt_http_client/lib/src/middlewares/basic_auth_middleware.dart create mode 100644 packages/wyatt_http_client/lib/src/middlewares/digest_auth_middleware.dart rename packages/wyatt_http_client/lib/src/{authentication/interfaces/url_authentication_client.dart => middlewares/middlewares.dart} (60%) create mode 100644 packages/wyatt_http_client/lib/src/middlewares/refresh_token_auth_middleware.dart delete mode 100644 packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart rename packages/wyatt_http_client/lib/src/{authentication/unsafe_authentication_client.dart => middlewares/unsafe_auth_middleware.dart} (53%) delete mode 100644 packages/wyatt_http_client/lib/src/mixins/body_transformer.dart delete mode 100644 packages/wyatt_http_client/lib/src/mixins/headers_transformer.dart delete mode 100644 packages/wyatt_http_client/lib/src/mixins/oauth2_transformer.dart rename packages/wyatt_http_client/lib/src/{mixins/request_transformer.dart => models/models.dart} (67%) delete mode 100644 packages/wyatt_http_client/lib/src/rest_client.dart rename packages/wyatt_http_client/lib/src/{authentication/interfaces/oauth2_client.dart => utils/delay.dart} (52%) create mode 100644 packages/wyatt_http_client/lib/src/utils/request_utils.dart diff --git a/packages/wyatt_http_client/README.md b/packages/wyatt_http_client/README.md index 8b55e735..ee2b78d5 100644 --- a/packages/wyatt_http_client/README.md +++ b/packages/wyatt_http_client/README.md @@ -1,39 +1,182 @@ - -TODO: Put a short description of the package here that helps potential users -know whether this package might be useful for them. +# Dart - HTTP Client -## Features +

+ + Style: Wyatt Analysis + + SDK: Dart & Flutter +

-TODO: List what your package can do. Maybe include images, gifs, or videos. +HTTP Client for Dart with Middlewares ! ## Getting started -TODO: List prerequisites and provide or point to information on how to -start using the package. +Simply add wyatt_http_client in pubspec.yaml, then + +```dart +import 'package:wyatt_http_client/wyatt_http_client.dart'; +``` ## Usage -TODO: Include short and useful examples for package users. Add longer examples -to `/example` folder. +Firstly you have to understand **Middleware** and **Pipeline** concepts. + +In `wyatt_http_client` a middleware is an object where requests and responses +pass through. And a pipeline is basicaly a list of middlewares. + +In a pipeline with middlewares A and B, if request pass through A, then B, +the response will pass through B then A. + +> You can `print(pipeline)` to get full process order of a pipeline. + +For example, if you want to log every request, and simplify an url you can use provided `SimpleLogger` and `UriPrefix` . ```dart -const like = 'sample'; +// Create the Pipeline +final Pipeline pipeline = Pipeline() + .addMiddleware( + UriPrefixMiddleware( + protocol: Protocols.http, + authority: 'localhost:80', + ), + ) + .addMiddleware(SimpleLoggerMiddleware()); ``` -## Additional information +Then if you print the pipeline, -TODO: Tell users more about the package: where to find more information, how to -contribute to the package, how to file issues, what response they can expect -from the package authors, and more. +``` +[Req] -> UriPrefix -> SimpleLogger +[Res] -> SimpleLogger +``` + +> The `response` doesn't pass through `UriPrefix` because it's an `OnRequestMiddleware` only. + +And you can create a client. + +```dart +final client = MiddlewareClient(pipeline: pipeline); +``` + +At this point you can use `client` like every Client from `package:http/http.dart` . + +## Recipes + +### Rest API with URL Authentication + +Let's build a client for a REST API where the (bad) authentication is through the URL. +We need some middlewares: + +* SimpleLogger, to log every request and response (useful for debug). +* BodyToJson, to automaticaly transform Map object to JSON. +* UriPrefix, to simplify the build of an API Object (save protocol and API prefix). +* UnsafeAuth, to use url based authentication. + +Let's start by creating the Pipeline: + +```dart +final Pipeline pipeline = Pipeline() + .addMiddleware( + UriPrefixMiddleware( + protocol: Protocols.http, + authority: 'localhost:80', + ), + ) + .addMiddleware(BodyToJsonMiddleware()) + .addMiddleware( + UnsafeAuthMiddleware( + username: 'wyatt', + password: 'motdepasse', + ), + ) + .addMiddleware(SimpleLoggerMiddleware()); +``` + +Then simply create a client and make a call. + +```dart +final client = MiddlewareClient(pipeline: pipeline); + +await client.get(Uri.parse('/protected')); +``` + +> Here it make a `GET` call on `http://localhost:80/protected?username=wyatt&password=motdepasse` + +And voilà. + +### Rest API with Oauth2 + +So now we want a real authentication. + +```dart +final Pipeline pipeline = Pipeline() + .addMiddleware( + UriPrefixMiddleware( + protocol: Protocols.http, + authority: 'localhost:80', + ), + ) + .addMiddleware(BodyToJsonMiddleware()) + .addMiddleware( + RefreshTokenAuthMiddleware( + authorizationEndpoint: '/auth/sign-in', + tokenEndpoint: '/auth/refresh', + accessTokenParser: (body) => body['access_token']! as String, + refreshTokenParser: (body) => body['refresh_token']! as String, + unauthorized: HttpStatus.forbidden, + ), + ) + .addMiddleware(SimpleLoggerMiddleware()); +``` + +> Here we just change `UnsafeAuthMiddleware` by `RefreshTokenAuthMiddleware` and the whole app while adapt to a new authentication system. + +### Create a new Middleware + +You can create your own middleware by implementing `Middleware` class, and use mixins to add `OnRequest` and/or `OnResponse` methods. + +```dart +class SimpleLoggerMiddleware + with OnRequestMiddleware, OnResponseMiddleware + implements Middleware { + + @override + String getName() => 'SimpleLogger'; + + @override + Future onRequest( + MiddlewareContext context, + MiddlewareRequest request, + ) async { + print('${getName()}::OnRequest'); + return request; + } + + @override + Future onResponse( + MiddlewareContext context, + MiddlewareResponse response, + ) async { + print('${getName()}::OnResponse'); + return response; + } +} +``` diff --git a/packages/wyatt_http_client/example/http_client_example.dart b/packages/wyatt_http_client/example/http_client_example.dart index e8766148..57b16bee 100644 --- a/packages/wyatt_http_client/example/http_client_example.dart +++ b/packages/wyatt_http_client/example/http_client_example.dart @@ -17,14 +17,7 @@ import 'dart:async'; import 'dart:io'; -import 'package:wyatt_http_client/src/authentication/basic_authentication_client.dart'; -import 'package:wyatt_http_client/src/authentication/bearer_authentication_client.dart'; -import 'package:wyatt_http_client/src/authentication/digest_authentication_client.dart'; -import 'package:wyatt_http_client/src/authentication/refresh_token_client.dart'; -import 'package:wyatt_http_client/src/authentication/unsafe_authentication_client.dart'; -import 'package:wyatt_http_client/src/rest_client.dart'; -import 'package:wyatt_http_client/src/utils/header_keys.dart'; -import 'package:wyatt_http_client/src/utils/protocols.dart'; +import 'package:wyatt_http_client/wyatt_http_client.dart'; String lastToken = ''; int token = 0; @@ -42,7 +35,7 @@ Future handleBasic(HttpRequest req) async { Future handleBasicNegotiate(HttpRequest req) async { if (req.headers.value('Authorization') == null) { - req.response.statusCode = HttpStatus.unauthorized; + req.response.statusCode = HttpStatus.unauthorized.statusCode; req.response.headers.set(HeaderKeys.wwwAuthenticate, 'Basic realm="Wyatt"'); print(req.response.headers.value('WWW-Authenticate')); return req.response.close(); @@ -56,7 +49,7 @@ Future handleBearer(HttpRequest req) async { Future handleDigest(HttpRequest req) async { if (req.headers.value('Authorization') == null) { - req.response.statusCode = HttpStatus.unauthorized; + req.response.statusCode = HttpStatus.unauthorized.statusCode; req.response.headers.set( 'WWW-Authenticate', 'Digest realm="Wyatt", ' @@ -110,7 +103,7 @@ Future handleOauth2RefreshToken(HttpRequest req) async { return req.response.close(); } else { lastToken = receivedToken; - req.response.statusCode = HttpStatus.unauthorized; + req.response.statusCode = HttpStatus.unauthorized.statusCode; return req.response.close(); } default: @@ -160,13 +153,13 @@ Future server() async { print('Authorized'); error = 0; } else { - request.response.statusCode = HttpStatus.unauthorized; + request.response.statusCode = HttpStatus.unauthorized.statusCode; } break; case '/test/oauth2-test-timeout': error++; print('Error $error'); - request.response.statusCode = HttpStatus.unauthorized; + request.response.statusCode = HttpStatus.unauthorized.statusCode; break; case '/test/oauth2-login': if (request.method == 'POST') { @@ -189,12 +182,12 @@ Future server() async { } break; case '/test/oauth2-refresh-error': - request.response.statusCode = HttpStatus.unauthorized; + request.response.statusCode = HttpStatus.unauthorized.statusCode; break; default: print(' => Unknown path or method'); - request.response.statusCode = HttpStatus.notFound; + request.response.statusCode = HttpStatus.notFound.statusCode; } request.response.close(); print('===================='); @@ -204,73 +197,98 @@ Future server() async { Future main() async { unawaited(server()); final base = 'localhost:8080'; - final restClient = RestClient(protocol: Protocols.http, authority: base); + final uriPrefix = UriPrefixMiddleware( + protocol: Protocols.http, + authority: base, + ); + final jsonEncoder = BodyToJsonMiddleware(); + final logger = SimpleLoggerMiddleware(); // Basic - final basic = BasicAuthenticationClient( + final basicAuth = BasicAuthMiddleware( username: 'username', password: 'password', - inner: restClient, + ); + final basic = MiddlewareClient( + pipeline: Pipeline.fromIterable([ + uriPrefix, + basicAuth, + logger, + ]), ); await basic.get(Uri.parse('/test/basic-test')); - // Basic with negotiate - final basicWithNegotiate = BasicAuthenticationClient( - username: 'username', - password: 'password', - preemptive: false, - inner: restClient, - ); - await basicWithNegotiate.get(Uri.parse('/test/basic-test-with-negotiate')); - // Digest - final digest = DigestAuthenticationClient( + final digestAuth = DigestAuthMiddleware( username: 'Mufasa', password: 'Circle Of Life', - inner: restClient, + ); + final digest = MiddlewareClient( + pipeline: Pipeline.fromIterable([ + uriPrefix, + digestAuth, + logger, + ]), ); await digest.get(Uri.parse('/test/digest-test')); - // Bearer - final bearer = BearerAuthenticationClient( - token: 'access-token-test', - inner: restClient, - ); - await bearer.get(Uri.parse('/test/bearer-test')); + // // Bearer + // final bearer = BearerAuthenticationClient( + // token: 'access-token-test', + // inner: restClient, + // ); + // await bearer.get(Uri.parse('/test/bearer-test')); - // API Key - final apiKey = BearerAuthenticationClient( - token: 'awesome-api-key', - authenticationMethod: 'ApiKey', - inner: restClient, - ); - await apiKey.get(Uri.parse('/test/apikey-test')); + // // API Key + // final apiKey = BearerAuthenticationClient( + // token: 'awesome-api-key', + // authenticationMethod: 'ApiKey', + // inner: restClient, + // ); + // await apiKey.get(Uri.parse('/test/apikey-test')); // Unsafe URL - final unsafe = UnsafeAuthenticationClient( + final unsafeAuth = UnsafeAuthMiddleware( username: 'Mufasa', password: 'Circle Of Life', - inner: restClient, + ); + final unsafe = MiddlewareClient( + pipeline: Pipeline.fromIterable([ + uriPrefix, + unsafeAuth, + logger, + ]), ); await unsafe.get(Uri.parse('/test/unsafe-test')); // OAuth2 - final refreshToken = RefreshTokenClient( + final refreshTokenAuth = RefreshTokenAuthMiddleware( authorizationEndpoint: '/test/oauth2-test?action=login', tokenEndpoint: '/test/oauth2-test?action=refresh', accessTokenParser: (body) => body['accessToken']! as String, refreshTokenParser: (body) => body['refreshToken']! as String, - inner: restClient, + ); + final refreshToken = MiddlewareClient( + pipeline: Pipeline.fromIterable([ + uriPrefix, + jsonEncoder, + refreshTokenAuth, + logger, + ]), ); await refreshToken.get(Uri.parse('/test/oauth2-test')); - await refreshToken.authorize({ - 'username': 'username', - 'password': 'password', - }); + // Login + await refreshToken.post( + Uri.parse('/test/oauth2-test'), + body: { + 'username': 'username', + 'password': 'password', + }, + ); await refreshToken.get(Uri.parse('/test/oauth2-test')); - await refreshToken.refresh(); - await refreshToken.get(Uri.parse('/test/oauth2-test')); - await refreshToken.get(Uri.parse('/test/oauth2-test?action=access-denied')); + // await refreshToken.refresh(); + // await refreshToken.get(Uri.parse('/test/oauth2-test')); + // await refreshToken.get(Uri.parse('/test/oauth2-test?action=access-denied')); exit(0); } diff --git a/packages/wyatt_http_client/example/http_client_fastapi_example.dart b/packages/wyatt_http_client/example/http_client_fastapi_example.dart index 93e40d3e..5393611c 100644 --- a/packages/wyatt_http_client/example/http_client_fastapi_example.dart +++ b/packages/wyatt_http_client/example/http_client_fastapi_example.dart @@ -19,7 +19,7 @@ import 'dart:convert'; import 'package:wyatt_http_client/src/middleware_client.dart'; import 'package:wyatt_http_client/src/middlewares/body_to_json_middleware.dart'; -import 'package:wyatt_http_client/src/middlewares/refresh_token_middleware.dart'; +import 'package:wyatt_http_client/src/middlewares/refresh_token_auth_middleware.dart'; import 'package:wyatt_http_client/src/middlewares/simple_logger_middleware.dart'; import 'package:wyatt_http_client/src/middlewares/uri_prefix_middleware.dart'; import 'package:wyatt_http_client/src/pipeline.dart'; @@ -354,7 +354,6 @@ class FastAPI { void main(List args) async { final Pipeline pipeline = Pipeline() - .addMiddleware(SimpleLoggerMiddleware()) .addMiddleware( UriPrefixMiddleware( protocol: Protocols.http, @@ -363,39 +362,37 @@ void main(List args) async { ) .addMiddleware(BodyToJsonMiddleware()) .addMiddleware( - RefreshTokenMiddleware( + RefreshTokenAuthMiddleware( authorizationEndpoint: '/api/v1/auth/sign-in-with-password', tokenEndpoint: '/api/v1/auth/refresh', accessTokenParser: (body) => body['access_token']! as String, refreshTokenParser: (body) => body['refresh_token']! as String, unauthorized: HttpStatus.forbidden, ), - ); + ) + .addMiddleware(SimpleLoggerMiddleware()); - print(pipeline.getLogic()); + print(pipeline); final client = MiddlewareClient(pipeline: pipeline); final api = FastAPI( client: client, ); - // await api.sendSignUpCode('git@pcl.ovh'); - // final verifiedAccount = await api.verifyCode( - // VerifyCode( - // email: 'git@pcl.ovh', - // verificationCode: '000000000', - // action: EmailVerificationAction.signUp, - // ), - // ); - // print(verifiedAccount); - // final registeredAccount = await api.signUp( - // SignUp(sessionId: verifiedAccount.sessionId ?? '', password: 'password'), - // ); - // print(registeredAccount); + await api.sendSignUpCode('git@pcl.ovh'); + final verifiedAccount = await api.verifyCode( + VerifyCode( + email: 'git@pcl.ovh', + verificationCode: '000000000', + action: EmailVerificationAction.signUp, + ), + ); + final registeredAccount = await api.signUp( + SignUp(sessionId: verifiedAccount.sessionId ?? '', password: 'password'), + ); final signedInAccount = await api.signInWithPassword( Login(email: 'git@pcl.ovh', password: 'password'), ); - // print(signedInAccount); final accountList = await api.getAccountList(); print(accountList); } diff --git a/packages/wyatt_http_client/example/http_client_test.dart b/packages/wyatt_http_client/example/http_client_test.dart deleted file mode 100644 index 36a66c15..00000000 --- a/packages/wyatt_http_client/example/http_client_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - - -Future main(List args) async { - // final client = Oauth2Client( - // accessToken: 'test-token', - // inner: RestClient(protocol: Protocols.http, authority: 'localhost:80'), - // ); - // final client = RestClient( - // protocol: Protocols.http, - // authority: 'localhost:80', - // inner: Oauth2Client( - // authorizationEndpoint: '/api/v1/account/test', - // tokenEndpoint: '/api/v1/account/test', - // accessToken: 'test-token', - // refreshToken: 'refresh-token', - // ), - // ); - // final client = RestClient(protocol: Protocols.http, authority: 'localhost:80'); - // final client =AwesomeRestClient(protocol: Protocols.http, authority: 'localhost:80'); - // var r = await client.post( - // Uri.parse('/api/v1/account/test'), - // body: { - // 'email': 'test@test.fr', - // }, - // ); -} diff --git a/packages/wyatt_http_client/example/pipeline.dart b/packages/wyatt_http_client/example/pipeline.dart index 4b9fa9fe..334fc509 100644 --- a/packages/wyatt_http_client/example/pipeline.dart +++ b/packages/wyatt_http_client/example/pipeline.dart @@ -17,8 +17,8 @@ import 'package:wyatt_http_client/src/middleware_client.dart'; import 'package:wyatt_http_client/src/middlewares/body_to_json_middleware.dart'; -import 'package:wyatt_http_client/src/middlewares/refresh_token_middleware.dart'; import 'package:wyatt_http_client/src/middlewares/simple_logger_middleware.dart'; +import 'package:wyatt_http_client/src/middlewares/unsafe_auth_middleware.dart'; import 'package:wyatt_http_client/src/middlewares/uri_prefix_middleware.dart'; import 'package:wyatt_http_client/src/pipeline.dart'; import 'package:wyatt_http_client/src/utils/protocols.dart'; @@ -117,8 +117,8 @@ import 'package:wyatt_http_client/src/utils/protocols.dart'; // } Future main(List args) async { + final UnsafeAuthMiddleware auth = UnsafeAuthMiddleware(); final Pipeline pipeline = Pipeline() - .addMiddleware(SimpleLoggerMiddleware()) .addMiddleware( UriPrefixMiddleware( protocol: Protocols.http, @@ -127,17 +127,33 @@ Future main(List args) async { ) .addMiddleware(BodyToJsonMiddleware()) .addMiddleware( - RefreshTokenMiddleware( - authorizationEndpoint: '/api/v1/account/test?action=authorize', - tokenEndpoint: '/api/v1/account/test?action=refresh', - accessTokenParser: (body) => body['access_token']! as String, - refreshTokenParser: (body) => body['refresh_token']! as String, + UnsafeAuthMiddleware( + username: 'wyatt', + password: 'motdepasse', ), - ); + ) + .addMiddleware(SimpleLoggerMiddleware()); + // .addMiddleware( + // RefreshTokenMiddleware( + // authorizationEndpoint: '/api/v1/account/test?action=authorize', + // tokenEndpoint: '/api/v1/account/test?action=refresh', + // accessTokenParser: (body) => body['access_token']! as String, + // refreshTokenParser: (body) => body['refresh_token']! as String, + // ), + // ); - print(pipeline.getLogic()); + print(pipeline); final client = MiddlewareClient(pipeline: pipeline); - final r = await client.post( + await client.post( + Uri.parse('/api/v1/account/test'), + body: { + 'email': 'test@test.fr', + }, + ); + auth + ..username = 'username' + ..password = 'password'; + await client.post( Uri.parse('/api/v1/account/test'), body: { 'email': 'test@test.fr', diff --git a/packages/wyatt_http_client/lib/src/authentication/basic_authentication_client.dart b/packages/wyatt_http_client/lib/src/authentication/basic_authentication_client.dart deleted file mode 100644 index 79bdef1b..00000000 --- a/packages/wyatt_http_client/lib/src/authentication/basic_authentication_client.dart +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import 'dart:convert'; -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart'; -import 'package:wyatt_http_client/src/utils/authentication_methods.dart'; -import 'package:wyatt_http_client/src/utils/header_keys.dart'; -import 'package:wyatt_http_client/src/utils/http_status.dart'; -import 'package:wyatt_http_client/src/utils/utils.dart'; - -class BasicAuthenticationClient extends HeaderAuthenticationClient { - final String username; - final String password; - final bool preemptive; - final String authenticationHeader; - - BasicAuthenticationClient({ - required this.username, - required this.password, - this.preemptive = true, - this.authenticationHeader = HeaderKeys.authorization, - BaseClient? inner, - }) : super(inner); - - @override - Map modifyHeader( - Map header, [ - BaseRequest? request, - ]) { - header[authenticationHeader] = '${AuthenticationMethods.basic} ' - '${base64Encode(utf8.encode('$username:$password'))}'; - return header; - } - - @override - Future send(BaseRequest request) async { - if (preemptive) { - // Just send request with modified header. - return super.send(request); - } - - // Try to send request without modified header, - // and if it fails, send it with. - final response = await inner.send(request); - if (response.statusCode == HttpStatus.unauthorized) { - // TODO(hpcl): save realm. - final newRequest = Utils.copyRequest(request); - return super.send(newRequest); - } else { - return response; - } - } -} diff --git a/packages/wyatt_http_client/lib/src/authentication/bearer_authentication_client.dart b/packages/wyatt_http_client/lib/src/authentication/bearer_authentication_client.dart deleted file mode 100644 index cf16c730..00000000 --- a/packages/wyatt_http_client/lib/src/authentication/bearer_authentication_client.dart +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart'; -import 'package:wyatt_http_client/src/utils/authentication_methods.dart'; -import 'package:wyatt_http_client/src/utils/header_keys.dart'; -import 'package:wyatt_http_client/src/utils/http_status.dart'; -import 'package:wyatt_http_client/src/utils/utils.dart'; - -class BearerAuthenticationClient extends HeaderAuthenticationClient { - final String token; - final bool preemptive; - final String authenticationHeader; - final String authenticationMethod; - - BearerAuthenticationClient({ - required this.token, - this.preemptive = true, - this.authenticationHeader = HeaderKeys.authorization, - this.authenticationMethod = AuthenticationMethods.bearer, - BaseClient? inner, - }) : super(inner); - - @override - Map modifyHeader( - Map header, [ - BaseRequest? request, - ]) { - header[authenticationHeader] = '$authenticationMethod $token'; - return header; - } - - @override - Future send(BaseRequest request) async { - if (preemptive) { - // Just send request with modified header. - return super.send(request); - } - - // Try to send request without modified header, - final response = await inner.send(request); - if (response.statusCode == HttpStatus.unauthorized) { - final newRequest = Utils.copyRequest(request); - return super.send(newRequest); - } else { - return response; - } - } -} diff --git a/packages/wyatt_http_client/lib/src/authentication/digest_authentication_client.dart b/packages/wyatt_http_client/lib/src/authentication/digest_authentication_client.dart deleted file mode 100644 index cc5b96ea..00000000 --- a/packages/wyatt_http_client/lib/src/authentication/digest_authentication_client.dart +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart'; -import 'package:wyatt_http_client/src/utils/digest_auth.dart'; -import 'package:wyatt_http_client/src/utils/header_keys.dart'; -import 'package:wyatt_http_client/src/utils/http_status.dart'; -import 'package:wyatt_http_client/src/utils/utils.dart'; - -class DigestAuthenticationClient extends HeaderAuthenticationClient { - final String username; - final String password; - final DigestAuth _digestAuth; - final String authenticationHeader; - final String wwwAuthenticateHeader; - - DigestAuthenticationClient({ - required this.username, - required this.password, - this.authenticationHeader = HeaderKeys.authorization, - this.wwwAuthenticateHeader = HeaderKeys.wwwAuthenticate, - BaseClient? inner, - }) : _digestAuth = DigestAuth(username, password), - super(inner); - - @override - Map modifyHeader( - Map header, [ - BaseRequest? request, - ]) { - if ((_digestAuth.isReady()) && request != null) { - header[authenticationHeader] = _digestAuth.getAuthString( - request.method, - request.url, - ); - } - return header; - } - - @override - Future send(BaseRequest request) async { - // Check if our DigestAuth is ready. - if (_digestAuth.isReady()) { - // If it is, try to send the request with the modified header. - return super.send(request); - } - - // If it isn't, try to send the request without the modified header. - final response = await inner.send(request); - - if (response.statusCode == HttpStatus.unauthorized) { - final newRequest = Utils.copyRequest(request); - final authInfo = - response.headers[HeaderKeys.wwwAuthenticate.toLowerCase()]; - _digestAuth.initFromAuthenticateHeader(authInfo); - return super.send(newRequest); - } else { - return response; - } - } -} diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/authenticated_client.dart b/packages/wyatt_http_client/lib/src/authentication/interfaces/authenticated_client.dart deleted file mode 100644 index 0c8cc7f1..00000000 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/authenticated_client.dart +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/implemented_base_client.dart'; - -abstract class AuthenticatedClient implements ImplementedBaseClient { - final Client _inner; - - AuthenticatedClient({ - Client? inner, - }) : _inner = inner ?? Client(); - - @override - void close() => _inner.close(); - - @override - Future head(Uri url, {Map? headers}) => - _inner.head(url, headers: headers); - - @override - Future get(Uri url, {Map? headers}) => - _inner.get(url, headers: headers); - - @override - Future post( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.post(url, headers: headers, body: body, encoding: encoding); - - @override - Future put( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.put(url, headers: headers, body: body, encoding: encoding); - - @override - Future patch( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.patch(url, headers: headers, body: body, encoding: encoding); - - @override - Future delete( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.delete(url, headers: headers, body: body, encoding: encoding); - - @override - Future read(Uri url, {Map? headers}) => - _inner.read(url, headers: headers); - - @override - Future readBytes(Uri url, {Map? headers}) => - _inner.readBytes(url, headers: headers); - - @override - Future send(BaseRequest request) => _inner.send(request); -} diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/authentication_client.dart b/packages/wyatt_http_client/lib/src/authentication/interfaces/authentication_client.dart deleted file mode 100644 index 94c844d4..00000000 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/authentication_client.dart +++ /dev/null @@ -1,234 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/mixins/body_transformer.dart'; -import 'package:wyatt_http_client/src/mixins/headers_transformer.dart'; -import 'package:wyatt_http_client/src/rest_client.dart'; -import 'package:wyatt_http_client/src/utils/protocols.dart'; -import 'package:wyatt_http_client/src/utils/utils.dart'; - -class AwesomeClient extends BaseClient { - final Client _inner; - - AwesomeClient({ - Client? inner, - }) : _inner = inner ?? Client(); - - @override - Future send(BaseRequest request) { - return _inner.send(request); - } -} - -class AwesomeRestClient extends AwesomeClient - with HeadersTransformer, BodyTransformer { - final Protocols protocol; - final String? authority; - - AwesomeRestClient({ - this.protocol = Protocols.https, - this.authority = '', - super.inner, - }); - - @override - Object? bodyMutator(Object? body) { - print('bodyMutator: Json encoding'); - return jsonEncode(body); - } - - @override - Map? headersMutator( - Object? body, - Map? headers, - ) { - print('headerMutator: Json encoding'); - final mutation = { - 'content-type': 'application/json; charset=utf-8', - }; - if (headers != null) { - headers.addAll(mutation); - return headers; - } else { - return mutation; - } - } - - @override - BaseRequest requestMutator(BaseRequest request) { - print('requestMutator: scheme + authority'); - final Uri uri = Uri.parse('${protocol.scheme}$authority${request.url}'); - return Utils.copyRequestWith(request, url: uri); - } -} - -class AwesomeUrlClient extends AuthenticatedClient { - AwesomeUrlClient(super.inner); -} - -class AwesomeOauth2Client extends AuthenticatedClient with HeadersTransformer { - AwesomeOauth2Client(super.inner); - - @override - Map? headersMutator( - Object? body, - Map? headers, - ) { - print('headersMutator: Token manager'); - final mutation = { - 'authorization': 'Bearer TOKEN', - }; - if (headers != null) { - headers.addAll(mutation); - return headers; - } else { - return mutation; - } - } -} - -abstract class AuthenticatedClient implements Client { - final Client _inner; - - Client get inner => _inner; - - AuthenticatedClient(BaseClient? inner) : _inner = inner ?? RestClient(); - - @override - void close() => _inner.close(); - - @override - Future head(Uri url, {Map? headers}) => - _inner.head(url, headers: headers); - - @override - Future get(Uri url, {Map? headers}) => - _inner.get(url, headers: headers); - - @override - Future post( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.post(url, headers: headers, body: body, encoding: encoding); - - @override - Future put( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.put(url, headers: headers, body: body, encoding: encoding); - - @override - Future patch( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.patch(url, headers: headers, body: body, encoding: encoding); - - @override - Future delete( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.delete(url, headers: headers, body: body, encoding: encoding); - - @override - Future read(Uri url, {Map? headers}) => - _inner.read(url, headers: headers); - - @override - Future readBytes(Uri url, {Map? headers}) => - _inner.readBytes(url, headers: headers); - - @override - Future send(BaseRequest request) => _inner.send(request); -} - -abstract class AuthenticationClient extends BaseClient { - final BaseClient _inner; - - BaseClient get inner => _inner; - - AuthenticationClient(BaseClient? inner) : _inner = inner ?? RestClient(); - - @override - Future head(Uri url, {Map? headers}) => - _inner.head(url, headers: headers); - - @override - Future get(Uri url, {Map? headers}) => - _inner.get(url, headers: headers); - - @override - Future post( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.post(url, headers: headers, body: body, encoding: encoding); - - @override - Future put( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.put(url, headers: headers, body: body, encoding: encoding); - - @override - Future patch( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.patch(url, headers: headers, body: body, encoding: encoding); - - @override - Future delete( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.delete(url, headers: headers, body: body, encoding: encoding); - - @override - Future send(BaseRequest request) { - return _inner.send(request); - } - - @override - void close() { - _inner.close(); - return super.close(); - } -} diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/header_authentication_client.dart b/packages/wyatt_http_client/lib/src/authentication/interfaces/header_authentication_client.dart deleted file mode 100644 index ef8aae8d..00000000 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/header_authentication_client.dart +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/authentication/interfaces/authentication_client.dart'; - -abstract class HeaderAuthenticationClient extends AuthenticationClient { - HeaderAuthenticationClient(super.inner); - - Map modifyHeader( - Map header, [ - BaseRequest? request, - ]) => - header; - - @override - Future send(BaseRequest request) { - final newHeader = modifyHeader(Map.from(request.headers), request); - request.headers.clear(); - request.headers.addAll(newHeader); - print(newHeader); - return super.send(request); - } -} diff --git a/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart b/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart deleted file mode 100644 index 01201966..00000000 --- a/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import 'dart:convert'; - -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/authentication/interfaces/oauth2_client.dart'; -import 'package:wyatt_http_client/src/utils/authentication_methods.dart'; -import 'package:wyatt_http_client/src/utils/header_keys.dart'; -import 'package:wyatt_http_client/src/utils/http_status.dart'; -import 'package:wyatt_http_client/src/utils/utils.dart'; - -// class Oauth2Client extends ImplementedBaseClient with RequestTransformer { -// final String authorizationEndpoint; -// final String tokenEndpoint; -// String? accessToken; -// String? refreshToken; -// String? tokenToUse; - -// Oauth2Client({ -// required this.authorizationEndpoint, -// required this.tokenEndpoint, -// this.accessToken, -// this.refreshToken, -// super.inner, -// }) : tokenToUse = accessToken; - -// @override -// BaseRequest requestMutator(BaseRequest request) { -// print('Oauth2Client::requestMutator -> add authorization: $accessToken'); -// final headers = request.headers; -// final mutation = { -// 'Authorization': 'Bearer $tokenToUse', -// }; -// if (tokenToUse?.isNotEmpty ?? false) { -// headers.addAll(mutation); -// return Utils.copyRequestWith(request, headers: headers); -// } -// return request; -// } - -// Future refresh() async { -// if (refreshToken?.isNotEmpty ?? false) { -// tokenToUse = refreshToken; - -// final response = await get( -// Uri.parse(tokenEndpoint), -// ); - -// if (response.statusCode == HttpStatus.ok) { -// // final body = json.decode(response.body) as Map; -// // accessToken = accessTokenParser(body); -// print('Oauth2Client::refresh -> ok'); -// } -// return response; -// } -// return null; -// } - -// @override -// Map? headersMutator( -// Object? body, -// Map? headers, -// ) { -// print( -// 'Oauth2Client::headersMutator -> add authorization: $accessToken', -// ); -// final mutation = { -// 'Authorization': 'Bearer $accessToken', -// }; -// if (accessToken.isNotEmpty) { -// if (headers != null) { -// headers.addAll(mutation); -// return headers; -// } else { -// return mutation; -// } -// } else { -// return headers; -// } -// } - -// } - -class RefreshTokenClient extends Oauth2Client { - final String authorizationEndpoint; - final String tokenEndpoint; - - String? accessToken; - final TokenParser accessTokenParser; - String? refreshToken; - final TokenParser refreshTokenParser; - - final String authenticationHeader; - final String authenticationMethod; - - RefreshTokenClient({ - required this.authorizationEndpoint, - required this.tokenEndpoint, - required this.accessTokenParser, - required this.refreshTokenParser, - this.authenticationHeader = HeaderKeys.authorization, - this.authenticationMethod = AuthenticationMethods.bearer, - BaseClient? inner, - }) : super(inner); - - @override - Map modifyHeader( - Map header, [ - BaseRequest? request, - ]) { - print('accessToken $accessToken'); - print('request $request'); - if (accessToken != null && request != null) { - header[authenticationHeader] = '$authenticationMethod $accessToken'; - return header; - } - return header; - } - - @override - Future authorize( - Map body, { - Map? headers, - }) async { - final response = await inner.post( - Uri.parse(authorizationEndpoint), - body: body, - headers: headers, - ); - - if (response.statusCode == HttpStatus.ok) { - final body = json.decode(response.body) as Map; - final accessToken = accessTokenParser(body); - final refreshToken = refreshTokenParser(body); - - if (accessToken.isNotEmpty) { - this.accessToken = accessToken; - } - if (refreshToken.isNotEmpty) { - this.refreshToken = refreshToken; - } - } - return response; - } - - @override - Future refresh() async { - if (refreshToken != null) { - final Map header = { - authenticationHeader: '$authenticationHeader $refreshToken', - }; - - final response = await inner.get( - Uri.parse(tokenEndpoint), - headers: header, - ); - - if (response.statusCode == HttpStatus.ok) { - final body = json.decode(response.body) as Map; - accessToken = accessTokenParser(body); - } - return response; - } - return null; - } - - @override - Future send(BaseRequest request) async { - final newHeader = modifyHeader(Map.from(request.headers), request); - request.headers.clear(); - request.headers.addAll(newHeader); - final response = await super.send(request); - - if (response.statusCode == HttpStatus.unauthorized) { - await refresh(); - return super.send(Utils.copyRequest(request)); - } - - return response; - } -} diff --git a/packages/wyatt_http_client/lib/src/implemented_client.dart b/packages/wyatt_http_client/lib/src/implemented_client.dart deleted file mode 100644 index a9790c33..00000000 --- a/packages/wyatt_http_client/lib/src/implemented_client.dart +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:http/http.dart'; - -abstract class ImplementedClient extends BaseClient { - final Client _inner; - - ImplementedClient({ - Client? inner, - }) : _inner = inner ?? Client(); - - @override - void close() => _inner.close(); - - @override - Future head(Uri url, {Map? headers}) => - _inner.head(url, headers: headers); - - @override - Future get(Uri url, {Map? headers}) => - _inner.get(url, headers: headers); - - @override - Future post( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.post(url, headers: headers, body: body, encoding: encoding); - - @override - Future put( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.put(url, headers: headers, body: body, encoding: encoding); - - @override - Future patch( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.patch(url, headers: headers, body: body, encoding: encoding); - - @override - Future delete( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) => - _inner.delete(url, headers: headers, body: body, encoding: encoding); - - @override - Future read(Uri url, {Map? headers}) => - _inner.read(url, headers: headers); - - @override - Future readBytes(Uri url, {Map? headers}) => - _inner.readBytes(url, headers: headers); - - @override - Future send(BaseRequest request) => _inner.send(request); -} diff --git a/packages/wyatt_http_client/lib/src/middleware.dart b/packages/wyatt_http_client/lib/src/middleware.dart index 4295a278..07cf6323 100644 --- a/packages/wyatt_http_client/lib/src/middleware.dart +++ b/packages/wyatt_http_client/lib/src/middleware.dart @@ -14,31 +14,25 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -part of 'pipeline.dart'; +import 'package:wyatt_http_client/src/models/middleware_context.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/models/middleware_response.dart'; -class Middleware { - /// The http [MiddlewareClient] used by this [Middleware] - MiddlewareClient? _client; - - String getName() => 'MiddlewareNode'; - - // ignore: avoid_setters_without_getters - set httpClient(MiddlewareClient? client) => _client = client; - - Client? get client => _client?.inner; - - Future onRequest( - MiddlewareRequest request, - ) async { - return request; - } - - Future onResponse(MiddlewareResponse response) async { - return response; - } - - @override - String toString() { - return getName(); - } +abstract class Middleware { + Middleware(); + String getName(); +} + +mixin OnRequestMiddleware { + Future onRequest( + MiddlewareContext context, + MiddlewareRequest request, + ); +} + +mixin OnResponseMiddleware { + Future onResponse( + MiddlewareContext context, + MiddlewareResponse response, + ); } diff --git a/packages/wyatt_http_client/lib/src/middleware_client.dart b/packages/wyatt_http_client/lib/src/middleware_client.dart index e6b0dd43..0f646f70 100644 --- a/packages/wyatt_http_client/lib/src/middleware_client.dart +++ b/packages/wyatt_http_client/lib/src/middleware_client.dart @@ -33,7 +33,7 @@ class MiddlewareClient extends BaseClient { Client? inner, }) : pipeline = pipeline ?? Pipeline(), inner = inner ?? Client() { - this.pipeline.setClient(this); + print('Using Pipeline:\n$pipeline'); } @override @@ -100,27 +100,30 @@ class MiddlewareClient extends BaseClient { body: body, encoding: encoding, ), - httpRequest: Request(method, url), - context: MiddlewareContext(pipeline: pipeline), ); + final requestContext = MiddlewareContext( + pipeline: pipeline, + client: this, + originalRequest: originalRequest, + ); + final modifiedRequest = await pipeline.onRequest( + requestContext, originalRequest.copyWith(), ); - final res = await Response.fromStream( - await send(modifiedRequest.httpRequest), - ); - final response = await pipeline.onResponse( - MiddlewareResponse( - httpResponse: res, - middlewareRequest: modifiedRequest, - context: MiddlewareContext( - pipeline: pipeline, - originalRequest: originalRequest, - ), + final originalResponse = MiddlewareResponse( + httpResponse: await Response.fromStream( + await send(modifiedRequest.request), ), ); - return response.httpResponse as Response; + final responseContext = + requestContext.copyWith(originalResponse: originalResponse); + + final modifiedResponse = + await pipeline.onResponse(responseContext, originalResponse.copyWith()); + + return modifiedResponse.httpResponse as Response; } } diff --git a/packages/wyatt_http_client/lib/src/middleware_node.dart b/packages/wyatt_http_client/lib/src/middleware_node.dart deleted file mode 100644 index 32697e40..00000000 --- a/packages/wyatt_http_client/lib/src/middleware_node.dart +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -part of 'pipeline.dart'; - -class MiddlewareNode { - final Pipeline pipeline; - - Middleware? middleware; - - late MiddlewareNode _parent; - late MiddlewareNode _child; - final bool _isEnd; - - /// Reference to the previous [MiddlewareNode] in the [Pipeline] - MiddlewareNode get parent => _parent; - - /// Reference to the next [MiddlewareNode] in the [Pipeline] - MiddlewareNode get child => _child; - - /// Whether this is the begin [MiddlewareNode] - bool get isBegin => _parent == this; - - /// Whether this is the end [MiddlewareNode] - bool get isEnd => _child == this; - - /// Whether this is the first [MiddlewareNode] - bool get isFirst => !isBegin && _parent == pipeline.begin; - - /// Whether this is the last [MiddlewareNode] - bool get isLast => !isEnd && _child == pipeline.end; - - MiddlewareNode._( - this.pipeline, - this.middleware, { - MiddlewareNode? parent, - MiddlewareNode? child, - }) : _isEnd = false { - _parent = parent ?? this; - _child = child ?? this; - } - - MiddlewareNode._end(this.pipeline) : _isEnd = true { - _child = this; - } - - MiddlewareNode._begin(this.pipeline) : _isEnd = true { - _parent = this; - } - - /// Creates a new [MiddlewareNode] right **before** this in [pipeline] - MiddlewareNode insertBefore(Middleware middleware) { - if (isBegin) { - throw StateError( - 'A MiddlewareNode cannot be inserted ' - 'before begin MiddlewareNode', - ); - } - final newMiddlewareNode = - MiddlewareNode._(pipeline, middleware, parent: _parent, child: this); - _parent._child = newMiddlewareNode; - _parent = newMiddlewareNode; - pipeline._length++; - return newMiddlewareNode; - } - - /// Creates a new [MiddlewareNode] right **after** this in [pipeline] - MiddlewareNode insertAfter(Middleware middleware) { - if (isEnd) { - throw StateError( - 'A MiddlewareNode cannot be inserted ' - 'after end MiddlewareNode', - ); - } - final newMiddlewareNode = - MiddlewareNode._(pipeline, middleware, parent: this, child: _child); - _child._parent = newMiddlewareNode; - _child = newMiddlewareNode; - pipeline._length++; - return newMiddlewareNode; - } - - MiddlewareNode remove() { - if (_isEnd) throw StateError('Cannot remove end MiddlewareNode'); - _child._parent = _parent; - _parent._child = _child; - pipeline._length--; - return child; - } -} diff --git a/packages/wyatt_http_client/lib/src/implemented_base_client.dart b/packages/wyatt_http_client/lib/src/middlewares/access_token_auth_middleware.dart similarity index 69% rename from packages/wyatt_http_client/lib/src/implemented_base_client.dart rename to packages/wyatt_http_client/lib/src/middlewares/access_token_auth_middleware.dart index e42ed84f..ece1af01 100644 --- a/packages/wyatt_http_client/lib/src/implemented_base_client.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/access_token_auth_middleware.dart @@ -1,31 +1,16 @@ // Copyright (C) 2022 WYATT GROUP // Please see the AUTHORS file for details. -// +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:http/http.dart'; - -class ImplementedBaseClient extends BaseClient { - final Client inner; - - ImplementedBaseClient({ - Client? inner, - }) : inner = inner ?? Client(); - - @override - Future send(BaseRequest request) { - return inner.send(request); - } - -} diff --git a/packages/wyatt_http_client/lib/src/middlewares/basic_auth_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/basic_auth_middleware.dart new file mode 100644 index 00000000..027ad964 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middlewares/basic_auth_middleware.dart @@ -0,0 +1,59 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:convert'; + +import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/models/middleware_context.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/utils/authentication_methods.dart'; +import 'package:wyatt_http_client/src/utils/header_keys.dart'; + +class BasicAuthMiddleware with OnRequestMiddleware implements Middleware { + String? username; + String? password; + final String authenticationHeader; + + BasicAuthMiddleware({ + this.username, + this.password, + this.authenticationHeader = HeaderKeys.authorization, + }); + + @override + String getName() => 'BasicAuth'; + + @override + Future onRequest( + MiddlewareContext context, + MiddlewareRequest request, + ) async { + if (username == null || password == null) { + return request; + } + print( + '${getName()}::OnRequest\n' + '>> Basic: ${base64Encode(utf8.encode('$username:$password'))}', + ); + final mutation = { + authenticationHeader: '${AuthenticationMethods.basic} ' + '${base64Encode(utf8.encode('$username:$password'))}', + }; + final Map headers = request.headers..addAll(mutation); + request.modifyRequest(request.unfreezedRequest.copyWith(headers: headers)); + return request; + } +} diff --git a/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart index af124dc3..a77048d8 100644 --- a/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart @@ -16,33 +16,34 @@ import 'dart:convert'; +import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/models/middleware_context.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; -import 'package:wyatt_http_client/src/pipeline.dart'; -class BodyToJsonMiddleware extends Middleware { +class BodyToJsonMiddleware with OnRequestMiddleware implements Middleware { @override String getName() => 'BodyToJson'; @override - Future onRequest(MiddlewareRequest request) { + Future onRequest( + MiddlewareContext context, + MiddlewareRequest request, + ) async { print( - '${getName()}::OnRequest: transforms body in json if Map then update ' + '${getName()}::OnRequest\n' + '>> Transforms body in json if Map then update ' 'headers with right content-type', ); - var newReq = request.unfreezedRequest; final mutation = { 'content-type': 'application/json; charset=utf-8', }; - if (newReq.body is Map) { - Map? headers = newReq.headers; - if (headers != null) { - headers.addAll(mutation); - } else { - headers = mutation; - } - newReq = newReq.copyWith(body: jsonEncode(newReq.body), headers: headers); - request.updateUnfreezedRequest(newReq); + if (request.body is Map) { + final Map headers = request.headers..addAll(mutation); + request.modifyRequest( + request.unfreezedRequest + .copyWith(headers: headers, body: jsonEncode(request.body)), + ); } - return super.onRequest(request); + return request; } } diff --git a/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart index 67c65728..65b766ca 100644 --- a/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart @@ -14,9 +14,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:wyatt_http_client/src/pipeline.dart'; +import 'package:wyatt_http_client/src/middleware.dart'; -class DefaultMiddleware extends Middleware { +class DefaultMiddleware implements Middleware { @override String getName() => 'DefaultMiddleware'; } diff --git a/packages/wyatt_http_client/lib/src/middlewares/digest_auth_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/digest_auth_middleware.dart new file mode 100644 index 00000000..538f6eb2 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middlewares/digest_auth_middleware.dart @@ -0,0 +1,92 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/models/middleware_context.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/models/middleware_response.dart'; +import 'package:wyatt_http_client/src/utils/digest_auth.dart'; +import 'package:wyatt_http_client/src/utils/header_keys.dart'; +import 'package:wyatt_http_client/src/utils/http_status.dart'; + +class DigestAuthMiddleware + with OnRequestMiddleware, OnResponseMiddleware + implements Middleware { + final String username; + final String password; + final DigestAuth _digestAuth; + final String authenticationHeader; + final String wwwAuthenticateHeader; + final HttpStatus unauthorized; + + DigestAuthMiddleware({ + required this.username, + required this.password, + this.authenticationHeader = HeaderKeys.authorization, + this.wwwAuthenticateHeader = HeaderKeys.wwwAuthenticate, + this.unauthorized = HttpStatus.unauthorized, + }) : _digestAuth = DigestAuth(username, password); + + @override + String getName() => 'DigestAuth'; + + @override + Future onRequest( + MiddlewareContext context, + MiddlewareRequest request, + ) async { + print( + '${getName()}::OnRequest\n' + '>> Digest ready: ${_digestAuth.isReady()}', + ); + if (_digestAuth.isReady()) { + final mutation = { + authenticationHeader: _digestAuth.getAuthString( + request.method, + request.url, + ), + }; + final Map headers = request.headers..addAll(mutation); + request + .modifyRequest(request.unfreezedRequest.copyWith(headers: headers)); + } + return request; + } + + @override + Future onResponse( + MiddlewareContext context, + MiddlewareResponse response, + ) async { + if (response.status == unauthorized) { + final authInfo = + response.headers[HeaderKeys.wwwAuthenticate.toLowerCase()]; + _digestAuth.initFromAuthenticateHeader(authInfo); + + final MiddlewareRequest? newRequest = context.lastRequest?.copyWith(); + + if (newRequest != null) { + final newResponse = await context.client.send(newRequest.request); + return MiddlewareResponse(httpResponse: newResponse); + } + } + print( + '${getName()}::OnResponse\n' + '>> Digest ready: ${_digestAuth.isReady()}', + ); + return response; + } +} diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/url_authentication_client.dart b/packages/wyatt_http_client/lib/src/middlewares/middlewares.dart similarity index 60% rename from packages/wyatt_http_client/lib/src/authentication/interfaces/url_authentication_client.dart rename to packages/wyatt_http_client/lib/src/middlewares/middlewares.dart index 9d2dad13..724b3ac9 100644 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/url_authentication_client.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/middlewares.dart @@ -1,30 +1,25 @@ // Copyright (C) 2022 WYATT GROUP // Please see the AUTHORS file for details. -// +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/authentication/interfaces/authentication_client.dart'; - -abstract class UrlAuthenticationClient extends AuthenticationClient { - UrlAuthenticationClient(super.inner); - - BaseRequest modifyRequest(BaseRequest request) => request; - - @override - Future send(BaseRequest request) { - final newRequest = modifyRequest(request); - return super.send(newRequest); - } -} +export 'access_token_auth_middleware.dart'; +export 'basic_auth_middleware.dart'; +export 'body_to_json_middleware.dart'; +export 'default_middleware.dart'; +export 'digest_auth_middleware.dart'; +export 'refresh_token_auth_middleware.dart'; +export 'simple_logger_middleware.dart'; +export 'unsafe_auth_middleware.dart'; +export 'uri_prefix_middleware.dart'; diff --git a/packages/wyatt_http_client/lib/src/middlewares/refresh_token_auth_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/refresh_token_auth_middleware.dart new file mode 100644 index 00000000..f470699a --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middlewares/refresh_token_auth_middleware.dart @@ -0,0 +1,191 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:convert'; + +import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/middleware_client.dart'; +import 'package:wyatt_http_client/src/models/middleware_context.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/models/middleware_response.dart'; +import 'package:wyatt_http_client/src/utils/authentication_methods.dart'; +import 'package:wyatt_http_client/src/utils/delay.dart'; +import 'package:wyatt_http_client/src/utils/header_keys.dart'; +import 'package:wyatt_http_client/src/utils/http_status.dart'; + +typedef TokenParser = String Function(Map); + +class RefreshTokenAuthMiddleware + with OnRequestMiddleware, OnResponseMiddleware + implements Middleware { + final String authorizationEndpoint; + final String tokenEndpoint; + + String? accessToken; + final TokenParser accessTokenParser; + String? refreshToken; + final TokenParser refreshTokenParser; + + final String authenticationHeader; + final String authenticationMethod; + final HttpStatus unauthorized; + final int maxAttempts; + + RefreshTokenAuthMiddleware({ + required this.authorizationEndpoint, + required this.tokenEndpoint, + required this.accessTokenParser, + required this.refreshTokenParser, + this.authenticationHeader = HeaderKeys.authorization, + this.authenticationMethod = AuthenticationMethods.bearer, + this.unauthorized = HttpStatus.unauthorized, + this.maxAttempts = 8, + }); + + @override + String getName() => 'RefreshToken'; + + Future refresh(MiddlewareContext context) async { + final subPipeline = context.pipeline.sub(this); + final httpClient = MiddlewareClient( + pipeline: subPipeline, + inner: context.client.inner, + ); + final headers = { + authenticationHeader: '$authenticationMethod $refreshToken', + }; + final response = MiddlewareResponse( + httpResponse: await httpClient.get( + Uri.parse(tokenEndpoint), + headers: headers, + ), + ); + if (response.status.isSuccess()) { + final body = jsonDecode(response.body) as Map; + accessToken = accessTokenParser(body); + + // Then modify current request with accessToken + final mutation = { + authenticationHeader: '$authenticationMethod $accessToken', + }; + final Map? headers = context.lastRequest?.headers + ?..addAll(mutation); + final newRequest = context.lastRequest?.copyWith( + unfreezedRequest: + context.lastRequest?.unfreezedRequest.copyWith(headers: headers), + ); + + return newRequest; + } + return null; + } + + Future retry(MiddlewareContext context) async { + // Retry + int attempt = 1; + while (attempt <= maxAttempts) { + // Delayed before retry + await Future.delayed(Delay.getRetryDelay(attempt)); + + final newRequest = await refresh(context); + if (newRequest != null) { + return newRequest; + } + attempt++; + } + return null; + } + + @override + Future onRequest( + MiddlewareContext context, + MiddlewareRequest request, + ) async { + print( + '${getName()}::OnRequest\n' + '>> accessToken: $accessToken\n' + '>> refreshToken: $refreshToken', + ); + // Check if it is authorization + if (context.originalRequest?.url == Uri.parse(authorizationEndpoint)) { + return request; + } + // Check if it is refresh + if (context.originalRequest?.url == Uri.parse(tokenEndpoint)) { + return request; + } + // If AccessToken not null then return request with authorization header + if (accessToken != null) { + final mutation = { + authenticationHeader: '$authenticationMethod $accessToken', + }; + final Map headers = request.headers..addAll(mutation); + request + .modifyRequest(request.unfreezedRequest.copyWith(headers: headers)); + return request; + } + // If AccessToken is null BUT there is a refreshToken, then try refreshing + if (refreshToken != null) { + MiddlewareRequest? newRequest = await refresh(context); + newRequest ??= await retry(context); + return newRequest ?? request; + } + // Pass + return request; + } + + @override + Future onResponse( + MiddlewareContext context, + MiddlewareResponse response, + ) async { + // Check if it is authorization + if (context.originalRequest?.url == Uri.parse(authorizationEndpoint)) { + // If success, then update tokens + if (response.status.isSuccess()) { + final body = jsonDecode(response.body) as Map; + final accessToken = accessTokenParser(body); + final refreshToken = refreshTokenParser(body); + + if (accessToken.isNotEmpty) { + this.accessToken = accessToken; + } + if (refreshToken.isNotEmpty) { + this.refreshToken = refreshToken; + } + } + } + + print( + '${getName()}::OnResponse\n' + '>> accessToken: $accessToken\n' + '>> refreshToken: $refreshToken', + ); + + if (response.status == unauthorized) { + // Refresh + MiddlewareRequest? newRequest = await refresh(context); + newRequest ??= await retry(context); + + if (newRequest != null) { + return response.copyWith( + httpResponse: await context.client.send(newRequest.request), + ); + } + } + return response; + } +} diff --git a/packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart deleted file mode 100644 index 02391f61..00000000 --- a/packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import 'dart:convert'; - -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/middleware_client.dart'; -import 'package:wyatt_http_client/src/models/middleware_request.dart'; -import 'package:wyatt_http_client/src/models/middleware_response.dart'; -import 'package:wyatt_http_client/src/pipeline.dart'; -import 'package:wyatt_http_client/src/utils/authentication_methods.dart'; -import 'package:wyatt_http_client/src/utils/header_keys.dart'; -import 'package:wyatt_http_client/src/utils/http_status.dart'; - -typedef TokenParser = String Function(Map); - -class RefreshTokenMiddleware extends Middleware { - final String authorizationEndpoint; - final String tokenEndpoint; - - String? accessToken; - final TokenParser accessTokenParser; - String? refreshToken; - final TokenParser refreshTokenParser; - - final String authenticationHeader; - final String authenticationMethod; - final HttpStatus unauthorized; - final int maxRetries; - - RefreshTokenMiddleware({ - required this.authorizationEndpoint, - required this.tokenEndpoint, - required this.accessTokenParser, - required this.refreshTokenParser, - this.authenticationHeader = HeaderKeys.authorization, - this.authenticationMethod = AuthenticationMethods.bearer, - this.unauthorized = HttpStatus.unauthorized, - this.maxRetries = 3, - }); - - @override - String getName() => 'RefreshToken'; - - @override - Future onRequest(MiddlewareRequest request) async { - print( - '${getName()}::OnRequest: accessToken: $accessToken', - ); - if (request.context.originalRequest?.unfreezedRequest.url == - Uri.parse(authorizationEndpoint)) { - return super.onRequest(request); - } - if (accessToken != null) { - // Modify header with accessToken - var newReq = request.unfreezedRequest; - final mutation = { - authenticationHeader: '$authenticationMethod $accessToken', - }; - Map? headers = newReq.headers; - if (headers != null) { - headers.addAll(mutation); - } else { - headers = mutation; - } - newReq = newReq.copyWith(headers: headers); - request.updateUnfreezedRequest(newReq); - return super.onRequest(request); - } - if (refreshToken != null) { - // Refresh accessToken with refreshToken before perform request - final subPipeline = request.context.pipeline.fromUntil(this); - final httpClient = MiddlewareClient( - pipeline: subPipeline, - inner: client, - ); - final Map headers = { - authenticationHeader: '$authenticationHeader $refreshToken', - }; - final response = - await httpClient.get(Uri.parse(tokenEndpoint), headers: headers); - final status = HttpStatus.from(response.statusCode); - if (status.isSuccess()) { - final body = jsonDecode(response.body) as Map; - accessToken = accessTokenParser(body); - - // Then modify current request with accessToken - var newReq = request.unfreezedRequest; - final mutation = { - authenticationHeader: '$authenticationMethod $accessToken', - }; - Map? headers = newReq.headers; - if (headers != null) { - headers.addAll(mutation); - } else { - headers = mutation; - } - newReq = newReq.copyWith(headers: headers); - request.updateUnfreezedRequest(newReq); - } else { - // Retry - int retries = 0; - while (retries < maxRetries) { - final Map headers = { - authenticationHeader: '$authenticationHeader $refreshToken', - }; - final response = - await httpClient.get(Uri.parse(tokenEndpoint), headers: headers); - final status = HttpStatus.from(response.statusCode); - if (status.isSuccess()) { - final body = jsonDecode(response.body) as Map; - accessToken = accessTokenParser(body); - - // Then modify current request with accessToken - var newReq = request.unfreezedRequest; - final mutation = { - authenticationHeader: '$authenticationMethod $accessToken', - }; - Map? headers = newReq.headers; - if (headers != null) { - headers.addAll(mutation); - } else { - headers = mutation; - } - newReq = newReq.copyWith(headers: headers); - request.updateUnfreezedRequest(newReq); - break; - } - retries++; - } - } - return super.onRequest(request); - } - // Pass - return super.onRequest(request); - } - - @override - Future onResponse(MiddlewareResponse response) async { - final res = await super.onResponse(response); - final status = HttpStatus.from(res.httpResponse.statusCode); - if (res.context.originalRequest?.unfreezedRequest.url == - Uri.parse(authorizationEndpoint)) { - if (status.isSuccess()) { - final body = jsonDecode((res.httpResponse as Response).body) as Map; - final accessToken = accessTokenParser(body); - final refreshToken = refreshTokenParser(body); - - if (accessToken.isNotEmpty) { - this.accessToken = accessToken; - } - if (refreshToken.isNotEmpty) { - this.refreshToken = refreshToken; - } - } - return res; - } - if (status == unauthorized) { - print( - '${getName()}::OnResponse: $unauthorized', - ); - // Refresh token then retry - final subPipeline = res.context.pipeline.fromUntil(this); - final httpClient = MiddlewareClient( - pipeline: subPipeline, - inner: client, - ); - final Map headers = { - authenticationHeader: '$authenticationHeader $refreshToken', - }; - final response = - await httpClient.get(Uri.parse(tokenEndpoint), headers: headers); - final refreshstatus = HttpStatus.from(response.statusCode); - if (refreshstatus.isSuccess()) { - print( - '${getName()}::OnResponse: refresh successfuly', - ); - final body = jsonDecode(response.body) as Map; - accessToken = accessTokenParser(body); - - // Then modify current request with accessToken - final midReq = res.middlewareRequest; - final newReq = midReq.httpRequest; - final mutation = { - authenticationHeader: '$authenticationMethod $accessToken', - }; - Map? headers = newReq.headers; - if (headers != null) { - headers.addAll(mutation); - } else { - headers = mutation; - } - midReq.updateHttpRequest(headers: headers); - final newRes = await httpClient.send(midReq.httpRequest); - return res.copyWith(httpResponse: res as Response); - } - } - return res; - } -} diff --git a/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart index 6118e03b..8e2d2637 100644 --- a/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart @@ -14,31 +14,41 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/models/middleware_context.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; import 'package:wyatt_http_client/src/models/middleware_response.dart'; -import 'package:wyatt_http_client/src/pipeline.dart'; -class SimpleLoggerMiddleware extends Middleware { +class SimpleLoggerMiddleware + with OnRequestMiddleware, OnResponseMiddleware + implements Middleware { @override String getName() => 'SimpleLogger'; @override - Future onRequest(MiddlewareRequest request) { + Future onRequest( + MiddlewareContext context, + MiddlewareRequest request, + ) async { print( - '${getName()}::OnRequest: ${request.httpRequest.method} ' - '${request.httpRequest.url}\n${request.unfreezedRequest.headers}' - '\n>> ${request.unfreezedRequest.body}', + '${getName()}::OnRequest\n' + '>> ${request.method} ${request.url}\n' + '>> Headers: ${request.headers}\n>> Body: ${request.encodedBody}', ); - return super.onRequest(request); + return request; } @override - Future onResponse(MiddlewareResponse response) async { - final res = await super.onResponse(response); + Future onResponse( + MiddlewareContext context, + MiddlewareResponse response, + ) async { print( - '${getName()}::OnResponse: ${res.httpResponse.statusCode} -> ' - 'received ${res.httpResponse.contentLength} bytes', + '${getName()}::OnResponse\n' + '>> Status: ${response.status.name.toUpperCase()}\n' + '>> Length: ${response.contentLength ?? '0'} bytes', + // '>> Body: ${response.body}', ); - return res; + return response; } } diff --git a/packages/wyatt_http_client/lib/src/authentication/unsafe_authentication_client.dart b/packages/wyatt_http_client/lib/src/middlewares/unsafe_auth_middleware.dart similarity index 53% rename from packages/wyatt_http_client/lib/src/authentication/unsafe_authentication_client.dart rename to packages/wyatt_http_client/lib/src/middlewares/unsafe_auth_middleware.dart index 650497f1..f929f623 100644 --- a/packages/wyatt_http_client/lib/src/authentication/unsafe_authentication_client.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/unsafe_auth_middleware.dart @@ -14,30 +14,43 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/authentication/interfaces/url_authentication_client.dart'; +import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/models/middleware_context.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; import 'package:wyatt_http_client/src/utils/convert.dart'; -import 'package:wyatt_http_client/src/utils/utils.dart'; -class UnsafeAuthenticationClient extends UrlAuthenticationClient { - final String username; - final String password; +class UnsafeAuthMiddleware with OnRequestMiddleware implements Middleware { + String? username; + String? password; final String usernameField; final String passwordField; - UnsafeAuthenticationClient({ - required this.username, - required this.password, + UnsafeAuthMiddleware({ + this.username, + this.password, this.usernameField = 'username', this.passwordField = 'password', - BaseClient? inner, - }) : super(inner); + }); @override - BaseRequest modifyRequest(BaseRequest request) { - final url = + String getName() => 'UnsafeAuth'; + + @override + Future onRequest( + MiddlewareContext context, + MiddlewareRequest request, + ) async { + if (username == null || password == null) { + return request; + } + final Uri uri = request.url + '?$usernameField=$username&$passwordField=$password'; - return Utils.copyRequestWith(request, url: url); + print( + '${getName()}::OnRequest\n' + '>> Append: ?$usernameField=$username&$passwordField=$password', + ); + request.modifyRequest(request.unfreezedRequest.copyWith(url: uri)); + return request; } } diff --git a/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart index b83e995f..c52f3fdf 100644 --- a/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart @@ -14,11 +14,12 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/models/middleware_context.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; -import 'package:wyatt_http_client/src/pipeline.dart'; import 'package:wyatt_http_client/src/utils/protocols.dart'; -class UriPrefixMiddleware extends Middleware { +class UriPrefixMiddleware with OnRequestMiddleware implements Middleware { final Protocols protocol; final String? authority; @@ -31,11 +32,17 @@ class UriPrefixMiddleware extends Middleware { String getName() => 'UriPrefix'; @override - Future onRequest(MiddlewareRequest request) { - final Uri uri = - Uri.parse('${protocol.scheme}$authority${request.httpRequest.url}'); - print('${getName()}::OnRequest: ${request.httpRequest.url} -> $uri'); - request.updateHttpRequest(url: uri); - return super.onRequest(request); + Future onRequest( + MiddlewareContext context, + MiddlewareRequest request, + ) async { + final Uri uri = Uri.parse('${protocol.scheme}$authority${request.url}'); + print( + '${getName()}::OnRequest\n' + '>> From: ${request.url}\n' + '>> To: $uri', + ); + request.modifyRequest(request.unfreezedRequest.copyWith(url: uri)); + return request; } } diff --git a/packages/wyatt_http_client/lib/src/mixins/body_transformer.dart b/packages/wyatt_http_client/lib/src/mixins/body_transformer.dart deleted file mode 100644 index b5931d48..00000000 --- a/packages/wyatt_http_client/lib/src/mixins/body_transformer.dart +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import 'dart:convert'; - -import 'package:http/http.dart'; - -mixin BodyTransformer on Client { - Object? bodyMutator(Object? body); - - @override - Future post( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) { - return super.post( - url, - headers: headers, - body: bodyMutator(body), - encoding: encoding, - ); - } - - @override - Future put( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) { - return super.put( - url, - headers: headers, - body: bodyMutator(body), - encoding: encoding, - ); - } - - @override - Future patch( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) { - return super.patch( - url, - headers: headers, - body: bodyMutator(body), - encoding: encoding, - ); - } - - @override - Future delete( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) { - return super.delete( - url, - headers: headers, - body: bodyMutator(body), - encoding: encoding, - ); - } -} diff --git a/packages/wyatt_http_client/lib/src/mixins/headers_transformer.dart b/packages/wyatt_http_client/lib/src/mixins/headers_transformer.dart deleted file mode 100644 index 42cc270b..00000000 --- a/packages/wyatt_http_client/lib/src/mixins/headers_transformer.dart +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:http/http.dart'; - -mixin HeadersTransformer on Client { - Map? headersMutator( - Object? body, - Map? headers, - ); - - @override - Future head( - Uri url, { - Map? headers, - }) { - return super.head( - url, - headers: headersMutator(null, headers), - ); - } - - @override - Future get( - Uri url, { - Map? headers, - }) { - return super.get( - url, - headers: headersMutator(null, headers), - ); - } - - @override - Future post( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) { - return super.post( - url, - headers: headersMutator(body, headers), - body: body, - encoding: encoding, - ); - } - - @override - Future put( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) { - return super.put( - url, - headers: headersMutator(body, headers), - body: body, - encoding: encoding, - ); - } - - @override - Future patch( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) { - return super.patch( - url, - headers: headersMutator(body, headers), - body: body, - encoding: encoding, - ); - } - - @override - Future delete( - Uri url, { - Map? headers, - Object? body, - Encoding? encoding, - }) { - return super.delete( - url, - headers: headersMutator(body, headers), - body: body, - encoding: encoding, - ); - } - - @override - Future read( - Uri url, { - Map? headers, - }) { - return super.read( - url, - headers: headersMutator(null, headers), - ); - } - - @override - Future readBytes( - Uri url, { - Map? headers, - }) { - return super.readBytes( - url, - headers: headersMutator(null, headers), - ); - } -} diff --git a/packages/wyatt_http_client/lib/src/mixins/oauth2_transformer.dart b/packages/wyatt_http_client/lib/src/mixins/oauth2_transformer.dart deleted file mode 100644 index a1abf27a..00000000 --- a/packages/wyatt_http_client/lib/src/mixins/oauth2_transformer.dart +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/implemented_base_client.dart'; - -mixin Oauth2Transformer on ImplementedBaseClient { - late final String authorizationEndpoint; - late final String tokenEndpoint; - String? accessToken; - String? refreshToken; - - BaseRequest requestAuthenticator(BaseRequest request); - - @override - Future send(BaseRequest request) { - final req = requestAuthenticator(request); - return super.send(req); - } -} diff --git a/packages/wyatt_http_client/lib/src/models/middleware_context.dart b/packages/wyatt_http_client/lib/src/models/middleware_context.dart index e00f6395..19877109 100644 --- a/packages/wyatt_http_client/lib/src/models/middleware_context.dart +++ b/packages/wyatt_http_client/lib/src/models/middleware_context.dart @@ -15,34 +15,48 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'package:wyatt_http_client/src/middleware_client.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; import 'package:wyatt_http_client/src/models/middleware_response.dart'; import 'package:wyatt_http_client/src/pipeline.dart'; class MiddlewareContext { Pipeline pipeline; + MiddlewareClient client; MiddlewareRequest? originalRequest; + MiddlewareRequest? lastRequest; MiddlewareResponse? originalResponse; + MiddlewareResponse? lastResponse; MiddlewareContext({ required this.pipeline, + required this.client, this.originalRequest, + this.lastRequest, this.originalResponse, + this.lastResponse, }); MiddlewareContext copyWith({ Pipeline? pipeline, + MiddlewareClient? client, MiddlewareRequest? originalRequest, + MiddlewareRequest? lastRequest, MiddlewareResponse? originalResponse, + MiddlewareResponse? lastResponse, }) { return MiddlewareContext( pipeline: pipeline ?? this.pipeline, + client: client ?? this.client, originalRequest: originalRequest ?? this.originalRequest, + lastRequest: lastRequest ?? this.lastRequest, originalResponse: originalResponse ?? this.originalResponse, + lastResponse: lastResponse ?? this.lastResponse, ); } @override - String toString() => 'MiddlewareContext(pipeline: $pipeline, ' - 'originalRequest: $originalRequest, originalResponse: $originalResponse)'; + String toString() { + return 'MiddlewareContext(pipeline: $pipeline, client: $client, originalRequest: $originalRequest, lastRequest: $lastRequest, originalResponse: $originalResponse, lastResponse: $lastResponse)'; + } } diff --git a/packages/wyatt_http_client/lib/src/models/middleware_request.dart b/packages/wyatt_http_client/lib/src/models/middleware_request.dart index b1355774..42c9ac34 100644 --- a/packages/wyatt_http_client/lib/src/models/middleware_request.dart +++ b/packages/wyatt_http_client/lib/src/models/middleware_request.dart @@ -15,83 +15,68 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:http/http.dart'; +import 'dart:convert'; -import 'package:wyatt_http_client/src/models/middleware_context.dart'; +import 'package:http/http.dart'; import 'package:wyatt_http_client/src/models/unfreezed_request.dart'; -import 'package:wyatt_http_client/src/utils/utils.dart'; +import 'package:wyatt_http_client/src/utils/convert.dart'; +import 'package:wyatt_http_client/src/utils/request_utils.dart'; class MiddlewareRequest { UnfreezedRequest unfreezedRequest; - Request httpRequest; - MiddlewareContext context; + Request _httpRequest; + + Request get request => _httpRequest; + + // Proxy + String get method => _httpRequest.method; + Uri get url => _httpRequest.url; + Map get headers => _httpRequest.headers; + Encoding get encoding => _httpRequest.encoding; + String get encodedBody => _httpRequest.body; + Object? get body => unfreezedRequest.body; MiddlewareRequest({ required this.unfreezedRequest, - required this.httpRequest, - required this.context, - }) { - context = context.copyWith(originalRequest: this); - } + }) : _httpRequest = Request(unfreezedRequest.method, unfreezedRequest.url); MiddlewareRequest copyWith({ UnfreezedRequest? unfreezedRequest, - Request? httpRequest, - MiddlewareContext? context, }) { return MiddlewareRequest( unfreezedRequest: unfreezedRequest ?? this.unfreezedRequest, - httpRequest: httpRequest ?? this.httpRequest, - context: context ?? this.context, ); } - void updateUnfreezedRequest(UnfreezedRequest unfreezedRequest) { - final request = httpRequest; - if (unfreezedRequest.headers != null) { - request.headers.addAll(unfreezedRequest.headers!); - } - if (unfreezedRequest.encoding != null) { - request.encoding = unfreezedRequest.encoding!; - } + void modifyRequest(UnfreezedRequest unfreezedRequest) { + String? _body; if (unfreezedRequest.body != null) { final body = unfreezedRequest.body; if (body is String) { - request.body = body; + _body = body; } else if (body is List) { - request.bodyBytes = body.cast(); + _body = String.fromCharCodes(body.cast()); } else if (body is Map) { - request.bodyFields = body.cast(); - } else { - throw ArgumentError('Invalid request body "$body".'); + _body = Convert.mapToQuery(body.cast()); } } + _httpRequest = RequestUtils.copyRequestWith( + _httpRequest, + method: unfreezedRequest.method, + url: unfreezedRequest.url, + headers: unfreezedRequest.headers, + body: _body, + ) as Request; + if (unfreezedRequest.encoding != null) { + _httpRequest.encoding = unfreezedRequest.encoding!; + } this.unfreezedRequest = unfreezedRequest; - httpRequest = request; } - void updateHttpRequest({ - String? method, - Uri? url, - Map? headers, - int? maxRedirects, - bool? followRedirects, - bool? persistentConnection, - String? body, - }) { - httpRequest = Utils.copyRequestWith( - httpRequest, - method: method, - url: url, - headers: headers, - maxRedirects: maxRedirects, - followRedirects: followRedirects, - persistentConnection: persistentConnection, - body: body, - ) as Request; + void apply() { + modifyRequest(unfreezedRequest); } @override - String toString() => 'MiddlewareRequest(unfreezedRequest: ' - '$unfreezedRequest, httpRequest: $httpRequest, context: $context)'; + String toString() => 'MiddlewareRequest(unfreezedRequest: $unfreezedRequest)'; } diff --git a/packages/wyatt_http_client/lib/src/models/middleware_response.dart b/packages/wyatt_http_client/lib/src/models/middleware_response.dart index 0645e776..9404fa3a 100644 --- a/packages/wyatt_http_client/lib/src/models/middleware_response.dart +++ b/packages/wyatt_http_client/lib/src/models/middleware_response.dart @@ -16,35 +16,37 @@ // along with this program. If not, see . import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/models/middleware_context.dart'; -import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/utils/http_status.dart'; class MiddlewareResponse { BaseResponse httpResponse; - MiddlewareRequest middlewareRequest; - MiddlewareContext context; + + // Proxy + int get statusCode => httpResponse.statusCode; + HttpStatus get status => HttpStatus.from(statusCode); + String get body { + if (httpResponse is Response) { + return (httpResponse as Response).body; + } else { + return ''; + } + } + int? get contentLength => httpResponse.contentLength; + Map get headers => httpResponse.headers; MiddlewareResponse({ required this.httpResponse, - required this.middlewareRequest, - required this.context, - }) { - context = context.copyWith(originalResponse: this); - } + }); MiddlewareResponse copyWith({ BaseResponse? httpResponse, - MiddlewareRequest? middlewareRequest, - MiddlewareContext? context, }) { return MiddlewareResponse( httpResponse: httpResponse ?? this.httpResponse, - middlewareRequest: middlewareRequest ?? this.middlewareRequest, - context: context ?? this.context, ); } @override - String toString() => 'MiddlewareResponse(httpResponse: $httpResponse, ' - 'middlewareRequest: $middlewareRequest, context: $context)'; + String toString() => + 'MiddlewareResponse(httpResponse: $httpResponse)'; } diff --git a/packages/wyatt_http_client/lib/src/mixins/request_transformer.dart b/packages/wyatt_http_client/lib/src/models/models.dart similarity index 67% rename from packages/wyatt_http_client/lib/src/mixins/request_transformer.dart rename to packages/wyatt_http_client/lib/src/models/models.dart index 24db0982..4170d54b 100644 --- a/packages/wyatt_http_client/lib/src/mixins/request_transformer.dart +++ b/packages/wyatt_http_client/lib/src/models/models.dart @@ -1,27 +1,20 @@ // Copyright (C) 2022 WYATT GROUP // Please see the AUTHORS file for details. -// +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/implemented_base_client.dart'; - -mixin RequestTransformer on ImplementedBaseClient { - BaseRequest requestMutator(BaseRequest request); - - @override - Future send(BaseRequest request) { - return super.send(requestMutator(request)); - } -} +export 'middleware_context.dart'; +export 'middleware_request.dart'; +export 'middleware_response.dart'; +export 'unfreezed_request.dart'; diff --git a/packages/wyatt_http_client/lib/src/pipeline.dart b/packages/wyatt_http_client/lib/src/pipeline.dart index 0d2b1477..aa0b8a1b 100644 --- a/packages/wyatt_http_client/lib/src/pipeline.dart +++ b/packages/wyatt_http_client/lib/src/pipeline.dart @@ -14,140 +14,94 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/middleware_client.dart'; +import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/models/middleware_context.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; import 'package:wyatt_http_client/src/models/middleware_response.dart'; -part 'middleware.dart'; -part 'middleware_node.dart'; - class Pipeline { - int _length = 0; - late final MiddlewareNode begin; - late final MiddlewareNode end; + final List _middlewares; - MiddlewareNode get first => begin.child; - MiddlewareNode get last => end.parent; - bool get isEmpty => _length == 0; - bool get isNotEmpty => !isEmpty; - int get length => _length; + int get length => _middlewares.length; - Pipeline() { - _initialize(); + Pipeline() : _middlewares = []; + Pipeline.fromIterable(Iterable middlewares) + : _middlewares = middlewares.toList(); + + /// Add a [Middleware] to this [Pipeline] + Pipeline addMiddleware(Middleware middleware) { + _middlewares.add(middleware); + return this; } - Pipeline.fromIterable(Iterable list) { - _initialize(); - MiddlewareNode parent = begin; - for (final element in list) { - if (element != null) { - parent = parent.insertAfter(element); + /// Create new [Pipeline] from the start or end to a specified [Middleware]. + Pipeline sub( + Middleware middleware, { + bool include = false, + bool fromEnd = false, + }) { + final nodes = []; + final list = fromEnd ? _middlewares.reversed : _middlewares; + for (final m in list) { + if (m != middleware) { + nodes.add(m); } - } - } - - Pipeline.from(Pipeline pipeline) - : this.fromIterable(pipeline.middlewares.map((node) => node.middleware)); - - void _initialize() { - _length = 0; - begin = MiddlewareNode._begin(this); - end = MiddlewareNode._end(this); - begin._child = end; - end._parent = begin; - } - - Iterable get middlewares sync* { - for (var middleware = first; - middleware != end; - middleware = middleware.child) { - yield middleware; - } - } - - int indexOf(MiddlewareNode node) { - int i = -1; - int j = -1; - for (final element in middlewares) { - j++; - if (element == node) { - i = j; - continue; - } - } - return i; - } - - int indexOfChild(Middleware middleware) { - int i = -1; - int j = -1; - for (final element in middlewares) { - j++; - if (element.middleware == middleware) { - i = j; - continue; - } - } - return i; - } - - /// Create new [Pipeline] from first [Middleware] to the specified one. - Pipeline fromUntil(Middleware middleware, {bool include = false}) { - final nodes = []; - for (final element in middlewares) { - if (element.middleware != middleware) { - nodes.add(element.middleware); - } - if (element.middleware == middleware) { + if (m == middleware) { if (include) { - nodes.add(element.middleware); + nodes.add(m); } break; } } - return Pipeline.fromIterable(nodes); + return Pipeline.fromIterable(fromEnd ? nodes.reversed : nodes); } - Pipeline addMiddleware(Middleware middleware) { - last.insertAfter(middleware); - return this; - } - - void setClient(MiddlewareClient? client) { - for (var node = first; node != end; node = node.child) { - node.middleware?.httpClient = client; - } - } - - Future onRequest(MiddlewareRequest request) async { - MiddlewareRequest req = request; - for (var node = first; node != end; node = node.child) { - req = await node.middleware?.onRequest(req) ?? req; + Future onRequest( + MiddlewareContext context, + MiddlewareRequest request, + ) async { + print('\n\nNEW REQUEST\n'); + MiddlewareRequest req = request..apply(); + MiddlewareContext ctx = context.copyWith(lastRequest: req); + for (final middleware in _middlewares) { + if (middleware is OnRequestMiddleware) { + req = await (middleware as OnRequestMiddleware) + .onRequest(ctx, request); + ctx = context.copyWith(lastRequest: req); + } } return req; } - Future onResponse(MiddlewareResponse response) async { + Future onResponse( + MiddlewareContext context, + MiddlewareResponse response, + ) async { + print('\n\nNEW RESPONSE\n'); MiddlewareResponse res = response; - for (var node = last; node != begin; node = node.parent) { - res = await node.middleware?.onResponse(res) ?? res; + MiddlewareContext ctx = context.copyWith(lastResponse: res); + for (final middleware in _middlewares.reversed) { + if (middleware is OnResponseMiddleware) { + res = await (middleware as OnResponseMiddleware) + .onResponse(ctx, response); + ctx = context.copyWith(lastResponse: res); + } } return res; } - String getLogic() { + @override + String toString() { final req = []; final res = []; - for (final m in middlewares) { - req.add('${m.middleware}'); - res.insert(0, '${m.middleware}'); + for (final middleware in _middlewares) { + if (middleware is OnRequestMiddleware) { + req.add(middleware.getName()); + } + if (middleware is OnResponseMiddleware) { + res.insert(0, middleware.getName()); + } } return '[Req] -> ${req.join(' -> ')}\n[Res] -> ${res.join(' -> ')}'; } - - @override - String toString() { - return getLogic(); - } } diff --git a/packages/wyatt_http_client/lib/src/rest_client.dart b/packages/wyatt_http_client/lib/src/rest_client.dart deleted file mode 100644 index 1925eef1..00000000 --- a/packages/wyatt_http_client/lib/src/rest_client.dart +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (C) 2022 WYATT GROUP -// Please see the AUTHORS file for details. -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -import 'dart:convert'; - -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/implemented_base_client.dart'; -import 'package:wyatt_http_client/src/mixins/body_transformer.dart'; -import 'package:wyatt_http_client/src/mixins/headers_transformer.dart'; -import 'package:wyatt_http_client/src/mixins/request_transformer.dart'; -import 'package:wyatt_http_client/src/utils/protocols.dart'; -import 'package:wyatt_http_client/src/utils/utils.dart'; - -class RestClient extends ImplementedBaseClient - with BodyTransformer, HeadersTransformer, RequestTransformer { - final Protocols protocol; - final String? authority; - - RestClient({ - this.protocol = Protocols.https, - this.authority = '', - super.inner, - }); - - @override - Object? bodyMutator(Object? body) { - print( - 'RestClient::bodyMutator -> encode in json if body is Map: ${body is Map}', - ); - if (body is Map) { - return jsonEncode(body); - } - return body; - } - - @override - Map? headersMutator( - Object? body, - Map? headers, - ) { - print( - 'RestClient::headersMutator -> add json content-type if body is Map: ${body is Map}', - ); - final mutation = { - 'content-type': 'application/json; charset=utf-8', - }; - if (body is Map) { - if (headers != null) { - headers.addAll(mutation); - return headers; - } else { - return mutation; - } - } - return headers; - } - - @override - BaseRequest requestMutator(BaseRequest request) { - print( - 'RestClient::requestMutator -> add prefix path: ${protocol.scheme}$authority', - ); - final Uri uri = Uri.parse('${protocol.scheme}$authority${request.url}'); - return Utils.copyRequestWith(request, url: uri); - } -} - -// class RestClient extends BaseClient { -// final Protocols protocol; -// final String? authority; - -// final Client _inner; - -// RestClient({ -// this.protocol = Protocols.https, -// this.authority = '', -// Client? inner, -// }) : _inner = inner ?? Client(); - -// String? forceJson(Object? body) { -// String? b; -// if (body != null && body is Map) { -// b = jsonEncode(body); -// } -// return b; -// } - -// Map? forceJsonHeader( -// Object? body, Map? headers,) { -// final Map h = headers ?? {}; -// if (body != null && body is Map) { -// h['Content-Type'] = 'application/json'; -// } -// return h; -// } - -// // @override -// // Future post( -// // Uri url, { -// // Map? headers, -// // Object? body, -// // Encoding? encoding, -// // }) { -// // final b = forceJson(body) ?? body; -// // final h = forceJsonHeader(body, headers) ?? headers; -// // print(b); -// // print(h); -// // return super.post( -// // url, -// // headers: h, -// // body: b, -// // encoding: encoding, -// // ); -// // } - -// @override -// Future put( -// Uri url, { -// Map? headers, -// Object? body, -// Encoding? encoding, -// }) { -// final b = forceJson(body) ?? body; -// final h = forceJsonHeader(body, headers) ?? headers; -// return super.put( -// url, -// headers: h, -// body: b, -// encoding: encoding, -// ); -// } - -// @override -// Future patch( -// Uri url, { -// Map? headers, -// Object? body, -// Encoding? encoding, -// }) { -// final b = forceJson(body) ?? body; -// final h = forceJsonHeader(body, headers) ?? headers; -// return super.patch( -// url, -// headers: h, -// body: b, -// encoding: encoding, -// ); -// } - -// @override -// Future delete( -// Uri url, { -// Map? headers, -// Object? body, -// Encoding? encoding, -// }) { -// final b = forceJson(body) ?? body; -// final h = forceJsonHeader(body, headers) ?? headers; -// return super.delete( -// url, -// headers: h, -// body: b, -// encoding: encoding, -// ); -// } - -// @override -// Future send(BaseRequest request) { -// final Uri uri = Uri.parse('${protocol.scheme}$authority${request.url}'); -// return _inner.send( -// Utils.copyRequestWith( -// request, -// url: uri, -// ), -// ); -// } -// } diff --git a/packages/wyatt_http_client/lib/src/utils/convert.dart b/packages/wyatt_http_client/lib/src/utils/convert.dart index 30f1718d..96746448 100644 --- a/packages/wyatt_http_client/lib/src/utils/convert.dart +++ b/packages/wyatt_http_client/lib/src/utils/convert.dart @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +import 'dart:convert'; + class Convert { static String toHex(List bytes, {bool upperCase = false}) { final buffer = StringBuffer(); @@ -29,6 +31,15 @@ class Convert { return buffer.toString(); } } + + static String mapToQuery(Map map, {Encoding? encoding}) { + final pairs = >[]; + map.forEach((key, value) => pairs.add([ + Uri.encodeQueryComponent(key, encoding: encoding ?? utf8), + Uri.encodeQueryComponent(value, encoding: encoding ?? utf8) + ]),); + return pairs.map((pair) => '${pair[0]}=${pair[1]}').join('&'); + } } extension UriX on Uri { diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart b/packages/wyatt_http_client/lib/src/utils/delay.dart similarity index 52% rename from packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart rename to packages/wyatt_http_client/lib/src/utils/delay.dart index 5704788c..af89f3ac 100644 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart +++ b/packages/wyatt_http_client/lib/src/utils/delay.dart @@ -14,22 +14,23 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/authentication/interfaces/header_authentication_client.dart'; +import 'dart:core'; +import 'dart:math'; -typedef TokenParser = String Function(Map); +abstract class Delay { + static Duration getRetryDelay(int attempt) { + assert(attempt >= 0, 'attempt cannot be negative'); + if (attempt <= 0) { + return Duration.zero; + } + final rand = Random(); + final Duration delayFactor = const Duration(milliseconds: 200); + final double randomizationFactor = 0.25; + final Duration maxDelay = const Duration(seconds: 30); -abstract class Oauth2Client extends HeaderAuthenticationClient { - Oauth2Client(super.inner); - - Future refresh() { - return Future.value(); - } - - Future authorize( - Map body, { - Map? headers, - }) { - return Future.value(); + final rf = randomizationFactor * (rand.nextDouble() * 2 - 1) + 1; + final exp = min(attempt, 31); // prevent overflows. + final delay = delayFactor * pow(2.0, exp) * rf; + return delay < maxDelay ? delay : maxDelay; } } diff --git a/packages/wyatt_http_client/lib/src/utils/protocols.dart b/packages/wyatt_http_client/lib/src/utils/protocols.dart index 6e483a42..97b9d5f3 100644 --- a/packages/wyatt_http_client/lib/src/utils/protocols.dart +++ b/packages/wyatt_http_client/lib/src/utils/protocols.dart @@ -18,6 +18,5 @@ enum Protocols { http, https; - String get name => toString().split('.').last; String get scheme => '$name://'; } diff --git a/packages/wyatt_http_client/lib/src/utils/request_utils.dart b/packages/wyatt_http_client/lib/src/utils/request_utils.dart new file mode 100644 index 00000000..b50c0063 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/utils/request_utils.dart @@ -0,0 +1,89 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:http/http.dart'; + +abstract class RequestUtils { + static Request _copyNormalRequestWith( + Request original, { + String? method, + Uri? url, + Map? headers, + int? maxRedirects, + bool? followRedirects, + bool? persistentConnection, + String? body, + }) { + final request = Request(method ?? original.method, url ?? original.url) + ..followRedirects = followRedirects ?? original.followRedirects + ..headers.addAll(headers ?? original.headers) + ..maxRedirects = maxRedirects ?? original.maxRedirects + ..persistentConnection = + persistentConnection ?? original.persistentConnection + ..body = body ?? original.body; + + return request; + } + + static BaseRequest copyRequestWith( + BaseRequest original, { + String? method, + Uri? url, + Map? headers, + int? maxRedirects, + bool? followRedirects, + bool? persistentConnection, + String? body, + }) { + if (original is Request) { + return _copyNormalRequestWith( + original, + method: method, + url: url, + headers: headers, + maxRedirects: maxRedirects, + followRedirects: followRedirects, + persistentConnection: persistentConnection, + body: body, + ); + } else { + throw UnimplementedError( + 'Cannot handle requests of type ${original.runtimeType}', + ); + } + } + + static Request _copyNormalRequest(Request original) { + final request = Request(original.method, original.url) + ..followRedirects = original.followRedirects + ..headers.addAll(original.headers) + ..maxRedirects = original.maxRedirects + ..persistentConnection = original.persistentConnection + ..body = original.body; + + return request; + } + + static BaseRequest copyRequest(BaseRequest original) { + if (original is Request) { + return _copyNormalRequest(original); + } else { + throw UnimplementedError( + 'Cannot handle requests of type ${original.runtimeType}', + ); + } + } +} diff --git a/packages/wyatt_http_client/lib/src/utils/utils.dart b/packages/wyatt_http_client/lib/src/utils/utils.dart index 9a7a4d20..5dfc082c 100644 --- a/packages/wyatt_http_client/lib/src/utils/utils.dart +++ b/packages/wyatt_http_client/lib/src/utils/utils.dart @@ -1,89 +1,23 @@ // Copyright (C) 2022 WYATT GROUP // Please see the AUTHORS file for details. -// +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:http/http.dart'; - -abstract class Utils { - static Request _copyNormalRequest(Request original) { - final request = Request(original.method, original.url) - ..followRedirects = original.followRedirects - ..headers.addAll(original.headers) - ..maxRedirects = original.maxRedirects - ..persistentConnection = original.persistentConnection - ..body = original.body; - - return request; - } - - static Request _copyNormalRequestWith( - Request original, { - String? method, - Uri? url, - Map? headers, - int? maxRedirects, - bool? followRedirects, - bool? persistentConnection, - String? body, - }) { - final request = Request(method ?? original.method, url ?? original.url) - ..followRedirects = followRedirects ?? original.followRedirects - ..headers.addAll(headers ?? original.headers) - ..maxRedirects = maxRedirects ?? original.maxRedirects - ..persistentConnection = - persistentConnection ?? original.persistentConnection - ..body = body ?? original.body; - - return request; - } - - static BaseRequest copyRequest(BaseRequest original) { - if (original is Request) { - return _copyNormalRequest(original); - } else { - throw UnimplementedError( - 'Cannot handle requests of type ${original.runtimeType}', - ); - } - } - - static BaseRequest copyRequestWith( - BaseRequest original, { - String? method, - Uri? url, - Map? headers, - int? maxRedirects, - bool? followRedirects, - bool? persistentConnection, - String? body, - }) { - if (original is Request) { - return _copyNormalRequestWith( - original, - method: method, - url: url, - headers: headers, - maxRedirects: maxRedirects, - followRedirects: followRedirects, - persistentConnection: persistentConnection, - body: body, - ); - } else { - throw UnimplementedError( - 'Cannot handle requests of type ${original.runtimeType}', - ); - } - } -} +export 'authentication_methods.dart'; +export 'digest_auth.dart'; +export 'header_keys.dart'; +export 'http_methods.dart'; +export 'http_status.dart'; +export 'protocols.dart'; +export 'request_utils.dart'; diff --git a/packages/wyatt_http_client/lib/wyatt_http_client.dart b/packages/wyatt_http_client/lib/wyatt_http_client.dart index c71cd239..e728cf6a 100644 --- a/packages/wyatt_http_client/lib/wyatt_http_client.dart +++ b/packages/wyatt_http_client/lib/wyatt_http_client.dart @@ -1,17 +1,24 @@ // Copyright (C) 2022 WYATT GROUP // Please see the AUTHORS file for details. -// +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with this program. If not, see . library wyatt_http_client; + +export 'src/middleware.dart'; +export 'src/middleware_client.dart'; +export 'src/middlewares/middlewares.dart'; +export 'src/models/models.dart'; +export 'src/pipeline.dart'; +export 'src/utils/utils.dart';