Tests / test (push) Canceled after 0s
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Native crypto / native-crypto (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
192 lines
6.2 KiB
Dart
192 lines
6.2 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
import '../lib/backend/ikar_transport.dart';
|
|
import '../lib/core/transport/ikar_network.dart';
|
|
import 'live_ikar_smoke.dart' as live;
|
|
|
|
Future<void> main() async {
|
|
final peers = <live.Device>[];
|
|
IkarTransport? visibleConnection;
|
|
final base = Uri.parse('https://ikar.kusoft.xyz');
|
|
final checks = <String>[];
|
|
final client = IkarNetwork.client();
|
|
try {
|
|
final healthResponse = await (await client.getUrl(
|
|
base.resolve('/health'),
|
|
)).close();
|
|
final health = jsonDecode(await utf8.decodeStream(healthResponse)) as Map;
|
|
live.check(
|
|
healthResponse.statusCode == 200 && health['status'] == 'ok',
|
|
'Public TLS health failed',
|
|
);
|
|
checks.add('public HTTPS health with certificate validation');
|
|
final stamp = DateTime.now().microsecondsSinceEpoch.toString().substring(6);
|
|
peers.add(
|
|
await live.Device.create(
|
|
base,
|
|
'node',
|
|
'+999${stamp}0',
|
|
'Ghost verification',
|
|
interactive: false,
|
|
),
|
|
);
|
|
peers.add(
|
|
await live.Device.create(
|
|
base,
|
|
'node',
|
|
'+999${stamp}1',
|
|
'Presence observer',
|
|
),
|
|
);
|
|
final hidden = peers[0], observer = peers[1];
|
|
final chatId = hidden.id ^ observer.id;
|
|
await hidden.ok(64, {
|
|
'chatId': chatId,
|
|
'message': {'text': 'Ghost initial message'},
|
|
});
|
|
|
|
Future<Map> presence(int status) async {
|
|
for (var attempt = 0; attempt < 30; attempt++) {
|
|
final result = await observer.ok(35, {
|
|
'contactIds': [hidden.id],
|
|
});
|
|
final entry = result['presence']['${hidden.id}'] as Map;
|
|
if (entry['status'] == status) return entry;
|
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
}
|
|
throw StateError('Presence failed to reach $status');
|
|
}
|
|
|
|
final initial = await presence(0);
|
|
live.check(
|
|
initial['seen'] == 0,
|
|
'Invisible first connection exposed activity',
|
|
);
|
|
checks.add(
|
|
'invisible login and SignalR connection expose neither online nor last seen',
|
|
);
|
|
await hidden.ok(1, {'interactive': true});
|
|
final active = await presence(1);
|
|
live.check(
|
|
(active['seen'] as num) > 0,
|
|
'Visible ping failed to publish activity',
|
|
);
|
|
await Future<void>.delayed(const Duration(milliseconds: 1100));
|
|
await hidden.ok(1, {'interactive': false});
|
|
final inactive = await presence(0);
|
|
live.check(
|
|
inactive['seen'] == active['seen'],
|
|
'Ghost ping changed last seen',
|
|
);
|
|
checks.add('switching to ghost freezes the previous public timestamp');
|
|
hidden.transport.interactive = false;
|
|
await hidden.transport.connectEvents();
|
|
await hidden.ok(1, {'interactive': false});
|
|
live.check(
|
|
(await presence(0))['seen'] == active['seen'],
|
|
'Reconnect exposed activity',
|
|
);
|
|
checks.add('invisible event reconnection retains hidden presence');
|
|
final delivered = hidden.transport.events
|
|
.firstWhere(
|
|
(event) =>
|
|
event['opcode'] == 128 &&
|
|
event['payload']?['message']?['text'] == 'Delivery to ghost',
|
|
)
|
|
.timeout(const Duration(seconds: 15));
|
|
final sent = await observer.ok(64, {
|
|
'chatId': chatId,
|
|
'message': {'text': 'Delivery to ghost'},
|
|
});
|
|
await delivered;
|
|
final history = await hidden.ok(49, {'chatId': chatId});
|
|
live.check(
|
|
(history['messages'] as List).any(
|
|
(message) => message['id'] == sent['message']['id'],
|
|
),
|
|
'Ghost history missed delivered message',
|
|
);
|
|
await hidden.ok(64, {
|
|
'chatId': chatId,
|
|
'message': {'text': 'Reply from ghost'},
|
|
});
|
|
live.check(
|
|
(await presence(0))['seen'] == active['seen'],
|
|
'Ghost messaging exposed security session activity',
|
|
);
|
|
checks.add(
|
|
'invisible client receives messages, reads history and replies without exposing activity',
|
|
);
|
|
final incoming = hidden.transport.events
|
|
.firstWhere((event) => event['opcode'] == 137)
|
|
.timeout(const Duration(seconds: 15));
|
|
final call = await observer.ok(78, {
|
|
'calleeIds': [hidden.id],
|
|
'isVideo': false,
|
|
'conversationId': 'ikar-ghost-$stamp',
|
|
});
|
|
final invitation = await incoming;
|
|
live.check(
|
|
invitation['payload']['callerId'] == observer.id,
|
|
'Ghost call invitation has wrong caller',
|
|
);
|
|
final endpoint =
|
|
jsonDecode(call['internalCallerParams'])['endpoint'] as String;
|
|
final callSocket = await IkarNetwork.connectWebSocket(endpoint);
|
|
final callSubscription = callSocket.listen((_) {});
|
|
try {
|
|
callSocket.add(
|
|
jsonEncode({'command': 'hangup', 'sequence': 1, 'reason': 'FINISHED'}),
|
|
);
|
|
await Future<void>.delayed(const Duration(milliseconds: 200));
|
|
} finally {
|
|
await callSocket.close();
|
|
await callSubscription.cancel();
|
|
}
|
|
live.check(
|
|
(await presence(0))['seen'] == active['seen'],
|
|
'Incoming call exposed ghost activity',
|
|
);
|
|
checks.add(
|
|
'invisible client receives a call invitation without exposing activity',
|
|
);
|
|
visibleConnection = IkarTransport(base)
|
|
..token = hidden.transport.token
|
|
..interactive = true;
|
|
await visibleConnection.connectEvents();
|
|
await presence(1);
|
|
await visibleConnection.close();
|
|
visibleConnection = null;
|
|
await presence(0);
|
|
checks.add(
|
|
'disconnecting a visible connection preserves the invisible connection',
|
|
);
|
|
await hidden.ok(1, {'interactive': true});
|
|
final restored = await presence(1);
|
|
live.check(
|
|
(restored['seen'] as num) > (active['seen'] as num),
|
|
'Visible activity did not resume',
|
|
);
|
|
checks.add('disabling ghost resumes online and public last seen');
|
|
final report = {
|
|
'passed': true,
|
|
'checks': checks,
|
|
'timestamp': DateTime.now().toUtc().toIso8601String(),
|
|
};
|
|
await File(
|
|
'artifacts/ghost-live-verification.json',
|
|
).writeAsString(const JsonEncoder.withIndent(' ').convert(report));
|
|
stdout.writeln(jsonEncode(report));
|
|
} finally {
|
|
await visibleConnection?.close();
|
|
client.close(force: true);
|
|
for (final peer in peers.reversed) {
|
|
try {
|
|
await peer.ok(199, {});
|
|
} finally {
|
|
await peer.close();
|
|
}
|
|
}
|
|
}
|
|
}
|