Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nextcloud): add interceptors #2165

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions packages/cookie_store/lib/cookie_store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,8 @@
/// This package has an implementation independent api and provides utilities
/// that make it easy to extend and implemented into existing storage backends
/// like SQLite databases.
///
/// Users of the [cookie_jar](https://pub.dev/packages/cookie_jar) package
/// can use the provided `CookieJarAdapter`.
library;

export 'src/cookie_jar_adapter.dart';
export 'src/cookie_persistence.dart';
export 'src/cookie_store.dart';
export 'src/storable_cookie.dart';
Expand Down
35 changes: 0 additions & 35 deletions packages/cookie_store/lib/src/cookie_jar_adapter.dart

This file was deleted.

1 change: 0 additions & 1 deletion packages/cookie_store/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ environment:
sdk: ^3.0.0

dependencies:
cookie_jar: ^4.0.0
meta: ^1.0.0
timezone: ^0.9.4
universal_io: ^2.0.0
Expand Down
10 changes: 8 additions & 2 deletions packages/neon_framework/lib/src/models/account.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import 'dart:convert';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';
import 'package:cookie_store/cookie_store.dart';
import 'package:crypto/crypto.dart';
import 'package:http/http.dart';
import 'package:meta/meta.dart';
import 'package:neon_framework/src/utils/cookie_store_interceptor.dart';
import 'package:neon_framework/src/utils/findable.dart';
import 'package:neon_framework/storage.dart';
import 'package:nextcloud/nextcloud.dart';
Expand Down Expand Up @@ -80,8 +80,14 @@ abstract class Account implements Credentials, Findable, Built<Account, AccountB
password: password,
appPassword: password,
userAgent: userAgent,
cookieJar: cookieStore != null ? CookieJarAdapter(cookieStore) : null,
httpClient: httpClient,
interceptors: cookieStore != null
? [
CookieStoreInterceptor(
cookieStore: cookieStore,
),
]
: null,
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'dart:async';

import 'package:cookie_store/cookie_store.dart';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:nextcloud/utils.dart';
import 'package:universal_io/io.dart';

/// A HttpInterceptor persisting cookies in the provided [cookieStore].
@internal
final class CookieStoreInterceptor<S extends http.BaseRequest, T extends http.BaseResponse>
extends CookieInterceptor<S, T> {
/// Creates a new interceptor persisting cookies.
CookieStoreInterceptor({
required this.cookieStore,
});

/// The optional cookie jar to persist the response cookies.
final CookieStore cookieStore;

@override
FutureOr<List<Cookie>> loadForRequest(Uri uri) {
return cookieStore.loadForRequest(uri);
}

@override
FutureOr<void> saveFromResponse(Uri uri, List<Cookie> cookies) {
return cookieStore.saveFromResponse(uri, cookies);
}
}
1 change: 0 additions & 1 deletion packages/neon_framework/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ dependencies:
built_collection: ^5.0.0
built_value: ^8.9.0
collection: ^1.0.0
cookie_jar: ^4.0.0
cookie_store:
git:
url: https://github.com/nextcloud/neon
Expand Down
76 changes: 64 additions & 12 deletions packages/nextcloud/lib/src/client.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import 'package:built_collection/built_collection.dart';
import 'package:cookie_jar/cookie_jar.dart' as cookie_jar;
import 'package:dynamite_runtime/http_client.dart';
import 'package:http/http.dart' as http;
import 'package:nextcloud/src/utils/cookie_jar_client.dart';
import 'package:nextcloud/src/interceptors/base_header_interceptor.dart';
import 'package:nextcloud/src/interceptors/cookie_interceptor.dart';
import 'package:nextcloud/src/interceptors/http_interceptor.dart';
import 'package:universal_io/io.dart';

/// A [HttpInterceptor] for the [NextcloudClient];
typedef NextCloudInterceptor = HttpInterceptor<http.BaseRequest, http.StreamedResponse>;

