1 /* Copyright (C) 2024 Richard Hao Cao
4 app
, BrowserWindow
, Menu
, shell
, clipboard
,
5 session
, protocol
, net
, dialog
6 } = require('electron')
9 if(!app
.requestSingleInstanceLock())
12 app
.on('ready', createWindow
);
13 app
.on('second-instance', (event
, args
, cwd
) => {
15 if (win
.isMinimized()) {
20 cmdlineProcess(args
,cwd
,1);
27 const repositoryurl
= "https://gitlab.com/jamesfengcao/uweb/-/raw/master/misc/ebrowser/";
28 const fs
= require('fs');
29 const readline
= require('readline');
30 const path
= require('path')
31 const process
= require('process')
38 var bForwardCookie
= false;
43 "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" +
44 process
.versions
.chrome
+" Safari/537.36";
45 app
.userAgentFallback
= defaultUA
;
46 var historyFile
= path
.join(__dirname
,'history.rec');
48 fs
.readFile(path
.join(__dirname
,'redirect.json'), 'utf8', (err
, jsonString
) => {
51 redirects
= JSON
.parse(jsonString
);
52 } catch (e
){console
.log(e
)}
55 async
function createWindow () {
57 let json
= await fs
.promises
.readFile(path
.join(__dirname
,'uas.json'), 'utf8');
58 useragents
= JSON
.parse(json
);
59 } catch (e
){console
.log(e
)}
63 const readInterface
= readline
.createInterface ({
64 input
: fs
.createReadStream (path
.join(__dirname
,'config'), 'utf8'),
67 for await (const line
of readInterface
) {
70 }catch(e
){console
.log(e
);}
73 win
= new BrowserWindow(
74 {width
: 800, height
: 600,autoHideMenuBar
: true,
76 nodeIntegration
: true,
77 contextIsolation
: false,
80 win
.setMenuBarVisibility(false);
81 win
.on('closed', function () {
85 win
.loadFile('index.html');
86 fs
.readFile(path
.join(__dirname
,'gredirect.json'), 'utf8', (err
, jsonString
) => {
89 gredirects
= JSON
.parse(jsonString
);
90 } catch (e
){console
.log(e
)}
93 fs
.readFile(path
.join(__dirname
,'proxy.json'), 'utf8', (err
, jsonString
) => {
96 proxies
= JSON
.parse(jsonString
, (key
,val
)=>{
97 if(!proxy
&& key
==="proxyRules"){
98 proxy
= {proxyRules
:val
};
102 } catch (e
){console
.log(e
)}
105 cmdlineProcess(process
.argv
, process
.cwd(), 0);
106 //app.commandLine.appendSwitch ('trace-warnings');
108 win
.webContents
.on('page-title-updated',(event
,cmd
)=>{
112 win
.webContents
.on('console-message',cbConsoleMsg
);
115 app
.on('window-all-closed', function () {
119 app
.on('activate', function () {
125 app
.on('will-quit', () => {
128 app
.on ('web-contents-created', (event
, contents
) => {
129 if (contents
.getType () === 'webview') {
130 contents
.setWindowOpenHandler(cbWindowOpenHandler
);
131 contents
.on('context-menu',onContextMenu
);
132 contents
.on('page-title-updated',cbTitleUpdate
);
133 //contents.on('console-message',cbConsoleMsg);
134 //contents.on('focus', ()=>{cbFocus(contents)});
135 //contents.on('blur',()=>{cbBlur()});
136 contents
.session
.webRequest
.onBeforeRequest(interceptRequest
);
137 contents
.on('did-finish-load',()=>{cbFinishLoad(contents
)});
141 function addrCommand(cmd
){
142 if(cmd
.length
<3) return;
143 let c0
= cmd
.charCodeAt(0);
146 args
= cmd
.substring(1).split(/\s+/);
150 session
.defaultSession
.setCertificateVerifyProc((request
, callback
) => {
154 session
.defaultSession
.setCertificateVerifyProc(null);
158 session
.defaultSession
.clearData();
163 session
.defaultSession
.clearCache();
166 session
.defaultSession
.clearHostResolverCache();
169 session
.defaultSession
.clearStorageData();
173 let opts
= JSON
.parse(args
.slice(1).join(""));
174 session
.defaultSession
.clearData(opts
);
175 }catch(e
){console
.log(e
)}
179 session
.defaultSession
.loadExtension(args
[1]);
186 let i
= parseInt(args
[1]);
187 if(i
>=0 && i
<gredirects
.length
)
192 case "js"://exetute js
196 bForwardCookie
= false;
197 msgbox_info("Cookie forwarding disabled");
201 msgbox_info("Cookie forwarding enabled for global redirection");
207 bHistory
= false; return;
209 bHistory
= true; return;
215 session
.defaultSession
.setProxy ({mode
:"direct"});
220 proxy
= proxies
[args
[1]]; //retrieve proxy
223 session
.defaultSession
.setProxy(proxy
);
227 bRedirect
= false; return;
229 bRedirect
= true; return;
232 session
.defaultSession
.setUserAgent(useragents
[args
[1]]);
234 session
.defaultSession
.setUserAgent(defaultUA
);
239 updateurl
= repositoryurl
;
242 if(!updateurl
.endsWith("/")) updateurl
= updateurl
+"/";
244 updateApp(updateurl
);
250 function gredirect_disable(){
257 function gredirect_enable(i
){
258 if(i
>=gredirects
.length
) return;
259 if(!gredirect
) registerHandler();
260 gredirect
=gredirects
[i
];
263 function cbConsoleMsg(e
, level
, msg
, line
, sourceid
){
265 console
.log(sourceid
);
269 function cbFinishLoad(webContents
){
270 if(!bHistory
) return;
271 let histItem
= webContents
.getTitle()+" "+webContents
.getURL()+"\n";
272 fs
.appendFile(historyFile
, histItem
, (err
) => {});
275 function cbFocus(webContents
){
276 let js
= "if(focusMesg){let m=focusMesg;focusMesg=null;m}";
277 win
.webContents
.executeJavaScript(js
,false).then((r
)=>{
278 //focusMesg as js code
280 if(r
) webContents
.executeJavaScript(r
,false);
284 function interceptRequest(details
, callback
){
285 if(!bJS
&& details
.url
.endsWith(".js")){
286 callback({ cancel
: true });
290 if(gredirect
|| !bRedirect
||(details
.resourceType
!== 'mainFrame' &&
291 details
.resourceType
!== 'subFrame')) break;
292 let oURL
= new URL(details
.url
);
293 let domain
= oURL
.hostname
;
296 let newDomain
= redirects
[domain
];
297 if(!newDomain
) break;
298 newUrl
= "https://"+newDomain
+oURL
.pathname
+oURL
.search
+oURL
.hash
;
300 callback({ cancel
: false, redirectURL
: newUrl
});
303 callback({ cancel
: false });
306 function cbWindowOpenHandler(details
){
307 let url
= details
.url
;
308 let js
= "newTab();tabs.children[tabs.children.length-1].src='"+
310 switch(details
.disposition
){
311 case "foreground-tab":
313 js
= js
+ "switchTab(tabs.children.length-1)";
315 win
.webContents
.executeJavaScript(js
,false);
316 return { action
: "deny" };
318 function cbTitleUpdate(event
,title
){
321 function menuArray(labelprefix
, linkUrl
){
322 const menuTemplate
= [
324 label
: labelprefix
+'Open Link',
326 shell
.openExternal(linkUrl
);
330 label
: labelprefix
+'Copy Link',
332 clipboard
.writeText(linkUrl
);
336 label
: labelprefix
+'Download',
338 win
.contentView
.children
[i
].webContents
.downloadURL(linkUrl
);
345 function onContextMenu(event
, params
){
346 let url
= params
.linkURL
;
349 mTemplate
.push({label
:url
,enabled
:false});
350 mTemplate
.push
.apply(mTemplate
,menuArray("",url
));
351 if((url
=params
.srcURL
))
352 mTemplate
.push
.apply(mTemplate
,menuArray("src: ",url
));
353 }else if((url
=params
.srcURL
)){
354 mTemplate
.push({label
:url
,enabled
:false});
355 mTemplate
.push
.apply(mTemplate
,menuArray("src: ",url
));
359 const contextMenu
= Menu
.buildFromTemplate(mTemplate
);
364 const menuTemplate
= [
368 { label
: 'Config folder', click
: ()=>{
369 shell
.openPath(__dirname
);
376 { label
: 'Check for updates', click
: ()=>{
377 addrCommand(":update");
379 { label
: 'Help', accelerator
: 'F1', click
: ()=>{
382 { label
: 'Stop', accelerator
: 'Ctrl+C', click
: ()=>{
383 let js
="tabs.children[iTab].stop()"
384 win
.webContents
.executeJavaScript(js
,false)
386 { label
: 'getURL', accelerator
: 'Ctrl+G', click
: ()=>{
387 let js
="{let q=document.forms[0].q;q.focus();q.value=tabs.children[iTab].src}"
388 win
.webContents
.executeJavaScript(js
,false)
390 { label
: 'Select', accelerator
: 'Ctrl+L', click
:()=>{
391 win
.webContents
.executeJavaScript("document.forms[0].q.select()",false);
393 { label
: 'New Tab', accelerator
: 'Ctrl+T', click
:()=>{
394 let js
= "newTab();document.forms[0].q.select();switchTab(tabs.children.length-1)";
395 win
.webContents
.executeJavaScript(js
,false);
397 { label
: 'Restore Tab', accelerator
: 'Ctrl+Shift+T', click
:()=>{
398 let js
= "{let u=closedUrls.pop();if(u){newTab();switchTab(tabs.children.length-1);tabs.children[iTab].src=u}}";
399 win
.webContents
.executeJavaScript(js
,false);
401 { label
: 'No redirect', accelerator
: 'Ctrl+R', click
: ()=>{
404 { label
: 'Redirect', accelerator
: 'Ctrl+Shift+R', click
: ()=>{
407 { label
: 'Close', accelerator
: 'Ctrl+W', click
: ()=>{
408 win
.webContents
.executeJavaScript("tabClose()",false).then((r
)=>{
409 if(""===r
) win
.close();
410 else win
.setTitle(r
);
413 { label
: 'Next Tab', accelerator
: 'Ctrl+Tab', click
: ()=>{
414 let js
="tabInc(1);getWinTitle()";
415 win
.webContents
.executeJavaScript(js
,false).then((r
)=>{
419 { label
: 'Previous Tab', accelerator
: 'Ctrl+Shift+Tab', click
: ()=>{
420 let js
="tabDec(-1);getWinTitle()";
421 win
.webContents
.executeJavaScript(js
,false).then((r
)=>{
425 { label
: 'Go backward', accelerator
: 'Ctrl+Left', click
: ()=>{
426 let js
="tabs.children[iTab].goBack()";
427 win
.webContents
.executeJavaScript(js
,false);
429 { label
: 'Go forward', accelerator
: 'Ctrl+Right', click
: ()=>{
430 let js
="tabs.children[iTab].goForward()";
431 win
.webContents
.executeJavaScript(js
,false);
433 { label
: 'Zoom in', accelerator
: 'Ctrl+Shift+=', click
: ()=>{
434 let js
="{let t=tabs.children[iTab];let s=t.getZoomFactor()*1.2;t.setZoomFactor(s)}";
435 win
.webContents
.executeJavaScript(js
,false);
437 { label
: 'Zoom out', accelerator
: 'Ctrl+-', click
: ()=>{
438 let js
="{let t=tabs.children[iTab];let s=t.getZoomFactor()/1.2;t.setZoomFactor(s)}";
439 win
.webContents
.executeJavaScript(js
,false);
441 { label
: 'Default zoom', accelerator
: 'Ctrl+0', click
: ()=>{
442 let js
="tabs.children[iTab].setZoomFactor(1)";
443 win
.webContents
.executeJavaScript(js
,false);
445 { label
: 'No focus', accelerator
: 'Esc', click
: ()=>{
446 let js
= `{let e=document.activeElement;
447 if(e)e.blur();try{tabs.children[iTab].stopFindInPage('clearSelection')}catch(er){}}`;
448 win
.webContents
.executeJavaScript(js
,false);
450 { label
: 'Reload', accelerator
: 'F5', click
: ()=>{
451 win
.webContents
.executeJavaScript("tabs.children[iTab].reload()",false);
453 { label
: 'Devtools', accelerator
: 'F12', click
: ()=>{
454 let js
= "try{tabs.children[iTab].openDevTools()}catch(e){console.log(e)}";
455 win
.webContents
.executeJavaScript(js
,false);
461 const menu
= Menu
.buildFromTemplate(menuTemplate
);
462 Menu
.setApplicationMenu(menu
);
465 function cmdlineProcess(argv
,cwd
,extra
){
466 let i1st
= 2+extra
; //index for the first query item
467 if(argv
.length
>i1st
){
468 if(i1st
+1==argv
.length
){//local file
469 let fname
= path
.join(cwd
, argv
[i1st
]);
470 if(fs
.existsSync(fname
)){
471 let js
= "tabs.children[iTab].src='file://"+fname
+"'";
472 win
.webContents
.executeJavaScript(js
,false);
473 win
.setTitle(argv
[i1st
]);
477 let url
=argv
.slice(i1st
).join(" ");
478 win
.webContents
.executeJavaScript("handleQuery(`"+url
+"`)",false);
483 async
function cbScheme_redir(req
){
484 if(!gredirect
) return null;
486 let newurl
= gredirect
+oUrl
;
489 headers
: req
.headers
,
491 referer
: req
.referer
,
493 bypassCustomProtocolHandlers
: true
496 let cookies
= await session
.defaultSession
.cookies
.get({url
: oUrl
});
497 let cookieS
= cookies
.map (cookie
=> cookie
.name
+ '=' + cookie
.value
).join(';');
498 options
.headers
['Cookie'] = cookieS
;
501 return fetch(newurl
, options
);
504 function registerHandler(){
505 protocol
.handle("http",cbScheme_redir
);
506 protocol
.handle("https",cbScheme_redir
);
507 protocol
.handle("ws",cbScheme_redir
);
508 protocol
.handle("wss",cbScheme_redir
);
510 function unregisterHandler(){
511 protocol
.unhandle("http",cbScheme_redir
);
512 protocol
.unhandle("https",cbScheme_redir
);
513 protocol
.unhandle("ws",cbScheme_redir
);
514 protocol
.unhandle("wss",cbScheme_redir
);
517 function forwardCookie(){
518 const choice
= dialog
.showMessageBoxSync(null, {
520 title
: 'Confirm cookie forwarding with global redirection',
521 message
: 'Cookies are used to access your account. Forwarding cookies is vulnerable to global redirection server, proceed to enable cookie forwarding with global redirection?',
522 buttons
: ['No','Yes']
524 if(1===choice
) bForwardCookie
=true;
526 function msgbox_info(msg
){
527 dialog
.showMessageBoxSync(null, {
535 async
function updateApp(url
){//url must ending with "/"
539 let res
= await
fetch(url
+"package.json");
540 let packageS
= await res
.text();
541 {//the last part of version string is the version number, must keep increasing
542 let head
= packageS
.slice(2,40);
543 let iV
= head
.indexOf("version");
545 msg
= "remote package.json corrupted"
549 let iE
= head
.indexOf('"',iV
+4);
550 let iS
= head
.lastIndexOf('.',iE
-1);
551 let nLatestVer
= parseInt(head
.substring(iS
+1,iE
));
553 let ver
= app
.getVersion();
554 iS
= ver
.lastIndexOf('.');
555 let nVer
= parseInt(ver
.substring(iS
+1));
556 if(nVer
>=nLatestVer
){
557 msg
= `Current version ${ver} is already up to date`;
560 const choice
= dialog
.showMessageBoxSync(null, {
562 title
: `Update from ${url}`,
563 message
: `Proceed to update from ${ver} to ${head.substring(iV,iE)}?`,
564 buttons
: ['YES','NO']
566 if(1===choice
) return;
569 writeFile("package.json", packageS
);
571 fetch2file(url
,"webview.js");
572 fetch2file(url
,"index.html");
573 msg
= "Update completed";
575 msg
= "Fail to update"
578 dialog
.showMessageBoxSync(null, {
580 title
: `Update from ${url}`,
586 async
function fetch2file(urlFolder
, filename
, bOverwritten
=true){
587 let pathname
=path
.join(__dirname
,filename
);
588 if(!bOverwritten
&& fs
.existsSync(pathname
)) return;
589 let res
= await
fetch(urlFolder
+filename
);
590 let str
= await res
.text();
591 writeFile(pathname
, str
);
594 async
function writeFile(filename
, str
){
595 let pathname
=filename
+".new";
596 fs
.writeFile(pathname
, str
, (err
) => {
597 if(err
) throw "Fail to write";
598 fs
.rename(pathname
,filename
,(e1
)=>{
599 if(e1
) throw "Fail to rename";
605 const readme
= "README.md";
606 const htmlFN
= path
.join(__dirname
,readme
+".html");
607 if(!fs
.existsSync(htmlFN
)){
608 const readmeP
= path
.join(__dirname
,readme
);
610 fs
.copyFileSync(readmeP
, htmlFN
);
611 const postscript
="<script src='https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js'></script><script>var d=document;var b=d.body;var t=b.textContent;t=t.slice(0,t.length-253);b.innerHTML=marked.parse(t);d.title=d.title||b.firstElementChild.innerText.trim();</script>";
612 fs
.appendFileSync(htmlFN
,postscript
);
617 let js
=`tabs.children[iTab].src="file://${htmlFN}"`;
618 win
.webContents
.executeJavaScript(js
,false)