Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0f5e006
Update deployment configuration for preview environment and ensure AI…
devnomadic Nov 25, 2025
566e8c2
Add API page for IP analysis and reputation checking; update home pag…
devnomadic Dec 25, 2025
4a7783b
Fix casing for AI reputation property in API response
devnomadic Dec 25, 2025
1bad56e
Implement API response handling and create results page
devnomadic Dec 27, 2025
47ebc4a
Enhance API response handling with HTML output and cloud provider sup…
devnomadic Dec 27, 2025
abd1096
Update API page to support HTML output and enhance syntax highlightin…
devnomadic Dec 27, 2025
f930863
Improve Azure manifest search logging and error handling; validate CI…
devnomadic Dec 27, 2025
f6b4436
Fix JSON output highlighting in API page and adjust manifest URL casing
devnomadic Dec 27, 2025
7afa715
Enhance JSON highlighting in API page by moving highlight logic to JS…
devnomadic Dec 27, 2025
f70e18d
Enhance API documentation and UI; add interactive mode for IP analysi…
devnomadic Dec 28, 2025
6972a8c
Add API hostname selection and enhance response display in API page
devnomadic Dec 28, 2025
ea90c08
Enable workers_dev for production and preview environments in wrangle…
devnomadic Dec 28, 2025
f38b961
Update cloudflare-worker.template.js
devnomadic Dec 28, 2025
d733fe4
Update cloudflare-worker.template.js
devnomadic Dec 28, 2025
726ff1c
Update cloudflare-worker.template.js
devnomadic Dec 28, 2025
a765ddc
Update cloudflare-worker.template.js
devnomadic Dec 28, 2025
ecd6f3f
Update cloudflare-worker.template.js
devnomadic Dec 28, 2025
6e2b947
Update wwwroot/openapi.json
devnomadic Dec 28, 2025
286149f
Update wwwroot/js/json-highlight.js
devnomadic Dec 28, 2025
3cc2ec3
Add prefix length validation for CIDR ranges in isIpInRange (#17)
Copilot Dec 28, 2025
6f59860
Update IP manifest filenames to lowercase and adjust last modified da…
devnomadic Dec 28, 2025
2ee2d48
Refactor code structure for improved readability and maintainability
devnomadic Dec 28, 2025
0dda346
Add environment-based base URL and set ENVIRONMENT variables for prod…
devnomadic Dec 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,11 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
# Deploy to static preview worker
# Deploy to preview environment
WORKER_NAME="abuseipdb-preview"

DEPLOYMENT_OUTPUT=$(npx wrangler deploy \
--name "${WORKER_NAME}" \
--env preview \
--compatibility-date 2024-06-08 \
cloudflare-worker.js 2>&1)

Expand Down
8 changes: 4 additions & 4 deletions Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@
<span class="build-label">Build:</span>
<a href="https://github.com/devnomadic/Albatross" alt="Albatross"><span class="build-value">@BuildInfo</span></a>
</div><div class="cloud-logos">
<a href="/ip-manifests/Azure.json" target="_blank">
<a href="/ip-manifests/azure.json" target="_blank">
<img src="/images/Azure.png" alt="Azure" width="3%" border="0" />
</a>
<a href="/ip-manifests/AWS.json" target="_blank">
<a href="/ip-manifests/aws.json" target="_blank">
<img src="/images/AWS.webp" alt="AWS" width="3%" border="0" />
</a>
<a href="/ip-manifests/GCP.json" target="_blank">
<a href="/ip-manifests/gcp.json" target="_blank">
<img src="/images/GCP.webp" alt="GCP" width="3%" border="0" />
</a>
<a href="/ip-manifests/Oracle.json" target="_blank">
<a href="/ip-manifests/oracle.json" target="_blank">
<img src="/images/OCI.png" alt="OCI" width="2%" border="0" />
</a>
<a href="https://radar.cloudflare.com/" target="_blank">
Expand Down
576 changes: 576 additions & 0 deletions Pages/Api.razor

Large diffs are not rendered by default.

297 changes: 297 additions & 0 deletions Pages/Api.razor.backup
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
@page "/api"
@inject HttpClient Http
@inject Albatross.Services.AbuseIPDBService AbuseIPDBService
@inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime
@using System.Text.Json
@using System.Net
@using System.Diagnostics

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" href="css/json-formatter.css">
<style>
body {
margin: 0;
padding: 20px;
background-color: #1e1e1e;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
}
pre {
margin: 0;
padding: 20px;
background-color: #1e1e1e;
color: #d4d4d4;
font-size: 14px;
line-height: 1.5;
overflow-x: auto;
border: none;
}
pre code {
background: transparent;
padding: 0;
color: #d4d4d4;
}
/* VS2015 Dark Theme Colors */
.hljs {
background: #1e1e1e;
color: #dcdcdc;
}
.hljs-attr {
color: #9cdcfe;
}
.hljs-string {
color: #ce9178;
}
.hljs-number {
color: #b5cea8;
}
.hljs-literal {
color: #569cd6;
}
.hljs-punctuation {
color: #d4d4d4;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/json.min.js"></script>
<script>
window.highlightJson = function() {
const codeBlock = document.querySelector('#json-container code');
if (codeBlock && typeof hljs !== 'undefined') {
hljs.highlightElement(codeBlock);
}
};
</script>
</head>
<body>
<pre id="json-container"><code class="language-json">@jsonOutput</code></pre>
</body>
</html>

@code {
private string jsonOutput = string.Empty;

protected override async Task OnInitializedAsync()
{
var startTime = Stopwatch.StartNew();
var requestId = Guid.NewGuid().ToString("N").Substring(0, 16);

try
{
// Parse query parameters
var uri = new Uri(NavigationManager.Uri);
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);

var ipAddress = query["ipaddress"];
var maxAgeInDaysStr = query["maxageindays"] ?? "30";
var verboseStr = query["verbose"] ?? "true";
var enableAiStr = query["enableai"] ?? "true";
var cloudProvider = query["cloudprovider"]; // Optional: aws, azure, gcp, oracle, or all

// Validate required parameter
if (string.IsNullOrEmpty(ipAddress))
{
jsonOutput = BuildErrorResponse("Missing required parameter: ipaddress", "MISSING_PARAMETER", 400, requestId, startTime);
return;
}

// Parse optional parameters
if (!int.TryParse(maxAgeInDaysStr, out int maxAgeInDays))
{
maxAgeInDays = 30;
}
bool verbose = verboseStr.ToLower() == "true";
bool enableAi = enableAiStr.ToLower() == "true";

// Validate IP address
if (!IsValidIPAddress(ipAddress))
{
jsonOutput = BuildErrorResponse("Invalid IP address. Please enter a valid public routable IPv4 or IPv6 address (private/reserved ranges are not allowed).", "INVALID_IP", 400, requestId, startTime);
return;
}

// Extract just the IP part (in case format is ip;maxdays)
string actualIpAddress = ipAddress.Contains(';')
? ipAddress.Split(';', 2)[0].Trim()
: ipAddress.Trim();

IPAddress parsedIp = IPAddress.Parse(actualIpAddress);

// Perform reputation check (worker handles cloud search)
var reputationResults = await AbuseIPDBService.CheckIPAsync(actualIpAddress, maxAgeInDays, verbose, enableAi, cloudProvider);

// Build unified response
jsonOutput = BuildSuccessResponse(actualIpAddress, reputationResults, requestId, startTime);
}
catch (Exception ex)
{
jsonOutput = BuildErrorResponse($"Internal server error: {ex.Message}", "INTERNAL_ERROR", 500, requestId, startTime);
}
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !string.IsNullOrEmpty(jsonOutput))
{
await JSRuntime.InvokeVoidAsync("highlightJson");
}
}

private string BuildSuccessResponse(string ipAddress, Albatross.Services.AbuseIPDBApiResponse reputationData, string requestId, Stopwatch timer)
{
timer.Stop();

var response = new
{
success = true,
data = new
{
ipAddress = ipAddress,
cloudMatches = reputationData.CloudMatches,
reputation = reputationData.Data,
asnInfo = reputationData.AsnInfo,
aiReputation = reputationData.AIReputation
},
metadata = new
{
requestId = requestId,
timestamp = DateTime.UtcNow.ToString("o"),
executionTimeMs = timer.ElapsedMilliseconds,
sources = new
{
cloudSearch = reputationData.CloudMatches != null ? "success" : "not-requested",
abuseipdb = reputationData.Data != null ? "success" : "error",
radar = reputationData.AsnInfo?.Success == true ? "success" : "error",
ai = reputationData.AIReputation?.Success == true ? "success" : (reputationData.AIReputation != null ? "error" : "disabled")
},
workerInfo = reputationData.WorkerInfo
}
};

var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};

return JsonSerializer.Serialize(response, options);
}