/// A client configuring the clients for all Nextcloud APIs.
///
/// To access the APIs of a particular app import the extensions through `package:nextcloud/{id}.dart`.
Expand All @@ -26,16 +32,32 @@ class NextcloudClient extends DynamiteClient with http.BaseClient {
String? password,
String? appPassword,
String? userAgent,
http.Client? httpClient,
cookie_jar.CookieJar? cookieJar,
}) : super(
httpClient: CookieJarClient(
httpClient: httpClient,
cookieJar: cookieJar,
baseHeaders: {
if (userAgent != null) HttpHeaders.userAgentHeader: userAgent,
},
),
super.httpClient,
@Deprecated('Use the CookieJarInterceptor instead') cookie_jar.CookieJar? cookieJar,
Iterable<NextCloudInterceptor>? interceptors,
}) : _interceptors = BuiltList.build((builder) {
if (interceptors != null) {
builder.addAll(interceptors);
}

if (cookieJar != null) {
builder.add(
CookieJarInterceptor(cookieJar: cookieJar),
);
}

// Adding last to not overwrite request headers to avoid invalid requests.
if (userAgent != null) {
builder.add(
BaseHeaderInterceptor(
baseHeaders: {
HttpHeaders.userAgentHeader: userAgent,
},
),
);
}
}),
super(
authentications: [
if (appPassword != null)
DynamiteHttpBearerAuthentication(
Expand All @@ -49,6 +71,36 @@ class NextcloudClient extends DynamiteClient with http.BaseClient {
],
);

final BuiltList<NextCloudInterceptor> _interceptors;

@override
Future<http.StreamedResponse> send(http.BaseRequest request) => httpClient.send(request);
Future<http.StreamedResponse> send(http.BaseRequest request) {
if (_interceptors.isNotEmpty) {
return _sendIntercepted(request);
}

return httpClient.send(request);
}

Future<http.StreamedResponse> _sendIntercepted(http.BaseRequest request) async {
var interceptedRequest = request;
for (final interceptor in _interceptors) {
if (interceptor.shouldInterceptRequest()) {
interceptedRequest = await interceptor.interceptRequest(
request: interceptedRequest,
);
}
}

var interceptedResponse = await httpClient.send(interceptedRequest);
for (final interceptor in _interceptors) {
if (interceptor.shouldInterceptResponse()) {
interceptedResponse = await interceptor.interceptResponse(
response: interceptedResponse,
);
}
}

return interceptedResponse;
}
Leptopoda marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:nextcloud/src/interceptors/http_interceptor.dart';

/// A HttpInterceptor that adds the given [baseHeaders] to a request.
///
/// The headers of a request will override any header in the `baseHeaders`.
@internal
final class BaseHeaderInterceptor<S extends http.BaseRequest> implements HttpInterceptor<S, Never> {
/// Creates a new base header interceptor.
const BaseHeaderInterceptor({
this.baseHeaders,
});

/// The base headers added to each request.
final Map<String, String>? baseHeaders;

@override
bool shouldInterceptRequest() => true;

@override
S interceptRequest({required S request}) {
baseHeaders?.forEach((key, value) {
request.headers.putIfAbsent(key, () => value);
});

return request;
}

@override
bool shouldInterceptResponse() => false;

@override
Never interceptResponse({required http.BaseResponse response}) {
throw UnsupportedError('Base headers can not be added to responses.');
}
}
90 changes: 90 additions & 0 deletions packages/nextcloud/lib/src/interceptors/cookie_interceptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import 'dart:async';

import 'package:cookie_jar/cookie_jar.dart' as cookie_jar;
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
import 'package:nextcloud/src/interceptors/http_interceptor.dart';
import 'package:universal_io/io.dart';

/// A HttpInterceptor to implement cookie persisting interceptors.
abstract class CookieInterceptor<S extends http.BaseRequest, T extends http.BaseResponse>
implements HttpInterceptor<S, T> {
@override
bool shouldInterceptRequest() => true;

@override
Future<S> interceptRequest({required S request}) async {
final cookies = await loadForRequest(request.url);
if (cookies.isNotEmpty) {
final buffer = StringBuffer();
final iterator = cookies.iterator..moveNext();

while (true) {
final cookie = iterator.current;

buffer
..write(cookie.name)
..write('=')
..write(cookie.value);

if (iterator.moveNext()) {
buffer.write('; ');
} else {
break;
}
}

request.headers['cookie'] = buffer.toString();
}

return request;
}

@override
bool shouldInterceptResponse() => true;

@override
Future<T> interceptResponse({required T response}) async {
final cookieHeader = response.headersSplitValues['Set-Cookie'];
if (cookieHeader != null) {
final url = response.request?.url;
if (url == null) {
throw http.ClientException('Response does not contain any url. Cookies are ignored.');
}

final cookies = cookieHeader.map(Cookie.fromSetCookieValue).toList();
await saveFromResponse(url, cookies);
}

return response;
}

/// Load the cookies for specified [uri].
FutureOr<List<Cookie>> loadForRequest(Uri uri);

/// Save the [cookies] for specified [uri].
FutureOr<void> saveFromResponse(Uri uri, List<Cookie> cookies);
}

/// A HttpInterceptor persisting cookies in the provided [cookieJar].
@internal
final class CookieJarInterceptor<S extends http.BaseRequest, T extends http.BaseResponse>
extends CookieInterceptor<S, T> {
/// Creates a new interceptor persisting cookies.
CookieJarInterceptor({
required this.cookieJar,
});

/// The optional cookie jar to persist the response cookies.
final cookie_jar.CookieJar cookieJar;

@override
Future<List<Cookie>> loadForRequest(Uri uri) {
return cookieJar.loadForRequest(uri);
}

@override
Future<void> saveFromResponse(Uri uri, List<Cookie> cookies) {
return cookieJar.saveFromResponse(uri, cookies);
}
}
23 changes: 23 additions & 0 deletions packages/nextcloud/lib/src/interceptors/http_interceptor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'dart:async';

import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';

/// Interceptor that can manipulate http requests and responses.
@immutable
abstract interface class HttpInterceptor<S extends http.BaseRequest, T extends http.BaseResponse> {
/// Whether this interceptor should intercept requests.
bool shouldInterceptRequest();

/// Intercepts the given [request].
///
/// Provided requests are not finalized yet. It is an error for an interceptor
/// to request itself.
FutureOr<S> interceptRequest({required S request});

/// Whether this interceptor should intercept response.
bool shouldInterceptResponse();

/// Intercepts the given [response].
FutureOr<T> interceptResponse({required T response});
}
Loading
Loading