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> 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 = {}; // 인스턴스 이름들 final srv = {}; // 인스턴스 → 대상 호스트 이름 final a = {}; // 호스트 이름 → IPv4 final from = {}; // 인스턴스 → 응답을 보낸 주소 (A 가 없을 때) final done = Completer(); 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.delayed(const Duration(milliseconds: 100)); } if (!done.isCompleted) done.complete(); await sub.cancel(); sock.close(); final out = []; final seen = {}; 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 next) { final parts = []; 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 = []; 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 = []; 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> findSmbServersBoth( Future> 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; }