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 function addrCommand(cmd
){
187 if(cmd
.length
<3) return;
188 let c0
= cmd
.charCodeAt(0);
194 args
= cmd
.substring(1).split(/\s+/);
198 session
.defaultSession
.setCertificateVerifyProc((request
, callback
) => {
202 session
.defaultSession
.setCertificateVerifyProc(null);
206 session
.defaultSession
.clearData();
211 session
.defaultSession
.clearCache();
214 session
.defaultSession
.clearHostResolverCache();
217 session
.defaultSession
.clearStorageData();
221 let opts
= JSON
.parse(args
.slice(1).join(""));
222 session
.defaultSession
.clearData(opts
);
223 }catch(e
){console
.log(e
)}
230 session
.defaultSession
.loadExtension(args
[1]);
237 let i
= parseInt(args
[1]);
238 if(i
>=0 && i
<gredirects
.length
)
243 case "js"://execute js
247 bForwardCookie
= false;
248 msgbox_info("Cookie forwarding disabled");
252 msgbox_info("Cookie forwarding enabled for global redirection");
258 session
.defaultSession
.setProxy ({mode
:"direct"});
263 proxy
= proxies
[args
[1]]; //retrieve proxy
265 session
.defaultSession
.setProxy(proxy
)
266 .then(() => {gredirect_disable()})
268 console
.error('Failed to set proxy:', error
);
273 bRedirect
= false; return;
275 bRedirect
= true; return;
278 let ua
= useragents
[args
[1]];
280 session
.defaultSession
.setUserAgent(ua
);
282 session
.defaultSession
.setUserAgent(defaultUA
);
287 updateApp(repositoryurl
);
290 let iSlash
= filename
.lastIndexOf('/');
292 let folder
= path
.join(__dirname
,filename
.substring(0,iSlash
));
293 fs
.mkdirSync(folder
,{ recursive
: true });
295 fetch2file(repositoryurl
,filename
);
302 function gredirect_disable(){
309 function gredirect_enable(i
){
310 if(i
>=gredirects
.length
) return;
311 if(!gredirect
) registerHandler();
312 gredirect
=gredirects
[i
];
315 function cbConsoleMsg(e
, level
, msg
, line
, sourceid
){
317 console
.log(sourceid
);
321 function interceptRequest(details
, callback
){
322 let url
= details
.url
;
323 if(58===url
.charCodeAt(1) || (!bJS
&& url
.endsWith(".js"))){
324 callback({ cancel
: true });
328 if(gredirect
|| !bRedirect
||(details
.resourceType
!== 'mainFrame' &&
329 details
.resourceType
!== 'subFrame')) break;
330 let oURL
= new URL(url
);
331 let domain
= oURL
.hostname
;
334 let newDomain
= redirects
[domain
];
335 if(!newDomain
) break;
336 newUrl
= "https://"+newDomain
+oURL
.pathname
+oURL
.search
+oURL
.hash
;
338 callback({ cancel
: false, redirectURL
: newUrl
});
341 callback({ cancel
: false });
344 function cbWindowOpenHandler(details
){
345 let url
= details
.url
;
346 let js
= "newTab();tabs.children[tabs.children.length-1].src='"+
348 switch(details
.disposition
){
349 case "foreground-tab":
351 js
= js
+ "switchTab(tabs.children.length-1)";
353 win
.webContents
.executeJavaScript(js
,false);
354 return { action
: "deny" };
356 function cbTitleUpdate(event
,title
){
359 function menuSelection(menuTemplate
, text
){
360 for(let i
=0; i
<selectMenus
.length
-1;i
++){
362 label
: selectMenus
[i
],
364 let cmd
= selectMenus
[i
+1].replace('%s',text
);
365 let js
= `handleQueries(\`${cmd}
\`)`;
366 win
.webContents
.executeJavaScript(js
,false);
371 function menuDownload(menuTemplate
, labelprefix
, linkUrl
){
372 for(let i
=0; i
<downloadMenus
.length
-1;i
++){
374 label
: labelprefix
+downloadMenus
[i
],
376 let cmd
= downloadMenus
[i
+1].replace('%u',linkUrl
);
377 let js
= `handleQueries(\`${cmd}
\`)`;
378 win
.webContents
.executeJavaScript(js
,false);
383 function menuArray(labelprefix
, linkUrl
){
386 label
: labelprefix
+translate('Open'),
388 shell
.openExternal(linkUrl
);
392 label
: labelprefix
+translate('Copy'),
394 clipboard
.writeText(linkUrl
);
398 label
: labelprefix
+translate('Download'),
400 win
.webContents
.downloadURL(linkUrl
);
405 menuDownload(menuTemplate
, labelprefix
, linkUrl
);
409 function onContextMenu(event
, params
){
410 let url
= params
.linkURL
;
413 mTemplate
.push({label
:url
,enabled
:false});
414 mTemplate
.push
.apply(mTemplate
,menuArray("",url
));
415 if((url
=params
.srcURL
))
416 mTemplate
.push
.apply(mTemplate
,menuArray("src: ",url
));
417 }else if((url
=params
.srcURL
)){
418 mTemplate
.push({label
:url
,enabled
:false});
419 mTemplate
.push
.apply(mTemplate
,menuArray("src: ",url
));
420 }else if((url
=params
.selectionText
)){
421 menuSelection(mTemplate
,url
);
425 const contextMenu
= Menu
.buildFromTemplate(mTemplate
);
429 async
function topMenu(){
430 const menuTemplate
= [];
432 let json
= await fs
.promises
.readFile(path
.join(__dirname
,'menu.json'), 'utf8');
433 let menus
= JSON
.parse(json
);
436 for(let i
=0;i
<menus
.length
-1; i
=i
+2){
437 let cmd
= menus
[i
+1];
438 let js
= `handleQueries("${cmd}")`;
440 label
: menus
[i
], click
: ()=>{
441 win
.webContents
.executeJavaScript(js
,false);
445 label
: translate('Tools'),
449 }catch(e
){console
.log(e
)}
452 label
: translate('Edit'),
454 { label
: translate('Config folder'), click
: ()=>{
455 shell
.openPath(__dirname
);
460 label
: translate('Help'),
462 { label
: translate('Check for updates'), click
: ()=>{
463 addrCommand(":update");
465 { label
: translate('Help'), accelerator
: 'F1', click
: ()=>{
468 { label
: translate('Stop'), accelerator
: 'Ctrl+C', click
: ()=>{
469 let js
="tabs.children[iTab].stop()"
470 win
.webContents
.executeJavaScript(js
,false)
472 { label
: translate('getURL'), accelerator
: 'Ctrl+G', click
: ()=>{
473 let js
="{let q=document.forms[0].q;q.focus();q.value=tabs.children[iTab].getURL();getWinTitle()}"
474 win
.webContents
.executeJavaScript(js
,false).then((r
)=>{
478 { label
: translate('Select'), accelerator
: 'Ctrl+L', click
:()=>{
479 win
.webContents
.executeJavaScript("document.forms[0].q.select()",false);
481 { label
: translate('New Tab'), accelerator
: 'Ctrl+T', click
:()=>{
482 let js
= "newTab();document.forms[0].q.select();switchTab(tabs.children.length-1)";
483 win
.webContents
.executeJavaScript(js
,false);
485 { label
: translate('Restore Tab'), accelerator
: 'Ctrl+Shift+T', click
:()=>{
486 let js
= "{let u=closedUrls.pop();if(u){newTab();switchTab(tabs.children.length-1);tabs.children[iTab].src=u}}";
487 win
.webContents
.executeJavaScript(js
,false);
489 { label
: translate('No redirect'), accelerator
: 'Ctrl+R', click
: ()=>{
492 { label
: translate('Redirect'), accelerator
: 'Ctrl+Shift+R', click
: ()=>{
495 { label
: translate('Close tab'), accelerator
: 'Ctrl+W', click
: ()=>{
496 win
.webContents
.executeJavaScript("tabClose()",false).then((r
)=>{
497 if(""===r
) win
.close();
498 else win
.setTitle(r
);
501 { label
: translate('Next Tab'), accelerator
: 'Ctrl+Tab', click
: ()=>{
502 let js
="tabInc(1);getWinTitle()";
503 win
.webContents
.executeJavaScript(js
,false).then((r
)=>{
507 { label
: translate('Previous Tab'), accelerator
: 'Ctrl+Shift+Tab', click
: ()=>{
508 let js
="tabDec(-1);getWinTitle()";
509 win
.webContents
.executeJavaScript(js
,false).then((r
)=>{
513 { label
: translate('Go backward'), accelerator
: 'Alt+Left', click
: ()=>{
514 let js
="tabs.children[iTab].goBack()";
515 win
.webContents
.executeJavaScript(js
,false);
517 { label
: translate('Go forward'), accelerator
: 'Alt+Right', click
: ()=>{
518 let js
="tabs.children[iTab].goForward()";
519 win
.webContents
.executeJavaScript(js
,false);
521 { label
: translate('Zoom in'), accelerator
: 'Ctrl+Shift+=', click
: ()=>{
522 let js
="{let t=tabs.children[iTab];let s=t.getZoomFactor()*1.2;t.setZoomFactor(s)}";
523 win
.webContents
.executeJavaScript(js
,false);
525 { label
: translate('Zoom out'), accelerator
: 'Ctrl+-', click
: ()=>{
526 let js
="{let t=tabs.children[iTab];let s=t.getZoomFactor()/1.2;t.setZoomFactor(s)}";
527 win
.webContents
.executeJavaScript(js
,false);
529 { label
: translate('Default zoom'), accelerator
: 'Ctrl+0', click
: ()=>{
530 let js
="tabs.children[iTab].setZoomFactor(1)";
531 win
.webContents
.executeJavaScript(js
,false);
533 { label
: translate('No focus'), accelerator
: 'Esc', click
: ()=>{
534 let js
= `{let e=document.activeElement;
535 if(e)e.blur();try{tabs.children[iTab].stopFindInPage('clearSelection')}catch(er){}}`;
536 win
.webContents
.executeJavaScript(js
,false);
538 { label
: translate('Reload'), accelerator
: 'F5', click
: ()=>{
539 win
.webContents
.executeJavaScript("tabs.children[iTab].reload()",false);
541 { label
: translate('Devtools'), accelerator
: 'F12', click
: ()=>{
542 let js
= "try{tabs.children[iTab].openDevTools()}catch(e){console.log(e)}";
543 win
.webContents
.executeJavaScript(js
,false);
549 const menu
= Menu
.buildFromTemplate(menuTemplate
);
550 Menu
.setApplicationMenu(menu
);
553 function cmdlineProcess(argv
,cwd
,extra
){
554 let i1st
= 2+extra
; //index for the first query item
555 if(argv
.length
>i1st
){
556 if(i1st
+1==argv
.length
){//local file
557 let fname
= path
.join(cwd
, argv
[i1st
]);
558 if(fs
.existsSync(fname
)){
559 let js
= "tabs.children[iTab].src='file://"+fname
+"'";
560 win
.webContents
.executeJavaScript(js
,false);
561 win
.setTitle(argv
[i1st
]);
565 let url
=argv
.slice(i1st
).join(" ");
566 win
.webContents
.executeJavaScript("handleQuery(`"+url
+"`)",false);
571 async
function cbScheme_redir(req
){
572 if(!gredirect
) return null;
574 let newurl
= gredirect
+oUrl
;
575 const parsedUrl
= url
.parse(newurl
);
576 let headers
= new Headers();
577 for (var pair
of req
.headers
.entries())
578 headers
.set(pair
[0],pair
[1]);
580 let cookies
= await session
.defaultSession
.cookies
.get({url
: oUrl
});
581 let cookieS
= cookies
.map (cookie
=> cookie
.name
+ '=' + cookie
.value
).join(';');
582 headers
.set('Cookie',cookieS
);
584 //missing referer header
585 //headers.set('referer',);
587 hostname
: parsedUrl
.hostname
,
588 port
: parsedUrl
.port
,
589 path
: parsedUrl
.path
,
593 return new Promise(async (resolve
, reject
) => {
594 const nreq
= https
.request(options
, (res
) => {
596 res
.on('data', (chunk
) => {
600 res
.on('end', () => {
602 body
= Buffer
.concat(body
);
603 const response
= new Response(body
, res
);
610 nreq
.on('error', (err
) => {
615 const reader
= req
.body
.getReader();
617 const { done
, value
} = await reader
.read();
623 console
.log(headers
);
624 console
.log(new TextDecoder("iso-8859-1").decode(value
));
632 function registerHandler(){
633 protocol
.handle("http",cbScheme_redir
);
634 protocol
.handle("https",cbScheme_redir
);
635 protocol
.handle("ws",cbScheme_redir
);
636 protocol
.handle("wss",cbScheme_redir
);
638 function unregisterHandler(){
639 protocol
.unhandle("http",cbScheme_redir
);
640 protocol
.unhandle("https",cbScheme_redir
);
641 protocol
.unhandle("ws",cbScheme_redir
);
642 protocol
.unhandle("wss",cbScheme_redir
);
645 function forwardCookie(){
646 const choice
= dialog
.showMessageBoxSync(null, {
648 title
: 'Confirm cookie forwarding with global redirection',
649 message
: 'Cookies are used to access your account. Forwarding cookies is vulnerable to global redirection server, proceed to enable cookie forwarding with global redirection?',
650 buttons
: ['No','Yes']
652 if(1===choice
) bForwardCookie
=true;
654 function msgbox_info(msg
){
655 dialog
.showMessageBoxSync(null, {
663 async
function updateApp(url
){//url must ending with "/"
667 let res
= await
fetch(url
+"package.json");
668 let packageS
= await res
.text();
669 {//the last part of version string is the version number, must keep increasing
670 let head
= packageS
.slice(2,40);
671 let iV
= head
.indexOf("version");
673 msg
= "remote package.json corrupted"
677 let iE
= head
.indexOf('"',iV
+4);
678 let iS
= head
.lastIndexOf('.',iE
-1);
679 let nLatestVer
= parseInt(head
.substring(iS
+1,iE
));
681 let ver
= app
.getVersion();
682 iS
= ver
.lastIndexOf('.');
683 let nVer
= parseInt(ver
.substring(iS
+1));
684 if(nVer
>=nLatestVer
){
685 msg
= `Current version ${ver} is already up to date`;
688 const choice
= dialog
.showMessageBoxSync(null, {
690 title
: `Update from ${url}`,
691 message
: `Proceed to update from ${ver} to ${head.substring(iV,iE)}?`,
692 buttons
: ['YES','NO']
694 if(1===choice
) return;
697 writeFile("package.json", packageS
);
699 fetch2file(url
,"webview.js");
700 fetch2file(url
,"index.html");
701 msg
= "Update completed";
703 msg
= "Fail to update"
706 dialog
.showMessageBoxSync(null, {
708 title
: `Update from ${url}`,
714 async
function fetch2file(urlFolder
, filename
, bOverwritten
=true){
715 let pathname
=path
.join(__dirname
,filename
);
716 if(!bOverwritten
&& fs
.existsSync(pathname
)) return;
717 let res
= await
fetch(urlFolder
+filename
);
718 let str
= await res
.text();
719 writeFile(pathname
, str
);
722 async
function writeFile(filename
, str
){
723 let pathname
=filename
+".new";
724 fs
.writeFile(pathname
, str
, (err
) => {
725 if(err
) throw "Fail to write";
726 fs
.rename(pathname
,filename
,(e1
)=>{
727 if(e1
) throw "Fail to rename";
733 const readme
= "README.md";
734 const htmlFN
= path
.join(__dirname
,readme
);
735 let js
=`{let t=tabs.children[iTab];t.dataset.jsonce=BML_md;t.src="file://${htmlFN}"}`;
736 win
.webContents
.executeJavaScript(js
,false)
739 function downloadContextMenuTemp(url
){
741 [{label
:url
,enabled
:false},
742 {label
: translate('Download')},
744 label
: translate('Copy'),
746 clipboard
.writeText(url
);
750 menuDownload(mTemplate
, "", url
);
753 async
function initTranslateRes(lang
){
754 let basename
=path
.join(__dirname
,"translate.");
755 let fname
= basename
+lang
;
756 if(!fs
.existsSync(fname
))
757 fname
= basename
+lang
.slice(0,2);
759 let json
= await fs
.promises
.readFile(fname
,'utf8');
760 translateRes
= JSON
.parse(json
);
765 function translate(str
){
767 if(translateRes
&& (result
=translateRes
[str
])) return result
;
771 function promiseContextMenu(menuTemplate
) {
772 return new Promise((resolve
, reject
) => {
773 menuTemplate
[1].click
= () => resolve(-1);
774 const menu
= Menu
.buildFromTemplate(menuTemplate
);
775 menu
.on('menu-will-close', () => resolve(-2));
780 function httpReq(url
, method
, filePath
){
781 fs
.readFile(filePath
, (err
, fileData
) => {
783 console
.error(`Error reading file: ${err.message}`);
790 "Content-Type":'application/octet-stream',
798 function bangcommand(q
,offset
){
799 let iS
= q
.indexOf(' ',offset
);
800 if(iS
<0) iS
=q
.length
;
801 let fname
= q
.substring(offset
,iS
);
802 let fpath
= path
.join(__dirname
,fname
+'.js');
803 if (fs
.existsSync(fpath
)) {
804 fs
.readFile(fpath
, 'utf8',(err
, js
)=>{
809 const prefix
= "(function(){";
810 const postfix
= "})(`";
812 const fjs
= `${prefix}${js}${postfix}${q}${end}`;