diff --git a/cert/index.html b/cert/index.html new file mode 100644 index 0000000..f3df652 --- /dev/null +++ b/cert/index.html @@ -0,0 +1,46 @@ + + + + SHRoNK IP Specification test + + + + +

SHRoNK IP Online specification test

+

This tool allows you to check the specification-following status of a SHRoNK IP server.

+

+ This online tool allows you to quickly verify which parts of the + SHRoNK IP specification + are properly implemented by a SHRoNK IP server. +

+
+ Warning! This tool cannot test for non-CORS enabled servers! + If the server does not have CORS enabled, this tool will not be able to test the server. +
+ +
+ +
+
+ + + +
+
+
+
+ + \ No newline at end of file diff --git a/cert/script.js b/cert/script.js new file mode 100644 index 0000000..c4466d1 --- /dev/null +++ b/cert/script.js @@ -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);