Add SHRoNK tester

This commit is contained in:
Nexus 2024-04-22 01:48:05 +01:00
parent b644a3f411
commit 1f350d8399
Signed by: nex
GPG key ID: 0FA334385D0B689F
2 changed files with 231 additions and 0 deletions

46
cert/index.html Normal file
View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>SHRoNK IP Specification test</title>
<script src="script.js" defer></script>
<style>
html {
font-family: sans-serif;
background: #29242A;
color: #dddadd
}
code, pre {
overflow: wrap;
word-wrap: break-word;
font-family: "Ubuntu Mono", "Jetbrains Mono", "Consolas", monospace;
max-width: 100%;
white-space: pre-wrap
}
</style>
</head>
<body>
<h1>SHRoNK IP Online specification test</h1>
<p>This tool allows you to check the specification-following status of a SHRoNK IP server.</p>
<p>
This online tool allows you to quickly verify which parts of the
<a href="https://github.com/SHRoNK-Corporation/shronk-ip-spec/blob/d1ca0b7/RFS0001.md" target="_blank" rel="noopener">SHRoNK IP specification</a>
are properly implemented by a SHRoNK IP server.
</p>
<blockquote>
Warning! This tool cannot test for <strong>non-CORS enabled</strong> servers!
If the server does not have CORS enabled, this tool will not be able to test the server.
</blockquote>
<hr/>
<div>
<form>
<label for="server">Server URL:</label>
<input type="url" id="server" name="server" placeholder="https://example.com" required>
<button type="submit">Test</button>
</form>
<br/>
<code><pre id="results"></pre></code>
</div>
</body>
</html>

185
cert/script.js Normal file
View file

@ -0,0 +1,185 @@
const VALID_KEYS = {
ip: {
required: true,
match: /^(\d{1,3}\.){3}\d{1,3}$/
},
city: {
required: false,
match: /^[a-zA-Z ]+$/
},
country: {
required: false,
match: /^[a-zA-Z ]+$/
},
countryCode: {
required: false,
match: /^[A-Z]{2}$/
},
asn: {
required: false,
match: /^\d+$/
},
isp: {
required: false,
match: /^.+$/ // ISP names can be anything really
},
lat: {
required: false,
match: /^-?\d+(\.\d+)?$/
},
lon: {
required: false,
match: /^-?\d+(\.\d+)?$/
},
hostname: {
required: false,
match: /^.+$/
},
timezone: {
required: false,
match: /^[^\/]\/.+$/
},
locale: {
required: false,
match: /^[a-z]{2}([_-][A-Z]{2})?$/
},
subnet: {
required: false,
match: /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/
},
abuse: {
required: false,
match: /^.+$/
}
}
const BOBS = [
"|",
"/",
"-",
"\\"
]
async function isCORSEnabled(url) {
try {
const response = await fetch(url, { method: "GET", mode: "cors"});
return response.ok;
} catch {
return false;
};
}
async function getRealIP() {
const response = await fetch("https://api.ipify.org?format=json");
const data = await response.json();
return data.ip;
}
async function checkRaw(url, real) {
const response = await fetch(url);
const text = await response.text();
return text === real;
}
async function checkRoot(url) {
const response = await fetch(url);
const data = await response.json();
return data
}
async function checkLookup(url) {
const response = await fetch(url);
const data = await response.json();
return data
}
async function checkImFeelingLucky(url) {
const response = await fetch(url);
const data = await response.json();
return data;
}
function checkReturnedJSON(data) {
let dataIncluded = 0;
for (let key in VALID_KEYS) {
if (data[key]) {
if(VALID_KEYS[key].match.test(data[key])) {
console.debug("Server included key %s (value: '%s')", key, data[key]);
dataIncluded++;
} else {
console.warn("Server included key %s (value: '%s'), but it's not valid", key, data[key]);
}
} else {
if (VALID_KEYS[key].required) {
console.warn("Server didn't include required key %s", key);
} else {
console.log("Server didn't include optional key %s", key)
}
}
}
return dataIncluded;
}
async function main(e) {
e.preventDefault();
const base_url = document.querySelector("input").value;
const log = document.getElementById("results");
log.textContent = "Checking: " + base_url + "\n";
log.textContent += "Checking if CORS is enabled on the target... ";
const corsEnabled = await isCORSEnabled(base_url);
log.textContent += corsEnabled ? "Yes\n" : "No\n";
if (!corsEnabled) {
log.textContent += "CORS is not enabled, the script will not work\n";
return;
}
log.textContent += "Getting external IP from trusted source... ";
const realIP = await getRealIP();
log.textContent += realIP + "\n";
log.textContent += "Checking /... ";
const root = await checkRoot(base_url);
const rootFeatures = checkReturnedJSON(root)
let rootOK = rootFeatures >= 1 && root.ip === realIP;
log.textContent += rootOK ? `OK (${rootFeatures}/${Object.keys(VALID_KEYS).length} features)\n` : "FAILED\n";
log.textContent += "Checking /lookup... ";
const lookup = await checkLookup(base_url + "/lookup?ip=" + realIP);
const lookupFeatures = checkReturnedJSON(lookup)
let lookupOK = lookupFeatures >= 1 && lookup.ip === realIP;
log.textContent += lookupOK ? `OK (${rootFeatures}/${Object.keys(VALID_KEYS).length} features)\n` : "FAILED\n";
log.textContent += "Checking /imfeelinglucky (this may take a while) ";
let changed = false;
for(let i=0; i<512; i++) {
console.debug("Checking /imfeelinglucky iteration %d/512", i);
const lucky = await checkImFeelingLucky(base_url + "/imfeelinglucky");
const luckyFeatures = checkReturnedJSON(lucky)
let luckyOK = luckyFeatures >= 1 && lucky.ip != realIP;
if (luckyOK) {
console.debug("Server modified IP, OK")
log.textContent = log.textContent.slice(0, -1)
log.textContent += `OK (${luckyFeatures}/${Object.keys(VALID_KEYS).length} features, took ${i} attempts for a modified IP)\n`;
changed = true;
break;
}
else {
console.debug("Server did not modify IP, retrying")
log.textContent = log.textContent.slice(0, -1)
log.textContent += BOBS[i % 4];
}
}
if(!changed) {
log.textContent += "FAILED (did not modify returned IP within 512 tries)\n";
}
log.textContent += "Checking /raw... ";
const raw = await checkRaw(base_url + "/raw", realIP);
log.textContent += raw ? "OK\n" : "FAILED\n";
}
document.querySelector("form").addEventListener("submit", main);