Files
999/lib/g4f/cookie_jar.dart
T
Alma Alexyz fbf04d90db feat: embed a Flutter G4F client with Cronet, cookie jar, and origin WebView
Port the Oracle off the Python g4f container onto on-device providers: Chromium HTTP via Cronet, a shared cookie session, IterList retry, hosted OpenAI-compat aggregators, and same-origin WebView fallbacks instead of CORS fetch.
2026-09-01 22:05:38 +05:00

192 lines
5.2 KiB
Dart

/// In-process cookie jar. Python g4f keeps a curl_cffi session; Dart/Cronet
/// does not, so every provider must share cookies across GET-then-POST.
class G4fCookieJar {
final Map<String, _StoredCookie> _cookies = {};
int get length => _cookies.length;
void clear() => _cookies.clear();
void set({
required String name,
required String value,
required String domain,
String path = '/',
DateTime? expires,
bool secure = false,
bool hostOnly = true,
}) {
if (name.isEmpty) return;
final canon = _canonDomain(domain);
if (canon.isEmpty) return;
final key = _key(canon, path, name);
if (value.isEmpty && expires != null) {
_cookies.remove(key);
return;
}
_cookies[key] = _StoredCookie(
name: name,
value: value,
domain: canon,
path: path.isEmpty ? '/' : path,
expires: expires,
secure: secure,
hostOnly: hostOnly,
);
}
/// Parse `Set-Cookie` from an HTTP response for [uri].
void absorb(Uri uri, Map<String, String> headers) {
final raw = headers['set-cookie'];
if (raw == null || raw.isEmpty) return;
for (final part in splitSetCookieHeader(raw)) {
final parsed = _parseSetCookie(part, uri);
if (parsed == null) continue;
if (parsed.expired) {
_cookies.remove(_key(parsed.domain, parsed.path, parsed.name));
continue;
}
_cookies[_key(parsed.domain, parsed.path, parsed.name)] = parsed;
}
}
String headerFor(Uri uri) {
final now = DateTime.now().toUtc();
final matching = <_StoredCookie>[];
final stale = <String>[];
_cookies.forEach((key, cookie) {
if (cookie.expires != null && !cookie.expires!.isAfter(now)) {
stale.add(key);
return;
}
if (cookie.matches(uri)) matching.add(cookie);
});
for (final key in stale) {
_cookies.remove(key);
}
matching.sort((a, b) => b.path.length.compareTo(a.path.length));
return matching.map((c) => '${c.name}=${c.value}').join('; ');
}
/// Test helper.
Map<String, String> asMapFor(Uri uri) {
final header = headerFor(uri);
if (header.isEmpty) return {};
final out = <String, String>{};
for (final part in header.split('; ')) {
final eq = part.indexOf('=');
if (eq <= 0) continue;
out[part.substring(0, eq)] = part.substring(eq + 1);
}
return out;
}
}
class _StoredCookie {
_StoredCookie({
required this.name,
required this.value,
required this.domain,
required this.path,
required this.secure,
required this.hostOnly,
this.expires,
});
final String name;
final String value;
final String domain;
final String path;
final DateTime? expires;
final bool secure;
final bool hostOnly;
bool get expired {
if (expires == null) return false;
return !expires!.isAfter(DateTime.now().toUtc());
}
bool matches(Uri uri) {
if (secure && uri.scheme != 'https') return false;
final host = uri.host.toLowerCase();
if (hostOnly) {
if (host != domain) return false;
} else {
if (host != domain && !host.endsWith('.$domain')) return false;
}
final reqPath = uri.path.isEmpty ? '/' : uri.path;
if (path == '/') return true;
if (reqPath == path) return true;
if (reqPath.startsWith(path.endsWith('/') ? path : '$path/')) return true;
return false;
}
}
String _canonDomain(String domain) {
var d = domain.trim().toLowerCase();
if (d.startsWith('.')) d = d.substring(1);
return d;
}
String _key(String domain, String path, String name) =>
'${_canonDomain(domain)}|$path|$name';
/// Split a folded Set-Cookie header (commas inside Expires dates).
List<String> splitSetCookieHeader(String header) {
return header
.split(RegExp(r',(?=\s*[^;=]+=)'))
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
}
_StoredCookie? _parseSetCookie(String raw, Uri uri) {
final parts = raw.split(';');
if (parts.isEmpty) return null;
final nv = parts.first.trim();
final eq = nv.indexOf('=');
if (eq <= 0) return null;
final name = nv.substring(0, eq).trim();
final value = nv.substring(eq + 1).trim();
if (name.isEmpty) return null;
String? domain;
var path = '/';
DateTime? expires;
var secure = false;
var hostOnly = true;
for (var i = 1; i < parts.length; i++) {
final attr = parts[i].trim();
if (attr.isEmpty) continue;
final aEq = attr.indexOf('=');
final key = (aEq < 0 ? attr : attr.substring(0, aEq)).trim().toLowerCase();
final val = aEq < 0 ? '' : attr.substring(aEq + 1).trim();
if (key == 'domain' && val.isNotEmpty) {
domain = _canonDomain(val);
hostOnly = false;
} else if (key == 'path' && val.isNotEmpty) {
path = val;
} else if (key == 'expires') {
expires ??= DateTime.tryParse(val)?.toUtc();
} else if (key == 'max-age') {
final secs = int.tryParse(val);
if (secs != null) {
expires = DateTime.now().toUtc().add(Duration(seconds: secs));
}
} else if (key == 'secure') {
secure = true;
}
}
return _StoredCookie(
name: name,
value: value,
domain: domain ?? uri.host.toLowerCase(),
path: path,
expires: expires,
secure: secure,
hostOnly: domain == null ? true : hostOnly,
);
}