diff --git a/HOSTING.md b/HOSTING.md index 4fd8f5f..6c5a8a9 100644 --- a/HOSTING.md +++ b/HOSTING.md @@ -29,6 +29,7 @@ This guide will walk you through the process of setting up your own instance of `HOME_DOMAIN` | The host to enable `/stat` endpoint `USE_LOCAL_DNS` | Default is `false`, so the Google DNS is used. Set it to `true` if you want to use the DNS resolver of your own host `CACHE_EXPIRY_SECONDS` | Option to override the default cache TTL of 86400 seconds (1 day) +`DEBUG_LEVEL` | Default level is 0 (disabled) and can be set up to level 3 for maximum information If `WHITELIST_HOSTS` is set, `BLACKLIST_HOSTS` is ignored. Both is mutually exclusive. diff --git a/src/client.js b/src/client.js index 9ee1fcc..213d1b5 100644 --- a/src/client.js +++ b/src/client.js @@ -12,6 +12,7 @@ import { validateCAARecords, isExceedHostLimit, isHttpCodeAllowed, + debugOutput, getExpiryDate } from "./util.js"; @@ -108,6 +109,7 @@ const listener = async function (req, res) { res.write('Host header is required'); return; } + debugOutput(1, `Received HTTP request for ${host}`); if (host === process.env.HOME_DOMAIN) { // handle CORS res.setHeader('Access-Control-Allow-Origin', '*'); @@ -141,6 +143,7 @@ const listener = async function (req, res) { totalSize += chunk.length; // Disconnect if the data stream is too large if (totalSize > MAX_DATA_SIZE) { + debugOutput(3, `Data stream for request ${host} too large`); req.destroy(); return; } @@ -162,6 +165,7 @@ const listener = async function (req, res) { if (cacheExists) { // Remove the cache entry resolveCache.delete(domain); + debugOutput(1, `Cache cleared for ${domain}`); } } } @@ -176,7 +180,10 @@ const listener = async function (req, res) { if (!cache || (Date.now() > cache.expire)) { cache = await buildCache(host); resolveCache.set(host, cache); - } + debugOutput(1, `No cache found for ${host}, storing new data`); + } else { + debugOutput(1, `Found cache for ${host}, using stored data`); + } if (cache.blacklisted) { if (blacklistRedirectUrl) { res.writeHead(302, { @@ -208,4 +215,4 @@ export { listener, pruneCache, buildCache, -} \ No newline at end of file +} diff --git a/src/util.js b/src/util.js index b0ee9b5..1d0b0df 100644 --- a/src/util.js +++ b/src/util.js @@ -7,6 +7,7 @@ import forge from "node-forge"; const recordParamDestUrl = 'forward-domain'; const recordParamHttpStatus = 'http-status'; const caaRegex = /^0 issue (")?letsencrypt\.org(;validationmethods=http-01)?\1$/; +const validDebugLevels = [0, 1, 2, 3]; /** * @type {Record} @@ -35,6 +36,10 @@ let whitelistMap = null; * @type {number | null} */ let cacheExpirySeconds = null; +/** + * @type {number | null} + */ +let debugLevel = null export function getExpiryDate() { if (cacheExpirySeconds === null) { @@ -96,6 +101,7 @@ export function clearConfig() { useLocalDNS = null; blacklistRedirectUrl = null; cacheExpirySeconds = null; + debugLevel = null; } /** @@ -138,6 +144,44 @@ export function isHostBlacklisted(domain = '', mockEnv = undefined) { } } +/** + * Returns the current date and time in the format: "YYYY-MM-DD HH:MM:SS". + * + * @returns {string} The current date and time as a formatted string. + */ +export function CurrentDate() { + const now = new Date(); + + return ( + now.getFullYear() + '-' + + String(now.getMonth() + 1).padStart(2, '0') + '-' + + String(now.getDate()).padStart(2, '0') + ' ' + + String(now.getHours()).padStart(2, '0') + ':' + + String(now.getMinutes()).padStart(2, '0') + ':' + + String(now.getSeconds()).padStart(2, '0') + ); +} + +/** + * Outputs debug messages to the console based on the specified debug level. + * Debugging is controlled by the `DEBUG_LEVEL` environment variable. + * + * @param {number} level - The debug level of the message (1 - 3). + * @param {string} msg - The debug message to output. + * + */ +export function debugOutput(level,msg) { + if (debugLevel === null) { + debugLevel = validDebugLevels.includes(parseInt(process.env.DEBUG_LEVEL)) ? parseInt(process.env.DEBUG_LEVEL) : 0; + } + if (debugLevel >= 1) { + const date = CurrentDate(); + if (level <= debugLevel) { + console.log(`[${date}] ${msg}`); + } + } +} + /** * @param {string} host */ @@ -201,6 +245,7 @@ const parseTxtRecordData = (value) => { export async function validateCAARecords(host, mockResolve = undefined) { if (useLocalDNS === null) { useLocalDNS = process.env.USE_LOCAL_DNS == 'true'; + debugOutput(3, `useLocalDNS: ${useLocalDNS}`); } let issueRecords; if (useLocalDNS && !mockResolve) { @@ -226,6 +271,7 @@ export async function validateCAARecords(host, mockResolve = undefined) { return null; } + debugOutput(3, `issueRecords for ${host}: ${issueRecords}`); return issueRecords; } @@ -247,6 +293,7 @@ export async function findTxtRecord(host, mockResolve = undefined) { const resolved = await Promise.any(resolvePromises).catch(() => null); if (resolved) { + debugOutput(2, `findTxtRecord for ${host}: ${resolved}`); for (const record of resolved) { const joinedRecord = record.join(';'); const txtData = parseTxtRecordData(joinedRecord); @@ -271,6 +318,7 @@ export async function findTxtRecord(host, mockResolve = undefined) { } const txtData = parseTxtRecordData(head.data); if (!txtData[recordParamDestUrl]) continue; + debugOutput(2, `findTxtRecord for ${host}: ${txtData[recordParamDestUrl]}`); return { url: txtData[recordParamDestUrl], httpStatus: txtData[recordParamHttpStatus], @@ -286,6 +334,7 @@ export async function findTxtRecord(host, mockResolve = undefined) { */ export function getCertExpiry(cert) { const x509 = forge.pki.certificateFromPem(cert); + debugOutput(3, `LE cert expire after: ${x509.validity.notAfter.getTime()}`); return x509.validity.notAfter.getTime() } @@ -337,4 +386,4 @@ export function combineURLs(baseURL, relativeURL) { */ export function isMainProcess(metaURL) { return [process.argv[1], process.env.pm_exec_path].includes(fileURLToPath(metaURL)); -} \ No newline at end of file +}