JJ_MKVMaker/lib/core/mdns_smb.dart

153 lines
5.6 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'lan_scan.dart' show LanHost;
/// X-plore j 보충 (관리자 지시 10/18): mDNS 로 SMB 서버 찾기 (_smb._tcp.local) - 포트 훑기보다 먼저, 빠르게 (1~2초).
/// "레거시 유니캐스트" 질의 (RFC 6762 6.7): 5353 이 아닌 빈 포트에서 224.0.0.251:5353 으로 묻고, 응답자가 그 포트로 바로 답한다 -
/// Android 에서 멀티캐스트 받기 (MulticastLock) 없이도 받을 수 있다. 묻기만 하고 아무것도 알리지 않는다.
/// 돌려주는 것: 서버 이름 (SRV 의 대상 · 없으면 인스턴스 이름) 과 IPv4 주소. 못 찾으면 빈 목록 (오류를 내지 않음)
Future<List<LanHost>> findSmbServersMdns({Duration wait = const Duration(milliseconds: 1500), bool Function()? cancelled}) async {
RawDatagramSocket? sock;
try {
sock = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
} catch (_) {
return const [];
}
final id = Random().nextInt(0xffff);
final ptr = <String>{}; // 인스턴스 이름들
final srv = <String, String>{}; // 인스턴스 → 대상 호스트 이름
final a = <String, String>{}; // 호스트 이름 → IPv4
final from = <String, String>{}; // 인스턴스 → 응답을 보낸 주소 (A 가 없을 때)
final done = Completer<void>();
final sub = sock.listen((ev) {
if (ev != RawSocketEvent.read) return;
final d = sock!.receive();
if (d == null) return;
try {
for (final r in parseMdns(d.data)) {
switch (r.type) {
case 12 when r.name.toLowerCase() == '_smb._tcp.local':
ptr.add(r.data);
from[r.data] = d.address.address;
case 33:
srv[r.name] = r.data;
case 1:
a[r.name.toLowerCase()] = r.data;
}
}
} catch (_) {}
});
sock.send(mdnsQuery(id, '_smb._tcp.local', 12), InternetAddress('224.0.0.251'), 5353);
// 잠깐 기다리며 모은다 (취소되면 바로)
final end = DateTime.now().add(wait);
while (DateTime.now().isBefore(end) && !(cancelled?.call() ?? false)) {
await Future<void>.delayed(const Duration(milliseconds: 100));
}
if (!done.isCompleted) done.complete();
await sub.cancel();
sock.close();
final out = <LanHost>[];
final seen = <String>{};
for (final inst in ptr) {
final host = srv[inst];
final ip = (host == null ? null : a[host.toLowerCase()]) ?? from[inst];
if (ip == null || !seen.add(ip)) continue;
final label = host ?? inst.replaceFirst(RegExp(r'\._smb\._tcp\.local$', caseSensitive: false), '');
out.add(LanHost(ip, label.endsWith('.') ? label.substring(0, label.length - 1) : label));
}
return out;
}
/// mDNS 질의 하나 (QU 비트 - 유니캐스트로 답해 달라)
Uint8List mdnsQuery(int id, String name, int type) {
final b = BytesBuilder();
b.add([id >> 8, id & 0xff, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]);
for (final part in name.split('.')) {
final x = utf8.encode(part);
b.addByte(x.length);
b.add(x);
}
b.addByte(0);
b.add([type >> 8, type & 0xff, 0x80, 0x01]); // 형식 · IN + QU
return b.toBytes();
}
/// 응답의 기록 (답 · 권한 · 추가 모두): (이름, 형식, 자료 - PTR/SRV 는 이름, A 는 주소)
List<({String name, int type, String data})> parseMdns(Uint8List m) {
final v = ByteData.sublistView(m);
int u16(int o) => v.getUint16(o);
String readName(int o, List<int> next) {
final parts = <String>[];
var jumped = false;
var guard = 0;
while (guard++ < 64) {
final len = m[o];
if (len == 0) {
o += 1;
break;
}
if (len & 0xc0 == 0xc0) {
final ptr = ((len & 0x3f) << 8) | m[o + 1];
if (!jumped) next.add(o + 2);
jumped = true;
o = ptr;
continue;
}
parts.add(utf8.decode(m.sublist(o + 1, o + 1 + len), allowMalformed: true));
o += 1 + len;
}
if (!jumped) next.add(o);
return parts.join('.');
}
final qd = u16(4), an = u16(6), ns = u16(8), ar = u16(10);
var o = 12;
for (var i = 0; i < qd; i++) {
final n = <int>[];
readName(o, n);
o = n.first + 4;
}
final out = <({String name, int type, String data})>[];
for (var i = 0; i < an + ns + ar; i++) {
final n = <int>[];
final name = readName(o, n);
o = n.first;
final type = u16(o), len = u16(o + 8);
final rd = o + 10;
String data = '';
if (type == 12) {
data = readName(rd, []);
} else if (type == 33 && len > 6) {
data = readName(rd + 6, []);
} else if (type == 1 && len == 4) {
data = '${m[rd]}.${m[rd + 1]}.${m[rd + 2]}.${m[rd + 3]}';
}
out.add((name: name, type: type, data: data));
o = rd + len;
}
return out;
}
/// 찾기 창의 기본: mDNS 를 먼저 (빠르게 · 이름까지), 이어서 포트 훑기 (mDNS 를 안 하는 NAS 도). 같은 주소는 하나로 (이름은 mDNS 것)
Future<List<LanHost>> findSmbServersBoth(
Future<List<LanHost>> Function({bool Function()? cancelled, void Function(int done, int total)? onProgress}) scan,
{bool Function()? cancelled, void Function(int done, int total)? onProgress}) async {
final m = await findSmbServersMdns(cancelled: cancelled);
if (cancelled?.call() ?? false) return m;
final s = await scan(cancelled: cancelled, onProgress: onProgress);
final byIp = {for (final h in s) h.address: h};
for (final h in m) {
byIp[h.address] = h;
}
final out = byIp.values.toList()
..sort((a, b) {
int key(String ip) => ip.split('.').fold(0, (x, y) => x * 256 + (int.tryParse(y) ?? 0));
return key(a.address).compareTo(key(b.address));
});
return out;
}