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>
/// 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", 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)
{
_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}`);
});
};