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.
This commit is contained in:
Namhyeon Go 2025-10-02 14:39:50 +09:00
parent a3e00d1762
commit 5fb255e1e3
8 changed files with 218 additions and 98 deletions

View File

@ -150,24 +150,6 @@ namespace WelsonJS.Launcher.Properties {
}
}
/// <summary>
/// 과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string CriminalIpApiKey {
get {
return ResourceManager.GetString("CriminalIpApiKey", resourceCulture);
}
}
/// <summary>
/// https://api.criminalip.io/v1/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string CriminalIpApiPrefix {
get {
return ResourceManager.GetString("CriminalIpApiPrefix", resourceCulture);
}
}
/// <summary>
/// yyyy-MM-dd HH:mm:ss과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
@ -295,6 +277,42 @@ namespace WelsonJS.Launcher.Properties {
}
}
/// <summary>
/// 과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string IpQueryApiKey {
get {
return ResourceManager.GetString("IpQueryApiKey", resourceCulture);
}
}
/// <summary>
/// 과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string IpQueryApiKey2 {
get {
return ResourceManager.GetString("IpQueryApiKey2", resourceCulture);
}
}
/// <summary>
/// https://api.criminalip.io/v1/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string IpQueryApiPrefix {
get {
return ResourceManager.GetString("IpQueryApiPrefix", resourceCulture);
}
}
/// <summary>
/// https://api.abuseipdb.com/api/v2/과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string IpQueryApiPrefix2 {
get {
return ResourceManager.GetString("IpQueryApiPrefix2", resourceCulture);
}
}
/// <summary>
/// false과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>

View File

@ -190,10 +190,10 @@
<data name="HttpClientTimeout" xml:space="preserve">
<value>90</value>
</data>
<data name="CriminalIpApiKey" xml:space="preserve">
<data name="IpQueryApiKey" xml:space="preserve">
<value />
</data>
<data name="CriminalIpApiPrefix" xml:space="preserve">
<data name="IpQueryApiPrefix" xml:space="preserve">
<value>https://api.criminalip.io/v1/</value>
</data>
<data name="DateTimeFormat" xml:space="preserve">
@ -214,4 +214,10 @@
<data name="ChromiumAppMode" xml:space="preserve">
<value>true</value>
</data>
<data name="IpQueryApiKey2" xml:space="preserve">
<value />
</data>
<data name="IpQueryApiPrefix2" xml:space="preserve">
<value>https://api.abuseipdb.com/api/v2/</value>
</data>
</root>

View File

@ -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<string> executableFiles)
{
foreach (var executableFile in executableFiles)
{
DiscoveredExecutables.Add(executableFile);
}
executableFiles.ForEach(x => DiscoveredExecutables.Add(x));
}
private async Task SafeDiscoverAsync(Action discoveryMethod)

View File

@ -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:
// <result target="1.1.1.1">
// <response provider="criminalip" status="200"><text>{"...json..."}</text></response>
// <response provider="abuseipdb" status="200"><text>{"...json..."}</text></response>
// </result>
//
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, "<error>Missing API key</error>", "application/xml", 500);
await Server.ServeResource(context, "<error>Missing IP target</error>", "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, $"<error>{ex.Message}</error>", "application/xml", 500);
_logger.Error("Error processing IP query request: " + ex.Message);
await Server.ServeResource(context, "<error>" + WebUtility.HtmlEncode(ex.Message) + "</error>", "application/xml", 500);
}
}
private async Task<XElement> 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;
}
}
}
}

View File

@ -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, "<error>Invalid query parameter</error>", "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, $"<error>Failed to process WHOIS request. {ex.Message}</error>", "application/xml", 500);
}
}

View File

@ -81,6 +81,7 @@
<Reference Include="System.Drawing" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Web" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />

View File

@ -19,8 +19,10 @@
<add key="BlobStoragePrefix" value="https://catswords.blob.core.windows.net/welsonjs/"/>
<add key="BlobConfigUrl" value="https://catswords.blob.core.windows.net/welsonjs/blob.config.xml"/>
<add key="HttpClientTimeout" value="90"/>
<add key="CitiApiKey" value=""/>
<add key="CitiApiPrefix" value="https://api.criminalip.io/v1/"/>
<add key="IpQueryApiKey" value=""/>
<add key="IpQueryApiPrefix" value="https://api.criminalip.io/v1/"/>
<add key="IpQueryApiKey2" value=""/>
<add key="IpQueryApiPrefix2" value="https://api.abuseipdb.com/api/v2/"/>
<add key="DateTimeFormat" value="yyyy-MM-dd HH:mm:ss"/>
<add key="NativeRequireSigned" value="false"/>
</appSettings>

View File

@ -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:
// <result target="...">
// <response provider="criminalip" status="200">
// <text>{ "...raw json..." }</text>
// </response>
// <response provider="abuseipdb" status="200">
// <text>{ "...raw json..." }</text>
// </response>
// </result>
// Normalize to an array of <response> nodes
const responsesNode = parsed?.result?.response;
const responses = Array.isArray(responsesNode)
? responsesNode
: (responsesNode ? [responsesNode] : []);
if (responses.length === 0) {
appendTextToEditor("\n// No <response> 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 <response><text> 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 <text> 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}`);
});
};