private string BuildErrorResponse(string errorMessage, string errorCode, int statusCode, string requestId, Stopwatch timer)
{
timer.Stop();

var response = new
{
success = false,
error = errorMessage,
code = errorCode,
status = statusCode,
metadata = new
{
requestId = requestId,
timestamp = DateTime.UtcNow.ToString("o"),
executionTimeMs = timer.ElapsedMilliseconds
}
};

var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

return JsonSerializer.Serialize(response, options);
}

private bool IsValidIPAddress(string input)
{
string actualIpAddress = input.Contains(';') ? input.Split(';', 2)[0].Trim() : input.Trim();
if (!IPAddress.TryParse(actualIpAddress, out IPAddress? parsedIP))
return false;
return IsPublicRoutableIP(parsedIP);
}

private bool IsPublicRoutableIP(IPAddress ip)
{
if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
{
return IsPublicIPv4(ip);
}
else if (ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6)
{
return IsPublicIPv6(ip);
}
return false;
}

private bool IsPublicIPv4(IPAddress ip)
{
byte[] bytes = ip.GetAddressBytes();
if (bytes[0] == 0) return false;
if (bytes[0] == 10) return false;
if (bytes[0] == 100 && (bytes[1] & 0xC0) == 64) return false;
if (bytes[0] == 127) return false;
if (bytes[0] == 169 && bytes[1] == 254) return false;
if (bytes[0] == 172 && (bytes[1] & 0xF0) == 16) return false;
if (bytes[0] == 192 && bytes[1] == 0 && bytes[2] == 0) return false;
if (bytes[0] == 192 && bytes[1] == 0 && bytes[2] == 2) return false;
if (bytes[0] == 192 && bytes[1] == 88 && bytes[2] == 99) return false;
if (bytes[0] == 192 && bytes[1] == 168) return false;
if (bytes[0] == 198 && (bytes[1] & 0xFE) == 18) return false;
if (bytes[0] == 198 && bytes[1] == 51 && bytes[2] == 100) return false;
if (bytes[0] == 203 && bytes[1] == 0 && bytes[2] == 113) return false;
if ((bytes[0] & 0xF0) == 224) return false;
if ((bytes[0] & 0xF0) == 240) return false;
if (bytes[0] == 255 && bytes[1] == 255 && bytes[2] == 255 && bytes[3] == 255) return false;
return true;
}

