diff --git a/CHANGELOG.md b/CHANGELOG.md index 2864af8..c59286d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## NEXT + +- `CookieJar` now returns `FutureOr` instead of `Future` for all methods. +- Deprecates `delete` in favor of `deleteWhere`. +- Adds `loadAll` to get all saved Cookies. +- Adds `endSession` to delete all session Cookies. + ## 4.0.8 - Simply replace `\` to `/` rather than using `Uri` when parsing a storage base directory. diff --git a/example/encryption.dart b/example/encryption.dart index 4be8a5f..530e205 100644 --- a/example/encryption.dart +++ b/example/encryption.dart @@ -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 results; final cookie = Cookie('test', 'hh') ..expires = DateTime.parse('1970-02-27 13:27:00'); diff --git a/lib/src/cookie_jar.dart b/lib/src/cookie_jar.dart index 36903f3..ac5190a 100644 --- a/lib/src/cookie_jar.dart +++ b/lib/src/cookie_jar.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:universal_io/io.dart' show Cookie; import 'jar/default.dart'; @@ -7,8 +9,66 @@ 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. +/// {@template CookieJar.publicSuffixNote} +/// > 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 . +/// {@endtemplate} +/// +/// ### 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(); @@ -16,18 +76,39 @@ abstract class CookieJar { 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 saveFromResponse(Uri uri, List cookies); + FutureOr saveFromResponse(Uri uri, List cookies); /// Load the cookies for specified [uri]. - Future> loadForRequest(Uri uri); + FutureOr> loadForRequest(Uri uri); + + /// Ends the current session deleting all session cookies. + FutureOr 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> loadAll(); - /// Delete all cookies in the [CookieJar]. - Future 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 deleteAll(); - /// Delete cookies with the specified [uri]. - Future 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 deleteWhere(bool Function(Cookie cookie) test); } diff --git a/lib/src/jar/default.dart b/lib/src/jar/default.dart index 4a0f936..26a4c07 100644 --- a/lib/src/jar/default.dart +++ b/lib/src/jar/default.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:universal_io/io.dart' show Cookie; import '../cookie_jar.dart'; @@ -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.publicSuffixNote} class DefaultCookieJar implements CookieJar { DefaultCookieJar({this.ignoreExpires = false}); - @override final bool ignoreExpires; /// An array to save cookies. @@ -88,7 +94,7 @@ class DefaultCookieJar implements CookieJar { } @override - Future> loadForRequest(Uri uri) async { + FutureOr> loadForRequest(Uri uri) { final list = []; final urlPath = uri.path; // Load cookies without "domain" attribute, include port. @@ -137,7 +143,7 @@ class DefaultCookieJar implements CookieJar { } @override - Future saveFromResponse(Uri uri, List cookies) async { + FutureOr saveFromResponse(Uri uri, List cookies) { for (final cookie in cookies) { String? domain = cookie.domain; String path; @@ -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 delete(Uri uri, [bool withDomainSharedCookie = false]) async { + @Deprecated('Use deleteWhere instead') + FutureOr delete(Uri uri, [bool withDomainSharedCookie = false]) { final host = uri.host; hostCookies.remove(host); if (withDomainSharedCookie) { @@ -186,7 +192,7 @@ class DefaultCookieJar implements CookieJar { /// Delete all cookies stored in the memory. @override - Future deleteAll() async { + FutureOr deleteAll() { domainCookies.clear(); hostCookies.clear(); } @@ -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> loadAll() { + final list = []; + + 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; + } } diff --git a/lib/src/jar/persist.dart b/lib/src/jar/persist.dart index 8c1ab13..7c07001 100644 --- a/lib/src/jar/persist.dart +++ b/lib/src/jar/persist.dart @@ -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. @@ -157,6 +157,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 delete(Uri uri, [bool withDomainSharedCookie = false]) async { await _checkInitialized(); @@ -171,6 +172,15 @@ class PersistCookieJar extends DefaultCookieJar { } } + @override + Future deleteWhere(bool Function(Cookie cookie) test) async { + await _checkInitialized(); + super.deleteWhere(test); + + 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 deleteAll() async { diff --git a/lib/src/jar/web.dart b/lib/src/jar/web.dart index 50c5577..a6fc637 100644 --- a/lib/src/jar/web.dart +++ b/lib/src/jar/web.dart @@ -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 delete(Uri uri, [bool withDomainSharedCookie = false]) async {} + void deleteAll() {} @override - Future deleteAll() async {} + List loadForRequest(Uri uri) => []; @override - Future> loadForRequest(Uri uri) async => []; + void saveFromResponse(Uri uri, List cookies) {} @override - Future saveFromResponse(Uri uri, List cookies) async {} + void endSession() {} + + @override + List loadAll() => []; } diff --git a/migration_guide.md b/migration_guide.md index 299a378..b4a3b2f 100644 --- a/migration_guide.md +++ b/migration_guide.md @@ -1,6 +1,6 @@ # Migration Guide -This document gathered all breaking changes and migrations requirement between versions. +This document gathered all breaking changes and migration requirements between versions.