using System.Diagnostics; using System.Net.Http; using System.Text; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L12-1: HTTP 요청 테스터 핸들러. "http" 프리픽스로 사용합니다. /// /// 예: http example.com → GET 요청 (http:// 자동 추가) /// http https://api.example → GET 요청 + 응답 코드·시간 /// http head https://example → HEAD 요청 /// http post https://example → POST (빈 바디) /// http 192.168.1.1 → 내부 IP GET /// Enter → 응답 요약을 클립보드에 복사. /// /// ⚠ 외부 URL: 사내 모드 차단. 내부 IP(10./192.168./172.16-31.)는 허용. /// public class HttpTesterHandler : IActionHandler { public string? Prefix => "http"; public PluginMetadata Metadata => new( "HTTP", "HTTP 요청 테스터 — GET · HEAD · POST · 응답 코드", "1.0", "AX"); private static readonly HttpClient _client = new(new HttpClientHandler { AllowAutoRedirect = true, MaxAutomaticRedirections = 3, ServerCertificateCustomValidationCallback = (_, _, _, _) => true, }) { Timeout = TimeSpan.FromSeconds(10), }; public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem("HTTP 요청 테스터", "예: http example.com / http head https://api / http post https://url", null, null, Symbol: "\uE774")); items.Add(new LauncherItem("http localhost", "로컬 서버 GET", null, null, Symbol: "\uE774")); items.Add(new LauncherItem("http 192.168.1.1", "내부 IP GET", null, null, Symbol: "\uE774")); items.Add(new LauncherItem("http head https://…", "HEAD 요청", null, null, Symbol: "\uE774")); items.Add(new LauncherItem("http post https://…", "POST 요청", null, null, Symbol: "\uE774")); return Task.FromResult>(items); } var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); string method, url; if (parts[0].ToUpperInvariant() is "GET" or "HEAD" or "POST" or "PUT" or "DELETE" or "OPTIONS") { method = parts[0].ToUpperInvariant(); url = parts.Length > 1 ? parts[1].Trim() : ""; } else { method = "GET"; url = q; } if (string.IsNullOrWhiteSpace(url)) { items.Add(new LauncherItem("URL을 입력하세요", $"예: http {method.ToLower()} https://example.com", null, null, Symbol: "\uE783")); return Task.FromResult>(items); } // 스키마 자동 추가 if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) && !url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { url = "http://" + url; } // 사내 모드 확인 var settings = (System.Windows.Application.Current as App)?.SettingsService?.Settings; var isInternal = settings?.InternalModeEnabled ?? true; if (isInternal && !IsInternalUrl(url)) { items.Add(new LauncherItem( "사내 모드 제한", $"외부 URL '{url}'은 차단됩니다. 설정에서 사외 모드를 활성화하세요.", null, null, Symbol: "\uE783")); return Task.FromResult>(items); } items.Add(new LauncherItem( $"{method} {TruncateUrl(url)}", "Enter를 눌러 요청 실행", null, ("request", $"{method}|{url}"), Symbol: "\uE774")); return Task.FromResult>(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("HTTP", "클립보드에 복사했습니다."); } catch { /* 비핵심 */ } return; } if (item.Data is not ("request", string reqData)) return; var idx = reqData.IndexOf('|'); var method = reqData[..idx]; var url = reqData[(idx + 1)..]; NotificationService.Notify("HTTP", $"{method} {TruncateUrl(url)} 요청 중…"); try { var sw = Stopwatch.StartNew(); HttpResponseMessage resp; using var reqMsg = new HttpRequestMessage(new HttpMethod(method), url); reqMsg.Headers.TryAddWithoutValidation("User-Agent", "AX-Copilot/2.0 HTTP-Tester"); resp = await _client.SendAsync(reqMsg, HttpCompletionOption.ResponseHeadersRead, ct); sw.Stop(); var sb = new StringBuilder(); sb.AppendLine($"URL: {url}"); sb.AppendLine($"메서드: {method}"); sb.AppendLine($"상태 코드: {(int)resp.StatusCode} {resp.ReasonPhrase}"); sb.AppendLine($"응답 시간: {sw.ElapsedMilliseconds}ms"); sb.AppendLine($"Content-Type: {resp.Content.Headers.ContentType}"); sb.AppendLine($"Content-Length: {resp.Content.Headers.ContentLength?.ToString() ?? "unknown"}"); // 주요 헤더 foreach (var h in new[] { "Server", "X-Powered-By", "Cache-Control", "ETag", "Last-Modified" }) if (resp.Headers.TryGetValues(h, out var vals)) sb.AppendLine($"{h}: {string.Join(", ", vals)}"); var summary = sb.ToString().TrimEnd(); System.Windows.Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(summary)); var status = (int)resp.StatusCode; var emoji = status < 300 ? "✔" : status < 400 ? "↪" : "✘"; NotificationService.Notify("HTTP", $"{emoji} {status} {resp.ReasonPhrase} ({sw.ElapsedMilliseconds}ms) · 결과 복사됨"); } catch (TaskCanceledException) { NotificationService.Notify("HTTP", "요청 타임아웃 (10초 초과)"); } catch (HttpRequestException ex) { NotificationService.Notify("HTTP", $"요청 오류: {ex.Message}"); } catch (Exception ex) { NotificationService.Notify("HTTP", $"오류: {ex.Message}"); } } // ── 헬퍼 ───────────────────────────────────────────────────────────────── private static bool IsInternalUrl(string url) { var lower = url.ToLowerInvariant(); return lower.Contains("://localhost") || lower.Contains("://127.0.0.1") || lower.Contains("://192.168.") || lower.Contains("://10.") || System.Text.RegularExpressions.Regex.IsMatch(lower, @"://172\.(1[6-9]|2\d|3[01])\."); } private static string TruncateUrl(string url) => url.Length > 60 ? url[..60] + "…" : url; }