private bool IsPublicIPv6(IPAddress ip)
{
byte[] bytes = ip.GetAddressBytes();
if (ip.Equals(IPAddress.IPv6Loopback)) return false;
if (ip.Equals(IPAddress.IPv6Any)) return false;
if (bytes[0] == 0 && bytes[1] == 0 && bytes[2] == 0 && bytes[3] == 0 &&
bytes[4] == 0 && bytes[5] == 0 && bytes[6] == 0 && bytes[7] == 0 &&
bytes[8] == 0 && bytes[9] == 0 && bytes[10] == 0xFF && bytes[11] == 0xFF) return false;
if (bytes[0] == 0xFE && (bytes[1] & 0xC0) == 0x80) return false;
if ((bytes[0] & 0xFE) == 0xFC) return false;
if (bytes[0] == 0xFF) return false;
if (bytes[0] == 0x20 && bytes[1] == 0x01 && bytes[2] == 0x0D && bytes[3] == 0xB8) return false;
return true;
}

private bool IsIpInRange(IPAddress ip, string cidrRange)
{
try
{
string[] parts = cidrRange.Split('/');
if (parts.Length != 2) return false;
IPAddress networkAddress = IPAddress.Parse(parts[0]);
int prefixLength = int.Parse(parts[1]);
byte[] ipBytes = ip.GetAddressBytes();
byte[] networkBytes = networkAddress.GetAddressBytes();
if (ipBytes.Length != networkBytes.Length) return false;
int maxPrefixLength = ipBytes.Length * 8;
if (prefixLength < 0 || prefixLength > maxPrefixLength) return false;
int numBytes = prefixLength / 8;
int remainingBits = prefixLength % 8;
for (int i = 0; i < numBytes && i < ipBytes.Length; i++)
{
if (ipBytes[i] != networkBytes[i]) return false;
}
if (remainingBits > 0 && numBytes < ipBytes.Length)
{
int mask = (0xFF << (8 - remainingBits)) & 0xFF;
if ((ipBytes[numBytes] & mask) != (networkBytes[numBytes] & mask)) return false;
}
return true;
}
catch { return false; }
}
}
17 changes: 12 additions & 5 deletions Pages/Home.razor
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,11 @@
\____/_/\____/\__,_/\__,_/ /___/_/ /____/\___/\__,_/_/ \___/_/ /_/
</b></pre><br>
-->
<img src="/images/ascii-text-art.png" alt="CloudIPSearch" width="38%" style="padding-bottom: 5px;" border="0"/> <br>
@{
var currentMonth = DateTime.Now.Month;
var logoPath = currentMonth == 12 ? "/images/ascii-text-art-christmas.png" : "/images/ascii-text-art.png";
}
<img src="@logoPath" alt="CloudIPSearch" width="38%" style="padding-bottom: 5px;" border="0"/> <br>
<img src="/images/ascii-text-art-albatross.png" alt="ascii-text-art-albatross" width="15%" border="0" /> <br><br>
<div class="ascii-art-pixel-matrix" role="img" aria-label="ASCII art pixel matrix">
<div class="pixel-matrix__grid">
Expand Down Expand Up @@ -172,6 +176,9 @@
<h6>🔒 <strong>Security:</strong></h6>
<p>All requests use HMAC-SHA256 authentication with timestamp validation. Only public routable IP addresses are accepted (private networks like 10.x.x.x, 192.168.x.x, and 127.x.x.x are blocked).</p>

<h6>🔌 <strong>API Access:</strong></h6>
<p>Programmatic access available via REST API with OpenAPI specification. Visit the <a href="/api" style="color: #0d6efd; text-decoration: none; font-weight: 500;">API Documentation</a> for interactive documentation, code examples, and OpenAPI spec download.</p>

<p><small class="text-muted">Data provided by AbuseIPDB, Cloudflare Radar, Cloudflare Workers AI, and official cloud provider IP manifests.</small></p>
</div>
</div>
Expand Down Expand Up @@ -519,10 +526,10 @@
// Load all manifest files in parallel for optimal performance
var downloadTasks = new[]
{
Http.GetStringAsync("ip-manifests/Azure.json"),
Http.GetStringAsync("ip-manifests/AWS.json"),
Http.GetStringAsync("ip-manifests/GCP.json"),
Http.GetStringAsync("ip-manifests/Oracle.json")
Http.GetStringAsync("ip-manifests/azure.json"),
Http.GetStringAsync("ip-manifests/aws.json"),
Http.GetStringAsync("ip-manifests/gcp.json"),
Http.GetStringAsync("ip-manifests/oracle.json")
};

statusMessage = "Loading cloud IP manifests...";
Expand Down
Loading
Loading