diff --git a/packages/wyatt_http_client/example/http_client_fastapi_example.dart b/packages/wyatt_http_client/example/http_client_fastapi_example.dart index 21c13b42..93e40d3e 100644 --- a/packages/wyatt_http_client/example/http_client_fastapi_example.dart +++ b/packages/wyatt_http_client/example/http_client_fastapi_example.dart @@ -17,8 +17,13 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; -import 'package:wyatt_http_client/src/authentication/refresh_token_client.dart'; -import 'package:wyatt_http_client/src/rest_client.dart'; +import 'package:wyatt_http_client/src/middleware_client.dart'; +import 'package:wyatt_http_client/src/middlewares/body_to_json_middleware.dart'; +import 'package:wyatt_http_client/src/middlewares/refresh_token_middleware.dart'; +import 'package:wyatt_http_client/src/middlewares/simple_logger_middleware.dart'; +import 'package:wyatt_http_client/src/middlewares/uri_prefix_middleware.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; +import 'package:wyatt_http_client/src/utils/http_status.dart'; import 'package:wyatt_http_client/src/utils/protocols.dart'; enum EmailVerificationAction { @@ -262,24 +267,14 @@ class Login { class FastAPI { final String baseUrl; - final RefreshTokenClient client; + final MiddlewareClient client; final int apiVersion; FastAPI({ this.baseUrl = 'localhost:80', - RefreshTokenClient? client, + MiddlewareClient? client, this.apiVersion = 1, - }) : client = client ?? - RefreshTokenClient( - authorizationEndpoint: '', - tokenEndpoint: '', - accessTokenParser: (body) => body['access_token']! as String, - refreshTokenParser: (body) => body['refresh_token']! as String, - inner: RestClient( - protocol: Protocols.http, - authority: baseUrl, - ), - ); + }) : client = client ?? MiddlewareClient(); String get apiPath => '/api/v$apiVersion'; @@ -323,14 +318,21 @@ class FastAPI { } Future signInWithPassword(Login login) async { - final r = await client.authorize(login.toMap()); - return TokenSuccess.fromJson(r.body); + final r = await client.post( + Uri.parse('$apiPath/auth/sign-in-with-password'), + body: login.toMap(), + ); + if (r.statusCode != 200) { + throw Exception('Invalid reponse: ${r.statusCode}'); + } else { + return TokenSuccess.fromJson(r.body); + } } - Future refresh() async { - final r = await client.refresh(); - return TokenSuccess.fromJson(r?.body ?? ''); - } + // Future refresh() async { + // final r = await client.refresh(); + // return TokenSuccess.fromJson(r?.body ?? ''); + // } Future> getAccountList() async { final r = await client.get( @@ -351,17 +353,30 @@ class FastAPI { } void main(List args) async { + final Pipeline pipeline = Pipeline() + .addMiddleware(SimpleLoggerMiddleware()) + .addMiddleware( + UriPrefixMiddleware( + protocol: Protocols.http, + authority: 'localhost:80', + ), + ) + .addMiddleware(BodyToJsonMiddleware()) + .addMiddleware( + RefreshTokenMiddleware( + authorizationEndpoint: '/api/v1/auth/sign-in-with-password', + tokenEndpoint: '/api/v1/auth/refresh', + accessTokenParser: (body) => body['access_token']! as String, + refreshTokenParser: (body) => body['refresh_token']! as String, + unauthorized: HttpStatus.forbidden, + ), + ); + + print(pipeline.getLogic()); + final client = MiddlewareClient(pipeline: pipeline); + final api = FastAPI( - client: RefreshTokenClient( - authorizationEndpoint: '/api/v1/auth/sign-in-with-password', - tokenEndpoint: '/api/v1/auth/refresh', - accessTokenParser: (body) => body['access_token']! as String, - refreshTokenParser: (body) => body['refresh_token']! as String, - inner: RestClient( - protocol: Protocols.http, - authority: 'localhost:80', - ), - ), + client: client, ); // await api.sendSignUpCode('git@pcl.ovh'); diff --git a/packages/wyatt_http_client/example/pipeline.dart b/packages/wyatt_http_client/example/pipeline.dart index 1ee6cade..4b9fa9fe 100644 --- a/packages/wyatt_http_client/example/pipeline.dart +++ b/packages/wyatt_http_client/example/pipeline.dart @@ -117,7 +117,7 @@ import 'package:wyatt_http_client/src/utils/protocols.dart'; // } Future main(List args) async { - final Pipeline pipeline1 = Pipeline() + final Pipeline pipeline = Pipeline() .addMiddleware(SimpleLoggerMiddleware()) .addMiddleware( UriPrefixMiddleware( @@ -125,21 +125,18 @@ Future main(List args) async { authority: 'localhost:80', ), ) - .addMiddleware(BodyToJsonMiddleware()); - - final Pipeline pipeline2 = Pipeline().addMiddleware( - RefreshTokenMiddleware( - authorizationEndpoint: - 'http://localhost:80/api/v1/account/test?action=authorize', - tokenEndpoint: 'http://localhost:80/api/v1/account/test?action=refresh', - innerClientMiddlewares: pipeline1.middleware, - ), - ); - - final Pipeline pipeline = pipeline1 + pipeline2; + .addMiddleware(BodyToJsonMiddleware()) + .addMiddleware( + RefreshTokenMiddleware( + authorizationEndpoint: '/api/v1/account/test?action=authorize', + tokenEndpoint: '/api/v1/account/test?action=refresh', + accessTokenParser: (body) => body['access_token']! as String, + refreshTokenParser: (body) => body['refresh_token']! as String, + ), + ); print(pipeline.getLogic()); - final client = MiddlewareClient(pipeline); + final client = MiddlewareClient(pipeline: pipeline); final r = await client.post( Uri.parse('/api/v1/account/test'), body: { diff --git a/packages/wyatt_http_client/lib/src/middleware.dart b/packages/wyatt_http_client/lib/src/middleware.dart index 1c36442c..4295a278 100644 --- a/packages/wyatt_http_client/lib/src/middleware.dart +++ b/packages/wyatt_http_client/lib/src/middleware.dart @@ -14,60 +14,27 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/middleware_client.dart'; -import 'package:wyatt_http_client/src/models/middleware_request.dart'; -import 'package:wyatt_http_client/src/models/middleware_response.dart'; +part of 'pipeline.dart'; class Middleware { - Middleware? child; - + /// The http [MiddlewareClient] used by this [Middleware] MiddlewareClient? _client; - Middleware({ - this.child, - }); + String getName() => 'MiddlewareNode'; - Middleware._({ - this.child, - MiddlewareClient? client, - }) : _client = client; + // ignore: avoid_setters_without_getters + set httpClient(MiddlewareClient? client) => _client = client; - String getName() => 'Middleware'; + Client? get client => _client?.inner; - void setClient(MiddlewareClient? client) { - _client = client; - child?.setClient(client); - } - - Client? getClient() { - return _client?.inner; - } - - Middleware deepCopy() { - if (child != null) { - return Middleware._(child: child?.deepCopy(), client: _client); - } else { - return Middleware._(client: _client); - } - } - - void addChild(Middleware middleware) { - if (child != null) { - child?.addChild(middleware); - } else { - child = middleware; - } - } - - MiddlewareRequest onRequest( + Future onRequest( MiddlewareRequest request, - ) { - return child?.onRequest(request) ?? request; + ) async { + return request; } - MiddlewareResponse onResponse(MiddlewareResponse response) { - return child?.onResponse(response) ?? response; + Future onResponse(MiddlewareResponse response) async { + return response; } @override diff --git a/packages/wyatt_http_client/lib/src/middleware_client.dart b/packages/wyatt_http_client/lib/src/middleware_client.dart index ce02e8da..e6b0dd43 100644 --- a/packages/wyatt_http_client/lib/src/middleware_client.dart +++ b/packages/wyatt_http_client/lib/src/middleware_client.dart @@ -17,33 +17,32 @@ import 'dart:convert'; import 'package:http/http.dart'; -import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/models/middleware_context.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; import 'package:wyatt_http_client/src/models/middleware_response.dart'; import 'package:wyatt_http_client/src/models/unfreezed_request.dart'; import 'package:wyatt_http_client/src/pipeline.dart'; +import 'package:wyatt_http_client/src/utils/http_methods.dart'; class MiddlewareClient extends BaseClient { final Client inner; - final Middleware middleware; final Pipeline pipeline; - MiddlewareClient( - this.pipeline, { - Middleware? middleware, + MiddlewareClient({ + Pipeline? pipeline, Client? inner, - }) : inner = inner ?? Client(), - middleware = middleware ?? pipeline.middleware { - this.middleware.setClient(this); + }) : pipeline = pipeline ?? Pipeline(), + inner = inner ?? Client() { + this.pipeline.setClient(this); } @override Future head(Uri url, {Map? headers}) => - _sendUnstreamed('HEAD', url, headers); + _sendUnstreamed(HttpMethods.head.method, url, headers); @override Future get(Uri url, {Map? headers}) => - _sendUnstreamed('GET', url, headers); + _sendUnstreamed(HttpMethods.get.method, url, headers); @override Future post( @@ -52,7 +51,7 @@ class MiddlewareClient extends BaseClient { Object? body, Encoding? encoding, }) => - _sendUnstreamed('POST', url, headers, body, encoding); + _sendUnstreamed(HttpMethods.post.method, url, headers, body, encoding); @override Future put( @@ -61,7 +60,7 @@ class MiddlewareClient extends BaseClient { Object? body, Encoding? encoding, }) => - _sendUnstreamed('PUT', url, headers, body, encoding); + _sendUnstreamed(HttpMethods.put.method, url, headers, body, encoding); @override Future patch( @@ -70,7 +69,7 @@ class MiddlewareClient extends BaseClient { Object? body, Encoding? encoding, }) => - _sendUnstreamed('PATCH', url, headers, body, encoding); + _sendUnstreamed(HttpMethods.patch.method, url, headers, body, encoding); @override Future delete( @@ -79,7 +78,7 @@ class MiddlewareClient extends BaseClient { Object? body, Encoding? encoding, }) => - _sendUnstreamed('DELETE', url, headers, body, encoding); + _sendUnstreamed(HttpMethods.delete.method, url, headers, body, encoding); @override Future send(BaseRequest request) { @@ -93,24 +92,34 @@ class MiddlewareClient extends BaseClient { Object? body, Encoding? encoding, ]) async { - final modifiedRequest = middleware.onRequest( - MiddlewareRequest( - unfreezedRequest: UnfreezedRequest( - method: method, - url: url, - headers: headers, - body: body, - encoding: encoding, - ), - httpRequest: Request(method, url), + final originalRequest = MiddlewareRequest( + unfreezedRequest: UnfreezedRequest( + method: method, + url: url, + headers: headers, + body: body, + encoding: encoding, ), + httpRequest: Request(method, url), + context: MiddlewareContext(pipeline: pipeline), + ); + final modifiedRequest = await pipeline.onRequest( + originalRequest.copyWith(), ); final res = await Response.fromStream( await send(modifiedRequest.httpRequest), ); - final response = - middleware.onResponse(MiddlewareResponse(httpResponse: res)); + final response = await pipeline.onResponse( + MiddlewareResponse( + httpResponse: res, + middlewareRequest: modifiedRequest, + context: MiddlewareContext( + pipeline: pipeline, + originalRequest: originalRequest, + ), + ), + ); return response.httpResponse as Response; } diff --git a/packages/wyatt_http_client/lib/src/middleware_node.dart b/packages/wyatt_http_client/lib/src/middleware_node.dart new file mode 100644 index 00000000..32697e40 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/middleware_node.dart @@ -0,0 +1,103 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +part of 'pipeline.dart'; + +class MiddlewareNode { + final Pipeline pipeline; + + Middleware? middleware; + + late MiddlewareNode _parent; + late MiddlewareNode _child; + final bool _isEnd; + + /// Reference to the previous [MiddlewareNode] in the [Pipeline] + MiddlewareNode get parent => _parent; + + /// Reference to the next [MiddlewareNode] in the [Pipeline] + MiddlewareNode get child => _child; + + /// Whether this is the begin [MiddlewareNode] + bool get isBegin => _parent == this; + + /// Whether this is the end [MiddlewareNode] + bool get isEnd => _child == this; + + /// Whether this is the first [MiddlewareNode] + bool get isFirst => !isBegin && _parent == pipeline.begin; + + /// Whether this is the last [MiddlewareNode] + bool get isLast => !isEnd && _child == pipeline.end; + + MiddlewareNode._( + this.pipeline, + this.middleware, { + MiddlewareNode? parent, + MiddlewareNode? child, + }) : _isEnd = false { + _parent = parent ?? this; + _child = child ?? this; + } + + MiddlewareNode._end(this.pipeline) : _isEnd = true { + _child = this; + } + + MiddlewareNode._begin(this.pipeline) : _isEnd = true { + _parent = this; + } + + /// Creates a new [MiddlewareNode] right **before** this in [pipeline] + MiddlewareNode insertBefore(Middleware middleware) { + if (isBegin) { + throw StateError( + 'A MiddlewareNode cannot be inserted ' + 'before begin MiddlewareNode', + ); + } + final newMiddlewareNode = + MiddlewareNode._(pipeline, middleware, parent: _parent, child: this); + _parent._child = newMiddlewareNode; + _parent = newMiddlewareNode; + pipeline._length++; + return newMiddlewareNode; + } + + /// Creates a new [MiddlewareNode] right **after** this in [pipeline] + MiddlewareNode insertAfter(Middleware middleware) { + if (isEnd) { + throw StateError( + 'A MiddlewareNode cannot be inserted ' + 'after end MiddlewareNode', + ); + } + final newMiddlewareNode = + MiddlewareNode._(pipeline, middleware, parent: this, child: _child); + _child._parent = newMiddlewareNode; + _child = newMiddlewareNode; + pipeline._length++; + return newMiddlewareNode; + } + + MiddlewareNode remove() { + if (_isEnd) throw StateError('Cannot remove end MiddlewareNode'); + _child._parent = _parent; + _parent._child = _child; + pipeline._length--; + return child; + } +} diff --git a/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart index 99cc7bc8..af124dc3 100644 --- a/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/body_to_json_middleware.dart @@ -16,21 +16,17 @@ import 'dart:convert'; -import 'package:wyatt_http_client/src/middleware.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; class BodyToJsonMiddleware extends Middleware { - BodyToJsonMiddleware({ - super.child, - }); + @override + String getName() => 'BodyToJson'; @override - String getName() => 'BodyToJsonMiddleware'; - - @override - MiddlewareRequest onRequest(MiddlewareRequest request) { + Future onRequest(MiddlewareRequest request) { print( - 'BodyToJson::OnRequest: transforms body in json if Map then update ' + '${getName()}::OnRequest: transforms body in json if Map then update ' 'headers with right content-type', ); var newReq = request.unfreezedRequest; diff --git a/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart index 7acd70a0..67c65728 100644 --- a/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/default_middleware.dart @@ -14,13 +14,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:wyatt_http_client/src/middleware.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; class DefaultMiddleware extends Middleware { - DefaultMiddleware({ - super.child, - }); - @override String getName() => 'DefaultMiddleware'; } diff --git a/packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart index 7394acb0..02391f61 100644 --- a/packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/refresh_token_middleware.dart @@ -14,59 +14,200 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:wyatt_http_client/src/middleware.dart'; +import 'dart:convert'; + +import 'package:http/http.dart'; import 'package:wyatt_http_client/src/middleware_client.dart'; -import 'package:wyatt_http_client/src/middlewares/default_middleware.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/models/middleware_response.dart'; import 'package:wyatt_http_client/src/pipeline.dart'; +import 'package:wyatt_http_client/src/utils/authentication_methods.dart'; +import 'package:wyatt_http_client/src/utils/header_keys.dart'; +import 'package:wyatt_http_client/src/utils/http_status.dart'; + +typedef TokenParser = String Function(Map); class RefreshTokenMiddleware extends Middleware { final String authorizationEndpoint; final String tokenEndpoint; String? accessToken; + final TokenParser accessTokenParser; String? refreshToken; + final TokenParser refreshTokenParser; - Middleware innerClientMiddlewares; + final String authenticationHeader; + final String authenticationMethod; + final HttpStatus unauthorized; + final int maxRetries; RefreshTokenMiddleware({ required this.authorizationEndpoint, required this.tokenEndpoint, - Middleware? innerClientMiddlewares, - super.child, - }) : innerClientMiddlewares = innerClientMiddlewares ?? DefaultMiddleware(); + required this.accessTokenParser, + required this.refreshTokenParser, + this.authenticationHeader = HeaderKeys.authorization, + this.authenticationMethod = AuthenticationMethods.bearer, + this.unauthorized = HttpStatus.unauthorized, + this.maxRetries = 3, + }); @override - String getName() => 'RefreshTokenMiddleware'; + String getName() => 'RefreshToken'; @override - MiddlewareRequest onRequest(MiddlewareRequest request) { + Future onRequest(MiddlewareRequest request) async { print( - 'RefreshToken::OnRequest: accessToken: $accessToken', + '${getName()}::OnRequest: accessToken: $accessToken', ); - if (accessToken == null) { - // Refresh token - final pipeline = Pipeline().addMiddleware(innerClientMiddlewares); - print(pipeline.getLogic()); - final client = MiddlewareClient( - pipeline, - inner: getClient(), + if (request.context.originalRequest?.unfreezedRequest.url == + Uri.parse(authorizationEndpoint)) { + return super.onRequest(request); + } + if (accessToken != null) { + // Modify header with accessToken + var newReq = request.unfreezedRequest; + final mutation = { + authenticationHeader: '$authenticationMethod $accessToken', + }; + Map? headers = newReq.headers; + if (headers != null) { + headers.addAll(mutation); + } else { + headers = mutation; + } + newReq = newReq.copyWith(headers: headers); + request.updateUnfreezedRequest(newReq); + return super.onRequest(request); + } + if (refreshToken != null) { + // Refresh accessToken with refreshToken before perform request + final subPipeline = request.context.pipeline.fromUntil(this); + final httpClient = MiddlewareClient( + pipeline: subPipeline, + inner: client, ); - final _ = client.post(Uri.parse(tokenEndpoint)); - } - var newReq = request.unfreezedRequest; - final mutation = { - 'authorization': accessToken ?? '', - }; - Map? headers = newReq.headers; - if (headers != null) { - headers.addAll(mutation); - } else { - headers = mutation; - } - newReq = newReq.copyWith(headers: headers); - request.updateUnfreezedRequest(newReq); + final Map headers = { + authenticationHeader: '$authenticationHeader $refreshToken', + }; + final response = + await httpClient.get(Uri.parse(tokenEndpoint), headers: headers); + final status = HttpStatus.from(response.statusCode); + if (status.isSuccess()) { + final body = jsonDecode(response.body) as Map; + accessToken = accessTokenParser(body); + // Then modify current request with accessToken + var newReq = request.unfreezedRequest; + final mutation = { + authenticationHeader: '$authenticationMethod $accessToken', + }; + Map? headers = newReq.headers; + if (headers != null) { + headers.addAll(mutation); + } else { + headers = mutation; + } + newReq = newReq.copyWith(headers: headers); + request.updateUnfreezedRequest(newReq); + } else { + // Retry + int retries = 0; + while (retries < maxRetries) { + final Map headers = { + authenticationHeader: '$authenticationHeader $refreshToken', + }; + final response = + await httpClient.get(Uri.parse(tokenEndpoint), headers: headers); + final status = HttpStatus.from(response.statusCode); + if (status.isSuccess()) { + final body = jsonDecode(response.body) as Map; + accessToken = accessTokenParser(body); + + // Then modify current request with accessToken + var newReq = request.unfreezedRequest; + final mutation = { + authenticationHeader: '$authenticationMethod $accessToken', + }; + Map? headers = newReq.headers; + if (headers != null) { + headers.addAll(mutation); + } else { + headers = mutation; + } + newReq = newReq.copyWith(headers: headers); + request.updateUnfreezedRequest(newReq); + break; + } + retries++; + } + } + return super.onRequest(request); + } + // Pass return super.onRequest(request); } + + @override + Future onResponse(MiddlewareResponse response) async { + final res = await super.onResponse(response); + final status = HttpStatus.from(res.httpResponse.statusCode); + if (res.context.originalRequest?.unfreezedRequest.url == + Uri.parse(authorizationEndpoint)) { + if (status.isSuccess()) { + final body = jsonDecode((res.httpResponse as Response).body) as Map; + final accessToken = accessTokenParser(body); + final refreshToken = refreshTokenParser(body); + + if (accessToken.isNotEmpty) { + this.accessToken = accessToken; + } + if (refreshToken.isNotEmpty) { + this.refreshToken = refreshToken; + } + } + return res; + } + if (status == unauthorized) { + print( + '${getName()}::OnResponse: $unauthorized', + ); + // Refresh token then retry + final subPipeline = res.context.pipeline.fromUntil(this); + final httpClient = MiddlewareClient( + pipeline: subPipeline, + inner: client, + ); + final Map headers = { + authenticationHeader: '$authenticationHeader $refreshToken', + }; + final response = + await httpClient.get(Uri.parse(tokenEndpoint), headers: headers); + final refreshstatus = HttpStatus.from(response.statusCode); + if (refreshstatus.isSuccess()) { + print( + '${getName()}::OnResponse: refresh successfuly', + ); + final body = jsonDecode(response.body) as Map; + accessToken = accessTokenParser(body); + + // Then modify current request with accessToken + final midReq = res.middlewareRequest; + final newReq = midReq.httpRequest; + final mutation = { + authenticationHeader: '$authenticationMethod $accessToken', + }; + Map? headers = newReq.headers; + if (headers != null) { + headers.addAll(mutation); + } else { + headers = mutation; + } + midReq.updateHttpRequest(headers: headers); + final newRes = await httpClient.send(midReq.httpRequest); + return res.copyWith(httpResponse: res as Response); + } + } + return res; + } } diff --git a/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart index d3c26aa0..6118e03b 100644 --- a/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/simple_logger_middleware.dart @@ -14,32 +14,29 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:wyatt_http_client/src/middleware.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; import 'package:wyatt_http_client/src/models/middleware_response.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; class SimpleLoggerMiddleware extends Middleware { - SimpleLoggerMiddleware({ - super.child, - }); + @override + String getName() => 'SimpleLogger'; @override - String getName() => 'SimpleLoggerMiddleware'; - - @override - MiddlewareRequest onRequest(MiddlewareRequest request) { + Future onRequest(MiddlewareRequest request) { print( - 'Logger::OnRequest: ${request.httpRequest.method} ' - '${request.httpRequest.url}', + '${getName()}::OnRequest: ${request.httpRequest.method} ' + '${request.httpRequest.url}\n${request.unfreezedRequest.headers}' + '\n>> ${request.unfreezedRequest.body}', ); return super.onRequest(request); } @override - MiddlewareResponse onResponse(MiddlewareResponse response) { - final res = super.onResponse(response); + Future onResponse(MiddlewareResponse response) async { + final res = await super.onResponse(response); print( - 'Logger::OnResponse: ${res.httpResponse.statusCode} -> ' + '${getName()}::OnResponse: ${res.httpResponse.statusCode} -> ' 'received ${res.httpResponse.contentLength} bytes', ); return res; diff --git a/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart b/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart index 9a8ec2dc..b83e995f 100644 --- a/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart +++ b/packages/wyatt_http_client/lib/src/middlewares/uri_prefix_middleware.dart @@ -14,8 +14,8 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:wyatt_http_client/src/middleware.dart'; import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; import 'package:wyatt_http_client/src/utils/protocols.dart'; class UriPrefixMiddleware extends Middleware { @@ -25,17 +25,16 @@ class UriPrefixMiddleware extends Middleware { UriPrefixMiddleware({ required this.protocol, required this.authority, - super.child, }); @override - String getName() => 'UriPrefixMiddleware'; + String getName() => 'UriPrefix'; @override - MiddlewareRequest onRequest(MiddlewareRequest request) { + Future onRequest(MiddlewareRequest request) { final Uri uri = Uri.parse('${protocol.scheme}$authority${request.httpRequest.url}'); - print('UriPrefix::OnRequest: ${request.httpRequest.url} -> $uri'); + print('${getName()}::OnRequest: ${request.httpRequest.url} -> $uri'); request.updateHttpRequest(url: uri); return super.onRequest(request); } diff --git a/packages/wyatt_http_client/lib/src/models/middleware_context.dart b/packages/wyatt_http_client/lib/src/models/middleware_context.dart new file mode 100644 index 00000000..e00f6395 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/models/middleware_context.dart @@ -0,0 +1,48 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/models/middleware_response.dart'; +import 'package:wyatt_http_client/src/pipeline.dart'; + +class MiddlewareContext { + Pipeline pipeline; + MiddlewareRequest? originalRequest; + MiddlewareResponse? originalResponse; + + MiddlewareContext({ + required this.pipeline, + this.originalRequest, + this.originalResponse, + }); + + MiddlewareContext copyWith({ + Pipeline? pipeline, + MiddlewareRequest? originalRequest, + MiddlewareResponse? originalResponse, + }) { + return MiddlewareContext( + pipeline: pipeline ?? this.pipeline, + originalRequest: originalRequest ?? this.originalRequest, + originalResponse: originalResponse ?? this.originalResponse, + ); + } + + @override + String toString() => 'MiddlewareContext(pipeline: $pipeline, ' + 'originalRequest: $originalRequest, originalResponse: $originalResponse)'; +} diff --git a/packages/wyatt_http_client/lib/src/models/middleware_request.dart b/packages/wyatt_http_client/lib/src/models/middleware_request.dart index b55481ff..b1355774 100644 --- a/packages/wyatt_http_client/lib/src/models/middleware_request.dart +++ b/packages/wyatt_http_client/lib/src/models/middleware_request.dart @@ -16,25 +16,33 @@ // along with this program. If not, see . import 'package:http/http.dart'; + +import 'package:wyatt_http_client/src/models/middleware_context.dart'; import 'package:wyatt_http_client/src/models/unfreezed_request.dart'; import 'package:wyatt_http_client/src/utils/utils.dart'; class MiddlewareRequest { UnfreezedRequest unfreezedRequest; Request httpRequest; + MiddlewareContext context; MiddlewareRequest({ required this.unfreezedRequest, required this.httpRequest, - }); + required this.context, + }) { + context = context.copyWith(originalRequest: this); + } MiddlewareRequest copyWith({ UnfreezedRequest? unfreezedRequest, Request? httpRequest, + MiddlewareContext? context, }) { return MiddlewareRequest( unfreezedRequest: unfreezedRequest ?? this.unfreezedRequest, httpRequest: httpRequest ?? this.httpRequest, + context: context ?? this.context, ); } @@ -85,5 +93,5 @@ class MiddlewareRequest { @override String toString() => 'MiddlewareRequest(unfreezedRequest: ' - '$unfreezedRequest, httpRequest: $httpRequest)'; + '$unfreezedRequest, httpRequest: $httpRequest, context: $context)'; } diff --git a/packages/wyatt_http_client/lib/src/models/middleware_response.dart b/packages/wyatt_http_client/lib/src/models/middleware_response.dart index 5e67ae74..0645e776 100644 --- a/packages/wyatt_http_client/lib/src/models/middleware_response.dart +++ b/packages/wyatt_http_client/lib/src/models/middleware_response.dart @@ -16,22 +16,35 @@ // along with this program. If not, see . import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/models/middleware_context.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; class MiddlewareResponse { BaseResponse httpResponse; - + MiddlewareRequest middlewareRequest; + MiddlewareContext context; + MiddlewareResponse({ required this.httpResponse, - }); + required this.middlewareRequest, + required this.context, + }) { + context = context.copyWith(originalResponse: this); + } MiddlewareResponse copyWith({ BaseResponse? httpResponse, + MiddlewareRequest? middlewareRequest, + MiddlewareContext? context, }) { return MiddlewareResponse( httpResponse: httpResponse ?? this.httpResponse, + middlewareRequest: middlewareRequest ?? this.middlewareRequest, + context: context ?? this.context, ); } @override - String toString() => 'MiddlewareResponse(httpResponse: $httpResponse)'; + String toString() => 'MiddlewareResponse(httpResponse: $httpResponse, ' + 'middlewareRequest: $middlewareRequest, context: $context)'; } diff --git a/packages/wyatt_http_client/lib/src/pipeline.dart b/packages/wyatt_http_client/lib/src/pipeline.dart index 479c1f1b..0d2b1477 100644 --- a/packages/wyatt_http_client/lib/src/pipeline.dart +++ b/packages/wyatt_http_client/lib/src/pipeline.dart @@ -14,44 +14,140 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import 'package:wyatt_http_client/src/middleware.dart'; -import 'package:wyatt_http_client/src/middlewares/default_middleware.dart'; +import 'package:http/http.dart'; +import 'package:wyatt_http_client/src/middleware_client.dart'; +import 'package:wyatt_http_client/src/models/middleware_request.dart'; +import 'package:wyatt_http_client/src/models/middleware_response.dart'; + +part 'middleware.dart'; +part 'middleware_node.dart'; class Pipeline { - final Middleware _middleware; + int _length = 0; + late final MiddlewareNode begin; + late final MiddlewareNode end; - Pipeline() : _middleware = DefaultMiddleware(); + MiddlewareNode get first => begin.child; + MiddlewareNode get last => end.parent; + bool get isEmpty => _length == 0; + bool get isNotEmpty => !isEmpty; + int get length => _length; + + Pipeline() { + _initialize(); + } + + Pipeline.fromIterable(Iterable list) { + _initialize(); + MiddlewareNode parent = begin; + for (final element in list) { + if (element != null) { + parent = parent.insertAfter(element); + } + } + } + + Pipeline.from(Pipeline pipeline) + : this.fromIterable(pipeline.middlewares.map((node) => node.middleware)); + + void _initialize() { + _length = 0; + begin = MiddlewareNode._begin(this); + end = MiddlewareNode._end(this); + begin._child = end; + end._parent = begin; + } + + Iterable get middlewares sync* { + for (var middleware = first; + middleware != end; + middleware = middleware.child) { + yield middleware; + } + } + + int indexOf(MiddlewareNode node) { + int i = -1; + int j = -1; + for (final element in middlewares) { + j++; + if (element == node) { + i = j; + continue; + } + } + return i; + } + + int indexOfChild(Middleware middleware) { + int i = -1; + int j = -1; + for (final element in middlewares) { + j++; + if (element.middleware == middleware) { + i = j; + continue; + } + } + return i; + } + + /// Create new [Pipeline] from first [Middleware] to the specified one. + Pipeline fromUntil(Middleware middleware, {bool include = false}) { + final nodes = []; + for (final element in middlewares) { + if (element.middleware != middleware) { + nodes.add(element.middleware); + } + if (element.middleware == middleware) { + if (include) { + nodes.add(element.middleware); + } + break; + } + } + return Pipeline.fromIterable(nodes); + } Pipeline addMiddleware(Middleware middleware) { - _middleware.addChild(middleware); + last.insertAfter(middleware); return this; } - Middleware get middleware { - return _middleware; + void setClient(MiddlewareClient? client) { + for (var node = first; node != end; node = node.child) { + node.middleware?.httpClient = client; + } } - Pipeline operator +(Pipeline other) { - final copy = _middleware.deepCopy()..addChild(other.middleware); - return Pipeline()..addMiddleware(copy); + Future onRequest(MiddlewareRequest request) async { + MiddlewareRequest req = request; + for (var node = first; node != end; node = node.child) { + req = await node.middleware?.onRequest(req) ?? req; + } + return req; + } + + Future onResponse(MiddlewareResponse response) async { + MiddlewareResponse res = response; + for (var node = last; node != begin; node = node.parent) { + res = await node.middleware?.onResponse(res) ?? res; + } + return res; } String getLogic() { final req = []; final res = []; - Middleware? m = _middleware; - while (m != null) { - if (m is! DefaultMiddleware) { - req.add('$m'); - res.insert(0, '$m'); - } - m = m.child; + for (final m in middlewares) { + req.add('${m.middleware}'); + res.insert(0, '${m.middleware}'); } return '[Req] -> ${req.join(' -> ')}\n[Res] -> ${res.join(' -> ')}'; } @override String toString() { - return middleware.toString(); + return getLogic(); } } diff --git a/packages/wyatt_http_client/lib/src/utils/http_methods.dart b/packages/wyatt_http_client/lib/src/utils/http_methods.dart new file mode 100644 index 00000000..49087965 --- /dev/null +++ b/packages/wyatt_http_client/lib/src/utils/http_methods.dart @@ -0,0 +1,28 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +enum HttpMethods { + head('HEAD'), + get('GET'), + post('POST'), + put('PUT'), + patch('PATCH'), + delete('DELETE'); + + final String method; + + const HttpMethods(this.method); +} diff --git a/packages/wyatt_http_client/lib/src/utils/http_status.dart b/packages/wyatt_http_client/lib/src/utils/http_status.dart index 931c3587..c5715c70 100644 --- a/packages/wyatt_http_client/lib/src/utils/http_status.dart +++ b/packages/wyatt_http_client/lib/src/utils/http_status.dart @@ -1,83 +1,123 @@ // Copyright (C) 2022 WYATT GROUP // Please see the AUTHORS file for details. -// +// // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. -// +// // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. -// +// // You should have received a copy of the GNU General Public License // along with this program. If not, see . /// Status codes for HTTP responses. Extracted from dart:io -abstract class HttpStatus { - static const int continue_ = 100; - static const int switchingProtocols = 101; - static const int processing = 102; - static const int ok = 200; - static const int created = 201; - static const int accepted = 202; - static const int nonAuthoritativeInformation = 203; - static const int noContent = 204; - static const int resetContent = 205; - static const int partialContent = 206; - static const int multiStatus = 207; - static const int alreadyReported = 208; - static const int imUsed = 226; - static const int multipleChoices = 300; - static const int movedPermanently = 301; - static const int found = 302; - static const int movedTemporarily = 302; // Common alias for found. - static const int seeOther = 303; - static const int notModified = 304; - static const int useProxy = 305; - static const int temporaryRedirect = 307; - static const int permanentRedirect = 308; - static const int badRequest = 400; - static const int unauthorized = 401; - static const int paymentRequired = 402; - static const int forbidden = 403; - static const int notFound = 404; - static const int methodNotAllowed = 405; - static const int notAcceptable = 406; - static const int proxyAuthenticationRequired = 407; - static const int requestTimeout = 408; - static const int conflict = 409; - static const int gone = 410; - static const int lengthRequired = 411; - static const int preconditionFailed = 412; - static const int requestEntityTooLarge = 413; - static const int requestUriTooLong = 414; - static const int unsupportedMediaType = 415; - static const int requestedRangeNotSatisfiable = 416; - static const int expectationFailed = 417; - static const int misdirectedRequest = 421; - static const int unprocessableEntity = 422; - static const int locked = 423; - static const int failedDependency = 424; - static const int upgradeRequired = 426; - static const int preconditionRequired = 428; - static const int tooManyRequests = 429; - static const int requestHeaderFieldsTooLarge = 431; - static const int connectionClosedWithoutResponse = 444; - static const int unavailableForLegalReasons = 451; - static const int clientClosedRequest = 499; - static const int internalServerError = 500; - static const int notImplemented = 501; - static const int badGateway = 502; - static const int serviceUnavailable = 503; - static const int gatewayTimeout = 504; - static const int httpVersionNotSupported = 505; - static const int variantAlsoNegotiates = 506; - static const int insufficientStorage = 507; - static const int loopDetected = 508; - static const int notExtended = 510; - static const int networkAuthenticationRequired = 511; +enum HttpStatus { + continue_(100), + switchingProtocols(101), + processing(102), + ok(200), + created(201), + accepted(202), + nonAuthoritativeInformation(203), + noContent(204), + resetContent(205), + partialContent(206), + multiStatus(207), + alreadyReported(208), + imUsed(226), + multipleChoices(300), + movedPermanently(301), + found(302), + movedTemporarily(302), // Common alias for found. + seeOther(303), + notModified(304), + useProxy(305), + temporaryRedirect(307), + permanentRedirect(308), + badRequest(400), + unauthorized(401), + paymentRequired(402), + forbidden(403), + notFound(404), + methodNotAllowed(405), + notAcceptable(406), + proxyAuthenticationRequired(407), + requestTimeout(408), + conflict(409), + gone(410), + lengthRequired(411), + preconditionFailed(412), + requestEntityTooLarge(413), + requestUriTooLong(414), + unsupportedMediaType(415), + requestedRangeNotSatisfiable(416), + expectationFailed(417), + misdirectedRequest(421), + unprocessableEntity(422), + locked(423), + failedDependency(424), + upgradeRequired(426), + preconditionRequired(428), + tooManyRequests(429), + requestHeaderFieldsTooLarge(431), + connectionClosedWithoutResponse(444), + unavailableForLegalReasons(451), + clientClosedRequest(499), + internalServerError(500), + notImplemented(501), + badGateway(502), + serviceUnavailable(503), + gatewayTimeout(504), + httpVersionNotSupported(505), + variantAlsoNegotiates(506), + insufficientStorage(507), + loopDetected(508), + notExtended(510), + networkAuthenticationRequired(511), // Client generated status code. - static const int networkConnectTimeoutError = 599; + networkConnectTimeoutError(599); + + final int statusCode; + + const HttpStatus(this.statusCode); + + bool equals(Object other) { + if (other is HttpStatus) { + return statusCode == other.statusCode; + } + if (other is int) { + return statusCode == other; + } + return false; + } + + bool isInfo() { + return statusCode >= 100 && statusCode < 200; + } + + bool isSuccess() { + return statusCode >= 200 && statusCode < 300; + } + + bool isRedirection() { + return statusCode >= 300 && statusCode < 400; + } + + bool isClientError() { + return statusCode >= 400 && statusCode < 500; + } + + bool isServerError() { + return statusCode >= 500 && statusCode < 600; + } + + factory HttpStatus.from(int status) { + return HttpStatus.values + .firstWhere((element) => element.statusCode == status); + } + }