Release IKAR 1.0.7: port navigation and sharing improvements, harden calls
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

This commit is contained in:
sevenhill
2026-10-08 20:38:54 +03:00
parent 68f7957043
commit 317cfdae38
37 changed files with 1483 additions and 282 deletions
+15
View File
@@ -130,6 +130,21 @@ Future<void> main(List<String> args) async {
'Nonmember could read chat: ${forbidden['payload']}',
);
checks.add('persistent history and nonmember isolation');
final lookup = await b.ok(71, {
'chatId': chatId,
'messageId': message['id'],
});
check(
lookup['message']['id'] == message['id'] &&
lookup['message']['text'] == 'IKAR live delivery',
'Message lookup failed',
);
final privateLookup = await outsider.transport.request(71, {
'chatId': chatId,
'messageId': message['id'],
});
check(privateLookup['cmd'] == 3, 'Nonmember could look up a message');
checks.add('reply lookup and nonmember isolation');
final retryCid = DateTime.now().microsecondsSinceEpoch;
final richPayload = {
'chatId': chatId,
+57 -23
View File
@@ -4,7 +4,7 @@ import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import '../lib/core/transport/ikar_network.dart';
import 'package:ikar/core/transport/ikar_network.dart';
import 'live_ikar_smoke.dart' as live;
List<int> attr(int type, List<int> value) {
@@ -36,7 +36,7 @@ class Frame {
}
class Turn {
Turn(this.socket) {
Turn(Socket socket) : write = socket.add, stop = socket.destroy {
socket.listen(
(chunk) {
buffer.addAll(chunk);
@@ -51,7 +51,26 @@ class Turn {
onDone: frames.close,
);
}
final Socket socket;
Turn.datagram(RawDatagramSocket socket, InternetAddress address)
: write = ((bytes) => socket.send(bytes, address, 3478)),
stop = socket.close {
socket.listen(
(event) {
if (event != RawSocketEvent.read) return;
Datagram? packet;
while ((packet = socket.receive()) != null) {
if (packet!.address.address == address.address &&
packet.port == 3478) {
frames.add(Frame(packet.data));
}
}
},
onError: frames.addError,
onDone: frames.close,
);
}
final void Function(List<int>) write;
final void Function() stop;
final frames = StreamController<Frame>.broadcast();
final buffer = <int>[];
late String username, password, realm, nonce;
@@ -63,7 +82,9 @@ class Turn {
..setUint32(4, 0x2112a442);
final bytes = header.buffer.asUint8List();
final random = Random.secure();
for (var i = 8; i < 20; i++) bytes[i] = random.nextInt(256);
for (var i = 8; i < 20; i++) {
bytes[i] = random.nextInt(256);
}
final prefix = [...bytes, ...body];
if (!auth) return Uint8List.fromList(prefix);
final key = md5.convert(utf8.encode('$username:$realm:$password')).bytes;
@@ -84,7 +105,7 @@ class Turn {
final response = frames.stream
.firstWhere((f) => f.transaction == transaction)
.timeout(const Duration(seconds: 10));
socket.add(wire);
write(wire);
return response;
}
@@ -94,7 +115,9 @@ class Turn {
..setUint16(2, address.$2 ^ 0x2112);
final ip = InternetAddress(address.$1).rawAddress;
final magic = [0x21, 0x12, 0xa4, 0x42];
for (var i = 0; i < 4; i++) value.setUint8(4 + i, ip[i] ^ magic[i]);
for (var i = 0; i < 4; i++) {
value.setUint8(4 + i, ip[i] ^ magic[i]);
}
return attr(0x12, value.buffer.asUint8List());
}
@@ -130,22 +153,24 @@ class Turn {
);
}
void send((String, int) address, String message) => socket.add(
void send((String, int) address, String message) => write(
encode(0x16, [...peer(address), ...attr(0x13, utf8.encode(message))]),
);
Future<String> receive() => frames.stream
.firstWhere((f) => f.type == 0x17)
.timeout(const Duration(seconds: 10))
.then((f) => utf8.decode(f.attributes[0x13]!));
void close() => socket.destroy();
void close() => stop();
}
Future<void> main(List<String> args) async {
final udp = args.contains('--udp');
final hosts = args.where((arg) => !arg.startsWith('--')).toList();
final devices = <live.Device>[];
final relays = <Turn>[];
WebSocket? signaling;
try {
for (var i = 0; i < 2; i++)
for (var i = 0; i < 2; i++) {
devices.add(
await live.Device.create(
Uri.parse('https://ikar.kusoft.xyz'),
@@ -154,6 +179,7 @@ Future<void> main(List<String> args) async {
'IKAR TURN test',
),
);
}
final call = await devices[0].ok(78, {
'calleeIds': [devices[1].id],
'isVideo': false,
@@ -164,18 +190,26 @@ Future<void> main(List<String> args) async {
);
final hello = jsonDecode(await signaling.first as String);
final turn = hello['conversationParams']['turn'];
final address = args.isNotEmpty
? args.first
final address = hosts.isNotEmpty
? hosts.first
: RegExp(
r'^turn:([^:?/]+)',
).firstMatch((turn['urls'] as List).first)!.group(1)!;
for (var i = 0; i < 2; i++) {
final relay = Turn(
await Socket.connect(
address,
3478,
).timeout(const Duration(seconds: 10)),
);
final relay = udp
? Turn.datagram(
await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0),
(await InternetAddress.lookup(
address,
type: InternetAddressType.IPv4,
)).first,
)
: Turn(
await Socket.connect(
address,
3478,
).timeout(const Duration(seconds: 10)),
);
relays.add(relay);
await relay.allocate(turn['username'], turn['credential']);
}
@@ -196,22 +230,22 @@ Future<void> main(List<String> args) async {
);
final report = {
'passed': true,
'transport': 'TCP',
'transport': udp ? 'UDP' : 'TCP',
'serverAddress': address,
'externalAccess': args.isEmpty,
'externalAccess': hosts.isEmpty,
'relayPorts': [a.relay.$2, b.relay.$2],
'bidirectionalData': true,
'client': 'Windows',
'timestamp': DateTime.now().toUtc().toIso8601String(),
};
await File(
args.isEmpty
? 'artifacts/public-turn-verification.json'
: 'artifacts/local-turn-verification.json',
'artifacts/${hosts.isEmpty ? 'public' : 'local'}-turn-verification${udp ? '-udp' : ''}.json',
).writeAsString(const JsonEncoder.withIndent(' ').convert(report));
stdout.writeln(jsonEncode(report));
} finally {
for (final relay in relays) relay.close();
for (final relay in relays) {
relay.close();
}
await signaling?.close();
for (final d in devices) {
await d.ok(199, {});