From 5fb255e1e3e2a999dacdb4c820b8c96173d59bb1 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Thu, 2 Oct 2025 14:39:50 +0900 Subject: [PATCH 1/2] Refactor IP query to support multiple providers Replaces the single CriminalIP API integration with a new IP query system supporting both CriminalIP and AbuseIPDB providers. Updates configuration keys, resource files, and the editor UI to handle multiple API endpoints and keys. Refactors backend logic to aggregate and return results from both providers in a unified XML format, and updates the frontend to parse and display these results. Adds improved error handling and logging for IP query and WHOIS operations. --- .../Properties/Resources.Designer.cs | 54 ++++--- .../Properties/Resources.resx | 10 +- .../ResourceTools/Completion.cs | 18 +-- .../ResourceTools/IpQuery.cs | 138 +++++++++++++++--- .../WelsonJS.Launcher/ResourceTools/Whois.cs | 2 + .../WelsonJS.Launcher.csproj | 1 + WelsonJS.Toolkit/WelsonJS.Launcher/app.config | 6 +- .../WelsonJS.Launcher/editor.html | 87 +++++------ 8 files changed, 218 insertions(+), 98 deletions(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs index a63df84..51db986 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs @@ -150,24 +150,6 @@ namespace WelsonJS.Launcher.Properties { } } - /// - /// 과(와) 유사한 지역화된 문자열을 찾습니다. - /// - internal static string CriminalIpApiKey { - get { - return ResourceManager.GetString("CriminalIpApiKey", resourceCulture); - } - } - - /// - /// https://api.criminalip.io/v1/과(와) 유사한 지역화된 문자열을 찾습니다. - /// - internal static string CriminalIpApiPrefix { - get { - return ResourceManager.GetString("CriminalIpApiPrefix", resourceCulture); - } - } - /// /// yyyy-MM-dd HH:mm:ss과(와) 유사한 지역화된 문자열을 찾습니다. /// @@ -295,6 +277,42 @@ namespace WelsonJS.Launcher.Properties { } } + /// + /// 과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string IpQueryApiKey { + get { + return ResourceManager.GetString("IpQueryApiKey", resourceCulture); + } + } + + /// + /// 과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string IpQueryApiKey2 { + get { + return ResourceManager.GetString("IpQueryApiKey2", resourceCulture); + } + } + + /// + /// https://api.criminalip.io/v1/과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string IpQueryApiPrefix { + get { + return ResourceManager.GetString("IpQueryApiPrefix", resourceCulture); + } + } + + /// + /// https://api.abuseipdb.com/api/v2/과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string IpQueryApiPrefix2 { + get { + return ResourceManager.GetString("IpQueryApiPrefix2", resourceCulture); + } + } + /// /// false과(와) 유사한 지역화된 문자열을 찾습니다. /// diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx index 36cdf34..b733285 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx @@ -190,10 +190,10 @@ 90 - + - + https://api.criminalip.io/v1/ @@ -214,4 +214,10 @@ true + + + + + https://api.abuseipdb.com/api/v2/ + \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs index ea11b3b..45d08fb 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Completion.cs @@ -6,7 +6,6 @@ using Microsoft.Win32; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Net; using System.Linq; @@ -45,8 +44,6 @@ namespace WelsonJS.Launcher.ResourceTools public async Task HandleAsync(HttpListenerContext context, string path) { - await Task.Delay(0); - string word = path.Substring(Prefix.Length); try @@ -121,10 +118,7 @@ namespace WelsonJS.Launcher.ResourceTools .Select(p => p.Trim()) .Where(p => !string.IsNullOrEmpty(p)); - foreach (string path in paths) - { - SearchAllExecutables(path, SearchOption.TopDirectoryOnly); - } + paths.ToList().ForEach(x => SearchAllExecutables(x, SearchOption.TopDirectoryOnly)); } private void DiscoverFromProgramDirectories() @@ -153,10 +147,7 @@ namespace WelsonJS.Launcher.ResourceTools Path.Combine(userProfile, "scoop", "apps") }; - foreach (string path in paths) - { - SearchAllExecutables(path); - } + paths.ToList().ForEach(x => SearchAllExecutables(x)); } private void SearchAllExecutables(string path, SearchOption searchOption = SearchOption.AllDirectories, int maxFiles = 1000) @@ -184,10 +175,7 @@ namespace WelsonJS.Launcher.ResourceTools private void AddDiscoveredExecutables(List executableFiles) { - foreach (var executableFile in executableFiles) - { - DiscoveredExecutables.Add(executableFile); - } + executableFiles.ForEach(x => DiscoveredExecutables.Add(x)); } private async Task SafeDiscoverAsync(Action discoveryMethod) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs index 42475c2..4976be3 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs @@ -1,12 +1,28 @@ -// CitiQuery.cs +// IpQuery.cs // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors // https://github.com/gnh1201/welsonjs -// +// +// PLEASE NOTE: +// Requires joining the IP Query API providers to provide IP information. +// WelsonJS has no affiliation with any IP Query API providers. +// +// Providers: +// 1) CriminalIP -> IpQueryApiPrefix, IpQueryApiKey +// 2) AbuseIPDB -> IpQueryApiPrefix2, IpQueryApiKey2 +// +// XML response structure: +// +// {"...json..."} +// {"...json..."} +// +// using System; -using System.Net.Http; using System.Net; +using System.Net.Http; using System.Threading.Tasks; +using System.Web; +using System.Xml.Linq; namespace WelsonJS.Launcher.ResourceTools { @@ -29,37 +45,121 @@ namespace WelsonJS.Launcher.ResourceTools { return path.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase); } - public async Task HandleAsync(HttpListenerContext context, string path) { try { string target = path.Substring(Prefix.Length).Trim(); - string apiKey = Program.GetAppConfig("CriminalIpApiKey"); - if (string.IsNullOrEmpty(apiKey)) + if (string.IsNullOrWhiteSpace(target)) { - await Server.ServeResource(context, "Missing API key", "application/xml", 500); + await Server.ServeResource(context, "Missing IP target", "application/xml", 400); return; } - string encoded = Uri.EscapeDataString(target); - string apiPrefix = Program.GetAppConfig("CriminalIpApiPrefix"); - string url = $"{apiPrefix}asset/ip/report?ip={encoded}&full=true"; + string crimPrefix = Program.GetAppConfig("IpQueryApiPrefix"); + string crimKey = Program.GetAppConfig("IpQueryApiKey"); - var request = new HttpRequestMessage(HttpMethod.Get, url); - request.Headers.Add("x-api-key", apiKey); - request.Headers.Add("User-Agent", context.Request.UserAgent); + string abusePrefix = Program.GetAppConfig("IpQueryApiPrefix2"); + string abuseKey = Program.GetAppConfig("IpQueryApiKey2"); - HttpResponseMessage response = await _httpClient.SendAsync(request); - string content = await response.Content.ReadAsStringAsync(); + var root = new XElement("result", new XAttribute("target", target)); - context.Response.StatusCode = (int)response.StatusCode; - await Server.ServeResource(context, content, "application/json", (int)response.StatusCode); + var p1 = QueryProviderAsync(context, target, "criminalip", crimPrefix, crimKey); + var p2 = QueryProviderAsync(context, target, "abuseipdb", abusePrefix, abuseKey); + + await Task.WhenAll(p1, p2); + + root.Add(p1.Result); + root.Add(p2.Result); + + bool anySuccess = + (int.TryParse((string)p1.Result.Attribute("status"), out var s1) && s1 >= 200 && s1 < 300) || + (int.TryParse((string)p2.Result.Attribute("status"), out var s2) && s2 >= 200 && s2 < 300); + + int httpCode = anySuccess ? 200 : 502; + + context.Response.StatusCode = httpCode; + await Server.ServeResource(context, root.ToString(), "application/xml", httpCode); } catch (Exception ex) { - await Server.ServeResource(context, $"{ex.Message}", "application/xml", 500); + _logger.Error("Error processing IP query request: " + ex.Message); + await Server.ServeResource(context, "" + WebUtility.HtmlEncode(ex.Message) + "", "application/xml", 500); } } + + private async Task QueryProviderAsync(HttpListenerContext ctx, string ip, string provider, string prefix, string key) + { + var node = new XElement("response", new XAttribute("provider", provider)); + + if (string.IsNullOrWhiteSpace(prefix) || string.IsNullOrWhiteSpace(key)) + { + node.Add(new XAttribute("status", 500)); + node.Add(new XElement("error", "Missing configuration for " + provider)); + return node; + } + + try + { + HttpRequestMessage req = BuildProviderRequest(ctx, ip, provider, prefix, key); + try + { + using (HttpResponseMessage resp = await _httpClient.SendAsync(req)) + using (JsSerializer ser = new JsSerializer()) + { + string body = ser.Pretty(await resp.Content.ReadAsStringAsync(), 4); + node.Add(new XAttribute("status", (int)resp.StatusCode)); + node.Add(new XElement("text", body)); + return node; + } + } + finally + { + req.Dispose(); + } + } + catch (Exception ex) + { + node.Add(new XAttribute("status", 500)); + node.Add(new XElement("error", ex.Message)); + return node; + } + } + + private static HttpRequestMessage BuildProviderRequest(HttpListenerContext ctx, string ip, string provider, string prefix, string key) + { + HttpRequestMessage req; + + if (string.Equals(provider, "criminalip", StringComparison.OrdinalIgnoreCase)) + { + string url = prefix.TrimEnd('/') + "/asset/ip/report?ip=" + Uri.EscapeDataString(ip) + "&full=true"; + req = new HttpRequestMessage(HttpMethod.Get, url); + req.Headers.TryAddWithoutValidation("x-api-key", key); + } + else if (string.Equals(provider, "abuseipdb", StringComparison.OrdinalIgnoreCase)) + { + var ub = new UriBuilder(prefix.TrimEnd('/') + "/check"); + var q = HttpUtility.ParseQueryString(ub.Query); + q["ipAddress"] = ip; + q["maxAgeInDays"] = "90"; + q["verbose"] = ""; + ub.Query = q.ToString(); + + req = new HttpRequestMessage(HttpMethod.Get, ub.Uri); + req.Headers.TryAddWithoutValidation("Accept", "application/json"); + req.Headers.TryAddWithoutValidation("Key", key); + } + else + { + throw new ArgumentException("Unsupported provider: " + provider); + } + + if (!string.IsNullOrEmpty(ctx.Request.UserAgent)) + { + req.Headers.TryAddWithoutValidation("User-Agent", ctx.Request.UserAgent); + } + + return req; + } } -} +} \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs index 463fb04..db65c39 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/Whois.cs @@ -37,6 +37,7 @@ namespace WelsonJS.Launcher.ResourceTools if (string.IsNullOrWhiteSpace(query) || query.Length > 255) { + _logger.Error("Invalid WHOIS query parameter."); await Server.ServeResource(context, "Invalid query parameter", "application/xml", 400); return; } @@ -61,6 +62,7 @@ namespace WelsonJS.Launcher.ResourceTools } catch (Exception ex) { + _logger.Error("Error processing WHOIS request: " + ex.Message); await Server.ServeResource(context, $"Failed to process WHOIS request. {ex.Message}", "application/xml", 500); } } diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index 229b09b..e609a60 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -81,6 +81,7 @@ + diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config index 5263b9e..7902a42 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config @@ -19,8 +19,10 @@ - - + + + + diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/editor.html b/WelsonJS.Toolkit/WelsonJS.Launcher/editor.html index 0d44d37..9a72022 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/editor.html +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/editor.html @@ -589,59 +589,62 @@ appendTextToEditor("\n// IP address is required."); return; } - - const apiKey = settingsRef.current.CriminalIpApiKey; - if (!apiKey || apiKey.trim() === '') { - appendTextToEditor("\n// Criminal IP API key is not set."); - return; - } - - const apiPrefix = settingsRef.current.CriminalIpApiPrefix; const ip = encodeURIComponent(hostname.trim()); - axios.get(`/ip-query/${hostname}`).then(response => { - if (!response) { - appendTextToEditor("\n// No data returned from Criminal IP."); + axios.get(`/ip-query/${hostname}`).then(res => { + if (!res || !res.data) { + appendTextToEditor("\n// No data returned from server."); return; } - const lines = []; - lines.push(`/*\nCriminal IP Report: ${hostname}\n`); - - // network port data - lines.push(`## Network ports:`); - if (response.port.data.length == 0) { - lines.push(`* No open ports found.`); - } else { - response.port.data.forEach(x => { - lines.push(`### ${x.open_port_no}/${x.socket}`); - lines.push(`* Application: ${x.app_name} ${x.app_version}`); - lines.push(`* Discovered hostnames: ${x.dns_names}`); - lines.push(`* Confirmed Time: ${x.confirmed_time}`); - }); + // Parse XML and keep attributes (provider, status) + const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@_" }); + const parsed = parser.parse(res.data); + + // Expected XML: + // + // + // { "...raw json..." } + // + // + // { "...raw json..." } + // + // + + // Normalize to an array of nodes + const responsesNode = parsed?.result?.response; + const responses = Array.isArray(responsesNode) + ? responsesNode + : (responsesNode ? [responsesNode] : []); + + if (responses.length === 0) { + appendTextToEditor("\n// No nodes found."); + return; } - // vulnerability data - lines.push(`## Vulnerabilities:`); - if (response.vulnerability.data.length == 0) { - lines.push(`* No vulnerabilities found.`); - } else { - response.vulnerability.data.forEach(x => { - lines.push(`### ${x.cve_id}`); - lines.push(`* ${x.cve_description}`); - lines.push(`* CVSSV2 Score: ${x.cvssv2_score}`); - lines.push(`* CVSSV3 Score: ${x.cvssv3_score}`); - }); + // Extract each content and print + let out = "\n// --- IP Query Results ---"; + for (const r of responses) { + const provider = r?.["@_provider"] || "unknown"; + const status = r?.["@_status"] || "n/a"; + + // Fast-XML-Parser usually makes a string, but be safe: + let txt = r?.text; + if (txt && typeof txt === "object") { + // If parser produced an object with a '#text' key or similar + txt = txt["#text"] || JSON.stringify(txt); + } + if (txt == null) txt = ""; + + out += `\n// provider=${provider}, status=${status}\n${txt}\n`; } + out = "\n/*" + out + "*/\n"; - lines.push(`*/\n`); - const report = lines.join('\n'); - - appendTextToEditor(report); - pushPromptMessage("system", report); + appendTextToEditor(out); + pushPromptMessage("system", out); }).catch(error => { console.error(error); - appendTextToEditor(`\n// Failed to query Criminal IP: ${error.message}`); + appendTextToEditor(`\n// Failed to query the IP: ${error.message}`); }); }; From 8cfff666e4d2a3a1a3199573822db9c80a1a49c3 Mon Sep 17 00:00:00 2001 From: "Namhyeon, Go" Date: Thu, 2 Oct 2025 14:44:22 +0900 Subject: [PATCH 2/2] Update WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs index 4976be3..4899a88 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/ResourceTools/IpQuery.cs @@ -94,7 +94,7 @@ namespace WelsonJS.Launcher.ResourceTools if (string.IsNullOrWhiteSpace(prefix) || string.IsNullOrWhiteSpace(key)) { - node.Add(new XAttribute("status", 500)); + node.Add(new XAttribute("status", 503)); node.Add(new XElement("error", "Missing configuration for " + provider)); return node; }