1 /* Copyright (C) 2024 Richard Hao Cao
2 Ebrowser is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4 Ebrowser is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
6 You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
9 app
, BrowserWindow
, Menu
, shell
, clipboard
,
10 session
, protocol
, dialog
, ipcMain
11 } = require('electron')
14 if(!app
.requestSingleInstanceLock())
17 app
.on('ready', createWindow
);
18 app
.on('second-instance', (event
, args
, cwd
) => {
20 if (win
.isMinimized()) {
25 cmdlineProcess(args
,cwd
,1);
30 Menu
.setApplicationMenu(null);
31 const fs
= require('fs');
32 const path
= require('path')
33 const https
= require('https');
34 const url
= require('url');
37 let langs
= app
.getPreferredSystemLanguages();
38 if(langs
.length
==0 || langs
[0].startsWith('en'))
41 initTranslateRes(langs
[0]);
44 var repositoryurl
= "https://gitlab.com/jamesfengcao/uweb/-/raw/master/misc/ebrowser/";
45 const readline
= require('readline');
46 const process
= require('process')
52 var bForwardCookie
= true;
56 var downloadMenus
; //[]
60 let sys
= "X11; Linux x86_64";
61 if (process
.platform
=== "win32")
62 sys
= "Window NT 10.0; Win64; x64";
63 else if (process
.platform
=== "darwin")
64 sys
= "Macintosh; Intel Mac OS X 10_15_7";
66 `Mozilla/5.0 (${sys}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/` +
67 process
.versions
.chrome
+" Safari/537.36";
69 app
.userAgentFallback
= defaultUA
;
71 fs
.readFile(path
.join(__dirname
,'redirect.json'), 'utf8', (err
, jsonString
) => {
74 redirects
= JSON
.parse(jsonString
);
75 } catch (e
){console
.log(e
)}
78 async
function createWindow () {
80 let json
= await fs
.promises
.readFile(path
.join(__dirname
,'uas.json'), 'utf8');
81 useragents
= JSON
.parse(json
);
82 } catch (e
){console
.log(e
)}
84 protocol
.handle("i",(req
)=>{return null;});
87 const readInterface
= readline
.createInterface ({
88 input
: fs
.createReadStream (path
.join(__dirname
,'config'), 'utf8'),
91 for await (const line
of readInterface
) {
94 }catch(e
){console
.log(e
);}
97 win
= new BrowserWindow(
98 {width
: 800, height
: 600,autoHideMenuBar
: true,
100 nodeIntegration
: true,
101 contextIsolation
: false,
104 win
.setMenuBarVisibility(false);
105 win
.on('closed', function () {
109 win
.loadFile('index.html');
110 fs
.readFile(path
.join(__dirname
,'gredirect.json'), 'utf8', (err
, jsonString
) => {
113 gredirects
= JSON
.parse(jsonString
);
114 } catch (e
){console
.log(e
)}
117 fs
.readFile(path
.join(__dirname
,'proxy.json'), 'utf8', (err
, jsonString
) => {
120 proxies
= JSON
.parse(jsonString
);
121 let match
= jsonString
.match(/"([^"]+)"/);
123 proxy
= proxies
[match
[1]];
124 } catch (e
){console
.log(e
)}
127 cmdlineProcess(process
.argv
, process
.cwd(), 0);
128 //app.commandLine.appendSwitch ('trace-warnings');
130 fs
.readFile(path
.join(__dirname
,'download.json'), 'utf8', (err
, jsonStr
) => {
133 downloadMenus
= JSON
.parse(jsonStr
);
134 }catch (e
){console
.log(e
)}
137 fs
.readFile(path
.join(__dirname
,'select.json'), 'utf8', (err
, jsonStr
) => {
140 selectMenus
= JSON
.parse(jsonStr
);
141 }catch (e
){console
.log(e
)}
144 win
.webContents
.on('page-title-updated',(event
,cmd
)=>{
148 session
.defaultSession
.on("will-download", async (e
, item
) => {
149 //item.setSavePath(save)
150 if(!downloadMenus
) return;
151 let menuT
= downloadContextMenuTemp(item
.getURL());
152 let button
= await
promiseContextMenu(menuT
);
153 if(-1===button
) return;
157 win
.webContents
.on('console-message',cbConsoleMsg
);
160 app
.on('window-all-closed', function () {
164 app
.on('activate', function () {
170 app
.on('will-quit', () => {
173 app
.on ('web-contents-created', (event
, contents
) => {
174 if (contents
.getType () === 'webview') {
175 contents
.setWindowOpenHandler(cbWindowOpenHandler
);
176 contents
.on('context-menu',onContextMenu
);
177 contents
.on('page-title-updated',cbTitleUpdate
);
178 contents
.session
.webRequest
.onBeforeRequest(interceptRequest
);
182 ipcMain
.on('command', (event
, cmd
) => {
186 async
function addrCommand(cmd
){
187 if(cmd
.length
<3) return;
188 let c0
= cmd
.charCodeAt(0);
194 let iS
= cmd
.indexOf(' ',1);
195 if(iS
<0) iS
= cmd
.length
;
196 let arg0
= cmd
.substring(1,iS
);
200 session
.defaultSession
.setCertificateVerifyProc((request
, callback
) => {
204 session
.defaultSession
.setCertificateVerifyProc(null);
207 if(cmd
.length
<=iS
+1){
208 session
.defaultSession
.clearData();
211 if(123===cmd
.charCodeAt(iS
+1)){//json
213 let opts
= JSON
.parse(cmd
.substring(iS
+1));
214 session
.defaultSession
.clearData(opts
);
215 }catch(e
){console
.log(e
)}
218 let args
= cmd
.substring(iS
+1).split(/\s+/);
221 session
.defaultSession
.clearCache();
225 session
.defaultSession
.clearStorageData({ storages
: ['cookies'] });
230 if(url
.charCodeAt(0)!==104) url
= "https://"+url
;
231 session
.defaultSession
.cookies
.get({ url
: url
}).then((cookies
) => {
232 cookies
.forEach((cookie
) => {
233 session
.defaultSession
.cookies
.remove(targetUrl
, cookie
.name
)})});
237 session
.defaultSession
.clearHostResolverCache();
240 session
.defaultSession
.clearStorageData();
249 session
.defaultSession
.loadExtension(cmd
.substring(iS
+1));
256 let i
= parseInt(cmd
.substring(iS
+1));
257 if(i
>=0 && i
<gredirects
.length
)
262 case "js"://execute js
266 bForwardCookie
= false;
267 msgbox_info("Cookie forwarding disabled");
271 msgbox_info("Cookie forwarding enabled for global redirection");
277 session
.defaultSession
.setProxy ({mode
:"direct"});
282 proxy
= proxies
[cmd
.substring(iS
+1)]; //retrieve proxy
284 session
.defaultSession
.setProxy(proxy
)
285 .then(() => {gredirect_disable()})
287 console
.error('Failed to set proxy:', error
);
292 bRedirect
= false; return;
294 bRedirect
= true; return;
297 let iHTTP
= cmd
.search(/https?:\/\//);
299 let iEnd
= cmd
.indexOf(' ',iHTTP
+10);
300 if(iEnd
<0) iEnd
= cmd
.length
;
301 let url
= cmd
.substring(iHTTP
,iEnd
);
302 let cookies
= await session
.defaultSession
.cookies
.get({url
: url
});
303 let cookieS
= cookies
.map (cookie
=> cookie
.name
+ '='
304 + cookie
.value
).join(';');
305 let args
= cmd
.substring(5).split(/\s+/);
306 for(let i
=1;i
<args
.length
;i
++){
307 let iC
= args
[i
].indexOf('%cookie');
309 args
[i
] = args
[i
].substring(0,i
)+cookieS
+args
[i
].substring(i
+7);
312 const { spawn
} = require('child_process');
313 const process
= spawn(args
[0],args
.slice(1));
314 process
.stdout
.on('data', (data
) => {
315 let str
= data
.toString();
317 let js
= "showHtml(`"+str
+"`)";
318 win
.webContents
.executeJavaScript(js
,false);
324 let ua
= useragents
[cmd
.substring(iS
+1)];
326 session
.defaultSession
.setUserAgent(ua
);
328 session
.defaultSession
.setUserAgent(defaultUA
);
333 updateApp(repositoryurl
);
335 filename
= cmd
.substring(iS
+1);
336 let iSlash
= filename
.lastIndexOf('/');
338 let folder
= path
.join(__dirname
,filename
.substring(0,iSlash
));
339 fs
.mkdirSync(folder
,{ recursive
: true });
341 fetch2file(repositoryurl
,filename
);
348 function gredirect_disable(){
355 function gredirect_enable(i
){
356 if(i
>=gredirects
.length
) return;
357 if(!gredirect
) registerHandler();
358 gredirect
=gredirects
[i
];
361 function cbConsoleMsg(e
, level
, msg
, line
, sourceid
){
363 console
.log(sourceid
);
367 function interceptRequest(details
, callback
){
368 let url
= details
.url
;
369 if(58===url
.charCodeAt(1) || (!bJS
&& url
.endsWith(".js"))){
370 callback({ cancel
: true });
374 if(gredirect
|| !bRedirect
||(details
.resourceType
!== 'mainFrame' &&
375 details
.resourceType
!== 'subFrame')) break;
376 let oURL
= new URL(url
);
377 let domain
= oURL
.hostname
;
380 let newDomain
= redirects
[domain
];
381 if(!newDomain
) break;
382 newUrl
= "https://"+newDomain
+oURL
.pathname
+oURL
.search
+oURL
.hash
;
384 callback({ cancel
: false, redirectURL
: newUrl
});
387 callback({ cancel
: false });
390 function cbWindowOpenHandler(details
){
391 let url
= details
.url
;
392 let js
= "newTab();tabs.children[tabs.children.length-1].src='"+
394 switch(details
.disposition
){
395 case "foreground-tab":
397 js
= js
+ "switchTab(tabs.children.length-1)";
399 win
.webContents
.executeJavaScript(js
,false);
400 return { action
: "deny" };
402 function cbTitleUpdate(event
,title
){
405 function menuSelection(menuTemplate
, text
){
406 for(let i
=0; i
<selectMenus
.length
-1;i
++){
408 label
: selectMenus
[i
],
410 let cmd
= selectMenus
[i
+1].replace('%s',text
);
411 let js
= `handleQueries(\`${cmd}
\`)`;
412 win
.webContents
.executeJavaScript(js
,false);
417 function menuDownload(menuTemplate
, labelprefix
, linkUrl
){
418 for(let i
=0; i
<downloadMenus
.length
-1;i
++){
420 label
: labelprefix
+downloadMenus
[i
],
422 let cmd
= downloadMenus
[i
+1].replace('%u',linkUrl
);
423 let js
= `handleQueries(\`${cmd}
\`)`;
424 win
.webContents
.executeJavaScript(js
,false);
429 function menuArray(labelprefix
, linkUrl
){
432 label
: labelprefix
+translate('Open'),
434 shell
.openExternal(linkUrl
);
438 label
: labelprefix
+translate('Copy'),
440 clipboard
.writeText(linkUrl
);
444 label
: labelprefix
+translate('Download'),
446 win
.webContents
.downloadURL(linkUrl
);
451 menuDownload(menuTemplate
, labelprefix
, linkUrl
);
455 function onContextMenu(event
, params
){
456 let url
= params
.linkURL
;
459 mTemplate
.push({label
:url
,enabled
:false});
460 mTemplate
.push
.apply(mTemplate
,menuArray("",url
));
461 if((url
=params
.srcURL
))
462 mTemplate
.push
.apply(mTemplate
,menuArray("src: ",url
));
463 }else if((url
=params
.srcURL
)){
464 mTemplate
.push({label
:url
,enabled
:false});
465 mTemplate
.push
.apply(mTemplate
,menuArray("src: ",url
));
466 }else if((url
=params
.selectionText
)){
467 menuSelection(mTemplate
,url
);
471 const contextMenu
= Menu
.buildFromTemplate(mTemplate
);
475 async
function topMenu(){
476 const menuTemplate
= [];
478 let json
= await fs
.promises
.readFile(path
.join(__dirname
,'menu.json'), 'utf8');
479 let menus
= JSON
.parse(json
);
482 for(let i
=0;i
<menus
.length
-1; i
=i
+2){
483 let cmd
= menus
[i
+1];
484 let js
= `handleQueries("${cmd}")`;
486 label
: menus
[i
], click
: ()=>{
487 win
.webContents
.executeJavaScript(js
,false);
491 label
: translate('Tools'),
495 }catch(e
){console
.log(e
)}
498 label
: translate('Edit'),
500 { label
: translate('Config folder'), click
: ()=>{
501 shell
.openPath(__dirname
);
506 label
: translate('Help'),
508 { label
: translate('Check for updates'), click
: ()=>{
509 addrCommand(":update");
511 { label
: translate('Help'), accelerator
: 'F1', click
: ()=>{
514 { label
: translate('Stop'), accelerator
: 'Ctrl+C', click
: ()=>{
515 let js
="tabs.children[iTab].stop()"
516 win
.webContents
.executeJavaScript(js
,false)
518 { label
: translate('getURL'), accelerator
: 'Ctrl+G', click
: ()=>{
519 let js
="{let q=document.forms[0].q;q.focus();q.value=tabs.children[iTab].getURL();getWinTitle()}"
520 win
.webContents
.executeJavaScript(js
,false).then((r
)=>{
524 { label
: translate('Select'), accelerator
: 'Ctrl+L', click
:()=>{
525 win
.webContents
.executeJavaScript("document.forms[0].q.select()",false);
527 { label
: translate('New Tab'), accelerator
: 'Ctrl+T', click
:()=>{
528 let js
= "newTab();document.forms[0].q.select();switchTab(tabs.children.length-1)";
529 win
.webContents
.executeJavaScript(js
,false);
531 { label
: translate('Restore Tab'), accelerator
: 'Ctrl+Shift+T', click
:()=>{
532 let js
= "{let u=closedUrls.pop();if(u){newTab();switchTab(tabs.children.length-1);tabs.children[iTab].src=u}}";
533 win
.webContents
.executeJavaScript(js
,false);
535 { label
: translate('No redirect'), accelerator
: 'Ctrl+R', click
: ()=>{
538 { label
: translate('Redirect'), accelerator
: 'Ctrl+Shift+R', click
: ()=>{
541 { label
: translate('Close tab'), accelerator
: 'Ctrl+W', click
: ()=>{
542 win
.webContents
.executeJavaScript("tabClose()",false).then((r
)=>{
543 if(""===r
) win
.close();
544 else win
.setTitle(r
);
547 { label
: translate('Next Tab'), accelerator
: 'Ctrl+Tab', click
: ()=>{
548 let js
="tabInc(1);getWinTitle()";
549 win
.webContents
.executeJavaScript(js
,false).then((r
)=>{
553 { label
: translate('Previous Tab'), accelerator
: 'Ctrl+Shift+Tab', click
: ()=>{
554 let js
="tabDec(-1);getWinTitle()";
555 win
.webContents
.executeJavaScript(js
,false).then((r
)=>{
559 { label
: translate('Go backward'), accelerator
: 'Alt+Left', click
: ()=>{
560 let js
="tabs.children[iTab].goBack()";
561 win
.webContents
.executeJavaScript(js
,false);
563 { label
: translate('Go forward'), accelerator
: 'Alt+Right', click
: ()=>{
564 let js
="tabs.children[iTab].goForward()";
565 win
.webContents
.executeJavaScript(js
,false);
567 { label
: translate('Zoom in'), accelerator
: 'Ctrl+Shift+=', click
: ()=>{
568 let js
="{let t=tabs.children[iTab];let s=t.getZoomFactor()*1.2;t.setZoomFactor(s)}";
569 win
.webContents
.executeJavaScript(js
,false);
571 { label
: translate('Zoom out'), accelerator
: 'Ctrl+-', click
: ()=>{
572 let js
="{let t=tabs.children[iTab];let s=t.getZoomFactor()/1.2;t.setZoomFactor(s)}";
573 win
.webContents
.executeJavaScript(js
,false);
575 { label
: translate('Default zoom'), accelerator
: 'Ctrl+0', click
: ()=>{
576 let js
="tabs.children[iTab].setZoomFactor(1)";
577 win
.webContents
.executeJavaScript(js
,false);
579 { label
: translate('No focus'), accelerator
: 'Esc', click
: ()=>{
580 let js
= `{let e=document.activeElement;
581 if(e)e.blur();try{tabs.children[iTab].stopFindInPage('clearSelection')}catch(er){}}`;
582 win
.webContents
.executeJavaScript(js
,false);
584 { label
: translate('Reload'), accelerator
: 'F5', click
: ()=>{
585 win
.webContents
.executeJavaScript("tabs.children[iTab].reload()",false);
587 { label
: translate('Devtools'), accelerator
: 'F12', click
: ()=>{
588 let js
= "try{tabs.children[iTab].openDevTools()}catch(e){console.log(e)}";
589 win
.webContents
.executeJavaScript(js
,false);
595 const menu
= Menu
.buildFromTemplate(menuTemplate
);
596 Menu
.setApplicationMenu(menu
);
599 function cmdlineProcess(argv
,cwd
,extra
){
600 let i1st
= 2+extra
; //index for the first query item
601 if(argv
.length
>i1st
){
602 if(i1st
+1==argv
.length
){//local file
603 let fname
= path
.join(cwd
, argv
[i1st
]);
604 if(fs
.existsSync(fname
)){
605 let js
= "tabs.children[iTab].src='file://"+fname
+"'";
606 win
.webContents
.executeJavaScript(js
,false);
607 win
.setTitle(argv
[i1st
]);
611 let url
=argv
.slice(i1st
).join(" ");
612 win
.webContents
.executeJavaScript("handleQuery(`"+url
+"`)",false);
617 async
function cbScheme_redir(req
){
618 if(!gredirect
) return null;
620 let newurl
= gredirect
+oUrl
;
621 const parsedUrl
= url
.parse(newurl
);
622 let headers
= new Headers();
623 for (var pair
of req
.headers
.entries())
624 headers
.set(pair
[0],pair
[1]);
626 let cookies
= await session
.defaultSession
.cookies
.get({url
: oUrl
});
627 let cookieS
= cookies
.map (cookie
=> cookie
.name
+ '=' + cookie
.value
).join(';');
628 headers
.set('cookie',cookieS
);
630 //missing referer header
631 //headers.set('referer',);
633 hostname
: parsedUrl
.hostname
,
634 port
: parsedUrl
.port
,
635 path
: parsedUrl
.path
,
639 return new Promise(async (resolve
, reject
) => {
640 const nreq
= https
.request(options
, (res
) => {
642 res
.on('data', (chunk
) => {
646 res
.on('end', () => {
648 body
= Buffer
.concat(body
);
649 const response
= new Response(body
, res
);
656 nreq
.on('error', (err
) => {
661 const reader
= req
.body
.getReader();
663 const { done
, value
} = await reader
.read();
669 console
.log(headers
);
670 console
.log(new TextDecoder("iso-8859-1").decode(value
));
678 function registerHandler(){
679 protocol
.handle("http",cbScheme_redir
);
680 protocol
.handle("https",cbScheme_redir
);
681 protocol
.handle("ws",cbScheme_redir
);
682 protocol
.handle("wss",cbScheme_redir
);
684 function unregisterHandler(){
685 protocol
.unhandle("http",cbScheme_redir
);
686 protocol
.unhandle("https",cbScheme_redir
);
687 protocol
.unhandle("ws",cbScheme_redir
);
688 protocol
.unhandle("wss",cbScheme_redir
);
691 function forwardCookie(){
692 const choice
= dialog
.showMessageBoxSync(null, {
694 title
: 'Confirm cookie forwarding with global redirection',
695 message
: 'Cookies are used to access your account. Forwarding cookies is vulnerable to global redirection server, proceed to enable cookie forwarding with global redirection?',
696 buttons
: ['No','Yes']
698 if(1===choice
) bForwardCookie
=true;
700 function msgbox_info(msg
){
701 dialog
.showMessageBoxSync(null, {
709 async
function updateApp(url
){//url must ending with "/"
713 let res
= await
fetch(url
+"package.json");
714 let packageS
= await res
.text();
715 {//the last part of version string is the version number, must keep increasing
716 let head
= packageS
.slice(2,40);
717 let iV
= head
.indexOf("version");
719 msg
= "remote package.json corrupted"
723 let iE
= head
.indexOf('"',iV
+4);
724 let iS
= head
.lastIndexOf('.',iE
-1);
725 let nLatestVer
= parseInt(head
.substring(iS
+1,iE
));
727 let ver
= app
.getVersion();
728 iS
= ver
.lastIndexOf('.');
729 let nVer
= parseInt(ver
.substring(iS
+1));
730 if(nVer
>=nLatestVer
){
731 msg
= `Current version ${ver} is already up to date`;
734 const choice
= dialog
.showMessageBoxSync(null, {
736 title
: `Update from ${url}`,
737 message
: `Proceed to update from ${ver} to ${head.substring(iV,iE)}?`,
738 buttons
: ['YES','NO']
740 if(1===choice
) return;
743 writeFile("package.json", packageS
);
745 fetch2file(url
,"webview.js");
746 fetch2file(url
,"index.html");
747 msg
= "Update completed";
749 msg
= "Fail to update"
752 dialog
.showMessageBoxSync(null, {
754 title
: `Update from ${url}`,
760 async
function fetch2file(urlFolder
, filename
, bOverwritten
=true){
761 let pathname
=path
.join(__dirname
,filename
);
762 if(!bOverwritten
&& fs
.existsSync(pathname
)) return;
763 let res
= await
fetch(urlFolder
+filename
);
764 let str
= await res
.text();
765 writeFile(pathname
, str
);
768 async
function writeFile(filename
, str
){
769 let pathname
=filename
+".new";
770 fs
.writeFile(pathname
, str
, (err
) => {
771 if(err
) throw "Fail to write";
772 fs
.rename(pathname
,filename
,(e1
)=>{
773 if(e1
) throw "Fail to rename";
779 const readme
= "README.md";
780 const htmlFN
= path
.join(__dirname
,readme
);
781 let js
=`{let t=tabs.children[iTab];t.dataset.jsonce=BML_md;t.src="file://${htmlFN}"}`;
782 win
.webContents
.executeJavaScript(js
,false)
785 function downloadContextMenuTemp(url
){
787 [{label
:url
,enabled
:false},
788 {label
: translate('Download')},
790 label
: translate('Copy'),
792 clipboard
.writeText(url
);
796 menuDownload(mTemplate
, "", url
);
799 async
function initTranslateRes(lang
){
800 let basename
=path
.join(__dirname
,"translate.");
801 let fname
= basename
+lang
;
802 if(!fs
.existsSync(fname
))
803 fname
= basename
+lang
.slice(0,2);
805 let json
= await fs
.promises
.readFile(fname
,'utf8');
806 translateRes
= JSON
.parse(json
);
811 function translate(str
){
813 if(translateRes
&& (result
=translateRes
[str
])) return result
;
817 function promiseContextMenu(menuTemplate
) {
818 return new Promise((resolve
, reject
) => {
819 menuTemplate
[1].click
= () => resolve(-1);
820 const menu
= Menu
.buildFromTemplate(menuTemplate
);
821 menu
.on('menu-will-close', () => resolve(-2));
826 function httpReq(url
, method
, filePath
){
827 fs
.readFile(filePath
, (err
, fileData
) => {
829 console
.error(`Error reading file: ${err.message}`);
836 "Content-Type":'application/octet-stream',
844 function bangcommand(q
,offset
){
845 let iS
= q
.indexOf(' ',offset
);
846 if(iS
<0) iS
=q
.length
;
847 let fname
= q
.substring(offset
,iS
);
848 let fpath
= path
.join(__dirname
,fname
+'.js');
849 if (fs
.existsSync(fpath
)) {
850 fs
.readFile(fpath
, 'utf8',(err
, js
)=>{
855 const prefix
= "(function(){";
856 const postfix
= "})(`";
858 const fjs
= `${prefix}${js}${postfix}${q}${end}`;