chromium-spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. import * as chai from 'chai'
  2. import * as chaiAsPromised from 'chai-as-promised'
  3. import { BrowserWindow, webContents, WebContents, session, ipcMain } from 'electron'
  4. import { emittedOnce } from './events-helpers'
  5. import { closeAllWindows } from './window-helpers'
  6. import * as https from 'https'
  7. import * as http from 'http'
  8. import * as path from 'path'
  9. import * as fs from 'fs'
  10. import { EventEmitter } from 'events'
  11. import { promisify } from 'util'
  12. const { expect } = chai
  13. chai.use(chaiAsPromised)
  14. const fixturesPath = path.resolve(__dirname, '..', 'spec', 'fixtures')
  15. describe('reporting api', () => {
  16. it('sends a report for a deprecation', async () => {
  17. const reports = new EventEmitter
  18. // The Reporting API only works on https with valid certs. To dodge having
  19. // to set up a trusted certificate, hack the validator.
  20. session.defaultSession.setCertificateVerifyProc((req, cb) => {
  21. cb(0)
  22. })
  23. const certPath = path.join(fixturesPath, 'certificates')
  24. const options = {
  25. key: fs.readFileSync(path.join(certPath, 'server.key')),
  26. cert: fs.readFileSync(path.join(certPath, 'server.pem')),
  27. ca: [
  28. fs.readFileSync(path.join(certPath, 'rootCA.pem')),
  29. fs.readFileSync(path.join(certPath, 'intermediateCA.pem'))
  30. ],
  31. requestCert: true,
  32. rejectUnauthorized: false
  33. }
  34. const server = https.createServer(options, (req, res) => {
  35. if (req.url === '/report') {
  36. let data = ''
  37. req.on('data', (d) => data += d.toString('utf-8'))
  38. req.on('end', () => {
  39. reports.emit('report', JSON.parse(data))
  40. })
  41. }
  42. res.setHeader('Report-To', JSON.stringify({
  43. group: 'default',
  44. max_age: 120,
  45. endpoints: [ {url: `https://localhost:${(server.address() as any).port}/report`} ],
  46. }))
  47. res.setHeader('Content-Type', 'text/html')
  48. // using the deprecated `webkitRequestAnimationFrame` will trigger a
  49. // "deprecation" report.
  50. res.end('<script>webkitRequestAnimationFrame(() => {})</script>')
  51. })
  52. await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
  53. const bw = new BrowserWindow({
  54. show: false,
  55. })
  56. try {
  57. const reportGenerated = emittedOnce(reports, 'report')
  58. const url = `https://localhost:${(server.address() as any).port}/a`
  59. await bw.loadURL(url)
  60. const [report] = await reportGenerated
  61. expect(report).to.be.an('array')
  62. expect(report[0].type).to.equal('deprecation')
  63. expect(report[0].url).to.equal(url)
  64. expect(report[0].body.id).to.equal('PrefixedRequestAnimationFrame')
  65. } finally {
  66. bw.destroy()
  67. server.close()
  68. }
  69. })
  70. describe('window.open', () => {
  71. it('denies custom open when nativeWindowOpen: true', async () => {
  72. const w = new BrowserWindow({
  73. show: false,
  74. webPreferences: {
  75. contextIsolation: false,
  76. nodeIntegration: true,
  77. nativeWindowOpen: true
  78. }
  79. });
  80. w.loadURL('about:blank');
  81. const previousListeners = process.listeners('uncaughtException');
  82. process.removeAllListeners('uncaughtException');
  83. try {
  84. const uncaughtException = new Promise<Error>(resolve => {
  85. process.once('uncaughtException', resolve);
  86. });
  87. expect(await w.webContents.executeJavaScript(`(${function () {
  88. const ipc = process.electronBinding('ipc').ipc;
  89. return ipc.sendSync(true, 'ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', ['', '', ''])[0];
  90. }})()`)).to.be.null('null');
  91. const exception = await uncaughtException;
  92. expect(exception.message).to.match(/denied: expected native window\.open/);
  93. } finally {
  94. previousListeners.forEach(l => process.on('uncaughtException', l));
  95. }
  96. });
  97. });
  98. })
  99. describe('window.postMessage', () => {
  100. afterEach(async () => {
  101. await closeAllWindows()
  102. })
  103. it('sets the source and origin correctly', async () => {
  104. const w = new BrowserWindow({show: false, webPreferences: {nodeIntegration: true}})
  105. w.loadURL(`file://${fixturesPath}/pages/window-open-postMessage-driver.html`)
  106. const [, message] = await emittedOnce(ipcMain, 'complete')
  107. expect(message.data).to.equal('testing')
  108. expect(message.origin).to.equal('file://')
  109. expect(message.sourceEqualsOpener).to.equal(true)
  110. expect(message.eventOrigin).to.equal('file://')
  111. })
  112. })
  113. describe('focus handling', () => {
  114. let webviewContents: WebContents = null as unknown as WebContents
  115. let w: BrowserWindow = null as unknown as BrowserWindow
  116. beforeEach(async () => {
  117. w = new BrowserWindow({
  118. show: true,
  119. webPreferences: {
  120. nodeIntegration: true,
  121. webviewTag: true
  122. }
  123. })
  124. const webviewReady = emittedOnce(w.webContents, 'did-attach-webview')
  125. await w.loadFile(path.join(fixturesPath, 'pages', 'tab-focus-loop-elements.html'))
  126. const [, wvContents] = await webviewReady
  127. webviewContents = wvContents
  128. await emittedOnce(webviewContents, 'did-finish-load')
  129. w.focus()
  130. })
  131. afterEach(() => {
  132. webviewContents = null as unknown as WebContents
  133. w.destroy()
  134. w = null as unknown as BrowserWindow
  135. })
  136. const expectFocusChange = async () => {
  137. const [, focusedElementId] = await emittedOnce(ipcMain, 'focus-changed')
  138. return focusedElementId
  139. }
  140. describe('a TAB press', () => {
  141. const tabPressEvent: any = {
  142. type: 'keyDown',
  143. keyCode: 'Tab'
  144. }
  145. it('moves focus to the next focusable item', async () => {
  146. let focusChange = expectFocusChange()
  147. w.webContents.sendInputEvent(tabPressEvent)
  148. let focusedElementId = await focusChange
  149. expect(focusedElementId).to.equal('BUTTON-element-1', `should start focused in element-1, it's instead in ${focusedElementId}`)
  150. focusChange = expectFocusChange()
  151. w.webContents.sendInputEvent(tabPressEvent)
  152. focusedElementId = await focusChange
  153. expect(focusedElementId).to.equal('BUTTON-element-2', `focus should've moved to element-2, it's instead in ${focusedElementId}`)
  154. focusChange = expectFocusChange()
  155. w.webContents.sendInputEvent(tabPressEvent)
  156. focusedElementId = await focusChange
  157. expect(focusedElementId).to.equal('BUTTON-wv-element-1', `focus should've moved to the webview's element-1, it's instead in ${focusedElementId}`)
  158. focusChange = expectFocusChange()
  159. webviewContents.sendInputEvent(tabPressEvent)
  160. focusedElementId = await focusChange
  161. expect(focusedElementId).to.equal('BUTTON-wv-element-2', `focus should've moved to the webview's element-2, it's instead in ${focusedElementId}`)
  162. focusChange = expectFocusChange()
  163. webviewContents.sendInputEvent(tabPressEvent)
  164. focusedElementId = await focusChange
  165. expect(focusedElementId).to.equal('BUTTON-element-3', `focus should've moved to element-3, it's instead in ${focusedElementId}`)
  166. focusChange = expectFocusChange()
  167. w.webContents.sendInputEvent(tabPressEvent)
  168. focusedElementId = await focusChange
  169. expect(focusedElementId).to.equal('BUTTON-element-1', `focus should've looped back to element-1, it's instead in ${focusedElementId}`)
  170. })
  171. })
  172. describe('a SHIFT + TAB press', () => {
  173. const shiftTabPressEvent: any = {
  174. type: 'keyDown',
  175. modifiers: ['Shift'],
  176. keyCode: 'Tab'
  177. }
  178. it('moves focus to the previous focusable item', async () => {
  179. let focusChange = expectFocusChange()
  180. w.webContents.sendInputEvent(shiftTabPressEvent)
  181. let focusedElementId = await focusChange
  182. expect(focusedElementId).to.equal('BUTTON-element-3', `should start focused in element-3, it's instead in ${focusedElementId}`)
  183. focusChange = expectFocusChange()
  184. w.webContents.sendInputEvent(shiftTabPressEvent)
  185. focusedElementId = await focusChange
  186. expect(focusedElementId).to.equal('BUTTON-wv-element-2', `focus should've moved to the webview's element-2, it's instead in ${focusedElementId}`)
  187. focusChange = expectFocusChange()
  188. webviewContents.sendInputEvent(shiftTabPressEvent)
  189. focusedElementId = await focusChange
  190. expect(focusedElementId).to.equal('BUTTON-wv-element-1', `focus should've moved to the webview's element-1, it's instead in ${focusedElementId}`)
  191. focusChange = expectFocusChange()
  192. webviewContents.sendInputEvent(shiftTabPressEvent)
  193. focusedElementId = await focusChange
  194. expect(focusedElementId).to.equal('BUTTON-element-2', `focus should've moved to element-2, it's instead in ${focusedElementId}`)
  195. focusChange = expectFocusChange()
  196. w.webContents.sendInputEvent(shiftTabPressEvent)
  197. focusedElementId = await focusChange
  198. expect(focusedElementId).to.equal('BUTTON-element-1', `focus should've moved to element-1, it's instead in ${focusedElementId}`)
  199. focusChange = expectFocusChange()
  200. w.webContents.sendInputEvent(shiftTabPressEvent)
  201. focusedElementId = await focusChange
  202. expect(focusedElementId).to.equal('BUTTON-element-3', `focus should've looped back to element-3, it's instead in ${focusedElementId}`)
  203. })
  204. })
  205. })
  206. describe('web security', () => {
  207. afterEach(closeAllWindows)
  208. let server: http.Server
  209. let serverUrl: string
  210. before(async () => {
  211. server = http.createServer((req, res) => {
  212. res.setHeader('Content-Type', 'text/html')
  213. res.end('<body>')
  214. })
  215. await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
  216. serverUrl = `http://localhost:${(server.address() as any).port}`
  217. })
  218. after(() => {
  219. server.close()
  220. })
  221. it('engages CORB when web security is not disabled', async () => {
  222. const w = new BrowserWindow({ show: true, webPreferences: { webSecurity: true, nodeIntegration: true } })
  223. const p = emittedOnce(ipcMain, 'success')
  224. await w.loadURL(`data:text/html,<script>
  225. const s = document.createElement('script')
  226. s.src = "${serverUrl}"
  227. // The script will load successfully but its body will be emptied out
  228. // by CORB, so we don't expect a syntax error.
  229. s.onload = () => { require('electron').ipcRenderer.send('success') }
  230. document.documentElement.appendChild(s)
  231. </script>`)
  232. await p
  233. })
  234. it('bypasses CORB when web security is disabled', async () => {
  235. const w = new BrowserWindow({ show: true, webPreferences: { webSecurity: false, nodeIntegration: true } })
  236. const p = emittedOnce(ipcMain, 'success')
  237. await w.loadURL(`data:text/html,
  238. <script>
  239. window.onerror = (e) => { require('electron').ipcRenderer.send('success', e) }
  240. </script>
  241. <script src="${serverUrl}"></script>`)
  242. await p
  243. })
  244. })
  245. describe('iframe using HTML fullscreen API while window is OS-fullscreened', () => {
  246. const fullscreenChildHtml = promisify(fs.readFile)(
  247. path.join(fixturesPath, 'pages', 'fullscreen-oopif.html')
  248. )
  249. let w: BrowserWindow, server: http.Server
  250. before(() => {
  251. server = http.createServer(async (_req, res) => {
  252. res.writeHead(200, { 'Content-Type': 'text/html' })
  253. res.write(await fullscreenChildHtml)
  254. res.end()
  255. })
  256. server.listen(8989, '127.0.0.1')
  257. })
  258. beforeEach(() => {
  259. w = new BrowserWindow({
  260. show: true,
  261. fullscreen: true,
  262. webPreferences: {
  263. nodeIntegration: true,
  264. nodeIntegrationInSubFrames: true
  265. }
  266. })
  267. })
  268. afterEach(async () => {
  269. await closeAllWindows()
  270. ;(w as any) = null
  271. server.close()
  272. })
  273. it('can fullscreen from out-of-process iframes (OOPIFs)', done => {
  274. ipcMain.once('fullscreenChange', async () => {
  275. const fullscreenWidth = await w.webContents.executeJavaScript(
  276. "document.querySelector('iframe').offsetWidth"
  277. )
  278. expect(fullscreenWidth > 0).to.be.true
  279. await w.webContents.executeJavaScript(
  280. "document.querySelector('iframe').contentWindow.postMessage('exitFullscreen', '*')"
  281. )
  282. await new Promise(resolve => setTimeout(resolve, 500))
  283. const width = await w.webContents.executeJavaScript(
  284. "document.querySelector('iframe').offsetWidth"
  285. )
  286. expect(width).to.equal(0)
  287. done()
  288. })
  289. const html =
  290. '<iframe style="width: 0" frameborder=0 src="http://localhost:8989" allowfullscreen></iframe>'
  291. w.loadURL(`data:text/html,${html}`)
  292. })
  293. it('can fullscreen from in-process iframes', done => {
  294. ipcMain.once('fullscreenChange', async () => {
  295. const fullscreenWidth = await w.webContents.executeJavaScript(
  296. "document.querySelector('iframe').offsetWidth"
  297. )
  298. expect(fullscreenWidth > 0).to.true
  299. await w.webContents.executeJavaScript('document.exitFullscreen()')
  300. const width = await w.webContents.executeJavaScript(
  301. "document.querySelector('iframe').offsetWidth"
  302. )
  303. expect(width).to.equal(0)
  304. done()
  305. })
  306. w.loadFile(path.join(fixturesPath, 'pages', 'fullscreen-ipif.html'))
  307. })
  308. })
  309. describe('enableWebSQL webpreference', () => {
  310. const standardScheme = (global as any).standardScheme;
  311. const origin = `${standardScheme}://fake-host`;
  312. const filePath = path.join(fixturesPath, 'pages', 'storage', 'web_sql.html');
  313. const sqlPartition = 'web-sql-preference-test';
  314. const sqlSession = session.fromPartition(sqlPartition);
  315. const securityError = 'An attempt was made to break through the security policy of the user agent.';
  316. let contents: WebContents, w: BrowserWindow;
  317. before(() => {
  318. sqlSession.protocol.registerFileProtocol(standardScheme, (request, callback) => {
  319. callback({ path: filePath });
  320. });
  321. });
  322. after(() => {
  323. sqlSession.protocol.unregisterProtocol(standardScheme);
  324. });
  325. afterEach(async () => {
  326. if (contents) {
  327. (contents as any).destroy();
  328. contents = null as any;
  329. }
  330. await closeAllWindows();
  331. (w as any) = null;
  332. });
  333. it('default value allows websql', async () => {
  334. contents = (webContents as any).create({
  335. session: sqlSession,
  336. nodeIntegration: true
  337. });
  338. contents.loadURL(origin);
  339. const [, error] = await emittedOnce(ipcMain, 'web-sql-response');
  340. expect(error).to.be.null('error');
  341. });
  342. it('when set to false can disallow websql', async () => {
  343. contents = (webContents as any).create({
  344. session: sqlSession,
  345. nodeIntegration: true,
  346. enableWebSQL: false
  347. });
  348. contents.loadURL(origin);
  349. const [, error] = await emittedOnce(ipcMain, 'web-sql-response');
  350. expect(error).to.equal(securityError);
  351. });
  352. it('when set to false does not disable indexedDB', async () => {
  353. contents = (webContents as any).create({
  354. session: sqlSession,
  355. nodeIntegration: true,
  356. enableWebSQL: false
  357. });
  358. contents.loadURL(origin);
  359. const [, error] = await emittedOnce(ipcMain, 'web-sql-response');
  360. expect(error).to.equal(securityError);
  361. const dbName = 'random';
  362. const result = await contents.executeJavaScript(`
  363. new Promise((resolve, reject) => {
  364. try {
  365. let req = window.indexedDB.open('${dbName}');
  366. req.onsuccess = (event) => {
  367. let db = req.result;
  368. resolve(db.name);
  369. }
  370. req.onerror = (event) => { resolve(event.target.code); }
  371. } catch (e) {
  372. resolve(e.message);
  373. }
  374. });
  375. `);
  376. expect(result).to.equal(dbName);
  377. });
  378. it('child webContents can override when the embedder has allowed websql', async () => {
  379. w = new BrowserWindow({
  380. show: false,
  381. webPreferences: {
  382. nodeIntegration: true,
  383. webviewTag: true,
  384. session: sqlSession
  385. }
  386. });
  387. w.webContents.loadURL(origin);
  388. const [, error] = await emittedOnce(ipcMain, 'web-sql-response');
  389. expect(error).to.be.null('error');
  390. const webviewResult = emittedOnce(ipcMain, 'web-sql-response');
  391. await w.webContents.executeJavaScript(`
  392. new Promise((resolve, reject) => {
  393. const webview = new WebView();
  394. webview.setAttribute('src', '${origin}');
  395. webview.setAttribute('webpreferences', 'enableWebSQL=0');
  396. webview.setAttribute('partition', '${sqlPartition}');
  397. webview.setAttribute('nodeIntegration', 'on');
  398. document.body.appendChild(webview);
  399. webview.addEventListener('dom-ready', () => resolve());
  400. });
  401. `);
  402. const [, childError] = await webviewResult;
  403. expect(childError).to.equal(securityError);
  404. });
  405. it('child webContents cannot override when the embedder has disallowed websql', async () => {
  406. w = new BrowserWindow({
  407. show: false,
  408. webPreferences: {
  409. nodeIntegration: true,
  410. enableWebSQL: false,
  411. webviewTag: true,
  412. session: sqlSession
  413. }
  414. });
  415. w.webContents.loadURL('data:text/html,<html></html>');
  416. const webviewResult = emittedOnce(ipcMain, 'web-sql-response');
  417. await w.webContents.executeJavaScript(`
  418. new Promise((resolve, reject) => {
  419. const webview = new WebView();
  420. webview.setAttribute('src', '${origin}');
  421. webview.setAttribute('webpreferences', 'enableWebSQL=1');
  422. webview.setAttribute('partition', '${sqlPartition}');
  423. webview.setAttribute('nodeIntegration', 'on');
  424. document.body.appendChild(webview);
  425. webview.addEventListener('dom-ready', () => resolve());
  426. });
  427. `);
  428. const [, childError] = await webviewResult;
  429. expect(childError).to.equal(securityError);
  430. });
  431. it('child webContents can use websql when the embedder has allowed websql', async () => {
  432. w = new BrowserWindow({
  433. show: false,
  434. webPreferences: {
  435. nodeIntegration: true,
  436. webviewTag: true,
  437. session: sqlSession
  438. }
  439. });
  440. w.webContents.loadURL(origin);
  441. const [, error] = await emittedOnce(ipcMain, 'web-sql-response');
  442. expect(error).to.be.null('error');
  443. const webviewResult = emittedOnce(ipcMain, 'web-sql-response');
  444. await w.webContents.executeJavaScript(`
  445. new Promise((resolve, reject) => {
  446. const webview = new WebView();
  447. webview.setAttribute('src', '${origin}');
  448. webview.setAttribute('webpreferences', 'enableWebSQL=1');
  449. webview.setAttribute('partition', '${sqlPartition}');
  450. webview.setAttribute('nodeIntegration', 'on');
  451. document.body.appendChild(webview);
  452. webview.addEventListener('dom-ready', () => resolve());
  453. });
  454. `);
  455. const [, childError] = await webviewResult;
  456. expect(childError).to.be.null('error');
  457. });
  458. });