using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L19-3: IP 주소 유틸리티 핸들러. "ip" 프리픽스로 사용합니다. /// /// 예: ip → 로컬 IP 주소 목록 /// ip my → 전체 어댑터 IP 목록 /// ip 192.168.1.100 → IP 분류·타입·이진 표현 /// ip 10.0.0.0/8 → CIDR 네트워크 정보 /// ip 192.168.1.0/24 → 네트워크 주소·브로드캐스트·호스트 범위·수 /// ip range 192.168.1.1 192.168.1.100 → 범위 내 IP 수 /// ip bin 192.168.1.1 → 이진 표현 /// ip hex 192.168.1.1 → 16진수 표현 /// ip int 192.168.1.1 → 정수(uint32) 표현 /// ip from 3232235777 → 정수 → IP 주소 /// Enter → 값 복사. /// public class IpInfoHandler : IActionHandler { public string? Prefix => "ip"; public PluginMetadata Metadata => new( "IP", "IP 주소 유틸리티 — 분류·CIDR·이진·16진·정수 변환", "1.0", "AX"); public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem("IP 주소 유틸리티", "ip my / ip 192.168.1.1 / ip 10.0.0.0/8 / ip bin/hex/int ", null, null, Symbol: "\uE968")); BuildLocalIpItems(items, brief: true); return Task.FromResult>(items); } var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries); var sub = parts[0].ToLowerInvariant(); // ip my if (sub is "my" or "local" or "내" or "로컬") { items.Add(new LauncherItem("로컬 네트워크 어댑터 IP 목록", "", null, null, Symbol: "\uE968")); BuildLocalIpItems(items, brief: false); return Task.FromResult>(items); } // ip range if (sub == "range" && parts.Length >= 3) { if (IPAddress.TryParse(parts[1], out var start) && IPAddress.TryParse(parts[2], out var end) && start.AddressFamily == AddressFamily.InterNetwork && end.AddressFamily == AddressFamily.InterNetwork) { var s = IpToUint(start); var e = IpToUint(end); if (s > e) (s, e) = (e, s); var count = e - s + 1; items.Add(new LauncherItem($"{UintToIp(s)} ~ {UintToIp(e)}", $"IP 수: {count:N0}개", null, null, Symbol: "\uE968")); items.Add(CopyItem("시작 IP", UintToIp(s))); items.Add(CopyItem("끝 IP", UintToIp(e))); items.Add(CopyItem("IP 개수", count.ToString("N0"))); } else { items.Add(ErrorItem("올바른 IPv4 주소를 입력하세요")); } return Task.FromResult>(items); } // ip from if (sub == "from" && parts.Length >= 2) { if (uint.TryParse(parts[1], out var uval)) { var ip = UintToIp(uval); items.Add(new LauncherItem($"{uval} → {ip}", "정수 → IPv4 변환", null, ("copy", ip), Symbol: "\uE968")); items.Add(CopyItem("IP 주소", ip)); items.Add(CopyItem("이진", ToBinary(uval))); items.Add(CopyItem("16진수", $"0x{uval:X8}")); } else items.Add(ErrorItem("올바른 32비트 정수를 입력하세요")); return Task.FromResult>(items); } // ip bin/hex/int if (sub is "bin" or "binary" or "이진" or "hex" or "int" or "integer") { if (parts.Length >= 2 && IPAddress.TryParse(parts[1], out var ip4) && ip4.AddressFamily == AddressFamily.InterNetwork) { BuildConversionItems(items, ip4); } else items.Add(ErrorItem("올바른 IPv4 주소를 입력하세요")); return Task.FromResult>(items); } // CIDR: 10.0.0.0/8 if (parts[0].Contains('/')) { BuildCidrItems(items, parts[0]); return Task.FromResult>(items); } // 단순 IP 주소 if (IPAddress.TryParse(parts[0], out var addr)) { if (addr.AddressFamily == AddressFamily.InterNetwork) BuildIpInfoItems(items, addr); else if (addr.AddressFamily == AddressFamily.InterNetworkV6) BuildIpv6Items(items, addr); else items.Add(ErrorItem("IPv4 또는 IPv6 주소를 입력하세요")); } else { items.Add(new LauncherItem($"인식할 수 없는 입력: '{parts[0]}'", "ip 192.168.1.1 / ip 10.0.0.0/8 / ip my / ip bin 1.2.3.4", null, null, Symbol: "\uE783")); } return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is ("copy", string text)) { try { System.Windows.Application.Current.Dispatcher.Invoke( () => Clipboard.SetText(text)); NotificationService.Notify("IP", "클립보드에 복사했습니다."); } catch { } } return Task.CompletedTask; } // ── 빌더 ──────────────────────────────────────────────────────────────── private static void BuildLocalIpItems(List items, bool brief) { try { var ifaces = NetworkInterface.GetAllNetworkInterfaces() .Where(n => n.OperationalStatus == OperationalStatus.Up && n.NetworkInterfaceType != NetworkInterfaceType.Loopback) .ToList(); if (ifaces.Count == 0) { items.Add(new LauncherItem("활성 네트워크 어댑터 없음", "", null, null, Symbol: "\uE968")); return; } foreach (var nic in ifaces) { var ipProps = nic.GetIPProperties(); var ipv4s = ipProps.UnicastAddresses .Where(u => u.Address.AddressFamily == AddressFamily.InterNetwork) .ToList(); if (ipv4s.Count == 0) continue; if (!brief) items.Add(new LauncherItem($"── {nic.Name} ──", $"{nic.Description} ({nic.Speed / 1_000_000} Mbps)", null, null, Symbol: "\uE968")); foreach (var uni in ipv4s) { var mask = uni.IPv4Mask?.ToString() ?? ""; var cidr = MaskToCidr(uni.IPv4Mask); var label = brief ? uni.Address.ToString() : $"{uni.Address}/{cidr}"; items.Add(new LauncherItem(label, brief ? nic.Name : $"마스크: {mask} CIDR: /{cidr} ({ClassifyIp(uni.Address)})", null, ("copy", uni.Address.ToString()), Symbol: "\uE968")); } if (!brief) { var gateways = ipProps.GatewayAddresses .Where(g => g.Address.AddressFamily == AddressFamily.InterNetwork) .Select(g => g.Address.ToString()).ToList(); if (gateways.Count > 0) items.Add(new LauncherItem("게이트웨이", string.Join(", ", gateways), null, ("copy", gateways[0]), Symbol: "\uE968")); } } } catch (Exception ex) { items.Add(ErrorItem($"네트워크 정보 조회 오류: {ex.Message}")); } } private static void BuildIpInfoItems(List items, IPAddress ip) { var type = ClassifyIp(ip); var uval = IpToUint(ip); items.Add(new LauncherItem($"{ip} ({type})", $"클래스: {GetClass(uval)} · Enter 복사", null, ("copy", ip.ToString()), Symbol: "\uE968")); items.Add(CopyItem("IP 주소", ip.ToString())); items.Add(CopyItem("분류", type)); items.Add(CopyItem("클래스", GetClass(uval))); items.Add(CopyItem("이진", ToBinary(uval))); items.Add(CopyItem("16진수", $"0x{uval:X8}")); items.Add(CopyItem("정수", uval.ToString())); BuildConversionItems(items, ip); } private static void BuildConversionItems(List items, IPAddress ip) { var uval = IpToUint(ip); var bin = ToBinary(uval); items.Add(new LauncherItem("── 변환 ──", "", null, null, Symbol: "\uE968")); items.Add(CopyItem("이진 (32bit)", bin)); items.Add(CopyItem("이진 (점구분)", ToBinaryDotted(uval))); items.Add(CopyItem("16진수", $"0x{uval:X8}")); items.Add(CopyItem("정수 (uint32)", uval.ToString())); } private static void BuildIpv6Items(List items, IPAddress ip) { items.Add(new LauncherItem($"{ip}", "IPv6 주소", null, ("copy", ip.ToString()), Symbol: "\uE968")); items.Add(CopyItem("전체 표기", ip.ToString())); var expanded = ExpandIpv6(ip); if (expanded != ip.ToString()) items.Add(CopyItem("확장 표기", expanded)); } private static void BuildCidrItems(List items, string cidr) { var slash = cidr.IndexOf('/'); if (slash < 0) { items.Add(ErrorItem("올바른 CIDR 형식: 10.0.0.0/8")); return; } var ipStr = cidr[..slash]; var lenStr = cidr[(slash + 1)..]; if (!IPAddress.TryParse(ipStr, out var addr) || addr.AddressFamily != AddressFamily.InterNetwork || !int.TryParse(lenStr, out var prefix) || prefix < 0 || prefix > 32) { items.Add(ErrorItem("올바른 IPv4 CIDR을 입력하세요 (예: 192.168.1.0/24)")); return; } var mask = CidrToMask(prefix); var maskIp = UintToIp(mask); var wildcard = ~mask; var network = IpToUint(addr) & mask; var broadcast = network | wildcard; var hostCount = prefix < 31 ? (broadcast - network - 1) : (broadcast - network + 1); var firstHost = prefix < 31 ? network + 1 : network; var lastHost = prefix < 31 ? broadcast - 1 : broadcast; items.Add(new LauncherItem($"{cidr} → {IpToHostCount(prefix)}", $"네트워크: {UintToIp(network)} 브로드캐스트: {UintToIp(broadcast)}", null, null, Symbol: "\uE968")); items.Add(CopyItem("네트워크 주소", UintToIp(network))); items.Add(CopyItem("브로드캐스트 주소", UintToIp(broadcast))); items.Add(CopyItem("서브넷 마스크", maskIp.ToString())); items.Add(CopyItem("와일드카드 마스크", UintToIp(wildcard))); items.Add(CopyItem("첫 번째 호스트", UintToIp(firstHost))); items.Add(CopyItem("마지막 호스트", UintToIp(lastHost))); items.Add(CopyItem("호스트 수", $"{hostCount:N0}")); items.Add(CopyItem("CIDR 표기", $"{UintToIp(network)}/{prefix}")); items.Add(CopyItem("이진 마스크", ToBinaryDotted(mask))); } // ── 헬퍼 ──────────────────────────────────────────────────────────────── private static string ClassifyIp(IPAddress ip) { var u = IpToUint(ip); if (u == 0x7F000001u) return "루프백 (localhost)"; if ((u & 0xFF000000u) == 0x7F000000u) return "루프백"; if ((u & 0xFF000000u) == 0x0A000000u) return "사설 (Class A: 10.x.x.x)"; if ((u & 0xFFF00000u) == 0xAC100000u) return "사설 (Class B: 172.16-31.x.x)"; if ((u & 0xFFFF0000u) == 0xC0A80000u) return "사설 (Class C: 192.168.x.x)"; if ((u & 0xFFFF0000u) == 0xA9FE0000u) return "링크-로컬 (APIPA: 169.254.x.x)"; if ((u & 0xF0000000u) == 0xE0000000u) return "멀티캐스트 (224.x.x.x)"; if (u == 0xFFFFFFFFu) return "브로드캐스트"; if (u == 0u) return "0.0.0.0 (비지정)"; return "공인 IP"; } private static string GetClass(uint u) { var b = (u >> 24) & 0xFF; return b switch { <= 127 => "Class A", <= 191 => "Class B", <= 223 => "Class C", <= 239 => "Class D (멀티캐스트)", _ => "Class E (예약)" }; } private static uint IpToUint(IPAddress ip) { var bytes = ip.GetAddressBytes(); if (bytes.Length != 4) return 0; return (uint)((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]); } private static string UintToIp(uint u) => $"{(u >> 24) & 0xFF}.{(u >> 16) & 0xFF}.{(u >> 8) & 0xFF}.{u & 0xFF}"; private static string ToBinary(uint u) => Convert.ToString((long)u, 2).PadLeft(32, '0'); private static string ToBinaryDotted(uint u) { var b = ToBinary(u); return $"{b[..8]}.{b[8..16]}.{b[16..24]}.{b[24..]}"; } private static uint CidrToMask(int prefix) => prefix == 0 ? 0u : (0xFFFFFFFFu << (32 - prefix)); private static int MaskToCidr(IPAddress? mask) { if (mask == null) return 0; var u = IpToUint(mask); int c = 0; while ((u & 0x80000000u) != 0) { c++; u <<= 1; } return c; } private static string IpToHostCount(int prefix) => prefix switch { 32 => "1 IP (단일 호스트)", 31 => "2 IP (P2P 링크)", 30 => "4 IP (2 호스트)", _ => $"{(1L << (32 - prefix)):N0} IP ({(1L << (32 - prefix)) - 2:N0} 호스트)" }; private static string ExpandIpv6(IPAddress ip) { var bytes = ip.GetAddressBytes(); var groups = new string[8]; for (int i = 0; i < 8; i++) groups[i] = $"{bytes[i * 2]:X2}{bytes[i * 2 + 1]:X2}"; return string.Join(":", groups).ToLowerInvariant(); } private static LauncherItem CopyItem(string label, string value) => new(label, value, null, ("copy", value), Symbol: "\uE968"); private static LauncherItem ErrorItem(string msg) => new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783"); }