summaryrefslogtreecommitdiffstats
path: root/uAssets/tools/validate/validate.js
diff options
context:
space:
mode:
Diffstat (limited to 'uAssets/tools/validate/validate.js')
-rw-r--r--uAssets/tools/validate/validate.js321
1 files changed, 321 insertions, 0 deletions
diff --git a/uAssets/tools/validate/validate.js b/uAssets/tools/validate/validate.js
new file mode 100644
index 0000000..ce448f8
--- /dev/null
+++ b/uAssets/tools/validate/validate.js
@@ -0,0 +1,321 @@
+/*******************************************************************************
+
+ uBlock Origin - a browser extension to block requests.
+ Copyright (C) 2022-present Raymond Hill
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://github.com/gorhill/uBlock
+*/
+
+// jshint node:true, esversion:8, laxbreak:true
+
+'use strict';
+
+/******************************************************************************/
+
+import fs from 'fs/promises';
+import https from 'https';
+import path from 'path';
+import process from 'process';
+
+import { StaticFilteringParser } from './uBlock/src/js/static-filtering-parser.js';
+import { LineIterator } from './uBlock/src/js/text-utils.js';
+
+import config from './config.js';
+
+/******************************************************************************/
+
+const commandLineArgs = (( ) => {
+ const args = new Map();
+ let name, value;
+ for ( const arg of process.argv.slice(2) ) {
+ const pos = arg.indexOf('=');
+ if ( pos === -1 ) {
+ name = arg;
+ value = '';
+ } else {
+ name = arg.slice(0, pos);
+ value = arg.slice(pos+1).trim();
+ }
+ args.set(name, value);
+ }
+ return args;
+})();
+
+/******************************************************************************/
+
+const stdOutput = [];
+
+const log = (text, silent = false) => {
+ stdOutput.push(text);
+ if ( silent === false ) {
+ console.log(text);
+ }
+};
+
+/******************************************************************************/
+
+const jsonSetMapReplacer = (k, v) => {
+ if ( v instanceof Set || v instanceof Map ) {
+ if ( v.size === 0 ) { return; }
+ return Array.from(v);
+ }
+ return v;
+};
+
+/******************************************************************************/
+
+const writeFile = async (fname, data) => {
+ const dir = path.dirname(fname);
+ await fs.mkdir(dir, { recursive: true });
+ const promise = fs.writeFile(fname, data);
+ writeOps.push(promise);
+ return promise;
+};
+const writeOps = [];
+
+/******************************************************************************/
+
+function sleep(ms) {
+ return new Promise(resolve => {
+ setTimeout(( ) => { resolve(); }, ms);
+ });
+}
+
+/******************************************************************************/
+
+// https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/
+
+async function validateHostnameWithQuery(url) {
+ return new Promise((resolve, reject) => {
+ const options = {
+ headers: {
+ accept: 'application/dns-json',
+ }
+ };
+ https.get(url, options, response => {
+ const data = [];
+ response.on('data', chunk => {
+ data.push(chunk.toString());
+ });
+ response.on('end', ( ) => {
+ let result;
+ try {
+ result = JSON.parse(data.join(''));
+ } catch(ex) {
+ }
+ resolve(result);
+ });
+ }).on('error', error => {
+ resolve();
+ });
+ });
+}
+
+async function validateHostname(hn) {
+ await sleep(config.throttle);
+ for ( const dnsQuery of config.dnsQueries ) {
+ const url = dnsQuery.replace('${hn}', hn);
+ const result = await validateHostnameWithQuery(url);
+ if ( result !== undefined && result.Status !== 2 ) { return result; }
+ }
+}
+
+/******************************************************************************/
+
+function parseHostnameList(parser, s, hostnames) {
+ let beg = 0;
+ let slen = s.length;
+ while ( beg < slen ) {
+ let end = s.indexOf('|', beg);
+ if ( end === -1 ) { end = slen; }
+ const hn = parser.normalizeHostnameValue(s.slice(beg, end));
+ beg = end + 1;
+ if ( hn === undefined ) { continue; }
+ if ( hn.includes('*') ) { continue; }
+ hostnames.push(hn);
+ }
+ return hostnames;
+}
+
+/******************************************************************************/
+
+function processNet(parser) {
+ const hostnames = [];
+ if ( parser.patternIsPlainHostname() ) {
+ hostnames.push(parser.getPattern());
+ } else if ( parser.patternIsLeftHostnameAnchored() ) {
+ const match = /^([^/?]+)/.exec(parser.getPattern());
+ if (
+ match !== null &&
+ match[1].includes('*') === false &&
+ match[1].startsWith('.') === false &&
+ match[1].endsWith('.') === false
+ ) {
+ hostnames.push(match[0]);
+ }
+ }
+ if ( parser.hasOptions() === false ) { return hostnames; }
+ for ( const { id, val } of parser.netOptions() ) {
+ if ( id !== parser.OPTTokenDomain ) { continue; }
+ parseHostnameList(parser, val, hostnames);
+ }
+ return hostnames;
+}
+
+/******************************************************************************/
+
+function processExt(parser) {
+ const hostnames = [];
+ if ( parser.hasOptions() === false ) { return hostnames; }
+ for ( const { hn } of parser.extOptions() ) {
+ if ( hn.includes('*') ) { continue; }
+ hostnames.push(hn);
+ }
+ return hostnames;
+}
+
+/******************************************************************************/
+
+// https://www.rfc-editor.org/rfc/rfc1035.html
+
+function checkHostname(hn, result) {
+ if ( result instanceof Object === false ) { return; }
+ if ( result.Status === 1 ) { return `${hn} format error`; }
+ if ( result.Status === 2 ) { return `${hn} dns server failure`; }
+ if ( result.Status === 3 ) { return `${hn} name error`; }
+ if ( result.Status === 4 ) { return `${hn} not implemented`; }
+ if ( result.Status === 5 ) { return `${hn} refused`; }
+ if ( result.Answer === undefined ) { return; }
+ for ( const entry of result.Answer ) {
+ if ( entry.data === undefined ) { continue; }
+ for ( const re of parkedDomainAuthorities ) {
+ if ( re.test(entry.data) === false ) { continue; }
+ return `${hn} parked`;
+ }
+ }
+}
+
+const parkedDomainAuthorities = [
+ /^traff-\d+\.hugedomains\.com\.?$/,
+ /^\d+\.parkingcrew\.net\.?$/,
+ /^ns\d\.centralnic\.net\.?(\s|$)/,
+ /^ns\d\.pananames\.com\.?(\s|$)/,
+];
+
+/******************************************************************************/
+
+function toProgressString(lineno, hn) {
+ const parts = [];
+ if ( lineno > 0 ) { parts.push(`${lineno}`); }
+ if ( hn ) { parts.push(hn); }
+ const s = parts.join(' ');
+ process.stdout.write(`\r${s.padEnd(lastProgressStr.length)}\r`);
+ lastProgressStr = s;
+}
+
+let lastProgressStr = '';
+
+/******************************************************************************/
+
+// TODO: resume from partial results
+
+async function processList(parser, text, lineto, fpath) {
+
+ const lineIter = new LineIterator(text);
+ const lines = [];
+
+ while ( lineIter.eot() === false ) {
+ lines.push(lineIter.next());
+ }
+
+ if ( lineto === undefined ) {
+ lineto = lines.length;
+ }
+
+ for ( let i = lines.length; i > 0; i-- ) {
+ if ( i > lineto ) { continue; }
+ toProgressString(i);
+
+ let line = lines[i-1];
+
+ parser.analyze(line);
+
+ if ( parser.shouldIgnore() ) { continue; }
+
+ let hostnames;
+ if ( parser.category !== parser.CATStaticNetFilter ) {
+ hostnames = processExt(parser);
+ } else if ( parser.patternHasUnicode() === false || parser.toASCII() ) {
+ hostnames = processNet(parser);
+ }
+ const badHostnames = [];
+ for ( const hn of hostnames ) {
+ if ( hn.endsWith('.onion') ) { continue; }
+ if ( /^\d+\.\d+\.\d+\.\d+$/.test(hn) ) { continue; }
+ let result = validatedHostnames.get(hn);
+ if ( result === undefined ) {
+ toProgressString(i, hn);
+ result = await validateHostname(hn);
+ validatedHostnames.set(hn, result);
+ }
+ const diagnostic = checkHostname(hn, result);
+ if ( diagnostic === undefined ) { continue; }
+ badHostnames.push(diagnostic);
+ }
+ if ( badHostnames.length !== 0 ) {
+ toProgressString(0);
+ const lineno = i;
+ badHostnames.forEach(v => {
+ log(`${lineno} ${v}`);
+ });
+ writeFile(fpath, stdOutput.join('\n'));
+ }
+ }
+ toProgressString(0);
+}
+
+const validatedHostnames = new Map();
+
+/******************************************************************************/
+
+async function main() {
+ const infile = commandLineArgs.get('in');
+ if ( infile === undefined || infile === '' ) { return; }
+ const outdir = commandLineArgs.get('out');
+ if ( outdir === undefined || outdir === '' ) { return; }
+
+ const infileParts = path.parse(infile);
+ const lineto = commandLineArgs.get('line') !== undefined
+ ? parseInt(commandLineArgs.get('line'), 10)
+ : undefined;
+
+ const partialResultPath = `${outdir}/${infileParts.name}.results.partial.txt`;
+ const parser = new StaticFilteringParser();
+
+ const text = await fs.readFile(infile, { encoding: 'utf8' });
+ await processList(parser, text, lineto, partialResultPath);
+
+ writeFile(`${outdir}/${infileParts.name}.results.txt`, stdOutput.join('\n'));
+ writeFile(`${outdir}/${infileParts.name}.dns.results.txt`, JSON.stringify(validatedHostnames, jsonSetMapReplacer, 1));
+
+ fs.rm(partialResultPath);
+
+ await Promise.all(writeOps);
+}
+
+main();
+
+/******************************************************************************/