using System.Net; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L9-2: IP 서브넷 계산기 핸들러. "subnet" 프리픽스로 사용합니다. /// /// 예: subnet 192.168.1.0/24 → 네트워크 정보 전체 표시 /// subnet 10.0.0.5/24 → 해당 IP가 속한 서브넷 분석 /// subnet 255.255.255.0 → 서브넷 마스크 → CIDR 변환 /// subnet 192.168.1.0 24 → 슬래시 없이 입력 가능 /// subnet range 192.168.1.10-50 → IP 범위 정보 /// Enter → 결과를 클립보드에 복사. /// public class SubnetHandler : IActionHandler { public string? Prefix => "subnet"; public PluginMetadata Metadata => new( "Subnet", "IP 서브넷 계산기 — CIDR · 네트워크 · 호스트 범위", "1.0", "AX"); // 자주 쓰는 CIDR 참조표 private static readonly (int Prefix, int Hosts, string Desc)[] CidrRef = [ (24, 254, "/24 — 클래스 C (254 호스트)"), (25, 126, "/25 — 128개 분할 (126 호스트)"), (26, 62, "/26 — 64개 분할 (62 호스트)"), (27, 30, "/27 — 32개 분할 (30 호스트)"), (28, 14, "/28 — 16개 분할 (14 호스트)"), (29, 6, "/29 — 8개 분할 (6 호스트)"), (30, 2, "/30 — 포인트-투-포인트 (2 호스트)"), (23, 510, "/23 — 512개 블록 (510 호스트)"), (22, 1022, "/22 — 1024개 블록 (1022 호스트)"), (20, 4094, "/20 — 4096개 블록 (4094 호스트)"), (16, 65534, "/16 — 클래스 B (65534 호스트)"), (8, 16777214, "/8 — 클래스 A (16M 호스트)"), ]; public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem( "서브넷 계산기", "예: subnet 192.168.1.0/24 또는 subnet 10.0.0.5 24", null, null, Symbol: "\uE968")); // CIDR 참조표 일부 foreach (var (cidrLen, hosts, desc) in CidrRef.Take(6)) { items.Add(new LauncherItem( desc, $"호스트: {hosts:N0}개", null, ("copy", desc), Symbol: "\uE968")); } return Task.FromResult>(items); } // "range" 서브커맨드 if (q.StartsWith("range ", StringComparison.OrdinalIgnoreCase)) { var rangeStr = q[6..].Trim(); items.AddRange(BuildRangeItems(rangeStr)); return Task.FromResult>(items); } // 서브넷 마스크 입력 (예: 255.255.255.0) if (TryParseMask(q, out var cidrFromMask)) { items.Add(new LauncherItem( $"CIDR: /{cidrFromMask}", $"서브넷 마스크 {q} = /{cidrFromMask}", null, ("copy", $"/{cidrFromMask}"), Symbol: "\uE968")); return Task.FromResult>(items); } // CIDR 파싱: "IP/prefix" 또는 "IP prefix" if (!TryParseCidr(q, out var ip, out var prefix)) { items.Add(new LauncherItem("형식 오류", "예: 192.168.1.0/24", null, null, Symbol: "\uE783")); return Task.FromResult>(items); } items.AddRange(BuildSubnetItems(ip, prefix)); 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("Subnet", "클립보드에 복사했습니다."); } catch { /* 비핵심 */ } } return Task.CompletedTask; } // ── 서브넷 계산 ────────────────────────────────────────────────────────── private static IEnumerable BuildSubnetItems(uint ip, int prefix) { var mask = prefix == 0 ? 0u : ~0u << (32 - prefix); var network = ip & mask; var broadcast = network | ~mask; var firstHost = prefix < 31 ? network + 1 : network; var lastHost = prefix < 31 ? broadcast - 1 : broadcast; var hostCount = prefix >= 31 ? (1u << (32 - prefix)) : (broadcast - network - 1); var summaryText = $""" 네트워크: {ToIp(network)}/{prefix} 서브넷마스크: {ToIp(mask)} 첫 호스트: {ToIp(firstHost)} 마지막 호스트: {ToIp(lastHost)} 브로드캐스트: {ToIp(broadcast)} 사용 가능 호스트: {hostCount:N0}개 """; yield return new LauncherItem( $"{ToIp(network)}/{prefix}", $"호스트 {hostCount:N0}개 · 전체 복사: Enter", null, ("copy", summaryText), Symbol: "\uE968"); yield return new LauncherItem("서브넷 마스크", ToIp(mask), null, ("copy", ToIp(mask)), Symbol: "\uE968"); yield return new LauncherItem("네트워크 주소", ToIp(network), null, ("copy", ToIp(network)), Symbol: "\uE968"); yield return new LauncherItem("브로드캐스트", ToIp(broadcast), null, ("copy", ToIp(broadcast)), Symbol: "\uE968"); yield return new LauncherItem("첫 호스트", ToIp(firstHost), null, ("copy", ToIp(firstHost)), Symbol: "\uE968"); yield return new LauncherItem("마지막 호스트", ToIp(lastHost), null, ("copy", ToIp(lastHost)), Symbol: "\uE968"); yield return new LauncherItem( "사용 가능 호스트", $"{hostCount:N0}개", null, ("copy", hostCount.ToString()), Symbol: "\uE968"); // 입력 IP가 이 서브넷에 속하는지 확인 if ((ip & mask) == network && ip != network && ip != broadcast) { yield return new LauncherItem( $"입력 IP {ToIp(ip)}", "이 서브넷에 속함 ✓", null, null, Symbol: "\uE73E"); } // 이진 표현 yield return new LauncherItem( "이진 마스크", ToBinary(mask), null, ("copy", ToBinary(mask)), Symbol: "\uE8C4"); } private static IEnumerable BuildRangeItems(string rangeStr) { // "192.168.1.10-50" 또는 "192.168.1.10-192.168.1.50" var parts = rangeStr.Split('-', 2); if (parts.Length != 2) { yield return new LauncherItem("형식 오류", "예: 192.168.1.10-50", null, null, Symbol: "\uE783"); yield break; } if (!TryParseIp(parts[0].Trim(), out var startIp)) { yield return new LauncherItem("IP 형식 오류", parts[0], null, null, Symbol: "\uE783"); yield break; } uint endIp; if (uint.TryParse(parts[1].Trim(), out var lastOctet)) { endIp = (startIp & 0xFFFFFF00) | lastOctet; } else if (!TryParseIp(parts[1].Trim(), out endIp)) { yield return new LauncherItem("IP 형식 오류", parts[1], null, null, Symbol: "\uE783"); yield break; } if (endIp < startIp) { yield return new LauncherItem("오류", "끝 IP가 시작 IP보다 작습니다", null, null, Symbol: "\uE783"); yield break; } var count = endIp - startIp + 1; yield return new LauncherItem( $"{ToIp(startIp)} — {ToIp(endIp)}", $"{count}개 IP", null, ("copy", $"{ToIp(startIp)} - {ToIp(endIp)} ({count}개)"), Symbol: "\uE968"); yield return new LauncherItem("시작 IP", ToIp(startIp), null, ("copy", ToIp(startIp)), Symbol: "\uE968"); yield return new LauncherItem("끝 IP", ToIp(endIp), null, ("copy", ToIp(endIp)), Symbol: "\uE968"); yield return new LauncherItem("IP 수", $"{count:N0}개", null, ("copy", count.ToString()), Symbol: "\uE968"); } // ── 파싱 헬퍼 ──────────────────────────────────────────────────────────── private static bool TryParseCidr(string s, out uint ip, out int prefix) { ip = 0; prefix = 24; // "IP/prefix" 형식 var slashIdx = s.IndexOf('/'); if (slashIdx >= 0) { if (!TryParseIp(s[..slashIdx].Trim(), out ip)) return false; if (!int.TryParse(s[(slashIdx + 1)..].Trim(), out prefix)) return false; prefix = Math.Clamp(prefix, 0, 32); return true; } // "IP prefix" 형식 (공백 구분) var spaceIdx = s.LastIndexOf(' '); if (spaceIdx >= 0 && int.TryParse(s[(spaceIdx + 1)..].Trim(), out var p)) { prefix = Math.Clamp(p, 0, 32); return TryParseIp(s[..spaceIdx].Trim(), out ip); } // IP만 입력 → /24 기본 return TryParseIp(s.Trim(), out ip); } private static bool TryParseIp(string s, out uint ip) { ip = 0; if (!IPAddress.TryParse(s, out var addr)) return false; if (addr.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) return false; var bytes = addr.GetAddressBytes(); ip = ((uint)bytes[0] << 24) | ((uint)bytes[1] << 16) | ((uint)bytes[2] << 8) | (uint)bytes[3]; return true; } private static bool TryParseMask(string s, out int prefix) { prefix = 0; if (!TryParseIp(s, out var mask)) return false; if (mask == 0) { prefix = 0; return true; } // 유효한 서브넷 마스크인지 확인 (연속된 1 뒤에 0) var inverted = ~mask; if ((inverted & (inverted + 1)) != 0) return false; prefix = 0; var tmp = mask; while ((tmp & 0x80000000) != 0) { prefix++; tmp <<= 1; } return true; } private static string ToIp(uint ip) => $"{ip >> 24}.{(ip >> 16) & 0xFF}.{(ip >> 8) & 0xFF}.{ip & 0xFF}"; private static string ToBinary(uint val) => $"{Convert.ToString((int)(val >> 16), 2).PadLeft(16, '0')} {Convert.ToString((int)(val & 0xFFFF), 2).PadLeft(16, '0')}"; }