diff --git a/CHANGELOG.md b/CHANGELOG.md index 60fc1efb..af4dc123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,30 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 2022-06-24 + +### Changes + +--- + +Packages with breaking changes: + + - There are no breaking changes in this release. + +Packages with other changes: + + - [`wyatt_http_client` - `v1.2.0`](#wyatt_http_client---v120) + +--- + +#### `wyatt_http_client` - `v1.2.0` + + - **FEAT**: add new middleware feature. + - **FEAT**: implements doublelinked list for middlewares. + - **FEAT**: [WIP] implements middleware system. + - **FEAT**: [WIP] work on middleware feature. + + ## 2022-05-23 ### Changes diff --git a/packages/wyatt_http_client/CHANGELOG.md b/packages/wyatt_http_client/CHANGELOG.md index a9868ded..f597ef73 100644 --- a/packages/wyatt_http_client/CHANGELOG.md +++ b/packages/wyatt_http_client/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.2.0 + + - **FEAT**: add new middleware feature. + - **FEAT**: implements doublelinked list for middlewares. + - **FEAT**: [WIP] implements middleware system. + - **FEAT**: [WIP] work on middleware feature. + ## 1.1.0 - **FEAT**: add oauth2 refresh token client. 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 new file mode 100644 index 00000000..5393611c --- /dev/null +++ b/packages/wyatt_http_client/example/http_client_fastapi_example.dart @@ -0,0 +1,398 @@ +// 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/middleware_client.dart'; +import 'package:wyatt_http_client/src/middlewares/body_to_json_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'; +import 'package:wyatt_http_client/src/utils/http_status.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 MiddlewareClient client; + final int apiVersion; + + FastAPI({ + this.baseUrl = 'localhost:80', + MiddlewareClient? client, + this.apiVersion = 1, + }) : client = client ?? MiddlewareClient(); + + 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.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> 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 Pipeline pipeline = Pipeline() + .addMiddleware( + UriPrefixMiddleware( + protocol: Protocols.http, + authority: 'localhost:80', + ), + ) + .addMiddleware(BodyToJsonMiddleware()) + .addMiddleware( + 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); + 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, + ), + ); + final registeredAccount = await api.signUp( + SignUp(sessionId: verifiedAccount.sessionId ?? '', password: 'password'), + ); + final signedInAccount = await api.signInWithPassword( + Login(email: 'git@pcl.ovh', password: 'password'), + ); + final accountList = await api.getAccountList(); + print(accountList); +} diff --git a/packages/wyatt_http_client/example/pipeline.dart b/packages/wyatt_http_client/example/pipeline.dart new file mode 100644 index 00000000..334fc509 --- /dev/null +++ b/packages/wyatt_http_client/example/pipeline.dart @@ -0,0 +1,162 @@ +// 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/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'; + +// 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 UnsafeAuthMiddleware auth = UnsafeAuthMiddleware(); + final Pipeline pipeline = Pipeline() + .addMiddleware( + UriPrefixMiddleware( + protocol: Protocols.http, + authority: 'localhost:80', + ), + ) + .addMiddleware(BodyToJsonMiddleware()) + .addMiddleware( + 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); + final client = MiddlewareClient(pipeline: pipeline); + 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/refresh_token_client.dart b/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart deleted file mode 100644 index 67d6a1d0..00000000 --- a/packages/wyatt_http_client/lib/src/authentication/refresh_token_client.dart +++ /dev/null @@ -1,115 +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 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, - ]) { - 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: jsonEncode(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; - } - } - } - - @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); - } - } - } - - @override - Future send(BaseRequest request) async { - 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/authentication/interfaces/header_authentication_client.dart b/packages/wyatt_http_client/lib/src/middleware.dart similarity index 54% rename from packages/wyatt_http_client/lib/src/authentication/interfaces/header_authentication_client.dart rename to packages/wyatt_http_client/lib/src/middleware.dart index c206748f..07cf6323 100644 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/header_authentication_client.dart +++ b/packages/wyatt_http_client/lib/src/middleware.dart @@ -14,23 +14,25 @@ // 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'; +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'; -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); - return super.send(request); - } +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 new file mode 100644 index 00000000..0f646f70 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middleware_client.dart @@ -0,0 +1,129 @@ +// 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/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 Pipeline pipeline; + + MiddlewareClient({ + Pipeline? pipeline, + Client? inner, + }) : pipeline = pipeline ?? Pipeline(), + inner = inner ?? Client() { + print('Using Pipeline:\n$pipeline'); + } + + @override + Future head(Uri url, {Map? headers}) => + _sendUnstreamed(HttpMethods.head.method, url, headers); + + @override + Future get(Uri url, {Map? headers}) => + _sendUnstreamed(HttpMethods.get.method, url, headers); + + @override + Future post( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _sendUnstreamed(HttpMethods.post.method, url, headers, body, encoding); + + @override + Future put( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _sendUnstreamed(HttpMethods.put.method, url, headers, body, encoding); + + @override + Future patch( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _sendUnstreamed(HttpMethods.patch.method, url, headers, body, encoding); + + @override + Future delete( + Uri url, { + Map? headers, + Object? body, + Encoding? encoding, + }) => + _sendUnstreamed(HttpMethods.delete.method, 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 originalRequest = MiddlewareRequest( + unfreezedRequest: UnfreezedRequest( + method: method, + url: url, + headers: headers, + body: body, + encoding: encoding, + ), + ); + final requestContext = MiddlewareContext( + pipeline: pipeline, + client: this, + originalRequest: originalRequest, + ); + + final modifiedRequest = await pipeline.onRequest( + requestContext, + originalRequest.copyWith(), + ); + + final originalResponse = MiddlewareResponse( + httpResponse: await Response.fromStream( + await send(modifiedRequest.request), + ), + ); + + 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/authentication/interfaces/authentication_client.dart b/packages/wyatt_http_client/lib/src/middlewares/access_token_auth_middleware.dart similarity index 59% rename from packages/wyatt_http_client/lib/src/authentication/interfaces/authentication_client.dart rename to packages/wyatt_http_client/lib/src/middlewares/access_token_auth_middleware.dart index 9e956216..ece1af01 100644 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/authentication_client.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/access_token_auth_middleware.dart @@ -1,37 +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'; -import 'package:wyatt_http_client/src/rest_client.dart'; - -abstract class AuthenticationClient extends BaseClient { - final BaseClient _inner; - - BaseClient get inner => _inner; - - AuthenticationClient(BaseClient? inner) : _inner = inner ?? RestClient(); - - @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/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 new file mode 100644 index 00000000..a77048d8 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart @@ -0,0 +1,49 @@ +// 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'; + +class BodyToJsonMiddleware with OnRequestMiddleware implements Middleware { + @override + String getName() => 'BodyToJson'; + + @override + Future onRequest( + MiddlewareContext context, + MiddlewareRequest request, + ) async { + print( + '${getName()}::OnRequest\n' + '>> Transforms body in json if Map then update ' + 'headers with right content-type', + ); + final mutation = { + 'content-type': 'application/json; charset=utf-8', + }; + if (request.body is Map) { + final Map headers = request.headers..addAll(mutation); + request.modifyRequest( + request.unfreezedRequest + .copyWith(headers: headers, body: jsonEncode(request.body)), + ); + } + return request; + } +} diff --git a/packages/wyatt_http_client/lib/src/authentication/interfaces/url_authentication_client.dart b/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart similarity index 62% rename from packages/wyatt_http_client/lib/src/authentication/interfaces/url_authentication_client.dart rename to packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart index 9d2dad13..65b766ca 100644 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/url_authentication_client.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart @@ -14,17 +14,9 @@ // 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; +import 'package:wyatt_http_client/src/middleware.dart'; +class DefaultMiddleware implements Middleware { @override - Future send(BaseRequest request) { - final newRequest = modifyRequest(request); - return super.send(newRequest); - } + 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/middlewares/middlewares.dart b/packages/wyatt_http_client/lib/src/middlewares/middlewares.dart new file mode 100644 index 00000000..724b3ac9 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middlewares/middlewares.dart @@ -0,0 +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 . + +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/simple_logger_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart new file mode 100644 index 00000000..8e2d2637 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart @@ -0,0 +1,54 @@ +// 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'; + +class SimpleLoggerMiddleware + with OnRequestMiddleware, OnResponseMiddleware + implements Middleware { + @override + String getName() => 'SimpleLogger'; + + @override + Future onRequest( + MiddlewareContext context, + MiddlewareRequest request, + ) async { + print( + '${getName()}::OnRequest\n' + '>> ${request.method} ${request.url}\n' + '>> Headers: ${request.headers}\n>> Body: ${request.encodedBody}', + ); + return request; + } + + @override + Future onResponse( + MiddlewareContext context, + MiddlewareResponse response, + ) async { + print( + '${getName()}::OnResponse\n' + '>> Status: ${response.status.name.toUpperCase()}\n' + '>> Length: ${response.contentLength ?? '0'} bytes', + // '>> Body: ${response.body}', + ); + 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/rest_client.dart b/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart similarity index 56% rename from packages/wyatt_http_client/lib/src/rest_client.dart rename to packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart index cde6f3d6..c52f3fdf 100644 --- a/packages/wyatt_http_client/lib/src/rest_client.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart @@ -14,25 +14,35 @@ // 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.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/protocols.dart'; -import 'package:wyatt_http_client/src/utils/utils.dart'; -class RestClient extends BaseClient { +class UriPrefixMiddleware with OnRequestMiddleware implements Middleware { final Protocols protocol; final String? authority; - final Client _inner; - - RestClient({ - this.protocol = Protocols.https, - this.authority = '', - Client? inner, - }) : _inner = inner ?? Client(); + UriPrefixMiddleware({ + required this.protocol, + required this.authority, + }); @override - Future send(BaseRequest request) { + String getName() => 'UriPrefix'; + + @override + Future onRequest( + MiddlewareContext context, + MiddlewareRequest request, + ) async { final Uri uri = Uri.parse('${protocol.scheme}$authority${request.url}'); - return _inner.send(Utils.copyRequestWith(request, url: uri)); + 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/models/middleware_context.dart b/packages/wyatt_http_client/lib/src/models/middleware_context.dart new file mode 100644 index 00000000..19877109 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/models/middleware_context.dart @@ -0,0 +1,62 @@ +// 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/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() { + 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 new file mode 100644 index 00000000..42c9ac34 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/models/middleware_request.dart @@ -0,0 +1,82 @@ +// 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 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/models/unfreezed_request.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; + + 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, + }) : _httpRequest = Request(unfreezedRequest.method, unfreezedRequest.url); + + MiddlewareRequest copyWith({ + UnfreezedRequest? unfreezedRequest, + }) { + return MiddlewareRequest( + unfreezedRequest: unfreezedRequest ?? this.unfreezedRequest, + ); + } + + void modifyRequest(UnfreezedRequest unfreezedRequest) { + String? _body; + if (unfreezedRequest.body != null) { + final body = unfreezedRequest.body; + if (body is String) { + _body = body; + } else if (body is List) { + _body = String.fromCharCodes(body.cast()); + } else if (body is Map) { + _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; + } + + void apply() { + modifyRequest(unfreezedRequest); + } + + @override + 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 new file mode 100644 index 00000000..9404fa3a --- /dev/null +++ b/packages/wyatt_http_client/lib/src/models/middleware_response.dart @@ -0,0 +1,52 @@ +// 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/utils/http_status.dart'; + +class MiddlewareResponse { + BaseResponse httpResponse; + + // 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, + }); + + 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/models.dart b/packages/wyatt_http_client/lib/src/models/models.dart new file mode 100644 index 00000000..4170d54b --- /dev/null +++ b/packages/wyatt_http_client/lib/src/models/models.dart @@ -0,0 +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 . + +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/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..aa0b8a1b --- /dev/null +++ b/packages/wyatt_http_client/lib/src/pipeline.dart @@ -0,0 +1,107 @@ +// 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'; + +class Pipeline { + final List _middlewares; + + int get length => _middlewares.length; + + Pipeline() : _middlewares = []; + Pipeline.fromIterable(Iterable middlewares) + : _middlewares = middlewares.toList(); + + /// Add a [Middleware] to this [Pipeline] + Pipeline addMiddleware(Middleware middleware) { + _middlewares.add(middleware); + return this; + } + + /// 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); + } + if (m == middleware) { + if (include) { + nodes.add(m); + } + break; + } + } + return Pipeline.fromIterable(fromEnd ? nodes.reversed : nodes); + } + + 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( + MiddlewareContext context, + MiddlewareResponse response, + ) async { + print('\n\nNEW RESPONSE\n'); + MiddlewareResponse res = response; + 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; + } + + @override + String toString() { + final req = []; + final res = []; + 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(' -> ')}'; + } +} 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/utils/delay.dart b/packages/wyatt_http_client/lib/src/utils/delay.dart new file mode 100644 index 00000000..af89f3ac --- /dev/null +++ b/packages/wyatt_http_client/lib/src/utils/delay.dart @@ -0,0 +1,36 @@ +// 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:core'; +import 'dart:math'; + +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); + + 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/authentication/interfaces/oauth2_client.dart b/packages/wyatt_http_client/lib/src/utils/http_methods.dart similarity index 61% rename from packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart rename to packages/wyatt_http_client/lib/src/utils/http_methods.dart index 1da70e99..49087965 100644 --- a/packages/wyatt_http_client/lib/src/authentication/interfaces/oauth2_client.dart +++ b/packages/wyatt_http_client/lib/src/utils/http_methods.dart @@ -14,21 +14,15 @@ // 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/authentication/interfaces/header_authentication_client.dart'; +enum HttpMethods { + head('HEAD'), + get('GET'), + post('POST'), + put('PUT'), + patch('PATCH'), + delete('DELETE'); -typedef TokenParser = String Function(Map); + final String method; -abstract class Oauth2Client extends HeaderAuthenticationClient { - Oauth2Client(super.inner); - - Future refresh() { - return Future.value(); - } - - Future authorize( - Map body, { - Map? headers, - }) { - return Future.value(); - } + 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); + } + } 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'; diff --git a/packages/wyatt_http_client/pubspec.yaml b/packages/wyatt_http_client/pubspec.yaml index 356f0f3f..a56f52db 100644 --- a/packages/wyatt_http_client/pubspec.yaml +++ b/packages/wyatt_http_client/pubspec.yaml @@ -1,7 +1,7 @@ name: wyatt_http_client description: A Dart client for RESTful APIs with authentication. repository: https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_http_client -version: 1.1.0 +version: 1.2.0 environment: sdk: '>=2.17.0 <3.0.0'