AX Commander 비교본 런처 기능 대량 이식
변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다. 핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다. 핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다. 문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다. 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
This commit is contained in:
264
src/AxCopilot/Handlers/DnsQueryHandler.cs
Normal file
264
src/AxCopilot/Handlers/DnsQueryHandler.cs
Normal file
@@ -0,0 +1,264 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L13-1: DNS 레코드 조회 핸들러. "dns" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: dns google.com → A/AAAA 레코드 조회
|
||||
/// dns google.com mx → MX 레코드
|
||||
/// dns google.com txt → TXT 레코드
|
||||
/// dns google.com ns → NS 레코드
|
||||
/// dns 8.8.8.8 → PTR(역방향) 조회
|
||||
/// Enter → 결과를 클립보드에 복사.
|
||||
///
|
||||
/// ⚠ 사내 모드: 내부 호스트만 조회 허용.
|
||||
/// </summary>
|
||||
public class DnsQueryHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "dns";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"DNS",
|
||||
"DNS 레코드 조회 — A · AAAA · MX · TXT · NS · PTR",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
items.Add(new LauncherItem("DNS 레코드 조회",
|
||||
"예: dns google.com / dns google.com mx / dns 8.8.8.8",
|
||||
null, null, Symbol: "\uE968"));
|
||||
items.Add(new LauncherItem("dns localhost", "로컬 A 레코드", null, null, Symbol: "\uE968"));
|
||||
items.Add(new LauncherItem("dns example.com mx", "MX 레코드", null, null, Symbol: "\uE968"));
|
||||
items.Add(new LauncherItem("dns example.com txt","TXT 레코드", null, null, Symbol: "\uE968"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var host = parts[0];
|
||||
var recType = parts.Length > 1 ? parts[1].ToUpperInvariant() : "A";
|
||||
|
||||
// 사내 모드 확인
|
||||
var settings = (System.Windows.Application.Current as App)?.SettingsService?.Settings;
|
||||
var isInternal = settings?.InternalModeEnabled ?? true;
|
||||
if (isInternal && !IsInternalHost(host))
|
||||
{
|
||||
items.Add(new LauncherItem(
|
||||
"사내 모드 제한",
|
||||
$"'{host}'은 외부 호스트입니다. 설정에서 사외 모드를 활성화하세요.",
|
||||
null, null, Symbol: "\uE783"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
items.Add(new LauncherItem(
|
||||
$"{host} [{recType}]",
|
||||
"Enter를 눌러 조회 실행",
|
||||
null,
|
||||
("query", $"{host}|{recType}"),
|
||||
Symbol: "\uE968"));
|
||||
|
||||
// 레코드 타입 빠른 선택 힌트
|
||||
if (parts.Length == 1)
|
||||
{
|
||||
foreach (var t in new[] { "A", "AAAA", "MX", "TXT", "NS" })
|
||||
items.Add(new LauncherItem($"dns {host} {t}", $"{t} 레코드 조회", null,
|
||||
("query", $"{host}|{t}"), Symbol: "\uE968"));
|
||||
}
|
||||
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
public async 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("DNS", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Data is not ("query", string queryData)) return;
|
||||
|
||||
var idx = queryData.IndexOf('|');
|
||||
var host = queryData[..idx];
|
||||
var recType = queryData[(idx + 1)..];
|
||||
|
||||
NotificationService.Notify("DNS", $"{host} [{recType}] 조회 중…");
|
||||
|
||||
try
|
||||
{
|
||||
var results = await QueryDnsAsync(host, recType, ct);
|
||||
if (results.Count == 0)
|
||||
{
|
||||
NotificationService.Notify("DNS", $"{host} [{recType}] — 레코드 없음");
|
||||
return;
|
||||
}
|
||||
|
||||
var summary = string.Join("\n", results);
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(summary));
|
||||
NotificationService.Notify("DNS", $"{results.Count}개 레코드 조회됨 · 클립보드 복사");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
NotificationService.Notify("DNS", "조회 취소됨");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
NotificationService.Notify("DNS", $"오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── DNS 조회 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static async Task<List<string>> QueryDnsAsync(string host, string type, CancellationToken ct)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
"A" or "AAAA" => await QueryAAsync(host, type, ct),
|
||||
"PTR" => await QueryPtrAsync(host, ct),
|
||||
_ => await QueryViaNslookupAsync(host, type, ct),
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<List<string>> QueryAAsync(string host, string type, CancellationToken ct)
|
||||
{
|
||||
var family = type == "AAAA" ? AddressFamily.InterNetworkV6 : AddressFamily.InterNetwork;
|
||||
var addrs = await Dns.GetHostAddressesAsync(host, family, ct);
|
||||
return addrs.Select(a => a.ToString()).ToList();
|
||||
}
|
||||
|
||||
private static async Task<List<string>> QueryPtrAsync(string ip, CancellationToken ct)
|
||||
{
|
||||
if (!IPAddress.TryParse(ip, out _))
|
||||
return [$"'{ip}'은 유효한 IP 주소가 아닙니다"];
|
||||
try
|
||||
{
|
||||
var entry = await Dns.GetHostEntryAsync(ip, ct);
|
||||
return [entry.HostName];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [$"PTR 레코드 없음: {ip}"];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>MX/TXT/NS/CNAME: nslookup 프로세스 실행으로 조회</summary>
|
||||
private static async Task<List<string>> QueryViaNslookupAsync(string host, string type, CancellationToken ct)
|
||||
{
|
||||
var psi = new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = "nslookup",
|
||||
Arguments = $"-type={type} {host}",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
};
|
||||
|
||||
using var proc = new System.Diagnostics.Process { StartInfo = psi };
|
||||
proc.Start();
|
||||
|
||||
var stdout = await proc.StandardOutput.ReadToEndAsync(ct);
|
||||
await proc.WaitForExitAsync(ct);
|
||||
|
||||
return ParseNslookupOutput(stdout, type);
|
||||
}
|
||||
|
||||
private static List<string> ParseNslookupOutput(string output, string type)
|
||||
{
|
||||
var results = new List<string>();
|
||||
var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// 서버 응답 헤더 건너뜀 (첫 2줄)
|
||||
var skip = true;
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (skip)
|
||||
{
|
||||
if (trimmed.StartsWith("Non-authoritative", StringComparison.OrdinalIgnoreCase) ||
|
||||
trimmed.StartsWith("Name:", StringComparison.OrdinalIgnoreCase) ||
|
||||
(type == "MX" && trimmed.Contains("mail exchanger")) ||
|
||||
(type == "TXT" && trimmed.Contains("text =")) ||
|
||||
(type == "NS" && trimmed.Contains("nameserver")))
|
||||
skip = false;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(trimmed)) continue;
|
||||
|
||||
// MX: "... mail exchanger = 10 aspmx.l.google.com"
|
||||
if (type == "MX" && trimmed.Contains("mail exchanger"))
|
||||
{
|
||||
var idx = trimmed.IndexOf('=');
|
||||
if (idx >= 0) results.Add(trimmed[(idx + 1)..].Trim());
|
||||
continue;
|
||||
}
|
||||
|
||||
// TXT: "... text = "v=spf1 …""
|
||||
if (type == "TXT" && trimmed.Contains("text ="))
|
||||
{
|
||||
var idx = trimmed.IndexOf("text =", StringComparison.OrdinalIgnoreCase);
|
||||
if (idx >= 0) results.Add(trimmed[(idx + 6)..].Trim().Trim('"'));
|
||||
continue;
|
||||
}
|
||||
|
||||
// NS: "nameserver = ns1.google.com"
|
||||
if (type == "NS" && trimmed.Contains("nameserver"))
|
||||
{
|
||||
var idx = trimmed.IndexOf('=');
|
||||
if (idx >= 0) results.Add(trimmed[(idx + 1)..].Trim());
|
||||
continue;
|
||||
}
|
||||
|
||||
// CNAME: "canonical name = …"
|
||||
if (type == "CNAME" && trimmed.Contains("canonical name"))
|
||||
{
|
||||
var idx = trimmed.IndexOf('=');
|
||||
if (idx >= 0) results.Add(trimmed[(idx + 1)..].Trim());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Address: 주소 행
|
||||
if (trimmed.StartsWith("Address:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var idx = trimmed.IndexOf(':');
|
||||
if (idx >= 0) results.Add(trimmed[(idx + 1)..].Trim());
|
||||
}
|
||||
}
|
||||
|
||||
return results.Count > 0 ? results : [$"조회 결과 없음 ({type})"];
|
||||
}
|
||||
|
||||
private static bool IsInternalHost(string host)
|
||||
{
|
||||
if (host is "localhost" or "127.0.0.1") return true;
|
||||
if (IPAddress.TryParse(host, out var addr))
|
||||
{
|
||||
var s = addr.ToString();
|
||||
return s.StartsWith("192.168.") || s.StartsWith("10.") ||
|
||||
System.Text.RegularExpressions.Regex.IsMatch(s,
|
||||
@"^172\.(1[6-9]|2\d|3[01])\.");
|
||||
}
|
||||
// 도메인 이름은 사내 모드에서 외부로 간주
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user