Compare commits

...

5 Commits

34 changed files with 2120 additions and 636 deletions

View File

@ -1,39 +1,182 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
* Copyright (C) 2022 WYATT GROUP
* Please see the AUTHORS file for details.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
* 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.
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/developing-packages).
* 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 <https://www.gnu.org/licenses/>.
-->
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
<p align="left">
<a href="https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_analysis">
<img src="https://img.shields.io/badge/Style-Wyatt%20Analysis-blue.svg?style=flat-square" alt="Style: Wyatt Analysis" />
</a>
<img src="https://img.shields.io/badge/SDK-Dart%20%7C%20Flutter-blue?style=flat-square" alt="SDK: Dart & Flutter" />
</p>
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<MiddlewareRequest> onRequest(
MiddlewareContext context,
MiddlewareRequest request,
) async {
print('${getName()}::OnRequest');
return request;
}
@override
Future<MiddlewareResponse> onResponse(
MiddlewareContext context,
MiddlewareResponse response,
) async {
print('${getName()}::OnResponse');
return response;
}
}
```

View File

@ -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<void> handleBasic(HttpRequest req) async {
Future<void> 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<void> handleBearer(HttpRequest req) async {
Future<void> 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<void> 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<void> 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<void> 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<void> server() async {
Future<void> 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(<String, String>{
'username': 'username',
'password': 'password',
});
// Login
await refreshToken.post(
Uri.parse('/test/oauth2-test'),
body: <String, String>{
'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);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
// 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<String, dynamic> toMap() {
return <String, dynamic>{
'email': email,
'verification_code': verificationCode,
'action': action.toSnakeCase(),
};
}
factory VerifyCode.fromMap(Map<String, dynamic> 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<String, dynamic>);
@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<String, dynamic> toMap() {
return <String, dynamic>{
'email': email,
'session_id': sessionId,
};
}
factory Account.fromMap(Map<String, dynamic> 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<String, dynamic>);
@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<String, dynamic> toMap() {
return <String, dynamic>{
'session_id': sessionId,
'password': password,
};
}
factory SignUp.fromMap(Map<String, dynamic> 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<String, dynamic>);
@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<String, dynamic> toMap() {
return <String, dynamic>{
'access_token': accessToken,
'refresh_token': refreshToken,
'account': account.toMap(),
};
}
factory TokenSuccess.fromMap(Map<String, dynamic> map) {
return TokenSuccess(
accessToken: map['access_token'] as String,
refreshToken: map['refresh_token'] as String,
account: Account.fromMap(map['account'] as Map<String, dynamic>),
);
}
String toJson() => json.encode(toMap());
factory TokenSuccess.fromJson(String source) =>
TokenSuccess.fromMap(json.decode(source) as Map<String, dynamic>);
@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<String, dynamic> toMap() {
return <String, dynamic>{
'email': email,
'password': password,
};
}
factory Login.fromMap(Map<String, dynamic> 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<String, dynamic>);
@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<void> sendSignUpCode(String email) async {
final r = await client.post(
Uri.parse('$apiPath/auth/send-sign-up-code'),
body: <String, String>{
'email': email,
},
);
if (r.statusCode != 201) {
throw Exception('Invalid reponse: ${r.statusCode}');
}
}
Future<Account> 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<String, dynamic>)['account']
as Map<String, dynamic>,
);
}
}
Future<Account> 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<TokenSuccess> 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<TokenSuccess> refresh() async {
// final r = await client.refresh();
// return TokenSuccess.fromJson(r?.body ?? '');
// }
Future<List<Account>> 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<String, dynamic>)['founds']
as List<Map<String, dynamic>>;
final result = <Account>[];
for (final element in list) {
result.add(Account.fromMap(element));
}
return result;
}
}
}
void main(List<String> 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);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<Response?> Function(Request)? requestHandler,
// FutureOr<Response> Function(Response)? responseHandler,
// FutureOr<Response> Function(Object error, StackTrace)? errorHandler,
// }) {
// requestHandler ??= (request) => null;
// responseHandler ??= (response) => response;
// FutureOr<Response> 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<Response> 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<void> main(List<String> 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: <String, String>{
'email': 'test@test.fr',
},
);
auth
..username = 'username'
..password = 'password';
await client.post(
Uri.parse('/api/v1/account/test'),
body: <String, String>{
'email': 'test@test.fr',
},
);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<String, String> modifyHeader(
Map<String, String> header, [
BaseRequest? request,
]) {
header[authenticationHeader] = '${AuthenticationMethods.basic} '
'${base64Encode(utf8.encode('$username:$password'))}';
return header;
}
@override
Future<StreamedResponse> 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;
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<String, String> modifyHeader(
Map<String, String> header, [
BaseRequest? request,
]) {
header[authenticationHeader] = '$authenticationMethod $token';
return header;
}
@override
Future<StreamedResponse> 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;
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<String, String> modifyHeader(
Map<String, String> header, [
BaseRequest? request,
]) {
if ((_digestAuth.isReady()) && request != null) {
header[authenticationHeader] = _digestAuth.getAuthString(
request.method,
request.url,
);
}
return header;
}
@override
Future<StreamedResponse> 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;
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<String, String> modifyHeader(
Map<String, String> header, [
BaseRequest? request,
]) {
if (accessToken != null && request != null) {
header[authenticationHeader] = '$authenticationMethod $accessToken';
return header;
}
return header;
}
@override
Future<void> authorize(
Map<String, dynamic> body, {
Map<String, String>? 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<String, dynamic>;
final accessToken = accessTokenParser(body);
final refreshToken = refreshTokenParser(body);
if (accessToken.isNotEmpty) {
this.accessToken = accessToken;
}
if (refreshToken.isNotEmpty) {
this.refreshToken = refreshToken;
}
}
}
@override
Future<void> refresh() async {
if (refreshToken != null) {
final Map<String, String> 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<String, dynamic>;
accessToken = accessTokenParser(body);
}
}
}
@override
Future<StreamedResponse> 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;
}
}

View File

@ -14,23 +14,25 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<String, String> modifyHeader(
Map<String, String> header, [
BaseRequest? request,
]) =>
header;
@override
Future<StreamedResponse> 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<MiddlewareRequest> onRequest(
MiddlewareContext context,
MiddlewareRequest request,
);
}
mixin OnResponseMiddleware {
Future<MiddlewareResponse> onResponse(
MiddlewareContext context,
MiddlewareResponse response,
);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<Response> head(Uri url, {Map<String, String>? headers}) =>
_sendUnstreamed(HttpMethods.head.method, url, headers);
@override
Future<Response> get(Uri url, {Map<String, String>? headers}) =>
_sendUnstreamed(HttpMethods.get.method, url, headers);
@override
Future<Response> post(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) =>
_sendUnstreamed(HttpMethods.post.method, url, headers, body, encoding);
@override
Future<Response> put(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) =>
_sendUnstreamed(HttpMethods.put.method, url, headers, body, encoding);
@override
Future<Response> patch(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) =>
_sendUnstreamed(HttpMethods.patch.method, url, headers, body, encoding);
@override
Future<Response> delete(
Uri url, {
Map<String, String>? headers,
Object? body,
Encoding? encoding,
}) =>
_sendUnstreamed(HttpMethods.delete.method, url, headers, body, encoding);
@override
Future<StreamedResponse> send(BaseRequest request) {
return inner.send(request);
}
Future<Response> _sendUnstreamed(
String method,
Uri url,
Map<String, String>? 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;
}
}

View File

@ -14,24 +14,3 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<StreamedResponse> send(BaseRequest request) {
return _inner.send(request);
}
@override
void close() {
_inner.close();
return super.close();
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<MiddlewareRequest> 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<String, String> headers = request.headers..addAll(mutation);
request.modifyRequest(request.unfreezedRequest.copyWith(headers: headers));
return request;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<MiddlewareRequest> 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<String, String> headers = request.headers..addAll(mutation);
request.modifyRequest(
request.unfreezedRequest
.copyWith(headers: headers, body: jsonEncode(request.body)),
);
}
return request;
}
}

View File

@ -14,17 +14,9 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<StreamedResponse> send(BaseRequest request) {
final newRequest = modifyRequest(request);
return super.send(newRequest);
}
String getName() => 'DefaultMiddleware';
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<MiddlewareRequest> 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<String, String> headers = request.headers..addAll(mutation);
request
.modifyRequest(request.unfreezedRequest.copyWith(headers: headers));
}
return request;
}
@override
Future<MiddlewareResponse> 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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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';

View File

@ -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 <https://www.gnu.org/licenses/>.
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<String, dynamic>);
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<MiddlewareRequest?> 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<String, dynamic>;
accessToken = accessTokenParser(body);
// Then modify current request with accessToken
final mutation = {
authenticationHeader: '$authenticationMethod $accessToken',
};
final Map<String, String>? headers = context.lastRequest?.headers
?..addAll(mutation);
final newRequest = context.lastRequest?.copyWith(
unfreezedRequest:
context.lastRequest?.unfreezedRequest.copyWith(headers: headers),
);
return newRequest;
}
return null;
}
Future<MiddlewareRequest?> retry(MiddlewareContext context) async {
// Retry
int attempt = 1;
while (attempt <= maxAttempts) {
// Delayed before retry
await Future<void>.delayed(Delay.getRetryDelay(attempt));
final newRequest = await refresh(context);
if (newRequest != null) {
return newRequest;
}
attempt++;
}
return null;
}
@override
Future<MiddlewareRequest> 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<String, String> 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<MiddlewareResponse> 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<String, dynamic>;
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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<MiddlewareRequest> 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<MiddlewareResponse> 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;
}
}

View File

@ -14,30 +14,43 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<MiddlewareRequest> 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;
}
}

View File

@ -14,25 +14,35 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<StreamedResponse> send(BaseRequest request) {
String getName() => 'UriPrefix';
@override
Future<MiddlewareRequest> 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;
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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)';
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<String, String> 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<int>());
} else if (body is Map) {
_body = Convert.mapToQuery(body.cast<String, String>());
}
}
_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)';
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<String, String> get headers => httpResponse.headers;
MiddlewareResponse({
required this.httpResponse,
});
MiddlewareResponse copyWith({
BaseResponse? httpResponse,
}) {
return MiddlewareResponse(
httpResponse: httpResponse ?? this.httpResponse,
);
}
@override
String toString() =>
'MiddlewareResponse(httpResponse: $httpResponse)';
}

View File

@ -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 <https://www.gnu.org/licenses/>.
export 'middleware_context.dart';
export 'middleware_request.dart';
export 'middleware_response.dart';
export 'unfreezed_request.dart';

View File

@ -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 <https://www.gnu.org/licenses/>.
import 'dart:convert';
class UnfreezedRequest {
final String method;
final Uri url;
final Map<String, String>? 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<String, String>? 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)';
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
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<Middleware> _middlewares;
int get length => _middlewares.length;
Pipeline() : _middlewares = <Middleware>[];
Pipeline.fromIterable(Iterable<Middleware> 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 = <Middleware>[];
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<MiddlewareRequest> 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<MiddlewareResponse> 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 = <String>[];
final res = <String>[];
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(' -> ')}';
}
}

View File

@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import 'dart:convert';
class Convert {
static String toHex(List<int> bytes, {bool upperCase = false}) {
final buffer = StringBuffer();
@ -29,6 +31,15 @@ class Convert {
return buffer.toString();
}
}
static String mapToQuery(Map<String, String> map, {Encoding? encoding}) {
final pairs = <List<String>>[];
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 {

View File

@ -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 <https://www.gnu.org/licenses/>.
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;
}
}

View File

@ -14,21 +14,15 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<String, dynamic>);
final String method;
abstract class Oauth2Client extends HeaderAuthenticationClient {
Oauth2Client(super.inner);
Future<void> refresh() {
return Future.value();
}
Future<void> authorize(
Map<String, dynamic> body, {
Map<String, String>? headers,
}) {
return Future.value();
}
const HttpMethods(this.method);
}

View File

@ -15,69 +15,109 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
/// 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);
}
}

View File

@ -18,6 +18,5 @@ enum Protocols {
http,
https;
String get name => toString().split('.').last;
String get scheme => '$name://';
}

View File

@ -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 <https://www.gnu.org/licenses/>.
import 'package:http/http.dart';
abstract class RequestUtils {
static Request _copyNormalRequestWith(
Request original, {
String? method,
Uri? url,
Map<String, String>? 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<String, String>? 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}',
);
}
}
}

View File

@ -14,76 +14,10 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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<String, String>? 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<String, String>? 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';

View File

@ -15,3 +15,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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';