Merge pull request #333 from gnh1201/dev
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
Deploy Jekyll with GitHub Pages dependencies preinstalled / build (push) Has been cancelled
Deploy Jekyll with GitHub Pages dependencies preinstalled / deploy (push) Has been cancelled

Refactor IP query to support multiple providers
This commit is contained in:
Namhyeon Go 2025-10-02 14:46:37 +09:00 committed by GitHub
commit 3542dc24b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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> /// <summary>
/// yyyy-MM-dd HH:mm:ss과(와) 유사한 지역화된 문자열을 찾습니다. /// yyyy-MM-dd HH:mm:ss과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary> /// </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> /// <summary>
/// false과(와) 유사한 지역화된 문자열을 찾습니다. /// false과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary> /// </summary>

View File

@ -190,10 +190,10 @@
<data name="HttpClientTimeout" xml:space="preserve"> <data name="HttpClientTimeout" xml:space="preserve">
<value>90</value> <value>90</value>
</data> </data>
<data name="CriminalIpApiKey" xml:space="preserve"> <data name="IpQueryApiKey" xml:space="preserve">
<value /> <value />
</data> </data>
<data name="CriminalIpApiPrefix" xml:space="preserve"> <data name="IpQueryApiPrefix" xml:space="preserve">
<value>https://api.criminalip.io/v1/</value> <value>https://api.criminalip.io/v1/</value>
</data> </data>
<data name="DateTimeFormat" xml:space="preserve"> <data name="DateTimeFormat" xml:space="preserve">
@ -214,4 +214,10 @@
<data name="ChromiumAppMode" xml:space="preserve"> <data name="ChromiumAppMode" xml:space="preserve">
<value>true</value> <value>true</value>
</data> </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> </root>

View File

@ -6,7 +6,6 @@
using Microsoft.Win32; using Microsoft.Win32;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Net; using System.Net;
using System.Linq; using System.Linq;
@ -45,8 +44,6 @@ namespace WelsonJS.Launcher.ResourceTools
public async Task HandleAsync(HttpListenerContext context, string path) public async Task HandleAsync(HttpListenerContext context, string path)
{ {
await Task.Delay(0);
string word = path.Substring(Prefix.Length); string word = path.Substring(Prefix.Length);
try try
@ -121,10 +118,7 @@ namespace WelsonJS.Launcher.ResourceTools
.Select(p => p.Trim()) .Select(p => p.Trim())
.Where(p => !string.IsNullOrEmpty(p)); .Where(p => !string.IsNullOrEmpty(p));
foreach (string path in paths) paths.ToList().ForEach(x => SearchAllExecutables(x, SearchOption.TopDirectoryOnly));
{
SearchAllExecutables(path, SearchOption.TopDirectoryOnly);
}
} }
private void DiscoverFromProgramDirectories() private void DiscoverFromProgramDirectories()
@ -153,10 +147,7 @@ namespace WelsonJS.Launcher.ResourceTools
Path.Combine(userProfile, "scoop", "apps") Path.Combine(userProfile, "scoop", "apps")
}; };
foreach (string path in paths) paths.ToList().ForEach(x => SearchAllExecutables(x));
{
SearchAllExecutables(path);
}
} }
private void SearchAllExecutables(string path, SearchOption searchOption = SearchOption.AllDirectories, int maxFiles = 1000) 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) private void AddDiscoveredExecutables(List<string> executableFiles)
{ {
foreach (var executableFile in executableFiles) executableFiles.ForEach(x => DiscoveredExecutables.Add(x));
{
DiscoveredExecutables.Add(executableFile);
}
} }
private async Task SafeDiscoverAsync(Action discoveryMethod) 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-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors // SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors
// https://github.com/gnh1201/welsonjs // 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;
using System.Net.Http;
using System.Net; using System.Net;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web;
using System.Xml.Linq;
namespace WelsonJS.Launcher.ResourceTools namespace WelsonJS.Launcher.ResourceTools
{ {
@ -29,37 +45,121 @@ namespace WelsonJS.Launcher.ResourceTools
{ {
return path.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase); return path.StartsWith(Prefix, StringComparison.OrdinalIgnoreCase);
} }
public async Task HandleAsync(HttpListenerContext context, string path) public async Task HandleAsync(HttpListenerContext context, string path)
{ {
try try
{ {
string target = path.Substring(Prefix.Length).Trim(); string target = path.Substring(Prefix.Length).Trim();
string apiKey = Program.GetAppConfig("CriminalIpApiKey"); if (string.IsNullOrWhiteSpace(target))
if (string.IsNullOrEmpty(apiKey))
{ {
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; return;
} }
string encoded = Uri.EscapeDataString(target); string crimPrefix = Program.GetAppConfig("IpQueryApiPrefix");
string apiPrefix = Program.GetAppConfig("CriminalIpApiPrefix"); string crimKey = Program.GetAppConfig("IpQueryApiKey");
string url = $"{apiPrefix}asset/ip/report?ip={encoded}&full=true";
var request = new HttpRequestMessage(HttpMethod.Get, url); string abusePrefix = Program.GetAppConfig("IpQueryApiPrefix2");
request.Headers.Add("x-api-key", apiKey); string abuseKey = Program.GetAppConfig("IpQueryApiKey2");
request.Headers.Add("User-Agent", context.Request.UserAgent);
HttpResponseMessage response = await _httpClient.SendAsync(request); var root = new XElement("result", new XAttribute("target", target));
string content = await response.Content.ReadAsStringAsync();
context.Response.StatusCode = (int)response.StatusCode; var p1 = QueryProviderAsync(context, target, "criminalip", crimPrefix, crimKey);
await Server.ServeResource(context, content, "application/json", (int)response.StatusCode); 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) 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", 503));
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) 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); await Server.ServeResource(context, "<error>Invalid query parameter</error>", "application/xml", 400);
return; return;
} }
@ -61,6 +62,7 @@ namespace WelsonJS.Launcher.ResourceTools
} }
catch (Exception ex) 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); 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.Drawing" />
<Reference Include="System.IO.Compression.FileSystem" /> <Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
<Reference Include="System.Web" />
<Reference Include="System.Windows.Forms" /> <Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />

View File

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

View File

@ -589,59 +589,62 @@
appendTextToEditor("\n// IP address is required."); appendTextToEditor("\n// IP address is required.");
return; 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()); const ip = encodeURIComponent(hostname.trim());
axios.get(`/ip-query/${hostname}`).then(response => { axios.get(`/ip-query/${hostname}`).then(res => {
if (!response) { if (!res || !res.data) {
appendTextToEditor("\n// No data returned from Criminal IP."); appendTextToEditor("\n// No data returned from server.");
return; return;
} }
const lines = []; // Parse XML and keep attributes (provider, status)
lines.push(`/*\nCriminal IP Report: ${hostname}\n`); const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@_" });
const parsed = parser.parse(res.data);
// network port data
lines.push(`## Network ports:`); // Expected XML:
if (response.port.data.length == 0) { // <result target="...">
lines.push(`* No open ports found.`); // <response provider="criminalip" status="200">
} else { // <text>{ "...raw json..." }</text>
response.port.data.forEach(x => { // </response>
lines.push(`### ${x.open_port_no}/${x.socket}`); // <response provider="abuseipdb" status="200">
lines.push(`* Application: ${x.app_name} ${x.app_version}`); // <text>{ "...raw json..." }</text>
lines.push(`* Discovered hostnames: ${x.dns_names}`); // </response>
lines.push(`* Confirmed Time: ${x.confirmed_time}`); // </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 // Extract each <response><text> content and print
lines.push(`## Vulnerabilities:`); let out = "\n// --- IP Query Results ---";
if (response.vulnerability.data.length == 0) { for (const r of responses) {
lines.push(`* No vulnerabilities found.`); const provider = r?.["@_provider"] || "unknown";
} else { const status = r?.["@_status"] || "n/a";
response.vulnerability.data.forEach(x => {
lines.push(`### ${x.cve_id}`); // Fast-XML-Parser usually makes <text> a string, but be safe:
lines.push(`* ${x.cve_description}`); let txt = r?.text;
lines.push(`* CVSSV2 Score: ${x.cvssv2_score}`); if (txt && typeof txt === "object") {
lines.push(`* CVSSV3 Score: ${x.cvssv3_score}`); // 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`); appendTextToEditor(out);
const report = lines.join('\n'); pushPromptMessage("system", out);
appendTextToEditor(report);
pushPromptMessage("system", report);
}).catch(error => { }).catch(error => {
console.error(error); console.error(error);
appendTextToEditor(`\n// Failed to query Criminal IP: ${error.message}`); appendTextToEditor(`\n// Failed to query the IP: ${error.message}`);
}); });
}; };