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

refactor the CookieJar interface for more implementation independence #56

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion example/encryption.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ void main() async {
final cj = PersistCookieJar(ignoreExpires: true, storage: storage);

final uri = Uri.parse('https://xxx.xxx.com/');
await cj.delete(uri);
await cj.deleteWhere((cookie) => cookie.domain == uri.host);
List<Cookie> results;
final cookie = Cookie('test', 'hh')
..expires = DateTime.parse('1970-02-27 13:27:00');
Expand Down
99 changes: 89 additions & 10 deletions lib/src/cookie_jar.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:universal_io/io.dart' show Cookie;

import 'jar/default.dart';
Expand All @@ -7,27 +9,104 @@ const _kIsWeb = bool.hasEnvironment('dart.library.js_util')
? bool.fromEnvironment('dart.library.js_util')
: identical(0, 0.0);

/// [CookieJar] is a cookie container and manager for HTTP requests.
/// [CookieJar] is a cookie container and manager for HTTP requests implementing [RFC6265](https://www.rfc-editor.org/rfc/rfc6265.html).
///
/// ## Implementation considerations
/// In most cases it is not needed to implement this interface.
/// Use a `PersistCookieJar` with a custom [Storage] backend.
///
/// ### Cookie value retrieval
/// A cookie jar does not need to retrieve cookies with all attributes present.
/// Retrieved cookies only need to have a valid [Cookie.name] and [Cookie.value].
/// It is up to the implementation to provide further information.
///
/// ### Cookie management
/// According to [RFC6265 section 7.2](https://www.rfc-editor.org/rfc/rfc6265.html#section-7.2)
/// user agents SHOULD provide users with a mechanism for managing the cookies stored in the cookie jar.
/// It must be documented if an implementer does not provide any of the optional
/// [loadAll], [deleteAll] and [deleteWhere] methods.
///
/// ### Public suffix validation
/// The default implementation does not validate the cookie domain against a public
/// suffix list:
/// > NOTE: A "public suffix" is a domain that is controlled by a public
/// > registry, such as "com", "co.uk", and "pvt.k12.wy.us". This step is
/// > essential for preventing attacker.com from disrupting the integrity of
/// > example.com by setting a cookie with a Domain attribute of "com".
/// > Unfortunately, the set of public suffixes (also known as "registry controlled domains")
/// > changes over time. If feasible, user agents SHOULD use an up-to-date
/// > public suffix list, such as the one maintained by the Mozilla project at <http://publicsuffix.org/>.
///
/// ### CookieJar limits and eviction policy
/// If a cookie jar has a limit to the number of cookies it can store,
/// the removal policy outlined in [RFC6265 section 5.3](https://www.rfc-editor.org/rfc/rfc6265.html#section-5.3)
/// must be followed:
/// > At any time, the user agent MAY "remove excess cookies" from the cookie store
/// > if the number of cookies sharing a domain field exceeds some implementation-defined
/// > upper bound (such as 50 cookies).
/// >
/// > At any time, the user agent MAY "remove excess cookies" from the cookie store
/// > if the cookie store exceeds some predetermined upper bound (such as 3000 cookies).
/// >
/// > When the user agent removes excess cookies from the cookie store, the user agent MUST
/// > evict cookies in the following priority order:
/// >
/// > Expired cookies.
/// > Cookies that share a domain field with more than a predetermined number of other cookies.
/// > All cookies.
/// >
/// > If two cookies have the same removal priority, the user agent MUST evict the
/// > cookie with the earliest last-access date first.
///
/// It is recommended to set an upper bound to the time a cookie is stored
/// as described in [RFC6265 section 7.3](https://www.rfc-editor.org/rfc/rfc6265.html#section-7.3):
/// > Although servers can set the expiration date for cookies to the distant future,
/// > most user agents do not actually retain cookies for multiple decades.
/// > Rather than choosing gratuitously long expiration periods, servers SHOULD
/// > promote user privacy by selecting reasonable cookie expiration periods based on the purpose of the cookie.
/// > For example, a typical session identifier might reasonably be set to expire in two weeks.
abstract class CookieJar {
/// Creates a [DefaultCookieJar] instance or a dummy [WebCookieJar] if run in a browser.
factory CookieJar({bool ignoreExpires = false}) {
if (_kIsWeb) {
return WebCookieJar();
}
return DefaultCookieJar(ignoreExpires: ignoreExpires);
}

/// Whether the [CookieJar] should ignore expired cookies during saves/loads.
final bool ignoreExpires = false;

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

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

/// Ends the current session deleting all session cookies.
FutureOr<void> endSession();

/// Loads all cookies in the jar.
///
/// User agents SHOULD provide users with a mechanism for managing the cookies stored in the cookie jar.
/// https://www.rfc-editor.org/rfc/rfc6265.html#section-7.2
///
/// Implementing this method is optional. It must be documented if the
/// implementer does not support this operation.
FutureOr<List<Cookie>> loadAll();

/// Delete all cookies in the [CookieJar].
Future<void> deleteAll();
/// Deletes all cookies in the jar.
///
/// User agents SHOULD provide users with a mechanism for managing the cookies stored in the cookie jar.
/// https://www.rfc-editor.org/rfc/rfc6265.html#section-7.2
///
/// Implementing this method is optional. It must be documented if the
/// implementer does not support this operation.
FutureOr<void> deleteAll();

/// Delete cookies with the specified [uri].
Future<void> delete(Uri uri, [bool withDomainSharedCookie = false]);
/// Removes all cookies in this store that satisfy the given [test].
///
/// User agents SHOULD provide users with a mechanism for managing the cookies stored in the cookie store.
/// https://www.rfc-editor.org/rfc/rfc6265.html#section-7.2
///
/// Implementing this method is optional. It must be documented if the
/// implementer does not support this operation.
FutureOr<void> deleteWhere(bool Function(Cookie cookie) test);
}
2 changes: 0 additions & 2 deletions lib/src/file_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ class FileStorage implements Storage {
/// A storage can be used across different jars, so this cannot be final.
late String _currentDirectory;

/// {@nodoc}
@visibleForTesting
final bool shouldCreateDirectory;

/// {@nodoc}
@visibleForTesting
String get currentDirectory => _currentDirectory;

Expand Down
54 changes: 48 additions & 6 deletions lib/src/jar/default.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:universal_io/io.dart' show Cookie;

import '../cookie_jar.dart';
Expand All @@ -10,10 +12,14 @@ import '../serializable_cookie.dart';
/// cleared after the app exited.
///
/// In order to save cookies into storages, use [PersistCookieJar] instead.
///
/// ### Public suffix validation
/// This cookie jar implementation does not validate the cookie domain against a
/// public suffix list:
/// {@macro CookieJar.publicSuffix}
class DefaultCookieJar implements CookieJar {
DefaultCookieJar({this.ignoreExpires = false});

@override
final bool ignoreExpires;

/// An array to save cookies.
Expand Down Expand Up @@ -88,7 +94,7 @@ class DefaultCookieJar implements CookieJar {
}

@override
Future<List<Cookie>> loadForRequest(Uri uri) async {
FutureOr<List<Cookie>> loadForRequest(Uri uri) {
final list = <Cookie>[];
final urlPath = uri.path;
// Load cookies without "domain" attribute, include port.
Expand Down Expand Up @@ -137,7 +143,7 @@ class DefaultCookieJar implements CookieJar {
}

@override
Future<void> saveFromResponse(Uri uri, List<Cookie> cookies) async {
FutureOr<void> saveFromResponse(Uri uri, List<Cookie> cookies) {
for (final cookie in cookies) {
String? domain = cookie.domain;
String path;
Expand Down Expand Up @@ -173,8 +179,8 @@ class DefaultCookieJar implements CookieJar {
/// This API will delete all cookies for the `uri.host`, it will ignored the `uri.path`.
///
/// [withDomainSharedCookie] `true` will delete the domain-shared cookies.
@override
Future<void> delete(Uri uri, [bool withDomainSharedCookie = false]) async {
@Deprecated('Use deleteWhere instead')
FutureOr<void> delete(Uri uri, [bool withDomainSharedCookie = false]) {
final host = uri.host;
hostCookies.remove(host);
if (withDomainSharedCookie) {
Expand All @@ -186,7 +192,7 @@ class DefaultCookieJar implements CookieJar {

/// Delete all cookies stored in the memory.
@override
Future<void> deleteAll() async {
FutureOr<void> deleteAll() {
domainCookies.clear();
hostCookies.clear();
}
Expand All @@ -206,4 +212,40 @@ class DefaultCookieJar implements CookieJar {
final list = path.split('/')..removeLast();
return list.join('/');
}

@override
void deleteWhere(bool Function(Cookie cookie) test) {
// Traverse all managed cookies and delete entries matching `test`.
for (final group in _cookies) {
for (final domainPair in group.values) {
for (final pathPair in domainPair.values) {
pathPair.removeWhere((key, value) => test(value.cookie));
}
}
}
}

@override
void endSession() {
deleteWhere((cookie) {
return cookie.expires == null && cookie.maxAge == null;
});
}

@override
FutureOr<List<Cookie>> loadAll() {
final list = <Cookie>[];

for (final group in _cookies) {
for (final domainPair in group.values) {
for (final pathPair in domainPair.values) {
for (final value in pathPair.values) {
list.add(value.cookie);
}
}
}
}

return list;
}
}
12 changes: 11 additions & 1 deletion lib/src/jar/persist.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import 'default.dart';
/// [PersistCookieJar] is a cookie manager which implements
/// the standard cookie policy declared in RFC.
/// [PersistCookieJar] persists the cookies in files, if the application exit,
/// the cookies always exist unless user explicitly called [delete].
/// the cookies always exist unless user explicitly deleted with [deleteWhere].
class PersistCookieJar extends DefaultCookieJar {
/// [persistSession] is whether persisting the cookies that without
/// "expires" or "max-age" attribute.
Expand Down Expand Up @@ -147,6 +147,7 @@ class PersistCookieJar extends DefaultCookieJar {
/// This API will delete all cookies for the `uri.host`, it will ignored the `uri.path`.
///
/// [withDomainSharedCookie] `true` will delete the domain-shared cookies.
@Deprecated('Use deleteWhere instead')
@override
Future<void> delete(Uri uri, [bool withDomainSharedCookie = false]) async {
await _checkInitialized();
Expand All @@ -161,6 +162,15 @@ class PersistCookieJar extends DefaultCookieJar {
}
}

@override
Future<void> deleteWhere(bool Function(Cookie cookie) test) async {
await _checkInitialized();
super.deleteWhere(test);
Leptopoda marked this conversation as resolved.
Show resolved Hide resolved

await storage.write(_indexKey, json.encode(_hostSet.toList()));
await storage.write(_domainsKey, json.encode(domainCookies));
}

/// Delete all cookies files in the [storage] and the memory.
@override
Future<void> deleteAll() async {
Expand Down
15 changes: 9 additions & 6 deletions lib/src/jar/web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@ import '../cookie_jar.dart';
/// A [WebCookieJar] will do nothing to handle cookies
/// since they are already handled by XHRs.
class WebCookieJar implements CookieJar {
WebCookieJar({this.ignoreExpires = false});
WebCookieJar();

@override
final bool ignoreExpires;
void deleteWhere(bool Function(Cookie cookie) test) {}

@override
Future<void> delete(Uri uri, [bool withDomainSharedCookie = false]) async {}
void deleteAll() {}

@override
Future<void> deleteAll() async {}
List<Cookie> loadForRequest(Uri uri) => [];

@override
Future<List<Cookie>> loadForRequest(Uri uri) async => [];
void saveFromResponse(Uri uri, List<Cookie> cookies) {}

@override
Future<void> saveFromResponse(Uri uri, List<Cookie> cookies) async {}
void endSession() {}

@override
List<Cookie> loadAll() => [];
}
32 changes: 32 additions & 0 deletions migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,38 @@ When new content need to be added to the migration guide, make sure they're foll

## Breaking versions

# NEXT

Version 5.0 brings a few refinements to the `CookieJar` interface.
Breaking changes include:

- Usage of `FutureOr` in interfaces.
Going forward a CookieJar can also return synchronously. If every call is
properly awaited nothing should break.
Usage in an `unawaited` method is no longer possible. The `WebCookieJar` has
been migrated to always complete synchronously.

- Changing cookie deletion:
To allow implementers further flexibility the `delete` method has been removed
from the `CookieJar` interface. Users should migrate to the more flexible
`deleteWhere` method:
```dart
final jar = CookieJar();
// Check what cookies you want to have deleted.
jar.deleteWhere((cookie) {
cookie.domain == 'example.com' || cookie.name == 'cookie1';
}));
```

- Optional cookie management interface:
Cookie management interfaces like `deleteAll`, `deleteWhere` or `loadAll` have
been made optional. It is up to the implementer to support these operations.
Consult your implementers documentation.

- Optional extra cookie parameters:
When loading cookies in any way from the store (`loadForRequest`, `deleteWhere` or `loadAll`)
implementers only have to provide the `Cookie.name` and `Cookie.value` attributes.

- [4.0.0](#400)

# 4.0.0
Expand Down
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
name: cookie_jar
description: A cookie manager for http requests in Dart, help you to deal with the cookie policies and persistence.
version: 4.0.8
version: 5.0.0
repository: https://github.com/flutterchina/cookie_jar
issue_tracker: https://github.com/flutterchina/cookie_jar/issues

environment:
sdk: '>=2.15.0 <3.0.0'
sdk: ">=2.15.0 <3.0.0"

dependencies:
meta: ^1.5.0
Expand Down
Loading