atlas-browser 0.2.0
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/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +16 -0
- package/CONTRIBUTING.md +63 -0
- package/LICENSE +21 -0
- package/PRIVACY.md +37 -0
- package/README.md +163 -0
- package/SECURITY.md +29 -0
- package/assets/logo.png +0 -0
- package/bin/cli.js +142 -0
- package/dist-electron/main/blocker.js +109 -0
- package/dist-electron/main/blocker.js.map +1 -0
- package/dist-electron/main/bookmark-manager.js +121 -0
- package/dist-electron/main/bookmark-manager.js.map +1 -0
- package/dist-electron/main/download-manager.js +118 -0
- package/dist-electron/main/download-manager.js.map +1 -0
- package/dist-electron/main/index.js +116 -0
- package/dist-electron/main/index.js.map +1 -0
- package/dist-electron/main/ipc-handlers.js +303 -0
- package/dist-electron/main/ipc-handlers.js.map +1 -0
- package/dist-electron/main/menu.js +229 -0
- package/dist-electron/main/menu.js.map +1 -0
- package/dist-electron/main/security-analyzer.js +71 -0
- package/dist-electron/main/security-analyzer.js.map +1 -0
- package/dist-electron/main/settings-manager.js +105 -0
- package/dist-electron/main/settings-manager.js.map +1 -0
- package/dist-electron/main/tab-manager.js +205 -0
- package/dist-electron/main/tab-manager.js.map +1 -0
- package/dist-electron/main/tor-manager.js +59 -0
- package/dist-electron/main/tor-manager.js.map +1 -0
- package/dist-electron/preload/preload.js +73 -0
- package/dist-electron/preload/preload.js.map +1 -0
- package/install.sh +120 -0
- package/package.json +67 -0
- package/src/main/blocker.ts +121 -0
- package/src/main/bookmark-manager.ts +99 -0
- package/src/main/download-manager.ts +103 -0
- package/src/main/index.ts +93 -0
- package/src/main/ipc-handlers.ts +283 -0
- package/src/main/menu.ts +192 -0
- package/src/main/security-analyzer.ts +97 -0
- package/src/main/settings-manager.ts +84 -0
- package/src/main/tab-manager.ts +249 -0
- package/src/main/tor-manager.ts +59 -0
- package/src/preload/preload.ts +85 -0
- package/src/renderer/bookmarks.html +84 -0
- package/src/renderer/browser-ui.js +427 -0
- package/src/renderer/downloads.html +94 -0
- package/src/renderer/history.html +111 -0
- package/src/renderer/index.html +152 -0
- package/src/renderer/internet-map.html +313 -0
- package/src/renderer/network-map.js +131 -0
- package/src/renderer/security-panel.js +13 -0
- package/src/renderer/settings.html +138 -0
- package/src/renderer/styles.css +688 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Session } from 'electron';
|
|
2
|
+
|
|
3
|
+
// Common tracker/ad domains to block
|
|
4
|
+
const BLOCKED_DOMAINS = new Set([
|
|
5
|
+
// Ad networks
|
|
6
|
+
'doubleclick.net', 'googlesyndication.com', 'googleadservices.com',
|
|
7
|
+
'google-analytics.com', 'googletagmanager.com', 'googletagservices.com',
|
|
8
|
+
'adservice.google.com', 'pagead2.googlesyndication.com',
|
|
9
|
+
'ads.facebook.com', 'pixel.facebook.com', 'connect.facebook.net',
|
|
10
|
+
'ad.doubleclick.net', 'adnxs.com', 'adsrvr.org',
|
|
11
|
+
'amazon-adsystem.com', 'ads-twitter.com',
|
|
12
|
+
'advertising.com', 'criteo.com', 'criteo.net',
|
|
13
|
+
'outbrain.com', 'taboola.com', 'revcontent.com',
|
|
14
|
+
// Trackers
|
|
15
|
+
'hotjar.com', 'fullstory.com', 'mouseflow.com',
|
|
16
|
+
'mixpanel.com', 'amplitude.com', 'segment.com', 'segment.io',
|
|
17
|
+
'optimizely.com', 'crazyegg.com', 'clicktale.net',
|
|
18
|
+
'newrelic.com', 'nr-data.net', 'pingdom.net',
|
|
19
|
+
'quantserve.com', 'scorecardresearch.com', 'imrworldwide.com',
|
|
20
|
+
'comscore.com', 'chartbeat.com', 'parsely.com',
|
|
21
|
+
// Social trackers
|
|
22
|
+
'platform.twitter.com', 'syndication.twitter.com',
|
|
23
|
+
'platform.linkedin.com', 'snap.licdn.com',
|
|
24
|
+
'static.ads-twitter.com',
|
|
25
|
+
// Fingerprinting
|
|
26
|
+
'fingerprintjs.com', 'cdn.jsdelivr.net/npm/@aspect',
|
|
27
|
+
// Misc trackers
|
|
28
|
+
'tracking.', 'analytics.', 'telemetry.',
|
|
29
|
+
'sentry.io', 'bugsnag.com',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
// URL patterns to block
|
|
33
|
+
const BLOCKED_PATTERNS = [
|
|
34
|
+
/\/ads\//i,
|
|
35
|
+
/\/tracking\//i,
|
|
36
|
+
/\/analytics\//i,
|
|
37
|
+
/\/pixel\//i,
|
|
38
|
+
/google-analytics/i,
|
|
39
|
+
/facebook.*pixel/i,
|
|
40
|
+
/doubleclick/i,
|
|
41
|
+
/\.gif\?.*utm_/i,
|
|
42
|
+
/__utm\.gif/i,
|
|
43
|
+
/beacon\./i,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
type BlockCallback = () => void;
|
|
47
|
+
|
|
48
|
+
export class Blocker {
|
|
49
|
+
private callbacks: Map<number, BlockCallback[]> = new Map();
|
|
50
|
+
private totalBlocked: number = 0;
|
|
51
|
+
private isEnabled: boolean = true;
|
|
52
|
+
|
|
53
|
+
setup(session: Session): void {
|
|
54
|
+
session.webRequest.onBeforeRequest((details, callback) => {
|
|
55
|
+
if (!this.isEnabled) {
|
|
56
|
+
callback({});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const shouldBlock = this.shouldBlock(details.url);
|
|
61
|
+
|
|
62
|
+
if (shouldBlock) {
|
|
63
|
+
this.totalBlocked++;
|
|
64
|
+
|
|
65
|
+
// Notify tab-specific callbacks
|
|
66
|
+
const tabCallbacks = details.webContentsId != null ? this.callbacks.get(details.webContentsId) : undefined;
|
|
67
|
+
if (tabCallbacks) {
|
|
68
|
+
tabCallbacks.forEach(cb => cb());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
callback({ cancel: true });
|
|
72
|
+
} else {
|
|
73
|
+
callback({});
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private shouldBlock(url: string): boolean {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = new URL(url);
|
|
81
|
+
const hostname = parsed.hostname;
|
|
82
|
+
|
|
83
|
+
// Check against blocked domains
|
|
84
|
+
for (const domain of BLOCKED_DOMAINS) {
|
|
85
|
+
if (hostname === domain || hostname.endsWith('.' + domain)) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check URL patterns
|
|
91
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
92
|
+
if (pattern.test(url)) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return false;
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
onBlocked(webContentsId: number, callback: BlockCallback): void {
|
|
104
|
+
const existing = this.callbacks.get(webContentsId) || [];
|
|
105
|
+
existing.push(callback);
|
|
106
|
+
this.callbacks.set(webContentsId, existing);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
toggle(): boolean {
|
|
110
|
+
this.isEnabled = !this.isEnabled;
|
|
111
|
+
return this.isEnabled;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get enabled(): boolean {
|
|
115
|
+
return this.isEnabled;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get blockedTotal(): number {
|
|
119
|
+
return this.totalBlocked;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { app } from 'electron';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
export interface Bookmark {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
url: string;
|
|
9
|
+
favicon: string;
|
|
10
|
+
folder: string;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const STORE_PATH = path.join(app.getPath('userData'), 'bookmarks.json');
|
|
15
|
+
|
|
16
|
+
function readStore(): Bookmark[] {
|
|
17
|
+
try { return JSON.parse(fs.readFileSync(STORE_PATH, 'utf8')); } catch { return []; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeStore(data: Bookmark[]): void {
|
|
21
|
+
fs.writeFileSync(STORE_PATH, JSON.stringify(data, null, 2));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class BookmarkManager {
|
|
25
|
+
getAll(): Bookmark[] {
|
|
26
|
+
return readStore();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getByFolder(folder: string): Bookmark[] {
|
|
30
|
+
return this.getAll().filter(b => b.folder === folder);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getBarBookmarks(): Bookmark[] {
|
|
34
|
+
return this.getByFolder('bar');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
add(title: string, url: string, favicon: string = '', folder: string = 'bar'): Bookmark {
|
|
38
|
+
const bookmark: Bookmark = {
|
|
39
|
+
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
40
|
+
title,
|
|
41
|
+
url,
|
|
42
|
+
favicon,
|
|
43
|
+
folder,
|
|
44
|
+
createdAt: Date.now(),
|
|
45
|
+
};
|
|
46
|
+
const all = this.getAll();
|
|
47
|
+
all.push(bookmark);
|
|
48
|
+
writeStore(all);
|
|
49
|
+
return bookmark;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
remove(id: string): boolean {
|
|
53
|
+
const all = this.getAll();
|
|
54
|
+
const filtered = all.filter(b => b.id !== id);
|
|
55
|
+
if (filtered.length === all.length) return false;
|
|
56
|
+
writeStore(filtered);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
update(id: string, updates: Partial<Bookmark>): boolean {
|
|
61
|
+
const all = this.getAll();
|
|
62
|
+
const idx = all.findIndex(b => b.id === id);
|
|
63
|
+
if (idx < 0) return false;
|
|
64
|
+
all[idx] = { ...all[idx], ...updates };
|
|
65
|
+
writeStore(all);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
isBookmarked(url: string): boolean {
|
|
70
|
+
return this.getAll().some(b => b.url === url);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
findByUrl(url: string): Bookmark | undefined {
|
|
74
|
+
return this.getAll().find(b => b.url === url);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
getFolders(): string[] {
|
|
78
|
+
const folders = new Set(this.getAll().map(b => b.folder));
|
|
79
|
+
return Array.from(folders);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
exportAll(): string {
|
|
83
|
+
return JSON.stringify(this.getAll(), null, 2);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
importAll(json: string): number {
|
|
87
|
+
const imported: Bookmark[] = JSON.parse(json);
|
|
88
|
+
const all = this.getAll();
|
|
89
|
+
let count = 0;
|
|
90
|
+
for (const bm of imported) {
|
|
91
|
+
if (!all.some(b => b.url === bm.url)) {
|
|
92
|
+
all.push({ ...bm, id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6) });
|
|
93
|
+
count++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
writeStore(all);
|
|
97
|
+
return count;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Session, DownloadItem, BrowserWindow } from 'electron';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import { app } from 'electron';
|
|
5
|
+
|
|
6
|
+
export interface DownloadRecord {
|
|
7
|
+
id: string;
|
|
8
|
+
filename: string;
|
|
9
|
+
url: string;
|
|
10
|
+
savePath: string;
|
|
11
|
+
totalBytes: number;
|
|
12
|
+
receivedBytes: number;
|
|
13
|
+
state: 'progressing' | 'completed' | 'cancelled' | 'interrupted';
|
|
14
|
+
startTime: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const STORE_PATH = path.join(app.getPath('userData'), 'downloads.json');
|
|
18
|
+
function readDL(): DownloadRecord[] { try { return JSON.parse(fs.readFileSync(STORE_PATH, 'utf8')); } catch { return []; } }
|
|
19
|
+
function writeDL(d: DownloadRecord[]): void { fs.writeFileSync(STORE_PATH, JSON.stringify(d, null, 2)); }
|
|
20
|
+
|
|
21
|
+
export class DownloadManager {
|
|
22
|
+
private activeDownloads: Map<string, DownloadItem> = new Map();
|
|
23
|
+
private window: BrowserWindow;
|
|
24
|
+
|
|
25
|
+
constructor(window: BrowserWindow) {
|
|
26
|
+
this.window = window;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setup(session: Session): void {
|
|
30
|
+
session.on('will-download', (_event, item, _webContents) => {
|
|
31
|
+
const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
32
|
+
const filename = item.getFilename();
|
|
33
|
+
const savePath = path.join(app.getPath('downloads'), filename);
|
|
34
|
+
item.setSavePath(savePath);
|
|
35
|
+
|
|
36
|
+
this.activeDownloads.set(id, item);
|
|
37
|
+
|
|
38
|
+
const record: DownloadRecord = {
|
|
39
|
+
id,
|
|
40
|
+
filename,
|
|
41
|
+
url: item.getURL(),
|
|
42
|
+
savePath,
|
|
43
|
+
totalBytes: item.getTotalBytes(),
|
|
44
|
+
receivedBytes: 0,
|
|
45
|
+
state: 'progressing',
|
|
46
|
+
startTime: Date.now(),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Save to history
|
|
50
|
+
const all = readDL();
|
|
51
|
+
all.unshift(record);
|
|
52
|
+
writeDL(all);
|
|
53
|
+
|
|
54
|
+
// Notify renderer
|
|
55
|
+
this.window.webContents.send('download-started', record);
|
|
56
|
+
|
|
57
|
+
item.on('updated', (_e, state) => {
|
|
58
|
+
record.receivedBytes = item.getReceivedBytes();
|
|
59
|
+
record.totalBytes = item.getTotalBytes();
|
|
60
|
+
record.state = state === 'progressing' ? 'progressing' : 'interrupted';
|
|
61
|
+
this.updateRecord(record);
|
|
62
|
+
this.window.webContents.send('download-progress', record);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
item.once('done', (_e, state) => {
|
|
66
|
+
record.receivedBytes = item.getReceivedBytes();
|
|
67
|
+
record.state = state === 'completed' ? 'completed' : 'cancelled';
|
|
68
|
+
this.updateRecord(record);
|
|
69
|
+
this.activeDownloads.delete(id);
|
|
70
|
+
this.window.webContents.send('download-done', record);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
getHistory(): DownloadRecord[] {
|
|
76
|
+
return readDL();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clearHistory(): void {
|
|
80
|
+
writeDL([]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pause(id: string): void {
|
|
84
|
+
this.activeDownloads.get(id)?.pause();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
resume(id: string): void {
|
|
88
|
+
this.activeDownloads.get(id)?.resume();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
cancel(id: string): void {
|
|
92
|
+
this.activeDownloads.get(id)?.cancel();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private updateRecord(record: DownloadRecord): void {
|
|
96
|
+
const all = readDL();
|
|
97
|
+
const idx = all.findIndex(d => d.id === record.id);
|
|
98
|
+
if (idx >= 0) {
|
|
99
|
+
all[idx] = record;
|
|
100
|
+
writeDL(all);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { app, BrowserWindow, session } from 'electron';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { TabManager } from './tab-manager';
|
|
4
|
+
import { Blocker } from './blocker';
|
|
5
|
+
import { SecurityAnalyzer } from './security-analyzer';
|
|
6
|
+
import { TorManager } from './tor-manager';
|
|
7
|
+
import { BookmarkManager } from './bookmark-manager';
|
|
8
|
+
import { DownloadManager } from './download-manager';
|
|
9
|
+
import { SettingsManager } from './settings-manager';
|
|
10
|
+
import { setupMenu } from './menu';
|
|
11
|
+
import { setupIpcHandlers } from './ipc-handlers';
|
|
12
|
+
|
|
13
|
+
const settingsManager = new SettingsManager();
|
|
14
|
+
const SEARCH_ANGEL_URL = settingsManager.get('homepage');
|
|
15
|
+
|
|
16
|
+
let mainWindow: BrowserWindow | null = null;
|
|
17
|
+
let tabManager: TabManager | null = null;
|
|
18
|
+
let blocker: Blocker | null = null;
|
|
19
|
+
let securityAnalyzer: SecurityAnalyzer | null = null;
|
|
20
|
+
let torManager: TorManager | null = null;
|
|
21
|
+
let bookmarkManager: BookmarkManager | null = null;
|
|
22
|
+
let downloadManager: DownloadManager | null = null;
|
|
23
|
+
|
|
24
|
+
function createWindow(): void {
|
|
25
|
+
mainWindow = new BrowserWindow({
|
|
26
|
+
width: 1400,
|
|
27
|
+
height: 900,
|
|
28
|
+
minWidth: 800,
|
|
29
|
+
minHeight: 600,
|
|
30
|
+
title: 'Atlas Browser',
|
|
31
|
+
titleBarStyle: 'hiddenInset',
|
|
32
|
+
trafficLightPosition: { x: 12, y: 12 },
|
|
33
|
+
backgroundColor: '#0a0a0f',
|
|
34
|
+
webPreferences: {
|
|
35
|
+
preload: path.join(__dirname, '..', 'preload', 'preload.js'),
|
|
36
|
+
nodeIntegration: false,
|
|
37
|
+
contextIsolation: true,
|
|
38
|
+
sandbox: false,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
mainWindow.loadFile(path.join(__dirname, '..', '..', 'src', 'renderer', 'index.html'));
|
|
43
|
+
|
|
44
|
+
// Initialize all components
|
|
45
|
+
blocker = new Blocker();
|
|
46
|
+
securityAnalyzer = new SecurityAnalyzer();
|
|
47
|
+
torManager = new TorManager();
|
|
48
|
+
bookmarkManager = new BookmarkManager();
|
|
49
|
+
downloadManager = new DownloadManager(mainWindow);
|
|
50
|
+
tabManager = new TabManager(mainWindow, SEARCH_ANGEL_URL, blocker, securityAnalyzer);
|
|
51
|
+
|
|
52
|
+
// Setup
|
|
53
|
+
setupIpcHandlers(mainWindow, tabManager, blocker, securityAnalyzer, torManager, bookmarkManager, downloadManager, settingsManager);
|
|
54
|
+
setupMenu(mainWindow, tabManager);
|
|
55
|
+
blocker.setup(session.defaultSession);
|
|
56
|
+
downloadManager.setup(session.defaultSession);
|
|
57
|
+
|
|
58
|
+
// First tab
|
|
59
|
+
mainWindow.webContents.once('did-finish-load', () => {
|
|
60
|
+
tabManager!.createTab(SEARCH_ANGEL_URL);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
mainWindow.on('closed', () => {
|
|
64
|
+
mainWindow = null;
|
|
65
|
+
tabManager = null;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Privacy flags
|
|
70
|
+
app.commandLine.appendSwitch('disable-features', 'SpareRendererForSitePerProcess');
|
|
71
|
+
app.commandLine.appendSwitch('disable-client-side-phishing-detection');
|
|
72
|
+
app.commandLine.appendSwitch('disable-component-update');
|
|
73
|
+
|
|
74
|
+
app.whenReady().then(() => {
|
|
75
|
+
session.defaultSession.setUserAgent(
|
|
76
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
|
|
77
|
+
);
|
|
78
|
+
session.defaultSession.cookies.flushStore();
|
|
79
|
+
createWindow();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
app.on('window-all-closed', () => {
|
|
83
|
+
if (settingsManager.get('clearDataOnExit')) {
|
|
84
|
+
session.defaultSession.clearStorageData({
|
|
85
|
+
storages: ['cookies', 'localstorage', 'cachestorage'],
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
app.quit();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
app.on('activate', () => {
|
|
92
|
+
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
93
|
+
});
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { ipcMain, BrowserWindow, shell, app } from 'electron';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import { TabManager } from './tab-manager';
|
|
5
|
+
import { Blocker } from './blocker';
|
|
6
|
+
import { SecurityAnalyzer } from './security-analyzer';
|
|
7
|
+
import { TorManager } from './tor-manager';
|
|
8
|
+
import { BookmarkManager } from './bookmark-manager';
|
|
9
|
+
import { DownloadManager } from './download-manager';
|
|
10
|
+
import { SettingsManager } from './settings-manager';
|
|
11
|
+
|
|
12
|
+
// ── History Storage ─────────────────────────────────────────
|
|
13
|
+
interface HistoryItem {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
url: string;
|
|
17
|
+
timestamp: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getHistoryPath(): string {
|
|
21
|
+
try { return path.join(app.getPath('userData'), 'history.json'); }
|
|
22
|
+
catch { return '/tmp/atlas-browser-history.json'; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readHistory(): HistoryItem[] {
|
|
26
|
+
try { return JSON.parse(fs.readFileSync(getHistoryPath(), 'utf8')); }
|
|
27
|
+
catch { return []; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeHistory(items: HistoryItem[]): void {
|
|
31
|
+
try { fs.writeFileSync(getHistoryPath(), JSON.stringify(items, null, 2)); }
|
|
32
|
+
catch { /* ignore */ }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function addHistoryItem(title: string, url: string): void {
|
|
36
|
+
if (url.startsWith('file://') || url === 'about:blank') return;
|
|
37
|
+
const items = readHistory();
|
|
38
|
+
items.unshift({
|
|
39
|
+
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
40
|
+
title,
|
|
41
|
+
url,
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
});
|
|
44
|
+
// Keep last 1000 entries
|
|
45
|
+
writeHistory(items.slice(0, 1000));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let sidebarOpen = false;
|
|
49
|
+
const SIDEBAR_WIDTH = 300;
|
|
50
|
+
const BOOKMARKS_BAR_HEIGHT = 28;
|
|
51
|
+
|
|
52
|
+
export function setupIpcHandlers(
|
|
53
|
+
window: BrowserWindow,
|
|
54
|
+
tabManager: TabManager,
|
|
55
|
+
blocker: Blocker,
|
|
56
|
+
security: SecurityAnalyzer,
|
|
57
|
+
torManager: TorManager,
|
|
58
|
+
bookmarks: BookmarkManager,
|
|
59
|
+
downloads: DownloadManager,
|
|
60
|
+
settings: SettingsManager
|
|
61
|
+
): void {
|
|
62
|
+
// ── Tab management ────────────────────────────────────────
|
|
63
|
+
ipcMain.on('new-tab', (_e, url?: string) => tabManager.createTab(url));
|
|
64
|
+
ipcMain.on('close-tab', (_e, tabId: number) => tabManager.closeTab(tabId));
|
|
65
|
+
ipcMain.on('switch-tab', (_e, tabId: number) => tabManager.switchTab(tabId));
|
|
66
|
+
|
|
67
|
+
// ── Navigation ────────────────────────────────────────────
|
|
68
|
+
ipcMain.on('navigate', (_e, url: string) => tabManager.navigateTo(url));
|
|
69
|
+
ipcMain.on('go-back', () => tabManager.goBack());
|
|
70
|
+
ipcMain.on('go-forward', () => tabManager.goForward());
|
|
71
|
+
ipcMain.on('reload', () => tabManager.reload());
|
|
72
|
+
ipcMain.on('go-home', () => tabManager.goHome());
|
|
73
|
+
|
|
74
|
+
// ── Internal pages ────────────────────────────────────────
|
|
75
|
+
ipcMain.on('open-page', (_e, page: string) => {
|
|
76
|
+
const pagePath = path.join(__dirname, '..', '..', 'src', 'renderer', `${page}.html`);
|
|
77
|
+
tabManager.createTab(`file://${pagePath}`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// ── Blocker ───────────────────────────────────────────────
|
|
81
|
+
ipcMain.handle('toggle-blocker', () => blocker.toggle());
|
|
82
|
+
ipcMain.handle('get-blocker-stats', () => ({
|
|
83
|
+
enabled: blocker.enabled,
|
|
84
|
+
totalBlocked: blocker.blockedTotal,
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
// ── Tor ───────────────────────────────────────────────────
|
|
88
|
+
ipcMain.handle('toggle-tor', async () => {
|
|
89
|
+
const enabled = await torManager.toggle();
|
|
90
|
+
window.webContents.send('tor-status-changed', enabled);
|
|
91
|
+
const tab = tabManager.getActiveTab();
|
|
92
|
+
if (tab) tab.view.webContents.reload();
|
|
93
|
+
return enabled;
|
|
94
|
+
});
|
|
95
|
+
ipcMain.handle('get-tor-status', () => ({ enabled: torManager.enabled }));
|
|
96
|
+
ipcMain.handle('get-tor-circuit', async () => torManager.getCircuitInfo());
|
|
97
|
+
|
|
98
|
+
// ── Sidebar ───────────────────────────────────────────────
|
|
99
|
+
ipcMain.on('toggle-sidebar', () => {
|
|
100
|
+
sidebarOpen = !sidebarOpen;
|
|
101
|
+
resizeBrowserView();
|
|
102
|
+
window.webContents.send('sidebar-toggled', sidebarOpen);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ── Security ──────────────────────────────────────────────
|
|
106
|
+
ipcMain.handle('get-security-info', async () => {
|
|
107
|
+
const tab = tabManager.getActiveTab();
|
|
108
|
+
if (!tab) return null;
|
|
109
|
+
return security.analyze(tab.view.webContents, tab.url);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ── Tabs ──────────────────────────────────────────────────
|
|
113
|
+
ipcMain.handle('get-tabs', () => tabManager.getAllTabs());
|
|
114
|
+
|
|
115
|
+
// ── Bookmarks ─────────────────────────────────────────────
|
|
116
|
+
ipcMain.handle('get-bookmarks', () => bookmarks.getAll());
|
|
117
|
+
ipcMain.handle('get-bar-bookmarks', () => bookmarks.getBarBookmarks());
|
|
118
|
+
ipcMain.handle('add-bookmark', (_e, title: string, url: string, favicon: string) => {
|
|
119
|
+
return bookmarks.add(title, url, favicon);
|
|
120
|
+
});
|
|
121
|
+
ipcMain.handle('remove-bookmark', (_e, id: string) => bookmarks.remove(id));
|
|
122
|
+
ipcMain.handle('is-bookmarked', (_e, url: string) => bookmarks.isBookmarked(url));
|
|
123
|
+
ipcMain.handle('toggle-bookmark', (_e, title: string, url: string) => {
|
|
124
|
+
const existing = bookmarks.findByUrl(url);
|
|
125
|
+
if (existing) {
|
|
126
|
+
bookmarks.remove(existing.id);
|
|
127
|
+
return false;
|
|
128
|
+
} else {
|
|
129
|
+
bookmarks.add(title, url);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── Downloads ─────────────────────────────────────────────
|
|
135
|
+
ipcMain.handle('get-downloads', () => downloads.getHistory());
|
|
136
|
+
ipcMain.handle('clear-downloads', () => {
|
|
137
|
+
downloads.clearHistory();
|
|
138
|
+
return true;
|
|
139
|
+
});
|
|
140
|
+
ipcMain.on('open-download', (_e, filePath: string) => {
|
|
141
|
+
shell.openPath(filePath);
|
|
142
|
+
});
|
|
143
|
+
ipcMain.on('show-download', (_e, filePath: string) => {
|
|
144
|
+
shell.showItemInFolder(filePath);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ── Settings ──────────────────────────────────────────────
|
|
148
|
+
ipcMain.handle('get-settings', () => settings.getAll());
|
|
149
|
+
ipcMain.handle('set-setting', (_e, key: string, value: any) => {
|
|
150
|
+
settings.set(key as any, value);
|
|
151
|
+
return settings.getAll();
|
|
152
|
+
});
|
|
153
|
+
ipcMain.handle('reset-settings', () => {
|
|
154
|
+
settings.reset();
|
|
155
|
+
return settings.getAll();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ── Zoom ──────────────────────────────────────────────────
|
|
159
|
+
ipcMain.handle('zoom-in', () => {
|
|
160
|
+
const tab = tabManager.getActiveTab();
|
|
161
|
+
if (!tab) return 100;
|
|
162
|
+
const level = tab.view.webContents.getZoomFactor();
|
|
163
|
+
const newLevel = Math.min(level + 0.1, 3.0);
|
|
164
|
+
tab.view.webContents.setZoomFactor(newLevel);
|
|
165
|
+
return Math.round(newLevel * 100);
|
|
166
|
+
});
|
|
167
|
+
ipcMain.handle('zoom-out', () => {
|
|
168
|
+
const tab = tabManager.getActiveTab();
|
|
169
|
+
if (!tab) return 100;
|
|
170
|
+
const level = tab.view.webContents.getZoomFactor();
|
|
171
|
+
const newLevel = Math.max(level - 0.1, 0.3);
|
|
172
|
+
tab.view.webContents.setZoomFactor(newLevel);
|
|
173
|
+
return Math.round(newLevel * 100);
|
|
174
|
+
});
|
|
175
|
+
ipcMain.handle('zoom-reset', () => {
|
|
176
|
+
const tab = tabManager.getActiveTab();
|
|
177
|
+
if (!tab) return 100;
|
|
178
|
+
tab.view.webContents.setZoomFactor(1.0);
|
|
179
|
+
return 100;
|
|
180
|
+
});
|
|
181
|
+
ipcMain.handle('get-zoom', () => {
|
|
182
|
+
const tab = tabManager.getActiveTab();
|
|
183
|
+
if (!tab) return 100;
|
|
184
|
+
return Math.round(tab.view.webContents.getZoomFactor() * 100);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ── History ────────────────────────────────────────────────
|
|
188
|
+
ipcMain.handle('get-history', () => readHistory());
|
|
189
|
+
ipcMain.handle('clear-history', () => { writeHistory([]); return true; });
|
|
190
|
+
ipcMain.handle('delete-history-item', (_e, id: string) => {
|
|
191
|
+
const items = readHistory().filter(i => i.id !== id);
|
|
192
|
+
writeHistory(items);
|
|
193
|
+
return true;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Track page visits for history
|
|
197
|
+
// Note: called from tab-manager on did-navigate
|
|
198
|
+
ipcMain.on('add-history', (_e, title: string, url: string) => {
|
|
199
|
+
addHistoryItem(title, url);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ── Phantom Mode (ephemeral Docker containers) ────────────
|
|
203
|
+
const PHANTOM_API = 'http://128.140.66.108:8080/api/v1/phantom';
|
|
204
|
+
const http = require('http');
|
|
205
|
+
|
|
206
|
+
ipcMain.handle('phantom-start', async () => {
|
|
207
|
+
const fetch = (await import('node:https')).default || require('http');
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const req = http.request(`${PHANTOM_API}/start`, {
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: { 'Content-Type': 'application/json' },
|
|
212
|
+
}, (res: any) => {
|
|
213
|
+
let data = '';
|
|
214
|
+
res.on('data', (chunk: string) => data += chunk);
|
|
215
|
+
res.on('end', () => {
|
|
216
|
+
try { resolve(JSON.parse(data)); }
|
|
217
|
+
catch { reject(new Error(data)); }
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
req.on('error', reject);
|
|
221
|
+
req.end();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
ipcMain.handle('phantom-end', async (_e, sessionId: string) => {
|
|
226
|
+
return new Promise((resolve, reject) => {
|
|
227
|
+
const req = http.request(`${PHANTOM_API}/end/${sessionId}`, {
|
|
228
|
+
method: 'DELETE',
|
|
229
|
+
}, (res: any) => {
|
|
230
|
+
let data = '';
|
|
231
|
+
res.on('data', (chunk: string) => data += chunk);
|
|
232
|
+
res.on('end', () => {
|
|
233
|
+
try { resolve(JSON.parse(data)); }
|
|
234
|
+
catch { resolve({ status: 'destroyed' }); }
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
req.on('error', reject);
|
|
238
|
+
req.end();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
ipcMain.handle('phantom-status', async (_e, sessionId: string) => {
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
http.get(`${PHANTOM_API}/status/${sessionId}`, (res: any) => {
|
|
245
|
+
let data = '';
|
|
246
|
+
res.on('data', (chunk: string) => data += chunk);
|
|
247
|
+
res.on('end', () => {
|
|
248
|
+
try { resolve(JSON.parse(data)); }
|
|
249
|
+
catch { resolve(null); }
|
|
250
|
+
});
|
|
251
|
+
}).on('error', reject);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ── DNS lookup (for Internet Map) ─────────────────────────
|
|
256
|
+
ipcMain.handle('dns-lookup', async (_e, domain: string) => {
|
|
257
|
+
const dns = require('dns').promises;
|
|
258
|
+
try {
|
|
259
|
+
const addresses = await dns.resolve4(domain);
|
|
260
|
+
return { domain, addresses, error: null };
|
|
261
|
+
} catch (err: any) {
|
|
262
|
+
return { domain, addresses: [], error: err.message };
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// ── Window resize ─────────────────────────────────────────
|
|
267
|
+
window.on('resize', () => resizeBrowserView());
|
|
268
|
+
|
|
269
|
+
function resizeBrowserView() {
|
|
270
|
+
const tab = tabManager.getActiveTab();
|
|
271
|
+
if (!tab) return;
|
|
272
|
+
const bounds = window.getBounds();
|
|
273
|
+
const showBar = settings.get('showBookmarksBar');
|
|
274
|
+
const chromeH = 82 + (showBar ? BOOKMARKS_BAR_HEIGHT : 0);
|
|
275
|
+
const sidebarW = sidebarOpen ? SIDEBAR_WIDTH : 0;
|
|
276
|
+
tab.view.setBounds({
|
|
277
|
+
x: 0,
|
|
278
|
+
y: chromeH,
|
|
279
|
+
width: bounds.width - sidebarW,
|
|
280
|
+
height: bounds.height - chromeH,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|