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
package/src/main/menu.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { app, Menu, BrowserWindow, MenuItemConstructorOptions } from 'electron';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { TabManager } from './tab-manager';
|
|
4
|
+
|
|
5
|
+
export function setupMenu(window: BrowserWindow, tabManager: TabManager): void {
|
|
6
|
+
const template: MenuItemConstructorOptions[] = [
|
|
7
|
+
{
|
|
8
|
+
label: 'Atlas Browser',
|
|
9
|
+
submenu: [
|
|
10
|
+
{ label: 'About Atlas Browser', role: 'about' },
|
|
11
|
+
{ type: 'separator' },
|
|
12
|
+
{
|
|
13
|
+
label: 'Settings',
|
|
14
|
+
accelerator: 'CmdOrCtrl+,',
|
|
15
|
+
click: () => {
|
|
16
|
+
const p = path.join(__dirname, '..', '..', 'src', 'renderer', 'settings.html');
|
|
17
|
+
tabManager.createTab(`file://${p}`);
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{ type: 'separator' },
|
|
21
|
+
{ role: 'hide' },
|
|
22
|
+
{ role: 'hideOthers' },
|
|
23
|
+
{ role: 'unhide' },
|
|
24
|
+
{ type: 'separator' },
|
|
25
|
+
{ role: 'quit' },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
label: 'File',
|
|
30
|
+
submenu: [
|
|
31
|
+
{
|
|
32
|
+
label: 'New Tab',
|
|
33
|
+
accelerator: 'CmdOrCtrl+T',
|
|
34
|
+
click: () => tabManager.createTab(),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: 'Close Tab',
|
|
38
|
+
accelerator: 'CmdOrCtrl+W',
|
|
39
|
+
click: () => {
|
|
40
|
+
const tab = tabManager.getActiveTab();
|
|
41
|
+
if (tab) tabManager.closeTab(tab.id);
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{ type: 'separator' },
|
|
45
|
+
{
|
|
46
|
+
label: 'Open Location',
|
|
47
|
+
accelerator: 'CmdOrCtrl+L',
|
|
48
|
+
click: () => window.webContents.send('focus-address-bar'),
|
|
49
|
+
},
|
|
50
|
+
{ type: 'separator' },
|
|
51
|
+
{
|
|
52
|
+
label: 'Downloads',
|
|
53
|
+
accelerator: 'CmdOrCtrl+J',
|
|
54
|
+
click: () => {
|
|
55
|
+
const p = path.join(__dirname, '..', '..', 'src', 'renderer', 'downloads.html');
|
|
56
|
+
tabManager.createTab(`file://${p}`);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
label: 'Bookmarks',
|
|
61
|
+
accelerator: 'CmdOrCtrl+Shift+B',
|
|
62
|
+
click: () => {
|
|
63
|
+
const p = path.join(__dirname, '..', '..', 'src', 'renderer', 'bookmarks.html');
|
|
64
|
+
tabManager.createTab(`file://${p}`);
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
label: 'Internet Map',
|
|
69
|
+
accelerator: 'CmdOrCtrl+Shift+M',
|
|
70
|
+
click: () => {
|
|
71
|
+
const p = path.join(__dirname, '..', '..', 'src', 'renderer', 'internet-map.html');
|
|
72
|
+
tabManager.createTab(`file://${p}`);
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
label: 'Edit',
|
|
79
|
+
submenu: [
|
|
80
|
+
{ role: 'undo' },
|
|
81
|
+
{ role: 'redo' },
|
|
82
|
+
{ type: 'separator' },
|
|
83
|
+
{ role: 'cut' },
|
|
84
|
+
{ role: 'copy' },
|
|
85
|
+
{ role: 'paste' },
|
|
86
|
+
{ role: 'selectAll' },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
label: 'View',
|
|
91
|
+
submenu: [
|
|
92
|
+
{
|
|
93
|
+
label: 'Reload',
|
|
94
|
+
accelerator: 'CmdOrCtrl+R',
|
|
95
|
+
click: () => tabManager.reload(),
|
|
96
|
+
},
|
|
97
|
+
{ type: 'separator' },
|
|
98
|
+
{
|
|
99
|
+
label: 'Developer Tools',
|
|
100
|
+
accelerator: 'CmdOrCtrl+Option+I',
|
|
101
|
+
click: () => {
|
|
102
|
+
const tab = tabManager.getActiveTab();
|
|
103
|
+
if (tab) tab.view.webContents.toggleDevTools();
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{ type: 'separator' },
|
|
107
|
+
{
|
|
108
|
+
label: 'Zoom In',
|
|
109
|
+
accelerator: 'CmdOrCtrl+=',
|
|
110
|
+
click: () => {
|
|
111
|
+
const tab = tabManager.getActiveTab();
|
|
112
|
+
if (tab) {
|
|
113
|
+
const z = tab.view.webContents.getZoomFactor();
|
|
114
|
+
tab.view.webContents.setZoomFactor(Math.min(z + 0.1, 3));
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
label: 'Zoom Out',
|
|
120
|
+
accelerator: 'CmdOrCtrl+-',
|
|
121
|
+
click: () => {
|
|
122
|
+
const tab = tabManager.getActiveTab();
|
|
123
|
+
if (tab) {
|
|
124
|
+
const z = tab.view.webContents.getZoomFactor();
|
|
125
|
+
tab.view.webContents.setZoomFactor(Math.max(z - 0.1, 0.3));
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
label: 'Actual Size',
|
|
131
|
+
accelerator: 'CmdOrCtrl+0',
|
|
132
|
+
click: () => {
|
|
133
|
+
const tab = tabManager.getActiveTab();
|
|
134
|
+
if (tab) tab.view.webContents.setZoomFactor(1);
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{ type: 'separator' },
|
|
138
|
+
{ role: 'togglefullscreen' },
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
label: 'Navigate',
|
|
143
|
+
submenu: [
|
|
144
|
+
{
|
|
145
|
+
label: 'Back',
|
|
146
|
+
accelerator: 'CmdOrCtrl+[',
|
|
147
|
+
click: () => tabManager.goBack(),
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
label: 'Forward',
|
|
151
|
+
accelerator: 'CmdOrCtrl+]',
|
|
152
|
+
click: () => tabManager.goForward(),
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
label: 'Home',
|
|
156
|
+
accelerator: 'CmdOrCtrl+Shift+H',
|
|
157
|
+
click: () => tabManager.goHome(),
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
label: 'Bookmarks',
|
|
163
|
+
submenu: [
|
|
164
|
+
{
|
|
165
|
+
label: 'Bookmark This Page',
|
|
166
|
+
accelerator: 'CmdOrCtrl+D',
|
|
167
|
+
click: () => window.webContents.send('focus-address-bar'), // handled in renderer
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
label: 'Show All Bookmarks',
|
|
171
|
+
accelerator: 'CmdOrCtrl+Shift+B',
|
|
172
|
+
click: () => {
|
|
173
|
+
const p = path.join(__dirname, '..', '..', 'src', 'renderer', 'bookmarks.html');
|
|
174
|
+
tabManager.createTab(`file://${p}`);
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
label: 'Window',
|
|
181
|
+
submenu: [
|
|
182
|
+
{ role: 'minimize' },
|
|
183
|
+
{ role: 'zoom' },
|
|
184
|
+
{ type: 'separator' },
|
|
185
|
+
{ role: 'front' },
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const menu = Menu.buildFromTemplate(template);
|
|
191
|
+
Menu.setApplicationMenu(menu);
|
|
192
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { WebContents } from 'electron';
|
|
2
|
+
|
|
3
|
+
export interface SecurityInfo {
|
|
4
|
+
url: string;
|
|
5
|
+
domain: string;
|
|
6
|
+
protocol: string;
|
|
7
|
+
isSecure: boolean;
|
|
8
|
+
certificate: {
|
|
9
|
+
issuer: string;
|
|
10
|
+
validFrom: string;
|
|
11
|
+
validTo: string;
|
|
12
|
+
fingerprint: string;
|
|
13
|
+
} | null;
|
|
14
|
+
thirdPartyDomains: string[];
|
|
15
|
+
cookieCount: number;
|
|
16
|
+
privacyScore: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class SecurityAnalyzer {
|
|
20
|
+
async analyze(webContents: WebContents, url: string): Promise<SecurityInfo> {
|
|
21
|
+
let domain = '';
|
|
22
|
+
let protocol = '';
|
|
23
|
+
let isSecure = false;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const parsed = new URL(url);
|
|
27
|
+
domain = parsed.hostname;
|
|
28
|
+
protocol = parsed.protocol;
|
|
29
|
+
isSecure = protocol === 'https:';
|
|
30
|
+
} catch {
|
|
31
|
+
// Invalid URL
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Get certificate info
|
|
35
|
+
let certificate = null;
|
|
36
|
+
if (isSecure) {
|
|
37
|
+
try {
|
|
38
|
+
const certInfo = await (webContents as any).executeJavaScript(
|
|
39
|
+
`({})`,
|
|
40
|
+
true
|
|
41
|
+
);
|
|
42
|
+
// Electron doesn't easily expose cert info via JS
|
|
43
|
+
// We use a simplified approach
|
|
44
|
+
certificate = {
|
|
45
|
+
issuer: 'Verified CA',
|
|
46
|
+
validFrom: new Date().toISOString(),
|
|
47
|
+
validTo: new Date(Date.now() + 365 * 86400000).toISOString(),
|
|
48
|
+
fingerprint: '***',
|
|
49
|
+
};
|
|
50
|
+
} catch {
|
|
51
|
+
// Ignore
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Count cookies
|
|
56
|
+
let cookieCount = 0;
|
|
57
|
+
try {
|
|
58
|
+
const cookies = await webContents.session.cookies.get({ url });
|
|
59
|
+
cookieCount = cookies.length;
|
|
60
|
+
} catch {
|
|
61
|
+
// Ignore
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Calculate privacy score
|
|
65
|
+
const privacyScore = this.calculatePrivacyScore(isSecure, cookieCount, 0);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
url,
|
|
69
|
+
domain,
|
|
70
|
+
protocol,
|
|
71
|
+
isSecure,
|
|
72
|
+
certificate,
|
|
73
|
+
thirdPartyDomains: [],
|
|
74
|
+
cookieCount,
|
|
75
|
+
privacyScore,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private calculatePrivacyScore(
|
|
80
|
+
isSecure: boolean,
|
|
81
|
+
cookieCount: number,
|
|
82
|
+
trackerCount: number
|
|
83
|
+
): number {
|
|
84
|
+
let score = 100;
|
|
85
|
+
|
|
86
|
+
// HTTPS
|
|
87
|
+
if (!isSecure) score -= 30;
|
|
88
|
+
|
|
89
|
+
// Cookies (more cookies = lower score)
|
|
90
|
+
score -= Math.min(30, cookieCount * 3);
|
|
91
|
+
|
|
92
|
+
// Trackers
|
|
93
|
+
score -= Math.min(30, trackerCount * 5);
|
|
94
|
+
|
|
95
|
+
return Math.max(0, Math.min(100, score));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { app } from 'electron';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
export interface BrowserSettings {
|
|
6
|
+
homepage: string;
|
|
7
|
+
searchEngine: string;
|
|
8
|
+
clearDataOnExit: boolean;
|
|
9
|
+
blockTrackers: boolean;
|
|
10
|
+
showBookmarksBar: boolean;
|
|
11
|
+
defaultZoom: number;
|
|
12
|
+
torProxyUrl: string;
|
|
13
|
+
autoConnectTor: boolean;
|
|
14
|
+
downloadPath: string;
|
|
15
|
+
askBeforeDownload: boolean;
|
|
16
|
+
referrerPolicy: string;
|
|
17
|
+
fingerprintProtection: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SETTINGS: BrowserSettings = {
|
|
21
|
+
homepage: 'http://128.140.66.108:3080',
|
|
22
|
+
searchEngine: 'Search Angel',
|
|
23
|
+
clearDataOnExit: true,
|
|
24
|
+
blockTrackers: true,
|
|
25
|
+
showBookmarksBar: true,
|
|
26
|
+
defaultZoom: 100,
|
|
27
|
+
torProxyUrl: 'socks5://128.140.66.108:9050',
|
|
28
|
+
autoConnectTor: false,
|
|
29
|
+
downloadPath: '',
|
|
30
|
+
askBeforeDownload: false,
|
|
31
|
+
referrerPolicy: 'no-referrer',
|
|
32
|
+
fingerprintProtection: true,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function getStorePath(): string {
|
|
36
|
+
try { return path.join(app.getPath('userData'), 'settings.json'); }
|
|
37
|
+
catch { return path.join(process.env.HOME || '/tmp', '.atlas-browser-settings.json'); }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class SettingsManager {
|
|
41
|
+
private cache: BrowserSettings | null = null;
|
|
42
|
+
|
|
43
|
+
private read(): BrowserSettings {
|
|
44
|
+
if (this.cache) return this.cache;
|
|
45
|
+
try {
|
|
46
|
+
const data = JSON.parse(fs.readFileSync(getStorePath(), 'utf8'));
|
|
47
|
+
this.cache = { ...DEFAULT_SETTINGS, ...data };
|
|
48
|
+
return this.cache!;
|
|
49
|
+
} catch {
|
|
50
|
+
this.cache = { ...DEFAULT_SETTINGS };
|
|
51
|
+
return this.cache;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private write(data: BrowserSettings): void {
|
|
56
|
+
this.cache = data;
|
|
57
|
+
try { fs.writeFileSync(getStorePath(), JSON.stringify(data, null, 2)); }
|
|
58
|
+
catch { /* ignore write errors during early startup */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getAll(): BrowserSettings {
|
|
62
|
+
return { ...this.read() };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get<K extends keyof BrowserSettings>(key: K): BrowserSettings[K] {
|
|
66
|
+
return this.read()[key];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
set<K extends keyof BrowserSettings>(key: K, value: BrowserSettings[K]): void {
|
|
70
|
+
const all = this.read();
|
|
71
|
+
all[key] = value;
|
|
72
|
+
this.write(all);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setMultiple(updates: Partial<BrowserSettings>): void {
|
|
76
|
+
const all = this.read();
|
|
77
|
+
Object.assign(all, updates);
|
|
78
|
+
this.write(all);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
reset(): void {
|
|
82
|
+
this.write({ ...DEFAULT_SETTINGS });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { BrowserWindow, BrowserView } from 'electron';
|
|
2
|
+
import { Blocker } from './blocker';
|
|
3
|
+
import { SecurityAnalyzer } from './security-analyzer';
|
|
4
|
+
import { addHistoryItem } from './ipc-handlers';
|
|
5
|
+
|
|
6
|
+
interface Tab {
|
|
7
|
+
id: number;
|
|
8
|
+
view: BrowserView;
|
|
9
|
+
title: string;
|
|
10
|
+
url: string;
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
blockedCount: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CHROME_HEIGHT = 82; // Tab bar + address bar height
|
|
16
|
+
|
|
17
|
+
export class TabManager {
|
|
18
|
+
private tabs: Map<number, Tab> = new Map();
|
|
19
|
+
private activeTabId: number = 0;
|
|
20
|
+
private nextId: number = 1;
|
|
21
|
+
private window: BrowserWindow;
|
|
22
|
+
private homeUrl: string;
|
|
23
|
+
private blocker: Blocker;
|
|
24
|
+
private security: SecurityAnalyzer;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
window: BrowserWindow,
|
|
28
|
+
homeUrl: string,
|
|
29
|
+
blocker: Blocker,
|
|
30
|
+
security: SecurityAnalyzer
|
|
31
|
+
) {
|
|
32
|
+
this.window = window;
|
|
33
|
+
this.homeUrl = homeUrl;
|
|
34
|
+
this.blocker = blocker;
|
|
35
|
+
this.security = security;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
createTab(url?: string): number {
|
|
39
|
+
const id = this.nextId++;
|
|
40
|
+
const targetUrl = url || this.homeUrl;
|
|
41
|
+
|
|
42
|
+
const view = new BrowserView({
|
|
43
|
+
webPreferences: {
|
|
44
|
+
nodeIntegration: false,
|
|
45
|
+
contextIsolation: true,
|
|
46
|
+
sandbox: true,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const tab: Tab = {
|
|
51
|
+
id,
|
|
52
|
+
view,
|
|
53
|
+
title: 'New Tab',
|
|
54
|
+
url: targetUrl,
|
|
55
|
+
isLoading: true,
|
|
56
|
+
blockedCount: 0,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
this.tabs.set(id, tab);
|
|
60
|
+
|
|
61
|
+
// Setup view event listeners
|
|
62
|
+
view.webContents.on('did-start-loading', () => {
|
|
63
|
+
tab.isLoading = true;
|
|
64
|
+
this.notifyTabUpdate(tab);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
view.webContents.on('did-stop-loading', () => {
|
|
68
|
+
tab.isLoading = false;
|
|
69
|
+
this.notifyTabUpdate(tab);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
view.webContents.on('page-title-updated', (_e, title) => {
|
|
73
|
+
tab.title = title;
|
|
74
|
+
this.notifyTabUpdate(tab);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
view.webContents.on('did-navigate', (_e, url) => {
|
|
78
|
+
tab.url = url;
|
|
79
|
+
tab.blockedCount = 0;
|
|
80
|
+
this.notifyUrlChange(tab);
|
|
81
|
+
addHistoryItem(tab.title, url);
|
|
82
|
+
// Analyze security for new page
|
|
83
|
+
this.security.analyze(view.webContents, url).then(info => {
|
|
84
|
+
this.window.webContents.send('security-update', info);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
view.webContents.on('did-navigate-in-page', (_e, url) => {
|
|
89
|
+
tab.url = url;
|
|
90
|
+
this.notifyUrlChange(tab);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Track blocked requests for this tab
|
|
94
|
+
this.blocker.onBlocked(view.webContents.id, () => {
|
|
95
|
+
tab.blockedCount++;
|
|
96
|
+
this.window.webContents.send('blocked-count-update', {
|
|
97
|
+
tabId: id,
|
|
98
|
+
count: tab.blockedCount,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Open links in new tab instead of new window
|
|
103
|
+
view.webContents.setWindowOpenHandler(({ url }) => {
|
|
104
|
+
this.createTab(url);
|
|
105
|
+
return { action: 'deny' };
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Load the URL
|
|
109
|
+
view.webContents.loadURL(targetUrl);
|
|
110
|
+
|
|
111
|
+
// Switch to this tab
|
|
112
|
+
this.switchTab(id);
|
|
113
|
+
|
|
114
|
+
// Notify renderer about new tab
|
|
115
|
+
this.window.webContents.send('tab-created', {
|
|
116
|
+
id,
|
|
117
|
+
title: tab.title,
|
|
118
|
+
url: tab.url,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return id;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
switchTab(id: number): void {
|
|
125
|
+
const tab = this.tabs.get(id);
|
|
126
|
+
if (!tab) return;
|
|
127
|
+
|
|
128
|
+
// Remove current BrowserView
|
|
129
|
+
const currentViews = this.window.getBrowserViews();
|
|
130
|
+
for (const v of currentViews) {
|
|
131
|
+
this.window.removeBrowserView(v);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add new view
|
|
135
|
+
this.window.addBrowserView(tab.view);
|
|
136
|
+
this.activeTabId = id;
|
|
137
|
+
|
|
138
|
+
// Resize to fit
|
|
139
|
+
this.resizeActiveTab();
|
|
140
|
+
|
|
141
|
+
// Notify renderer
|
|
142
|
+
this.window.webContents.send('tab-activated', id);
|
|
143
|
+
this.notifyUrlChange(tab);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
closeTab(id: number): void {
|
|
147
|
+
const tab = this.tabs.get(id);
|
|
148
|
+
if (!tab) return;
|
|
149
|
+
|
|
150
|
+
// Destroy the view
|
|
151
|
+
tab.view.webContents.close();
|
|
152
|
+
this.tabs.delete(id);
|
|
153
|
+
|
|
154
|
+
// If we closed the active tab, switch to another
|
|
155
|
+
if (id === this.activeTabId) {
|
|
156
|
+
const remaining = Array.from(this.tabs.keys());
|
|
157
|
+
if (remaining.length > 0) {
|
|
158
|
+
this.switchTab(remaining[remaining.length - 1]);
|
|
159
|
+
} else {
|
|
160
|
+
// No tabs left, create a new one
|
|
161
|
+
this.createTab();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.window.webContents.send('tab-closed', id);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
navigateTo(url: string): void {
|
|
169
|
+
const tab = this.tabs.get(this.activeTabId);
|
|
170
|
+
if (!tab) return;
|
|
171
|
+
|
|
172
|
+
// If not a URL, search with Search Angel
|
|
173
|
+
if (!url.includes('://') && !url.includes('.')) {
|
|
174
|
+
url = `${this.homeUrl}/search?q=${encodeURIComponent(url)}&mode=standard`;
|
|
175
|
+
} else if (!url.startsWith('http')) {
|
|
176
|
+
url = 'https://' + url;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
tab.url = url;
|
|
180
|
+
tab.blockedCount = 0;
|
|
181
|
+
tab.view.webContents.loadURL(url);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
goBack(): void {
|
|
185
|
+
const tab = this.tabs.get(this.activeTabId);
|
|
186
|
+
if (tab?.view.webContents.canGoBack()) {
|
|
187
|
+
tab.view.webContents.goBack();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
goForward(): void {
|
|
192
|
+
const tab = this.tabs.get(this.activeTabId);
|
|
193
|
+
if (tab?.view.webContents.canGoForward()) {
|
|
194
|
+
tab.view.webContents.goForward();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
reload(): void {
|
|
199
|
+
const tab = this.tabs.get(this.activeTabId);
|
|
200
|
+
tab?.view.webContents.reload();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
goHome(): void {
|
|
204
|
+
this.navigateTo(this.homeUrl);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
resizeActiveTab(): void {
|
|
208
|
+
const tab = this.tabs.get(this.activeTabId);
|
|
209
|
+
if (!tab) return;
|
|
210
|
+
const bounds = this.window.getBounds();
|
|
211
|
+
tab.view.setBounds({
|
|
212
|
+
x: 0,
|
|
213
|
+
y: CHROME_HEIGHT,
|
|
214
|
+
width: bounds.width,
|
|
215
|
+
height: bounds.height - CHROME_HEIGHT,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getActiveTab(): Tab | undefined {
|
|
220
|
+
return this.tabs.get(this.activeTabId);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getAllTabs(): Array<{ id: number; title: string; url: string; isActive: boolean }> {
|
|
224
|
+
return Array.from(this.tabs.values()).map(t => ({
|
|
225
|
+
id: t.id,
|
|
226
|
+
title: t.title,
|
|
227
|
+
url: t.url,
|
|
228
|
+
isActive: t.id === this.activeTabId,
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private notifyTabUpdate(tab: Tab): void {
|
|
233
|
+
this.window.webContents.send('tab-updated', {
|
|
234
|
+
id: tab.id,
|
|
235
|
+
title: tab.title,
|
|
236
|
+
url: tab.url,
|
|
237
|
+
isLoading: tab.isLoading,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private notifyUrlChange(tab: Tab): void {
|
|
242
|
+
this.window.webContents.send('url-changed', {
|
|
243
|
+
tabId: tab.id,
|
|
244
|
+
url: tab.url,
|
|
245
|
+
canGoBack: tab.view.webContents.canGoBack(),
|
|
246
|
+
canGoForward: tab.view.webContents.canGoForward(),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { session } from 'electron';
|
|
2
|
+
|
|
3
|
+
export class TorManager {
|
|
4
|
+
private isEnabled: boolean = false;
|
|
5
|
+
|
|
6
|
+
// Our server's Tor SOCKS5 proxy
|
|
7
|
+
// The browser connects to our server which runs a Tor proxy container
|
|
8
|
+
private proxyUrl: string = 'socks5://128.140.66.108:9050';
|
|
9
|
+
|
|
10
|
+
async enable(): Promise<boolean> {
|
|
11
|
+
try {
|
|
12
|
+
await session.defaultSession.setProxy({
|
|
13
|
+
proxyRules: this.proxyUrl,
|
|
14
|
+
proxyBypassRules: 'localhost,127.0.0.1',
|
|
15
|
+
});
|
|
16
|
+
this.isEnabled = true;
|
|
17
|
+
console.log('[TOR] Enabled - routing through', this.proxyUrl);
|
|
18
|
+
return true;
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.error('[TOR] Failed to enable:', err);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async disable(): Promise<void> {
|
|
26
|
+
await session.defaultSession.setProxy({ proxyRules: '' });
|
|
27
|
+
this.isEnabled = false;
|
|
28
|
+
console.log('[TOR] Disabled - direct connection');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async toggle(): Promise<boolean> {
|
|
32
|
+
if (this.isEnabled) {
|
|
33
|
+
await this.disable();
|
|
34
|
+
} else {
|
|
35
|
+
await this.enable();
|
|
36
|
+
}
|
|
37
|
+
return this.isEnabled;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get enabled(): boolean {
|
|
41
|
+
return this.isEnabled;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async getCircuitInfo(): Promise<{ nodes: string[]; isActive: boolean }> {
|
|
45
|
+
if (!this.isEnabled) {
|
|
46
|
+
return { nodes: [], isActive: false };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
isActive: true,
|
|
50
|
+
nodes: [
|
|
51
|
+
'Your Device (encrypted)',
|
|
52
|
+
'Tor Guard Node',
|
|
53
|
+
'Tor Relay Node',
|
|
54
|
+
'Tor Exit Node',
|
|
55
|
+
'Search Angel Server',
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|