using System.Text;
using System.Text.Json;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
///
/// L11-2: JWT 토큰 디코더 핸들러. "jwt" 프리픽스로 사용합니다.
///
/// 예: jwt → 클립보드의 JWT 자동 분석
/// jwt eyJhbGci... → 토큰 직접 입력
/// jwt header → 클립보드 JWT 헤더만 표시
/// jwt payload → 클립보드 JWT 페이로드만 표시
/// Enter → 결과를 클립보드에 복사.
/// 주의: 서명(signature) 검증은 수행하지 않음 — 분석 전용.
///
public class JwtHandler : IActionHandler
{
public string? Prefix => "jwt";
public PluginMetadata Metadata => new(
"JWT",
"JWT 토큰 디코더 — 헤더 · 페이로드 · 만료일 분석",
"1.0",
"AX");
public Task> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List();
if (string.IsNullOrWhiteSpace(q))
{
var clip = GetClipboard();
if (LooksJwt(clip))
{
items.AddRange(DecodeJwt(clip, "all"));
}
else
{
items.Add(new LauncherItem("JWT 디코더",
"JWT 토큰을 클립보드에 복사하거나 직접 입력하세요",
null, null, Symbol: "\uE72E"));
items.Add(new LauncherItem("jwt eyJ…", "토큰 직접 입력", null, null, Symbol: "\uE72E"));
items.Add(new LauncherItem("jwt header", "헤더만 표시", null, null, Symbol: "\uE72E"));
items.Add(new LauncherItem("jwt payload", "페이로드만 표시", null, null, Symbol: "\uE72E"));
}
return Task.FromResult>(items);
}
var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
var sub = parts[0].ToLowerInvariant();
switch (sub)
{
case "header":
{
var src = GetTokenSource(parts);
items.AddRange(DecodeJwt(src, "header"));
break;
}
case "payload":
case "claims":
case "body":
{
var src = GetTokenSource(parts);
items.AddRange(DecodeJwt(src, "payload"));
break;
}
default:
{
// 토큰 자체 입력 (eyJ로 시작)
var token = LooksJwt(q) ? q : GetClipboard();
if (!LooksJwt(token))
{
items.Add(new LauncherItem("JWT 형식 아님",
"eyJ…로 시작하는 JWT 토큰을 입력하거나 클립보드에 복사하세요",
null, null, Symbol: "\uE783"));
}
else
{
items.AddRange(DecodeJwt(token, "all"));
}
break;
}
}
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("JWT", "클립보드에 복사했습니다.");
}
catch { /* 비핵심 */ }
}
return Task.CompletedTask;
}
// ── JWT 디코딩 ────────────────────────────────────────────────────────────
private static IEnumerable DecodeJwt(string token, string mode)
{
if (!LooksJwt(token))
{
yield return new LauncherItem("JWT 없음",
"클립보드에 eyJ…로 시작하는 JWT가 없습니다", null, null, Symbol: "\uE783");
yield break;
}
var parts = token.Split('.');
if (parts.Length < 2)
{
yield return new LauncherItem("형식 오류", "JWT는 최소 2개의 점(.)으로 구성됩니다", null, null, Symbol: "\uE783");
yield break;
}
// 헤더 디코딩
if (mode is "header" or "all")
{
var headerJson = TryDecodeBase64Url(parts[0]);
if (headerJson != null)
{
var pretty = TryPrettyJson(headerJson) ?? headerJson;
yield return new LauncherItem("─ 헤더 ─", "", null, null, Symbol: "\uE72E");
foreach (var item in ExtractJsonFields(headerJson, "헤더"))
yield return item;
yield return new LauncherItem("헤더 JSON", "전체 복사", null, ("copy", pretty), Symbol: "\uE72E");
}
}
// 페이로드 디코딩
if (mode is "payload" or "all")
{
if (parts.Length < 2)
{
yield return new LauncherItem("페이로드 없음", "JWT에 페이로드가 없습니다", null, null, Symbol: "\uE783");
yield break;
}
var payloadJson = TryDecodeBase64Url(parts[1]);
if (payloadJson != null)
{
yield return new LauncherItem("─ 페이로드 ─", "", null, null, Symbol: "\uE72E");
// 만료일(exp) 특별 처리
var expItem = ExtractExpiry(payloadJson);
if (expItem != null) yield return expItem;
foreach (var item in ExtractJsonFields(payloadJson, "페이로드"))
yield return item;
var pretty = TryPrettyJson(payloadJson) ?? payloadJson;
yield return new LauncherItem("페이로드 JSON", "전체 복사", null, ("copy", pretty), Symbol: "\uE72E");
}
}
// 서명 유무
if (mode == "all")
{
var hasSig = parts.Length >= 3 && !string.IsNullOrEmpty(parts[2]);
yield return new LauncherItem(
"서명",
hasSig ? "있음 (검증 미지원 — 분석 전용)" : "없음 (alg:none)",
null, null, Symbol: "\uE72E");
}
}
private static IEnumerable ExtractJsonFields(string json, string section)
{
JsonDocument doc;
try { doc = JsonDocument.Parse(json); }
catch { yield break; }
using (doc)
{
foreach (var prop in doc.RootElement.EnumerateObject())
{
var val = prop.Value.ValueKind switch
{
JsonValueKind.String => prop.Value.GetString() ?? "",
JsonValueKind.Number => prop.Value.GetRawText(),
JsonValueKind.True => "true",
JsonValueKind.False => "false",
JsonValueKind.Null => "null",
_ => prop.Value.GetRawText(),
};
// exp, iat, nbf 는 타임스탬프 → 날짜 변환해서 별도 표시
if (prop.Name is "exp" or "iat" or "nbf")
{
if (long.TryParse(val, out var ts))
{
var dt = DateTimeOffset.FromUnixTimeSeconds(ts).ToLocalTime();
var label = prop.Name switch { "exp" => "만료(exp)", "iat" => "발급(iat)", _ => "유효 시작(nbf)" };
yield return new LauncherItem(label, dt.ToString("yyyy-MM-dd HH:mm:ss"), null, ("copy", dt.ToString("o")), Symbol: "\uE72E");
continue;
}
}
var display = val.Length > 60 ? val[..60] + "…" : val;
yield return new LauncherItem(prop.Name, display, null, ("copy", val), Symbol: "\uE72E");
}
}
}
private static LauncherItem? ExtractExpiry(string payloadJson)
{
try
{
var doc = JsonDocument.Parse(payloadJson);
if (!doc.RootElement.TryGetProperty("exp", out var expProp)) return null;
if (!expProp.TryGetInt64(out var exp)) return null;
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var dt = DateTimeOffset.FromUnixTimeSeconds(exp).ToLocalTime();
var remain = exp - now;
string status;
if (remain < 0)
status = $"만료됨 ({Math.Abs(remain / 60)}분 전)";
else if (remain < 60)
status = $"곧 만료 ({remain}초 남음)";
else if (remain < 3600)
status = $"유효 ({remain / 60}분 남음)";
else if (remain < 86400)
status = $"유효 ({remain / 3600}시간 남음)";
else
status = $"유효 ({remain / 86400}일 남음)";
return new LauncherItem(
$"만료 상태: {status}",
dt.ToString("yyyy-MM-dd HH:mm:ss"),
null, null, Symbol: remain < 0 ? "\uE783" : "\uE73E");
}
catch { return null; }
}
// ── 헬퍼 ─────────────────────────────────────────────────────────────────
private static string? TryDecodeBase64Url(string input)
{
try
{
// Base64Url → Base64
var base64 = input.Replace('-', '+').Replace('_', '/');
var pad = (4 - base64.Length % 4) % 4;
base64 += new string('=', pad);
var bytes = Convert.FromBase64String(base64);
return Encoding.UTF8.GetString(bytes);
}
catch { return null; }
}
private static string? TryPrettyJson(string json)
{
try
{
var doc = JsonDocument.Parse(json);
return JsonSerializer.Serialize(doc.RootElement,
new JsonSerializerOptions { WriteIndented = true });
}
catch { return null; }
}
private static bool LooksJwt(string? s)
{
if (string.IsNullOrWhiteSpace(s)) return false;
s = s.Trim();
return s.StartsWith("eyJ", StringComparison.Ordinal) && s.Contains('.');
}
private static string GetTokenSource(string[] parts)
{
// parts[1]에 토큰이 있으면 사용, 없으면 클립보드
if (parts.Length > 1 && LooksJwt(parts[1])) return parts[1];
return GetClipboard();
}
private static string GetClipboard()
{
try
{
return System.Windows.Application.Current.Dispatcher.Invoke(
() => Clipboard.ContainsText() ? Clipboard.GetText().Trim() : "");
}
catch { return ""; }
}
}