@crawlith/core 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +70 -0
- package/dist/analysis/analysis_list.html +35 -0
- package/dist/analysis/analysis_page.html +123 -0
- package/dist/analysis/analyze.d.ts +40 -5
- package/dist/analysis/analyze.js +395 -347
- package/dist/analysis/clustering.d.ts +23 -0
- package/dist/analysis/clustering.js +206 -0
- package/dist/analysis/content.d.ts +1 -1
- package/dist/analysis/content.js +11 -5
- package/dist/analysis/duplicate.d.ts +34 -0
- package/dist/analysis/duplicate.js +305 -0
- package/dist/analysis/heading.d.ts +116 -0
- package/dist/analysis/heading.js +356 -0
- package/dist/analysis/images.d.ts +1 -1
- package/dist/analysis/images.js +6 -5
- package/dist/analysis/links.d.ts +1 -1
- package/dist/analysis/links.js +8 -8
- package/dist/{scoring/orphanSeverity.d.ts → analysis/orphan.d.ts} +12 -23
- package/dist/{scoring/orphanSeverity.js → analysis/orphan.js} +9 -3
- package/dist/analysis/scoring.js +11 -2
- package/dist/analysis/seo.d.ts +8 -4
- package/dist/analysis/seo.js +41 -30
- package/dist/analysis/soft404.d.ts +17 -0
- package/dist/analysis/soft404.js +62 -0
- package/dist/analysis/structuredData.d.ts +1 -1
- package/dist/analysis/structuredData.js +5 -4
- package/dist/analysis/templates.d.ts +2 -0
- package/dist/analysis/templates.js +7 -0
- package/dist/application/index.d.ts +2 -0
- package/dist/application/index.js +2 -0
- package/dist/application/usecase.d.ts +3 -0
- package/dist/application/usecase.js +1 -0
- package/dist/application/usecases.d.ts +114 -0
- package/dist/application/usecases.js +201 -0
- package/dist/audit/index.js +1 -1
- package/dist/audit/transport.d.ts +1 -1
- package/dist/audit/transport.js +5 -4
- package/dist/audit/types.d.ts +1 -0
- package/dist/constants.d.ts +17 -0
- package/dist/constants.js +23 -0
- package/dist/core/scope/scopeManager.js +3 -0
- package/dist/core/security/ipGuard.d.ts +11 -0
- package/dist/core/security/ipGuard.js +71 -3
- package/dist/crawler/crawl.d.ts +4 -22
- package/dist/crawler/crawl.js +4 -335
- package/dist/crawler/crawler.d.ts +87 -0
- package/dist/crawler/crawler.js +683 -0
- package/dist/crawler/extract.d.ts +4 -1
- package/dist/crawler/extract.js +7 -2
- package/dist/crawler/fetcher.d.ts +2 -1
- package/dist/crawler/fetcher.js +26 -11
- package/dist/crawler/metricsRunner.d.ts +23 -1
- package/dist/crawler/metricsRunner.js +202 -72
- package/dist/crawler/normalize.d.ts +41 -0
- package/dist/crawler/normalize.js +119 -3
- package/dist/crawler/parser.d.ts +1 -3
- package/dist/crawler/parser.js +2 -49
- package/dist/crawler/resolver.d.ts +11 -0
- package/dist/crawler/resolver.js +67 -0
- package/dist/crawler/sitemap.d.ts +6 -0
- package/dist/crawler/sitemap.js +27 -17
- package/dist/crawler/trap.d.ts +5 -1
- package/dist/crawler/trap.js +23 -2
- package/dist/db/CrawlithDB.d.ts +110 -0
- package/dist/db/CrawlithDB.js +500 -0
- package/dist/db/graphLoader.js +42 -30
- package/dist/db/index.d.ts +11 -0
- package/dist/db/index.js +41 -29
- package/dist/db/migrations.d.ts +2 -0
- package/dist/db/{schema.js → migrations.js} +90 -43
- package/dist/db/pluginRegistry.d.ts +9 -0
- package/dist/db/pluginRegistry.js +19 -0
- package/dist/db/repositories/EdgeRepository.d.ts +13 -0
- package/dist/db/repositories/EdgeRepository.js +20 -0
- package/dist/db/repositories/MetricsRepository.d.ts +16 -8
- package/dist/db/repositories/MetricsRepository.js +28 -7
- package/dist/db/repositories/PageRepository.d.ts +15 -2
- package/dist/db/repositories/PageRepository.js +169 -25
- package/dist/db/repositories/SiteRepository.d.ts +9 -0
- package/dist/db/repositories/SiteRepository.js +13 -0
- package/dist/db/repositories/SnapshotRepository.d.ts +14 -5
- package/dist/db/repositories/SnapshotRepository.js +64 -5
- package/dist/db/reset.d.ts +9 -0
- package/dist/db/reset.js +32 -0
- package/dist/db/statements.d.ts +12 -0
- package/dist/db/statements.js +40 -0
- package/dist/diff/compare.d.ts +0 -5
- package/dist/diff/compare.js +0 -12
- package/dist/diff/service.d.ts +16 -0
- package/dist/diff/service.js +41 -0
- package/dist/domain/index.d.ts +4 -0
- package/dist/domain/index.js +4 -0
- package/dist/events.d.ts +56 -0
- package/dist/events.js +1 -0
- package/dist/graph/graph.d.ts +36 -42
- package/dist/graph/graph.js +26 -17
- package/dist/graph/hits.d.ts +23 -0
- package/dist/graph/hits.js +111 -0
- package/dist/graph/metrics.d.ts +0 -4
- package/dist/graph/metrics.js +25 -9
- package/dist/graph/pagerank.d.ts +17 -4
- package/dist/graph/pagerank.js +126 -91
- package/dist/graph/simhash.d.ts +6 -0
- package/dist/graph/simhash.js +14 -0
- package/dist/index.d.ts +29 -8
- package/dist/index.js +29 -8
- package/dist/lock/hashKey.js +1 -1
- package/dist/lock/lockManager.d.ts +5 -1
- package/dist/lock/lockManager.js +38 -13
- package/dist/plugin-system/plugin-cli.d.ts +10 -0
- package/dist/plugin-system/plugin-cli.js +31 -0
- package/dist/plugin-system/plugin-config.d.ts +16 -0
- package/dist/plugin-system/plugin-config.js +36 -0
- package/dist/plugin-system/plugin-loader.d.ts +17 -0
- package/dist/plugin-system/plugin-loader.js +122 -0
- package/dist/plugin-system/plugin-registry.d.ts +25 -0
- package/dist/plugin-system/plugin-registry.js +167 -0
- package/dist/plugin-system/plugin-types.d.ts +205 -0
- package/dist/plugin-system/plugin-types.js +1 -0
- package/dist/ports/index.d.ts +9 -0
- package/dist/ports/index.js +1 -0
- package/{src/report/sitegraph_template.ts → dist/report/crawl.html} +330 -81
- package/dist/report/crawlExport.d.ts +3 -0
- package/dist/report/{sitegraphExport.js → crawlExport.js} +3 -3
- package/dist/report/crawl_template.d.ts +1 -0
- package/dist/report/crawl_template.js +7 -0
- package/dist/report/export.d.ts +3 -0
- package/dist/report/export.js +81 -0
- package/dist/report/html.js +15 -216
- package/dist/report/insight.d.ts +27 -0
- package/dist/report/insight.js +103 -0
- package/dist/scoring/health.d.ts +56 -0
- package/dist/scoring/health.js +213 -0
- package/dist/utils/chalk.d.ts +6 -0
- package/dist/utils/chalk.js +41 -0
- package/dist/utils/secureConfig.d.ts +23 -0
- package/dist/utils/secureConfig.js +128 -0
- package/package.json +12 -6
- package/CHANGELOG.md +0 -7
- package/dist/db/schema.d.ts +0 -2
- package/dist/graph/cluster.d.ts +0 -6
- package/dist/graph/cluster.js +0 -173
- package/dist/graph/duplicate.d.ts +0 -10
- package/dist/graph/duplicate.js +0 -251
- package/dist/report/sitegraphExport.d.ts +0 -3
- package/dist/report/sitegraph_template.d.ts +0 -1
- package/dist/report/sitegraph_template.js +0 -630
- package/dist/scoring/hits.d.ts +0 -9
- package/dist/scoring/hits.js +0 -111
- package/src/analysis/analyze.ts +0 -548
- package/src/analysis/content.ts +0 -62
- package/src/analysis/images.ts +0 -28
- package/src/analysis/links.ts +0 -41
- package/src/analysis/scoring.ts +0 -59
- package/src/analysis/seo.ts +0 -82
- package/src/analysis/structuredData.ts +0 -62
- package/src/audit/dns.ts +0 -49
- package/src/audit/headers.ts +0 -98
- package/src/audit/index.ts +0 -66
- package/src/audit/scoring.ts +0 -232
- package/src/audit/transport.ts +0 -258
- package/src/audit/types.ts +0 -102
- package/src/core/network/proxyAdapter.ts +0 -21
- package/src/core/network/rateLimiter.ts +0 -39
- package/src/core/network/redirectController.ts +0 -47
- package/src/core/network/responseLimiter.ts +0 -34
- package/src/core/network/retryPolicy.ts +0 -57
- package/src/core/scope/domainFilter.ts +0 -45
- package/src/core/scope/scopeManager.ts +0 -52
- package/src/core/scope/subdomainPolicy.ts +0 -39
- package/src/core/security/ipGuard.ts +0 -92
- package/src/crawler/crawl.ts +0 -382
- package/src/crawler/extract.ts +0 -34
- package/src/crawler/fetcher.ts +0 -233
- package/src/crawler/metricsRunner.ts +0 -124
- package/src/crawler/normalize.ts +0 -108
- package/src/crawler/parser.ts +0 -190
- package/src/crawler/sitemap.ts +0 -73
- package/src/crawler/trap.ts +0 -96
- package/src/db/graphLoader.ts +0 -105
- package/src/db/index.ts +0 -70
- package/src/db/repositories/EdgeRepository.ts +0 -29
- package/src/db/repositories/MetricsRepository.ts +0 -49
- package/src/db/repositories/PageRepository.ts +0 -128
- package/src/db/repositories/SiteRepository.ts +0 -32
- package/src/db/repositories/SnapshotRepository.ts +0 -74
- package/src/db/schema.ts +0 -177
- package/src/diff/compare.ts +0 -84
- package/src/graph/cluster.ts +0 -192
- package/src/graph/duplicate.ts +0 -286
- package/src/graph/graph.ts +0 -172
- package/src/graph/metrics.ts +0 -110
- package/src/graph/pagerank.ts +0 -125
- package/src/graph/simhash.ts +0 -61
- package/src/index.ts +0 -30
- package/src/lock/hashKey.ts +0 -51
- package/src/lock/lockManager.ts +0 -124
- package/src/lock/pidCheck.ts +0 -13
- package/src/report/html.ts +0 -227
- package/src/report/sitegraphExport.ts +0 -58
- package/src/scoring/hits.ts +0 -131
- package/src/scoring/orphanSeverity.ts +0 -176
- package/src/utils/version.ts +0 -18
- package/tests/__snapshots__/orphanSeverity.test.ts.snap +0 -49
- package/tests/analysis.unit.test.ts +0 -98
- package/tests/analyze.integration.test.ts +0 -98
- package/tests/audit/dns.test.ts +0 -31
- package/tests/audit/headers.test.ts +0 -45
- package/tests/audit/scoring.test.ts +0 -133
- package/tests/audit/security.test.ts +0 -12
- package/tests/audit/transport.test.ts +0 -112
- package/tests/clustering.test.ts +0 -118
- package/tests/crawler.test.ts +0 -358
- package/tests/db.test.ts +0 -159
- package/tests/diff.test.ts +0 -67
- package/tests/duplicate.test.ts +0 -110
- package/tests/fetcher.test.ts +0 -106
- package/tests/fetcher_safety.test.ts +0 -85
- package/tests/fixtures/analyze-crawl.json +0 -26
- package/tests/hits.test.ts +0 -134
- package/tests/html_report.test.ts +0 -58
- package/tests/lock/lockManager.test.ts +0 -138
- package/tests/metrics.test.ts +0 -196
- package/tests/normalize.test.ts +0 -101
- package/tests/orphanSeverity.test.ts +0 -160
- package/tests/pagerank.test.ts +0 -98
- package/tests/parser.test.ts +0 -117
- package/tests/proxy_safety.test.ts +0 -57
- package/tests/redirect_safety.test.ts +0 -73
- package/tests/safety.test.ts +0 -114
- package/tests/scope.test.ts +0 -66
- package/tests/scoring.test.ts +0 -59
- package/tests/sitemap.test.ts +0 -88
- package/tests/soft404.test.ts +0 -41
- package/tests/trap.test.ts +0 -39
- package/tests/visualization_data.test.ts +0 -46
- package/tsconfig.json +0 -11
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { Readable } from 'stream';
|
|
2
|
-
|
|
3
|
-
export class ResponseLimiter {
|
|
4
|
-
static async streamToString(
|
|
5
|
-
stream: Readable,
|
|
6
|
-
maxBytes: number,
|
|
7
|
-
onOversized?: (bytes: number) => void
|
|
8
|
-
): Promise<string> {
|
|
9
|
-
return new Promise((resolve, reject) => {
|
|
10
|
-
let accumulated = 0;
|
|
11
|
-
const chunks: Buffer[] = [];
|
|
12
|
-
|
|
13
|
-
stream.on('data', (chunk: any) => {
|
|
14
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
15
|
-
accumulated += buffer.length;
|
|
16
|
-
if (accumulated > maxBytes) {
|
|
17
|
-
stream.destroy();
|
|
18
|
-
if (onOversized) onOversized(accumulated);
|
|
19
|
-
reject(new Error('Oversized response'));
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
chunks.push(buffer);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
stream.on('end', () => {
|
|
26
|
-
resolve(Buffer.concat(chunks).toString('utf-8'));
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
stream.on('error', (err) => {
|
|
30
|
-
reject(err);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
export interface RetryConfig {
|
|
2
|
-
maxRetries: number;
|
|
3
|
-
baseDelay: number;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export class RetryPolicy {
|
|
7
|
-
static DEFAULT_CONFIG: RetryConfig = {
|
|
8
|
-
maxRetries: 3,
|
|
9
|
-
baseDelay: 500
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
static async execute<T>(
|
|
13
|
-
operation: (attempt: number) => Promise<T>,
|
|
14
|
-
isRetryable: (error: any) => boolean,
|
|
15
|
-
config: RetryConfig = RetryPolicy.DEFAULT_CONFIG
|
|
16
|
-
): Promise<T> {
|
|
17
|
-
let lastError: any;
|
|
18
|
-
|
|
19
|
-
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
20
|
-
try {
|
|
21
|
-
return await operation(attempt);
|
|
22
|
-
} catch (error) {
|
|
23
|
-
lastError = error;
|
|
24
|
-
|
|
25
|
-
if (attempt === config.maxRetries || !isRetryable(error)) {
|
|
26
|
-
throw error;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const delay = config.baseDelay * Math.pow(2, attempt);
|
|
30
|
-
const jitter = delay * 0.1 * (Math.random() * 2 - 1);
|
|
31
|
-
const finalDelay = Math.max(0, delay + jitter);
|
|
32
|
-
|
|
33
|
-
await new Promise(resolve => setTimeout(resolve, finalDelay));
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
throw lastError;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
static isRetryableStatus(status: number): boolean {
|
|
41
|
-
return status === 429 || (status >= 500 && status <= 599);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
static isNetworkError(error: any): boolean {
|
|
45
|
-
const code = error?.code || error?.cause?.code;
|
|
46
|
-
return [
|
|
47
|
-
'ETIMEDOUT',
|
|
48
|
-
'ECONNRESET',
|
|
49
|
-
'EADDRINUSE',
|
|
50
|
-
'ECONNREFUSED',
|
|
51
|
-
'EPIPE',
|
|
52
|
-
'ENOTFOUND',
|
|
53
|
-
'ENETUNREACH',
|
|
54
|
-
'EAI_AGAIN'
|
|
55
|
-
].includes(code);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
export class DomainFilter {
|
|
2
|
-
private allowed: Set<string>;
|
|
3
|
-
private denied: Set<string>;
|
|
4
|
-
|
|
5
|
-
constructor(allowed: string[] = [], denied: string[] = []) {
|
|
6
|
-
this.allowed = new Set(allowed.map(d => this.normalize(d)));
|
|
7
|
-
this.denied = new Set(denied.map(d => this.normalize(d)));
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Normalizes a hostname: lowercase, strip trailing dot.
|
|
12
|
-
* Note: We expect hostnames, not URLs.
|
|
13
|
-
*/
|
|
14
|
-
private normalize(hostname: string): string {
|
|
15
|
-
let h = hostname.toLowerCase().trim();
|
|
16
|
-
if (h.endsWith('.')) {
|
|
17
|
-
h = h.slice(0, -1);
|
|
18
|
-
}
|
|
19
|
-
// Use URL to handle punycode and basic validation if possible
|
|
20
|
-
try {
|
|
21
|
-
// We wrap it in a dummy URL to let the browser/node logic normalize it
|
|
22
|
-
const url = new URL(`http://${h}`);
|
|
23
|
-
return url.hostname;
|
|
24
|
-
} catch {
|
|
25
|
-
return h;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
isAllowed(hostname: string): boolean {
|
|
30
|
-
const normalized = this.normalize(hostname);
|
|
31
|
-
|
|
32
|
-
// 1. Deny list match -> Reject
|
|
33
|
-
if (this.denied.has(normalized)) {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 2. Allow list not empty AND no match -> Reject
|
|
38
|
-
if (this.allowed.size > 0 && !this.allowed.has(normalized)) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// 3. Otherwise -> Allow
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { DomainFilter } from './domainFilter.js';
|
|
2
|
-
import { SubdomainPolicy } from './subdomainPolicy.js';
|
|
3
|
-
|
|
4
|
-
export interface ScopeOptions {
|
|
5
|
-
allowedDomains?: string[];
|
|
6
|
-
deniedDomains?: string[];
|
|
7
|
-
includeSubdomains?: boolean;
|
|
8
|
-
rootUrl: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type EligibilityResult = 'allowed' | 'blocked_by_domain_filter' | 'blocked_subdomain';
|
|
12
|
-
|
|
13
|
-
export class ScopeManager {
|
|
14
|
-
private domainFilter: DomainFilter;
|
|
15
|
-
private subdomainPolicy: SubdomainPolicy;
|
|
16
|
-
private explicitAllowed: Set<string>;
|
|
17
|
-
|
|
18
|
-
constructor(options: ScopeOptions) {
|
|
19
|
-
this.domainFilter = new DomainFilter(options.allowedDomains, options.deniedDomains);
|
|
20
|
-
this.subdomainPolicy = new SubdomainPolicy(options.rootUrl, options.includeSubdomains);
|
|
21
|
-
this.explicitAllowed = new Set((options.allowedDomains || []).map(d => {
|
|
22
|
-
let h = d.toLowerCase().trim();
|
|
23
|
-
if (h.endsWith('.')) h = h.slice(0, -1);
|
|
24
|
-
return h;
|
|
25
|
-
}));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
isUrlEligible(url: string): EligibilityResult {
|
|
29
|
-
let hostname: string;
|
|
30
|
-
try {
|
|
31
|
-
hostname = new URL(url).hostname.toLowerCase();
|
|
32
|
-
if (hostname.endsWith('.')) hostname = hostname.slice(0, -1);
|
|
33
|
-
} catch {
|
|
34
|
-
return 'blocked_by_domain_filter'; // Invalid URL is effectively blocked
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!this.domainFilter.isAllowed(hostname)) {
|
|
38
|
-
return 'blocked_by_domain_filter';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// If explicit whitelist is used, and this domain is in it, allow it
|
|
42
|
-
if (this.explicitAllowed.has(hostname)) {
|
|
43
|
-
return 'allowed';
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (!this.subdomainPolicy.isAllowed(hostname)) {
|
|
47
|
-
return 'blocked_subdomain';
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return 'allowed';
|
|
51
|
-
}
|
|
52
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
export class SubdomainPolicy {
|
|
2
|
-
private rootHost: string;
|
|
3
|
-
private includeSubdomains: boolean;
|
|
4
|
-
|
|
5
|
-
constructor(rootUrl: string, includeSubdomains: boolean = false) {
|
|
6
|
-
try {
|
|
7
|
-
this.rootHost = new URL(rootUrl).hostname.toLowerCase();
|
|
8
|
-
if (this.rootHost.endsWith('.')) {
|
|
9
|
-
this.rootHost = this.rootHost.slice(0, -1);
|
|
10
|
-
}
|
|
11
|
-
} catch {
|
|
12
|
-
this.rootHost = '';
|
|
13
|
-
}
|
|
14
|
-
this.includeSubdomains = includeSubdomains;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
isAllowed(hostname: string): boolean {
|
|
18
|
-
let target = hostname.toLowerCase().trim();
|
|
19
|
-
if (target.endsWith('.')) {
|
|
20
|
-
target = target.slice(0, -1);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Exact match is always allowed if rootHost is set
|
|
24
|
-
if (target === this.rootHost) {
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (!this.includeSubdomains) {
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Label-based check for subdomains
|
|
33
|
-
// target must end with .rootHost
|
|
34
|
-
if (!target.endsWith(`.${this.rootHost}`)) {
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import * as dns from 'dns';
|
|
2
|
-
import * as net from 'net';
|
|
3
|
-
import { promisify } from 'util';
|
|
4
|
-
|
|
5
|
-
const resolve4 = promisify(dns.resolve4);
|
|
6
|
-
const resolve6 = promisify(dns.resolve6);
|
|
7
|
-
|
|
8
|
-
export class IPGuard {
|
|
9
|
-
/**
|
|
10
|
-
* Checks if an IP address is internal/private
|
|
11
|
-
*/
|
|
12
|
-
static isInternal(ip: string): boolean {
|
|
13
|
-
if (net.isIPv4(ip)) {
|
|
14
|
-
const parts = ip.split('.').map(Number);
|
|
15
|
-
|
|
16
|
-
// 127.0.0.0/8
|
|
17
|
-
if (parts[0] === 127) return true;
|
|
18
|
-
|
|
19
|
-
// 10.0.0.0/8
|
|
20
|
-
if (parts[0] === 10) return true;
|
|
21
|
-
|
|
22
|
-
// 192.168.0.0/16
|
|
23
|
-
if (parts[0] === 192 && parts[1] === 168) return true;
|
|
24
|
-
|
|
25
|
-
// 172.16.0.0 – 172.31.255.255
|
|
26
|
-
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
|
|
27
|
-
|
|
28
|
-
// 169.254.0.0/16
|
|
29
|
-
if (parts[0] === 169 && parts[1] === 254) return true;
|
|
30
|
-
|
|
31
|
-
// 0.0.0.0/8
|
|
32
|
-
if (parts[0] === 0) return true;
|
|
33
|
-
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (net.isIPv6(ip)) {
|
|
38
|
-
// Normalize IPv6
|
|
39
|
-
const expanded = IPGuard.expandIPv6(ip);
|
|
40
|
-
|
|
41
|
-
// ::1
|
|
42
|
-
if (expanded === '0000:0000:0000:0000:0000:0000:0000:0001') return true;
|
|
43
|
-
|
|
44
|
-
// fc00::/7 (Unique Local Address) -> fc or fd
|
|
45
|
-
const firstWord = parseInt(expanded.split(':')[0], 16);
|
|
46
|
-
if ((firstWord & 0xfe00) === 0xfc00) return true;
|
|
47
|
-
|
|
48
|
-
// fe80::/10 (Link Local)
|
|
49
|
-
if ((firstWord & 0xffc0) === 0xfe80) return true;
|
|
50
|
-
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return true; // Unknown format, block it for safety
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Resolves a hostname and validates all result IPs
|
|
59
|
-
*/
|
|
60
|
-
static async validateHost(host: string): Promise<boolean> {
|
|
61
|
-
if (net.isIP(host)) {
|
|
62
|
-
return !IPGuard.isInternal(host);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const res4 = await resolve4(host).catch(() => [] as string[]);
|
|
67
|
-
const res6 = await resolve6(host).catch(() => [] as string[]);
|
|
68
|
-
const ips = [...res4, ...res6];
|
|
69
|
-
|
|
70
|
-
if (ips.length === 0) return true; // Let the fetcher handle DNS failures
|
|
71
|
-
|
|
72
|
-
return ips.every(ip => !IPGuard.isInternal(ip));
|
|
73
|
-
} catch (_e) {
|
|
74
|
-
// If resolution fails drastically, we block for safety or let fetcher try
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private static expandIPv6(ip: string): string {
|
|
80
|
-
if (ip === '::') return '0000:0000:0000:0000:0000:0000:0000:0000';
|
|
81
|
-
let full = ip;
|
|
82
|
-
if (ip.includes('::')) {
|
|
83
|
-
const parts = ip.split('::');
|
|
84
|
-
const left = parts[0].split(':').filter(x => x !== '');
|
|
85
|
-
const right = parts[1].split(':').filter(x => x !== '');
|
|
86
|
-
const missing = 8 - (left.length + right.length);
|
|
87
|
-
const middle = Array(missing).fill('0000');
|
|
88
|
-
full = [...left, ...middle, ...right].join(':');
|
|
89
|
-
}
|
|
90
|
-
return full.split(':').map(part => part.padStart(4, '0')).join(':');
|
|
91
|
-
}
|
|
92
|
-
}
|