Update server.js
Browse files
server.js
CHANGED
|
@@ -45,9 +45,9 @@ let tosdrCache = new Map();
|
|
| 45 |
let geoReader = null;
|
| 46 |
let useMaxMind = false;
|
| 47 |
|
| 48 |
-
//
|
| 49 |
const mutex = new Mutex();
|
| 50 |
-
const scanMutex = withTimeout(mutex, 5 * 60 * 1000); // 5 دقائق
|
| 51 |
|
| 52 |
// ==================== مجمع المتصفح (Browser Pool) ====================
|
| 53 |
class BrowserPool {
|
|
@@ -100,7 +100,7 @@ class BrowserPool {
|
|
| 100 |
'--disable-background-timer-throttling',
|
| 101 |
'--disable-renderer-backgrounding'
|
| 102 |
],
|
| 103 |
-
protocolTimeout: 240000,
|
| 104 |
});
|
| 105 |
console.log('🚀 Browser launched');
|
| 106 |
this.requestCount = 1;
|
|
@@ -288,47 +288,20 @@ async function getTosdrGrade(url) {
|
|
| 288 |
}
|
| 289 |
}
|
| 290 |
|
| 291 |
-
// ==================== فح
|
| 292 |
-
async function analyzePrivacyPolicy(url) {
|
| 293 |
-
try {
|
| 294 |
-
const { page, context } = await pool.newTab();
|
| 295 |
-
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
| 296 |
-
await page.waitForTimeout(2000);
|
| 297 |
-
const policyData = await page.evaluate(() => {
|
| 298 |
-
const bodyText = document.body.innerText.toLowerCase();
|
| 299 |
-
const links = Array.from(document.querySelectorAll('a'))
|
| 300 |
-
.map(a => ({ text: a.innerText.toLowerCase(), href: a.href }))
|
| 301 |
-
.filter(l => l.text.includes('privacy') || l.text.includes('policy') || l.text.includes('data') || l.text.includes('terms'));
|
| 302 |
-
const hasPrivacyLink = links.length > 0;
|
| 303 |
-
const privacyUrl = links.find(l => l.text.includes('privacy'))?.href || null;
|
| 304 |
-
const dataSaleMentions = bodyText.includes('sell') && (bodyText.includes('data') || bodyText.includes('information'));
|
| 305 |
-
const thirdPartyShare = bodyText.includes('third party') || bodyText.includes('third-party') || bodyText.includes('partners');
|
| 306 |
-
const retentionMention = bodyText.match(/retain.*?(\d+)\s*(day|month|year)/i);
|
| 307 |
-
const dataRetention = retentionMention ? `${retentionMention[1]} ${retentionMention[2]}` : 'Not specified';
|
| 308 |
-
return {
|
| 309 |
-
has_privacy_link: hasPrivacyLink,
|
| 310 |
-
privacy_policy_url: privacyUrl,
|
| 311 |
-
data_sale_indicated: dataSaleMentions,
|
| 312 |
-
third_party_sharing: thirdPartyShare,
|
| 313 |
-
data_retention_period: dataRetention,
|
| 314 |
-
analyzed_text_length: bodyText.length
|
| 315 |
-
};
|
| 316 |
-
});
|
| 317 |
-
await pool.closeTab(context);
|
| 318 |
-
return policyData;
|
| 319 |
-
} catch (e) {
|
| 320 |
-
console.error('Privacy policy analysis failed:', e.message);
|
| 321 |
-
return null;
|
| 322 |
-
}
|
| 323 |
-
}
|
| 324 |
|
| 325 |
-
/
|
| 326 |
async function takeScreenshot(url) {
|
| 327 |
const { page, context } = await pool.newTab();
|
| 328 |
try {
|
| 329 |
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
| 330 |
-
await page.waitForTimeout(
|
| 331 |
-
const buffer = await page.screenshot({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
return `data:image/jpeg;base64,${buffer.toString('base64')}`;
|
| 333 |
} catch (e) {
|
| 334 |
console.error('Screenshot failed:', e.message);
|
|
@@ -468,6 +441,55 @@ async function collectFingerprintRisks(url) {
|
|
| 468 |
}
|
| 469 |
}
|
| 470 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
async function trackRedirectChain(url) {
|
| 472 |
const TRACKING_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'mc_eid', 'ref', 'affiliate', 'clickid', 'adid', 'msclkid', 'twclid', '_ga', 'igshid'];
|
| 473 |
const chain = [];
|
|
@@ -540,7 +562,7 @@ async function performBlacklightScan(url) {
|
|
| 540 |
} catch (e) {}
|
| 541 |
return result;
|
| 542 |
} catch (e) {
|
| 543 |
-
console.error('Blacklight scan failed
|
| 544 |
return { hosts: {}, cookies: [], error: e.message };
|
| 545 |
}
|
| 546 |
}
|
|
@@ -612,178 +634,15 @@ function calculatePrivacyScore(blacklight, enrichedTrackers, tosdrGrade, securit
|
|
| 612 |
return { score, grade };
|
| 613 |
}
|
| 614 |
|
| 615 |
-
// ==================== نقطة نهاية
|
| 616 |
app.get('/api/scan', async (req, res) => {
|
| 617 |
-
const url = normalizeUrl(req.query.url);
|
| 618 |
-
if (!url) return res.status(400).json({ error: 'Invalid URL' });
|
| 619 |
-
|
| 620 |
-
// استخدام runExclusive لضمان تحرير القفل تلقائيًا
|
| 621 |
-
try {
|
| 622 |
-
const result = await scanMutex.runExclusive(async () => {
|
| 623 |
-
const startTime = Date.now();
|
| 624 |
-
|
| 625 |
-
const [
|
| 626 |
-
screenshotResult, hiddenStorageResult, cookieConsentResult,
|
| 627 |
-
fingerprintResult, redirectChainResult, blacklightResult,
|
| 628 |
-
tosdrResult, securityResult, privacyPolicyResult
|
| 629 |
-
] = await Promise.allSettled([
|
| 630 |
-
takeScreenshot(url),
|
| 631 |
-
checkHiddenStorage(url),
|
| 632 |
-
analyzeCookieConsent(url),
|
| 633 |
-
collectFingerprintRisks(url),
|
| 634 |
-
trackRedirectChain(url),
|
| 635 |
-
performBlacklightScan(url),
|
| 636 |
-
getTosdrGrade(url),
|
| 637 |
-
performSecurityCheck(url),
|
| 638 |
-
analyzePrivacyPolicy(url)
|
| 639 |
-
]);
|
| 640 |
-
|
| 641 |
-
const screenshotData = screenshotResult.status === 'fulfilled' ? screenshotResult.value : null;
|
| 642 |
-
const blacklightData = blacklightResult.status === 'fulfilled' ? blacklightResult.value : { hosts: {}, cookies: [] };
|
| 643 |
-
const tosdrData = tosdrResult.status === 'fulfilled' ? tosdrResult.value : null;
|
| 644 |
-
const securityData = securityResult.status === 'fulfilled' ? securityResult.value : { ssl: { valid: false } };
|
| 645 |
-
const hiddenData = hiddenStorageResult.status === 'fulfilled' ? hiddenStorageResult.value : null;
|
| 646 |
-
const cookieConsentData = cookieConsentResult.status === 'fulfilled' ? cookieConsentResult.value : null;
|
| 647 |
-
const fingerprintData = fingerprintResult.status === 'fulfilled' ? fingerprintResult.value : null;
|
| 648 |
-
const redirectData = redirectChainResult.status === 'fulfilled' ? redirectChainResult.value : null;
|
| 649 |
-
const privacyPolicyData = privacyPolicyResult.status === 'fulfilled' ? privacyPolicyResult.value : null;
|
| 650 |
-
|
| 651 |
-
// تجميع المتتبعين
|
| 652 |
-
const thirdPartyDomains = [
|
| 653 |
-
...(blacklightData.hosts?.thirdParty || []),
|
| 654 |
-
...(blacklightData.hosts?.requests?.third_party || [])
|
| 655 |
-
];
|
| 656 |
-
const uniqueDomains = [...new Set(thirdPartyDomains)];
|
| 657 |
-
const enrichedTrackers = [];
|
| 658 |
-
for (const domain of uniqueDomains) {
|
| 659 |
-
try {
|
| 660 |
-
const ddgInfo = getDDGInfo(domain);
|
| 661 |
-
const ghosteryInfo = await getGhosteryInfo(domain);
|
| 662 |
-
enrichedTrackers.push({
|
| 663 |
-
domain,
|
| 664 |
-
owner: ghosteryInfo?.organization || ddgInfo?.owner || getBaseDomain(domain),
|
| 665 |
-
category: ghosteryInfo?.category || ddgInfo?.category || 'unknown',
|
| 666 |
-
prevalence: ddgInfo?.prevalence || 0
|
| 667 |
-
});
|
| 668 |
-
} catch (e) {
|
| 669 |
-
enrichedTrackers.push({ domain, owner: getBaseDomain(domain), category: 'unknown', prevalence: 0 });
|
| 670 |
-
}
|
| 671 |
-
}
|
| 672 |
-
|
| 673 |
-
// Geo mapping
|
| 674 |
-
const uniqueIps = [...new Set((blacklightData.hosts?.requests?.third_party || []).map(r => r.ip_addr).filter(Boolean))];
|
| 675 |
-
const geoDestinations = [];
|
| 676 |
-
for (const ip of uniqueIps) {
|
| 677 |
-
try {
|
| 678 |
-
let geoData = null;
|
| 679 |
-
if (useMaxMind && geoReader) {
|
| 680 |
-
const mmGeo = geoReader.get(ip);
|
| 681 |
-
if (mmGeo) {
|
| 682 |
-
geoData = {
|
| 683 |
-
ip,
|
| 684 |
-
country: mmGeo.country?.names?.en || 'Unknown',
|
| 685 |
-
city: mmGeo.city?.names?.en || 'Unknown',
|
| 686 |
-
latitude: mmGeo.location?.latitude,
|
| 687 |
-
longitude: mmGeo.location?.longitude
|
| 688 |
-
};
|
| 689 |
-
}
|
| 690 |
-
}
|
| 691 |
-
if (!geoData) {
|
| 692 |
-
const geoLite = geoip.lookup(ip);
|
| 693 |
-
if (geoLite) {
|
| 694 |
-
geoData = {
|
| 695 |
-
ip,
|
| 696 |
-
country: geoLite.country,
|
| 697 |
-
city: geoLite.city,
|
| 698 |
-
latitude: geoLite.ll?.[0] || null,
|
| 699 |
-
longitude: geoLite.ll?.[1] || null
|
| 700 |
-
};
|
| 701 |
-
}
|
| 702 |
-
}
|
| 703 |
-
if (geoData) geoDestinations.push(geoData);
|
| 704 |
-
} catch (e) {}
|
| 705 |
-
}
|
| 706 |
-
|
| 707 |
-
// PII Leakage
|
| 708 |
-
const leakageAlerts = [];
|
| 709 |
-
for (const r of (blacklightData.hosts?.requests?.third_party || [])) {
|
| 710 |
-
if ((r.method === 'POST' || r.method === 'PUT') && r.body) {
|
| 711 |
-
try {
|
| 712 |
-
const detected = piiFilter.detect(r.body);
|
| 713 |
-
if (detected?.length > 0) {
|
| 714 |
-
leakageAlerts.push({
|
| 715 |
-
severity: 'high',
|
| 716 |
-
destination: r.url,
|
| 717 |
-
method: r.method,
|
| 718 |
-
types: detected.map(p => p.type),
|
| 719 |
-
message: 'Potential PII detected in request to third-party domain.'
|
| 720 |
-
});
|
| 721 |
-
}
|
| 722 |
-
} catch (e) {}
|
| 723 |
-
}
|
| 724 |
-
}
|
| 725 |
-
|
| 726 |
-
const { score, grade } = calculatePrivacyScore(
|
| 727 |
-
blacklightData, enrichedTrackers, tosdrData,
|
| 728 |
-
securityData, cookieConsentData, fingerprintData,
|
| 729 |
-
redirectData, privacyPolicyData
|
| 730 |
-
);
|
| 731 |
-
|
| 732 |
-
return {
|
| 733 |
-
success: true,
|
| 734 |
-
url,
|
| 735 |
-
final_url: blacklightData.uri_dest || url,
|
| 736 |
-
scan_time_sec: (Date.now() - startTime) / 1000,
|
| 737 |
-
privacy_score: { score, grade },
|
| 738 |
-
trackers: { count: enrichedTrackers.length, list: enrichedTrackers.slice(0, 20) },
|
| 739 |
-
cookies: {
|
| 740 |
-
total: blacklightData.cookies?.length || 0,
|
| 741 |
-
third_party: blacklightData.cookies?.filter(c => c.thirdParty)?.length || 0
|
| 742 |
-
},
|
| 743 |
-
fingerprinting: {
|
| 744 |
-
canvas: !!(blacklightData.canvasFingerprinters?.length),
|
| 745 |
-
fonts: !!(blacklightData.canvasFontFingerprinters?.length),
|
| 746 |
-
live_risks: fingerprintData
|
| 747 |
-
},
|
| 748 |
-
session_recording: !!(blacklightData.sessionRecorders?.length),
|
| 749 |
-
key_logging: !!(blacklightData.keyLogging?.length),
|
| 750 |
-
hidden_storage: hiddenData ? {
|
| 751 |
-
localStorage: hiddenData.localStorage?.length || 0,
|
| 752 |
-
sessionStorage: hiddenData.sessionStorage?.length || 0,
|
| 753 |
-
indexedDB: hiddenData.indexedDB
|
| 754 |
-
} : null,
|
| 755 |
-
cookie_consent: cookieConsentData,
|
| 756 |
-
redirect_chain: redirectData,
|
| 757 |
-
security: securityData,
|
| 758 |
-
tosdr: tosdrData,
|
| 759 |
-
privacy_policy_analysis: privacyPolicyData,
|
| 760 |
-
geo_mapping: { data_destinations: geoDestinations },
|
| 761 |
-
leakage_detection: { alerts: leakageAlerts },
|
| 762 |
-
screenshot: screenshotData,
|
| 763 |
-
raw: blacklightData
|
| 764 |
-
};
|
| 765 |
-
});
|
| 766 |
-
|
| 767 |
-
res.json(result);
|
| 768 |
-
} catch (e) {
|
| 769 |
-
console.error('Scan error:', e);
|
| 770 |
-
// التحقق مما إذا كان الخطأ بسبب انتهاء مهلة القفل
|
| 771 |
-
if (e.message.includes('timeout')) {
|
| 772 |
-
res.status(503).json({ error: 'Server is busy, please try again later.' });
|
| 773 |
-
} else {
|
| 774 |
-
res.status(500).json({ success: false, error: e.message });
|
| 775 |
-
}
|
| 776 |
-
}
|
| 777 |
-
});
|
| 778 |
-
|
| 779 |
-
// ==================== نقطة نهاية SSE (تقدم حي) ====================
|
| 780 |
-
app.get('/api/scan/live', async (req, res) => {
|
| 781 |
const url = normalizeUrl(req.query.url);
|
| 782 |
if (!url) {
|
| 783 |
res.status(400).send('Invalid URL');
|
| 784 |
return;
|
| 785 |
}
|
| 786 |
|
|
|
|
| 787 |
res.writeHead(200, {
|
| 788 |
'Content-Type': 'text/event-stream',
|
| 789 |
'Cache-Control': 'no-cache',
|
|
@@ -797,33 +656,86 @@ app.get('/api/scan/live', async (req, res) => {
|
|
| 797 |
|
| 798 |
sendEvent('start', { url, message: 'Scan started...' });
|
| 799 |
|
|
|
|
| 800 |
try {
|
| 801 |
await scanMutex.runExclusive(async () => {
|
| 802 |
const startTime = Date.now();
|
| 803 |
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 814 |
];
|
| 815 |
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
const
|
| 825 |
-
|
| 826 |
-
|
|
|
|
|
|
|
| 827 |
|
| 828 |
// تجميع المتتبعين
|
| 829 |
const thirdPartyDomains = [
|
|
@@ -900,12 +812,20 @@ app.get('/api/scan/live', async (req, res) => {
|
|
| 900 |
}
|
| 901 |
}
|
| 902 |
|
|
|
|
|
|
|
| 903 |
const { score, grade } = calculatePrivacyScore(
|
| 904 |
-
blacklightData,
|
| 905 |
-
|
| 906 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 907 |
);
|
| 908 |
|
|
|
|
| 909 |
const finalResult = {
|
| 910 |
success: true,
|
| 911 |
url,
|
|
@@ -920,23 +840,23 @@ app.get('/api/scan/live', async (req, res) => {
|
|
| 920 |
fingerprinting: {
|
| 921 |
canvas: !!(blacklightData.canvasFingerprinters?.length),
|
| 922 |
fonts: !!(blacklightData.canvasFontFingerprinters?.length),
|
| 923 |
-
live_risks:
|
| 924 |
},
|
| 925 |
session_recording: !!(blacklightData.sessionRecorders?.length),
|
| 926 |
key_logging: !!(blacklightData.keyLogging?.length),
|
| 927 |
-
hidden_storage:
|
| 928 |
-
localStorage:
|
| 929 |
-
sessionStorage:
|
| 930 |
-
indexedDB:
|
| 931 |
} : null,
|
| 932 |
-
cookie_consent:
|
| 933 |
-
redirect_chain:
|
| 934 |
security: securityData,
|
| 935 |
-
tosdr:
|
| 936 |
-
privacy_policy_analysis:
|
| 937 |
geo_mapping: { data_destinations: geoDestinations },
|
| 938 |
leakage_detection: { alerts: leakageAlerts },
|
| 939 |
-
screenshot:
|
| 940 |
raw: blacklightData
|
| 941 |
};
|
| 942 |
|
|
@@ -945,11 +865,7 @@ app.get('/api/scan/live', async (req, res) => {
|
|
| 945 |
});
|
| 946 |
} catch (e) {
|
| 947 |
console.error('Live scan error:', e);
|
| 948 |
-
|
| 949 |
-
sendEvent('error', { error: 'Server is busy, please try again later.' });
|
| 950 |
-
} else {
|
| 951 |
-
sendEvent('error', { error: e.message });
|
| 952 |
-
}
|
| 953 |
} finally {
|
| 954 |
res.end();
|
| 955 |
}
|
|
@@ -970,6 +886,6 @@ app.get('/health', (req, res) => res.json({ status: 'ok' }));
|
|
| 970 |
await downloadDDGTrackerRadar();
|
| 971 |
}
|
| 972 |
app.listen(PORT, '0.0.0.0', () => {
|
| 973 |
-
console.log(`🚀 Private Eye API running on port ${PORT}`);
|
| 974 |
});
|
| 975 |
})();
|
|
|
|
| 45 |
let geoReader = null;
|
| 46 |
let useMaxMind = false;
|
| 47 |
|
| 48 |
+
// قفل لمنع تداخل الطلبات (مهم لـ Hugging Face)
|
| 49 |
const mutex = new Mutex();
|
| 50 |
+
const scanMutex = withTimeout(mutex, 5 * 60 * 1000); // 5 دقائق كحد أقصى
|
| 51 |
|
| 52 |
// ==================== مجمع المتصفح (Browser Pool) ====================
|
| 53 |
class BrowserPool {
|
|
|
|
| 100 |
'--disable-background-timer-throttling',
|
| 101 |
'--disable-renderer-backgrounding'
|
| 102 |
],
|
| 103 |
+
protocolTimeout: 240000, // 4 دقائق
|
| 104 |
});
|
| 105 |
console.log('🚀 Browser launched');
|
| 106 |
this.requestCount = 1;
|
|
|
|
| 288 |
}
|
| 289 |
}
|
| 290 |
|
| 291 |
+
// ==================== أدوات المتصفح (كل منها تفتح وتغلق context فورًا) ====================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
|
| 293 |
+
/** لقطة شاشة سريعة وخفيفة */
|
| 294 |
async function takeScreenshot(url) {
|
| 295 |
const { page, context } = await pool.newTab();
|
| 296 |
try {
|
| 297 |
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
| 298 |
+
await page.waitForTimeout(500);
|
| 299 |
+
const buffer = await page.screenshot({
|
| 300 |
+
type: 'jpeg',
|
| 301 |
+
quality: 50,
|
| 302 |
+
fullPage: false,
|
| 303 |
+
clip: { x: 0, y: 0, width: 1024, height: 768 }
|
| 304 |
+
});
|
| 305 |
return `data:image/jpeg;base64,${buffer.toString('base64')}`;
|
| 306 |
} catch (e) {
|
| 307 |
console.error('Screenshot failed:', e.message);
|
|
|
|
| 441 |
}
|
| 442 |
}
|
| 443 |
|
| 444 |
+
async function analyzePrivacyPolicy(url) {
|
| 445 |
+
const { page, context } = await pool.newTab();
|
| 446 |
+
try {
|
| 447 |
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
| 448 |
+
await page.waitForTimeout(2000);
|
| 449 |
+
const privacyLink = await page.evaluate(() => {
|
| 450 |
+
const links = Array.from(document.querySelectorAll('a'));
|
| 451 |
+
const privacyLink = links.find(a =>
|
| 452 |
+
a.innerText.toLowerCase().includes('privacy') ||
|
| 453 |
+
a.innerText.toLowerCase().includes('policy')
|
| 454 |
+
);
|
| 455 |
+
return privacyLink ? privacyLink.href : null;
|
| 456 |
+
});
|
| 457 |
+
|
| 458 |
+
let policyUrl = privacyLink;
|
| 459 |
+
let analysisText = '';
|
| 460 |
+
|
| 461 |
+
if (policyUrl) {
|
| 462 |
+
await page.goto(policyUrl, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
| 463 |
+
await page.waitForTimeout(2000);
|
| 464 |
+
analysisText = await page.evaluate(() => document.body.innerText.toLowerCase());
|
| 465 |
+
} else {
|
| 466 |
+
analysisText = await page.evaluate(() => document.body.innerText.toLowerCase());
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
const dataSaleMentions = analysisText.includes('sell') && (analysisText.includes('data') || analysisText.includes('information'));
|
| 470 |
+
const thirdPartyShare = analysisText.includes('third party') || analysisText.includes('third-party') || analysisText.includes('partners');
|
| 471 |
+
const retentionMention = analysisText.match(/retain.*?(\d+)\s*(day|month|year)/i);
|
| 472 |
+
const dataRetention = retentionMention ? `${retentionMention[1]} ${retentionMention[2]}` : 'Not specified';
|
| 473 |
+
|
| 474 |
+
return {
|
| 475 |
+
has_privacy_link: !!privacyLink,
|
| 476 |
+
privacy_policy_url: policyUrl || null,
|
| 477 |
+
data_sale_indicated: dataSaleMentions,
|
| 478 |
+
third_party_sharing: thirdPartyShare,
|
| 479 |
+
data_retention_period: dataRetention,
|
| 480 |
+
analyzed_text_length: analysisText.length,
|
| 481 |
+
source: policyUrl ? 'privacy_policy_page' : 'homepage'
|
| 482 |
+
};
|
| 483 |
+
} catch (e) {
|
| 484 |
+
console.error('Privacy policy analysis failed:', e.message);
|
| 485 |
+
return null;
|
| 486 |
+
} finally {
|
| 487 |
+
await pool.closeTab(context);
|
| 488 |
+
}
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
// ==================== أدوات لا تحتاج متصفح ====================
|
| 492 |
+
|
| 493 |
async function trackRedirectChain(url) {
|
| 494 |
const TRACKING_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'mc_eid', 'ref', 'affiliate', 'clickid', 'adid', 'msclkid', 'twclid', '_ga', 'igshid'];
|
| 495 |
const chain = [];
|
|
|
|
| 562 |
} catch (e) {}
|
| 563 |
return result;
|
| 564 |
} catch (e) {
|
| 565 |
+
console.error('Blacklight scan failed:', e.message);
|
| 566 |
return { hosts: {}, cookies: [], error: e.message };
|
| 567 |
}
|
| 568 |
}
|
|
|
|
| 634 |
return { score, grade };
|
| 635 |
}
|
| 636 |
|
| 637 |
+
// ==================== نقطة النهاية الوحيدة: SSE Live Scan ====================
|
| 638 |
app.get('/api/scan', async (req, res) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
const url = normalizeUrl(req.query.url);
|
| 640 |
if (!url) {
|
| 641 |
res.status(400).send('Invalid URL');
|
| 642 |
return;
|
| 643 |
}
|
| 644 |
|
| 645 |
+
// إعداد رؤوس SSE
|
| 646 |
res.writeHead(200, {
|
| 647 |
'Content-Type': 'text/event-stream',
|
| 648 |
'Cache-Control': 'no-cache',
|
|
|
|
| 656 |
|
| 657 |
sendEvent('start', { url, message: 'Scan started...' });
|
| 658 |
|
| 659 |
+
// استخدام القفل لضمان طلب واحد فقط في وقت واحد
|
| 660 |
try {
|
| 661 |
await scanMutex.runExclusive(async () => {
|
| 662 |
const startTime = Date.now();
|
| 663 |
|
| 664 |
+
// تخزين النتائج مؤقتًا
|
| 665 |
+
const results = {
|
| 666 |
+
screenshot: null,
|
| 667 |
+
hiddenStorage: null,
|
| 668 |
+
cookieConsent: null,
|
| 669 |
+
fingerprint: null,
|
| 670 |
+
redirectChain: null,
|
| 671 |
+
blacklight: null,
|
| 672 |
+
tosdr: null,
|
| 673 |
+
security: null,
|
| 674 |
+
privacyPolicy: null
|
| 675 |
+
};
|
| 676 |
+
|
| 677 |
+
// تعريف الأدوات مع وظيفة إرسال التقدم فور الانتهاء
|
| 678 |
+
const tools = [
|
| 679 |
+
{
|
| 680 |
+
name: 'screenshot',
|
| 681 |
+
fn: () => takeScreenshot(url),
|
| 682 |
+
onDone: (data) => { results.screenshot = data; sendEvent('progress', { step: 'screenshot', status: 'done' }); }
|
| 683 |
+
},
|
| 684 |
+
{
|
| 685 |
+
name: 'hidden_storage',
|
| 686 |
+
fn: () => checkHiddenStorage(url),
|
| 687 |
+
onDone: (data) => { results.hiddenStorage = data; sendEvent('progress', { step: 'hidden_storage', status: 'done' }); }
|
| 688 |
+
},
|
| 689 |
+
{
|
| 690 |
+
name: 'cookie_consent',
|
| 691 |
+
fn: () => analyzeCookieConsent(url),
|
| 692 |
+
onDone: (data) => { results.cookieConsent = data; sendEvent('progress', { step: 'cookie_consent', status: 'done' }); }
|
| 693 |
+
},
|
| 694 |
+
{
|
| 695 |
+
name: 'fingerprint_risks',
|
| 696 |
+
fn: () => collectFingerprintRisks(url),
|
| 697 |
+
onDone: (data) => { results.fingerprint = data; sendEvent('progress', { step: 'fingerprint_risks', status: 'done' }); }
|
| 698 |
+
},
|
| 699 |
+
{
|
| 700 |
+
name: 'redirect_chain',
|
| 701 |
+
fn: () => trackRedirectChain(url),
|
| 702 |
+
onDone: (data) => { results.redirectChain = data; sendEvent('progress', { step: 'redirect_chain', status: 'done' }); }
|
| 703 |
+
},
|
| 704 |
+
{
|
| 705 |
+
name: 'blacklight_scan',
|
| 706 |
+
fn: () => performBlacklightScan(url),
|
| 707 |
+
onDone: (data) => { results.blacklight = data; sendEvent('progress', { step: 'blacklight_scan', status: 'done' }); }
|
| 708 |
+
},
|
| 709 |
+
{
|
| 710 |
+
name: 'tosdr',
|
| 711 |
+
fn: () => getTosdrGrade(url),
|
| 712 |
+
onDone: (data) => { results.tosdr = data; sendEvent('progress', { step: 'tosdr', status: 'done' }); }
|
| 713 |
+
},
|
| 714 |
+
{
|
| 715 |
+
name: 'security',
|
| 716 |
+
fn: () => performSecurityCheck(url),
|
| 717 |
+
onDone: (data) => { results.security = data; sendEvent('progress', { step: 'security', status: 'done' }); }
|
| 718 |
+
},
|
| 719 |
+
{
|
| 720 |
+
name: 'privacy_policy',
|
| 721 |
+
fn: () => analyzePrivacyPolicy(url),
|
| 722 |
+
onDone: (data) => { results.privacyPolicy = data; sendEvent('progress', { step: 'privacy_policy', status: 'done' }); }
|
| 723 |
+
}
|
| 724 |
];
|
| 725 |
|
| 726 |
+
// بدء جميع الأدوات بالتوازي
|
| 727 |
+
const promises = tools.map(tool =>
|
| 728 |
+
tool.fn()
|
| 729 |
+
.then(data => { tool.onDone(data); return data; })
|
| 730 |
+
.catch(err => { console.error(`${tool.name} failed:`, err.message); tool.onDone(null); return null; })
|
| 731 |
+
);
|
| 732 |
+
|
| 733 |
+
// انتظار انتهاء الجميع
|
| 734 |
+
const settled = await Promise.allSettled(promises);
|
| 735 |
+
// (القيم الفعلية موجودة في results لأننا خزناها عبر onDone)
|
| 736 |
+
|
| 737 |
+
// معالجة البيانات النهائية (خاصة blacklight)
|
| 738 |
+
const blacklightData = results.blacklight || { hosts: {}, cookies: [] };
|
| 739 |
|
| 740 |
// تجميع المتتبعين
|
| 741 |
const thirdPartyDomains = [
|
|
|
|
| 812 |
}
|
| 813 |
}
|
| 814 |
|
| 815 |
+
// حساب النتيجة
|
| 816 |
+
const securityData = results.security || { ssl: { valid: false } };
|
| 817 |
const { score, grade } = calculatePrivacyScore(
|
| 818 |
+
blacklightData,
|
| 819 |
+
enrichedTrackers,
|
| 820 |
+
results.tosdr,
|
| 821 |
+
securityData,
|
| 822 |
+
results.cookieConsent,
|
| 823 |
+
results.fingerprint,
|
| 824 |
+
results.redirectChain,
|
| 825 |
+
results.privacyPolicy
|
| 826 |
);
|
| 827 |
|
| 828 |
+
// بناء النتيجة النهائية
|
| 829 |
const finalResult = {
|
| 830 |
success: true,
|
| 831 |
url,
|
|
|
|
| 840 |
fingerprinting: {
|
| 841 |
canvas: !!(blacklightData.canvasFingerprinters?.length),
|
| 842 |
fonts: !!(blacklightData.canvasFontFingerprinters?.length),
|
| 843 |
+
live_risks: results.fingerprint
|
| 844 |
},
|
| 845 |
session_recording: !!(blacklightData.sessionRecorders?.length),
|
| 846 |
key_logging: !!(blacklightData.keyLogging?.length),
|
| 847 |
+
hidden_storage: results.hiddenStorage ? {
|
| 848 |
+
localStorage: results.hiddenStorage.localStorage?.length || 0,
|
| 849 |
+
sessionStorage: results.hiddenStorage.sessionStorage?.length || 0,
|
| 850 |
+
indexedDB: results.hiddenStorage.indexedDB
|
| 851 |
} : null,
|
| 852 |
+
cookie_consent: results.cookieConsent,
|
| 853 |
+
redirect_chain: results.redirectChain,
|
| 854 |
security: securityData,
|
| 855 |
+
tosdr: results.tosdr,
|
| 856 |
+
privacy_policy_analysis: results.privacyPolicy,
|
| 857 |
geo_mapping: { data_destinations: geoDestinations },
|
| 858 |
leakage_detection: { alerts: leakageAlerts },
|
| 859 |
+
screenshot: results.screenshot,
|
| 860 |
raw: blacklightData
|
| 861 |
};
|
| 862 |
|
|
|
|
| 865 |
});
|
| 866 |
} catch (e) {
|
| 867 |
console.error('Live scan error:', e);
|
| 868 |
+
sendEvent('error', { error: e.message });
|
|
|
|
|
|
|
|
|
|
|
|
|
| 869 |
} finally {
|
| 870 |
res.end();
|
| 871 |
}
|
|
|
|
| 886 |
await downloadDDGTrackerRadar();
|
| 887 |
}
|
| 888 |
app.listen(PORT, '0.0.0.0', () => {
|
| 889 |
+
console.log(`🚀 Private Eye Live API running on port ${PORT}`);
|
| 890 |
});
|
| 891 |
})();
|