Files
IKAR/tool/public_turn_smoke.dart
T

222 lines
7.2 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import '../lib/core/transport/ikar_network.dart';
import 'live_ikar_smoke.dart' as live;
List<int> attr(int type, List<int> value) {
final h = ByteData(4)
..setUint16(0, type)
..setUint16(2, value.length);
return [
...h.buffer.asUint8List(),
...value,
...List.filled((4 - value.length % 4) % 4, 0),
];
}
class Frame {
Frame(this.bytes) {
final header = ByteData.sublistView(bytes);
type = header.getUint16(0);
for (var offset = 20; offset + 4 <= bytes.length;) {
final at = header.getUint16(offset),
length = header.getUint16(offset + 2);
attributes[at] = bytes.sublist(offset + 4, offset + 4 + length);
offset += 4 + length + (4 - length % 4) % 4;
}
}
final Uint8List bytes;
late final int type;
final attributes = <int, Uint8List>{};
String get transaction => base64Encode(bytes.sublist(8, 20));
}
class Turn {
Turn(this.socket) {
socket.listen(
(chunk) {
buffer.addAll(chunk);
while (buffer.length >= 20) {
final size = 20 + (buffer[2] << 8 | buffer[3]);
if (buffer.length < size) break;
frames.add(Frame(Uint8List.fromList(buffer.sublist(0, size))));
buffer.removeRange(0, size);
}
},
onError: frames.addError,
onDone: frames.close,
);
}
final Socket socket;
final frames = StreamController<Frame>.broadcast();
final buffer = <int>[];
late String username, password, realm, nonce;
late (String, int) relay;
Uint8List encode(int type, List<int> body, {bool auth = false}) {
final header = ByteData(20)
..setUint16(0, type)
..setUint16(2, body.length + (auth ? 24 : 0))
..setUint32(4, 0x2112a442);
final bytes = header.buffer.asUint8List();
final random = Random.secure();
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;
final integrity = Hmac(sha1, key).convert(prefix).bytes;
return Uint8List.fromList([...prefix, ...attr(8, integrity)]);
}
Future<Frame> request(int type, List<int> body, {bool auth = false}) async {
final credentials = auth
? [
...attr(6, utf8.encode(username)),
...attr(0x14, utf8.encode(realm)),
...attr(0x15, utf8.encode(nonce)),
]
: <int>[];
final wire = encode(type, [...credentials, ...body], auth: auth);
final transaction = base64Encode(wire.sublist(8, 20));
final response = frames.stream
.firstWhere((f) => f.transaction == transaction)
.timeout(const Duration(seconds: 10));
socket.add(wire);
return response;
}
List<int> peer((String, int) address) {
final value = ByteData(8)
..setUint8(1, 1)
..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]);
return attr(0x12, value.buffer.asUint8List());
}
Future<void> allocate(String user, String secret) async {
username = user;
password = secret;
final transport = attr(0x19, [17, 0, 0, 0]);
final challenge = await request(3, transport);
live.check(
challenge.type == 0x113,
'TURN did not issue an authentication challenge',
);
realm = utf8.decode(challenge.attributes[0x14]!);
nonce = utf8.decode(challenge.attributes[0x15]!);
live.check(realm == 'ikar.kusoft.xyz', 'Incorrect TURN realm');
final result = await request(3, transport, auth: true);
live.check(result.type == 0x103, 'TURN allocation rejected');
final value = ByteData.sublistView(result.attributes[0x16]!);
live.check(value.getUint8(1) == 1, 'Unexpected relay address family');
final port = value.getUint16(2) ^ 0x2112;
final magic = [0x21, 0x12, 0xa4, 0x42];
final ip = List.generate(
4,
(i) => value.getUint8(4 + i) ^ magic[i],
).join('.');
relay = (ip, port);
}
Future<void> permission((String, int) address) async {
live.check(
(await request(8, peer(address), auth: true)).type == 0x108,
'TURN permission rejected',
);
}
void send((String, int) address, String message) => socket.add(
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();
}
Future<void> main(List<String> args) async {
final devices = <live.Device>[];
final relays = <Turn>[];
WebSocket? signaling;
try {
for (var i = 0; i < 2; i++)
devices.add(
await live.Device.create(
Uri.parse('https://ikar.kusoft.xyz'),
Platform.environment['IKAR_TEST_NODE'] ?? 'node',
'+999${DateTime.now().microsecondsSinceEpoch}$i',
'IKAR TURN test',
),
);
final call = await devices[0].ok(78, {
'calleeIds': [devices[1].id],
'isVideo': false,
'conversationId': 'ikar-turn-${DateTime.now().microsecondsSinceEpoch}',
});
signaling = await IkarNetwork.connectWebSocket(
jsonDecode(call['internalCallerParams'])['endpoint'],
);
final hello = jsonDecode(await signaling.first as String);
final turn = hello['conversationParams']['turn'];
final address = args.isNotEmpty
? args.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)),
);
relays.add(relay);
await relay.allocate(turn['username'], turn['credential']);
}
final a = relays[0], b = relays[1];
await a.permission(b.relay);
await b.permission(a.relay);
final incoming = b.receive();
a.send(b.relay, 'IKAR public TURN');
live.check(
await incoming == 'IKAR public TURN',
'TURN forward data not relayed',
);
final reply = a.receive();
b.send(a.relay, 'IKAR public TURN reply');
live.check(
await reply == 'IKAR public TURN reply',
'TURN reply not relayed',
);
final report = {
'passed': true,
'transport': 'TCP',
'serverAddress': address,
'externalAccess': args.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',
).writeAsString(const JsonEncoder.withIndent(' ').convert(report));
stdout.writeln(jsonEncode(report));
} finally {
for (final relay in relays) relay.close();
await signaling?.close();
for (final d in devices) {
await d.ok(199, {});
await d.close();
}
}
}