diff --git a/mobile/lib/shared/me/me_agent_inventory.dart b/mobile/lib/shared/me/me_agent_inventory.dart index 293710157e..f5e89c6cd9 100644 --- a/mobile/lib/shared/me/me_agent_inventory.dart +++ b/mobile/lib/shared/me/me_agent_inventory.dart @@ -63,7 +63,12 @@ Map _inventoryNames(List events, String owner) { final id = event.getTagValue('d'); if (id == null || !RegExp(r'^[0-9a-f]{64}$').hasMatch(id)) continue; // Verify before allowing a newer tombstone or record to replace a head. - nostr.Event.fromJson(jsonEncode(event.toJson())); + try { + nostr.Event.fromJson(jsonEncode(event.toJson())); + } catch (_) { + // A corrupt record must not hide unrelated verified agents. + continue; + } final previous = heads[id]; if (previous == null || meEventIsNewer(event, previous)) heads[id] = event; } diff --git a/mobile/lib/shared/me/me_archived_agents.dart b/mobile/lib/shared/me/me_archived_agents.dart index c6a9cab746..9171a8814f 100644 --- a/mobile/lib/shared/me/me_archived_agents.dart +++ b/mobile/lib/shared/me/me_archived_agents.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:isolate'; @@ -28,12 +29,24 @@ final meArchivedAgentsProvider = FutureProvider>((ref) async { ) .timeout(const Duration(seconds: 3)); if (response.statusCode != 200) return {}; + final chunks = StreamIterator( + response.stream.timeout(const Duration(seconds: 3)), + ); final bytes = []; - await for (final chunk in response.stream.timeout( - const Duration(seconds: 3), - )) { - if (bytes.length + chunk.length > 256 * 1024) return {}; - bytes.addAll(chunk); + try { + await (() async { + while (await chunks.moveNext()) { + final chunk = chunks.current; + if (bytes.length + chunk.length > 256 * 1024) { + throw const FormatException( + 'Relay information exceeds size limit.', + ); + } + bytes.addAll(chunk); + } + })().timeout(const Duration(seconds: 3)); + } finally { + await chunks.cancel(); } sync.check(); final info = jsonDecode(utf8.decode(bytes)); diff --git a/mobile/test/shared/me/me_archived_agents_test.dart b/mobile/test/shared/me/me_archived_agents_test.dart index 3a2c6cb7db..99a53534ba 100644 --- a/mobile/test/shared/me/me_archived_agents_test.dart +++ b/mobile/test/shared/me/me_archived_agents_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:buzz/shared/me/me_archived_agents.dart'; @@ -68,6 +69,40 @@ void main() { }, ); + test('slow metadata has a total deadline and cancels its stream', () async { + final owner = nostr.Keys.generate(); + final relay = _ArchiveRelay(null); + var cancelled = false; + final body = StreamController>(onCancel: () => cancelled = true); + final client = MockClient.streaming( + (_, _) async => http.StreamedResponse(body.stream, 200), + ); + final container = ProviderContainer( + overrides: [ + meSyncProvider.overrideWithValue( + MeSync( + relay, + RelayConfig(baseUrl: 'https://relay.test', nsec: owner.nsec), + ), + ), + meRelayInformationClientProvider.overrideWithValue(client), + ], + ); + addTearDown(container.dispose); + addTearDown(client.close); + addTearDown(body.close); + final trickle = Timer.periodic(const Duration(milliseconds: 100), (_) { + body.add([32]); + }); + addTearDown(trickle.cancel); + final result = await container + .read(meArchivedAgentsProvider.future) + .timeout(const Duration(seconds: 5)); + expect(result, isEmpty); + expect(cancelled, isTrue); + expect(relay.author, isNull); + }); + test( 'contact keys and unavailable metadata are not archive authority', () async { diff --git a/mobile/test/shared/me/me_assistants_test.dart b/mobile/test/shared/me/me_assistants_test.dart index 0d5f4bcd8e..3e952feace 100644 --- a/mobile/test/shared/me/me_assistants_test.dart +++ b/mobile/test/shared/me/me_assistants_test.dart @@ -237,6 +237,11 @@ void main() { ], inventory: [ listing(agent.public), + for (final field in ['id', 'sig']) + NostrEvent.fromJson( + listing(agent.public, time: 3, content: '{}').toJson() + ..[field] = '0' * (field == 'id' ? 64 : 128), + ), listing(archivedAgent.public), listing(removed.public), listing(removed.public, time: 2, content: '{}'),