2019-08-27 21:55:19 +00:00
import { expect } from 'chai' ;
import * as http from 'http' ;
import * as https from 'https' ;
import * as path from 'path' ;
import * as fs from 'fs' ;
import * as ChildProcess from 'child_process' ;
2022-08-23 01:25:57 +00:00
import { app , session , BrowserWindow , net , ipcMain , Session , webFrameMain , WebFrameMain } from 'electron/main' ;
2019-08-27 21:55:19 +00:00
import * as send from 'send' ;
import * as auth from 'basic-auth' ;
2023-01-25 21:01:25 +00:00
import { closeAllWindows } from './lib/window-helpers' ;
2023-02-23 23:53:53 +00:00
import { defer , listen } from './lib/spec-helpers' ;
import { once } from 'events' ;
import { setTimeout } from 'timers/promises' ;
2019-05-28 21:12:59 +00:00
describe ( 'session module' , ( ) = > {
2022-08-16 19:23:13 +00:00
const fixtures = path . resolve ( __dirname , 'fixtures' ) ;
2019-05-28 21:12:59 +00:00
const url = 'http://127.0.0.1' ;
describe ( 'session.defaultSession' , ( ) = > {
it ( 'returns the default session' , ( ) = > {
expect ( session . defaultSession ) . to . equal ( session . fromPartition ( '' ) ) ;
} ) ;
} ) ;
describe ( 'session.fromPartition(partition, options)' , ( ) = > {
it ( 'returns existing session with same partition' , ( ) = > {
expect ( session . fromPartition ( 'test' ) ) . to . equal ( session . fromPartition ( 'test' ) ) ;
} ) ;
} ) ;
2023-03-20 14:34:49 +00:00
describe ( 'session.fromPath(path)' , ( ) = > {
it ( 'returns storage path of a session which was created with an absolute path' , ( ) = > {
const tmppath = require ( 'electron' ) . app . getPath ( 'temp' ) ;
const ses = session . fromPath ( tmppath ) ;
expect ( ses . storagePath ) . to . equal ( tmppath ) ;
} ) ;
} ) ;
2019-05-28 21:12:59 +00:00
describe ( 'ses.cookies' , ( ) = > {
const name = '0' ;
const value = '0' ;
2019-08-29 06:50:14 +00:00
afterEach ( closeAllWindows ) ;
2019-05-28 21:12:59 +00:00
2019-12-11 07:44:49 +00:00
// Clear cookie of defaultSession after each test.
afterEach ( async ( ) = > {
const { cookies } = session . defaultSession ;
const cs = await cookies . get ( { url } ) ;
for ( const c of cs ) {
await cookies . remove ( url , c . name ) ;
}
} ) ;
2019-05-30 22:05:02 +00:00
it ( 'should get cookies' , async ( ) = > {
2019-05-28 21:12:59 +00:00
const server = http . createServer ( ( req , res ) = > {
res . setHeader ( 'Set-Cookie' , [ ` ${ name } = ${ value } ` ] ) ;
res . end ( 'finished' ) ;
server . close ( ) ;
} ) ;
2023-02-20 11:30:57 +00:00
const { port } = await listen ( server ) ;
2019-08-29 06:50:14 +00:00
const w = new BrowserWindow ( { show : false } ) ;
2019-05-30 22:05:02 +00:00
await w . loadURL ( ` ${ url } : ${ port } ` ) ;
const list = await w . webContents . session . cookies . get ( { url } ) ;
const cookie = list . find ( cookie = > cookie . name === name ) ;
expect ( cookie ) . to . exist . and . to . have . property ( 'value' , value ) ;
2019-05-28 21:12:59 +00:00
} ) ;
it ( 'sets cookies' , async ( ) = > {
const { cookies } = session . defaultSession ;
const name = '1' ;
const value = '1' ;
2019-11-01 20:37:02 +00:00
await cookies . set ( { url , name , value , expirationDate : ( + new Date ( ) ) / 1000 + 120 } ) ;
2019-12-11 07:44:49 +00:00
const c = ( await cookies . get ( { url } ) ) [ 0 ] ;
expect ( c . name ) . to . equal ( name ) ;
expect ( c . value ) . to . equal ( value ) ;
expect ( c . session ) . to . equal ( false ) ;
} ) ;
it ( 'sets session cookies' , async ( ) = > {
const { cookies } = session . defaultSession ;
const name = '2' ;
const value = '1' ;
await cookies . set ( { url , name , value } ) ;
const c = ( await cookies . get ( { url } ) ) [ 0 ] ;
expect ( c . name ) . to . equal ( name ) ;
expect ( c . value ) . to . equal ( value ) ;
expect ( c . session ) . to . equal ( true ) ;
} ) ;
it ( 'sets cookies without name' , async ( ) = > {
const { cookies } = session . defaultSession ;
const value = '3' ;
await cookies . set ( { url , value } ) ;
const c = ( await cookies . get ( { url } ) ) [ 0 ] ;
expect ( c . name ) . to . be . empty ( ) ;
expect ( c . value ) . to . equal ( value ) ;
2019-05-28 21:12:59 +00:00
} ) ;
2020-04-02 18:28:43 +00:00
for ( const sameSite of < const > [ 'unspecified' , 'no_restriction' , 'lax' , 'strict' ] ) {
it ( ` sets cookies with samesite= ${ sameSite } ` , async ( ) = > {
const { cookies } = session . defaultSession ;
const value = 'hithere' ;
await cookies . set ( { url , value , sameSite } ) ;
const c = ( await cookies . get ( { url } ) ) [ 0 ] ;
expect ( c . name ) . to . be . empty ( ) ;
expect ( c . value ) . to . equal ( value ) ;
expect ( c . sameSite ) . to . equal ( sameSite ) ;
} ) ;
}
2020-07-09 17:18:49 +00:00
it ( 'fails to set cookies with samesite=garbage' , async ( ) = > {
2020-04-02 18:28:43 +00:00
const { cookies } = session . defaultSession ;
const value = 'hithere' ;
await expect ( cookies . set ( { url , value , sameSite : 'garbage' as any } ) ) . to . eventually . be . rejectedWith ( 'Failed to convert \'garbage\' to an appropriate cookie same site value' ) ;
} ) ;
2019-10-09 06:57:40 +00:00
it ( 'gets cookies without url' , async ( ) = > {
const { cookies } = session . defaultSession ;
const name = '1' ;
const value = '1' ;
2019-11-01 20:37:02 +00:00
await cookies . set ( { url , name , value , expirationDate : ( + new Date ( ) ) / 1000 + 120 } ) ;
2019-10-09 06:57:40 +00:00
const cs = await cookies . get ( { domain : '127.0.0.1' } ) ;
expect ( cs . some ( c = > c . name === name && c . value === value ) ) . to . equal ( true ) ;
} ) ;
2019-05-28 21:12:59 +00:00
it ( 'yields an error when setting a cookie with missing required fields' , async ( ) = > {
2019-06-13 02:49:36 +00:00
const { cookies } = session . defaultSession ;
const name = '1' ;
const value = '1' ;
await expect (
cookies . set ( { url : '' , name , value } )
2023-03-16 12:48:14 +00:00
) . to . eventually . be . rejectedWith ( 'Failed to set cookie with an invalid domain attribute' ) ;
2019-06-13 02:49:36 +00:00
} ) ;
it ( 'yields an error when setting a cookie with an invalid URL' , async ( ) = > {
const { cookies } = session . defaultSession ;
const name = '1' ;
const value = '1' ;
await expect (
cookies . set ( { url : 'asdf' , name , value } )
2023-03-16 12:48:14 +00:00
) . to . eventually . be . rejectedWith ( 'Failed to set cookie with an invalid domain attribute' ) ;
2019-05-28 21:12:59 +00:00
} ) ;
it ( 'should overwrite previous cookies' , async ( ) = > {
const { cookies } = session . defaultSession ;
const name = 'DidOverwrite' ;
2020-03-20 15:12:18 +00:00
for ( const value of [ 'No' , 'Yes' ] ) {
2019-05-28 21:12:59 +00:00
await cookies . set ( { url , name , value , expirationDate : ( + new Date ( ) ) / 1000 + 120 } ) ;
const list = await cookies . get ( { url } ) ;
expect ( list . some ( cookie = > cookie . name === name && cookie . value === value ) ) . to . equal ( true ) ;
}
} ) ;
it ( 'should remove cookies' , async ( ) = > {
const { cookies } = session . defaultSession ;
const name = '2' ;
const value = '2' ;
await cookies . set ( { url , name , value , expirationDate : ( + new Date ( ) ) / 1000 + 120 } ) ;
await cookies . remove ( url , name ) ;
const list = await cookies . get ( { url } ) ;
expect ( list . some ( cookie = > cookie . name === name && cookie . value === value ) ) . to . equal ( false ) ;
} ) ;
2023-04-04 13:48:51 +00:00
// DISABLED-FIXME
it ( 'should set cookie for standard scheme' , async ( ) = > {
2019-05-28 21:12:59 +00:00
const { cookies } = session . defaultSession ;
const domain = 'fake-host' ;
const url = ` ${ standardScheme } :// ${ domain } ` ;
const name = 'custom' ;
const value = '1' ;
await cookies . set ( { url , name , value , expirationDate : ( + new Date ( ) ) / 1000 + 120 } ) ;
const list = await cookies . get ( { url } ) ;
expect ( list ) . to . have . lengthOf ( 1 ) ;
expect ( list [ 0 ] ) . to . have . property ( 'name' , name ) ;
expect ( list [ 0 ] ) . to . have . property ( 'value' , value ) ;
expect ( list [ 0 ] ) . to . have . property ( 'domain' , domain ) ;
} ) ;
it ( 'emits a changed event when a cookie is added or removed' , async ( ) = > {
const { cookies } = session . fromPartition ( 'cookies-changed' ) ;
const name = 'foo' ;
const value = 'bar' ;
2023-02-23 23:53:53 +00:00
const a = once ( cookies , 'changed' ) ;
2019-05-28 21:12:59 +00:00
await cookies . set ( { url , name , value , expirationDate : ( + new Date ( ) ) / 1000 + 120 } ) ;
const [ , setEventCookie , setEventCause , setEventRemoved ] = await a ;
2023-02-23 23:53:53 +00:00
const b = once ( cookies , 'changed' ) ;
2019-05-28 21:12:59 +00:00
await cookies . remove ( url , name ) ;
const [ , removeEventCookie , removeEventCause , removeEventRemoved ] = await b ;
expect ( setEventCookie . name ) . to . equal ( name ) ;
expect ( setEventCookie . value ) . to . equal ( value ) ;
expect ( setEventCause ) . to . equal ( 'explicit' ) ;
expect ( setEventRemoved ) . to . equal ( false ) ;
expect ( removeEventCookie . name ) . to . equal ( name ) ;
expect ( removeEventCookie . value ) . to . equal ( value ) ;
expect ( removeEventCause ) . to . equal ( 'explicit' ) ;
expect ( removeEventRemoved ) . to . equal ( true ) ;
} ) ;
describe ( 'ses.cookies.flushStore()' , async ( ) = > {
2019-05-30 22:05:02 +00:00
it ( 'flushes the cookies to disk' , async ( ) = > {
2019-05-28 21:12:59 +00:00
const name = 'foo' ;
const value = 'bar' ;
const { cookies } = session . defaultSession ;
await cookies . set ( { url , name , value } ) ;
await cookies . flushStore ( ) ;
} ) ;
} ) ;
2022-06-01 06:12:47 +00:00
it ( 'should survive an app restart for persistent partition' , async function ( ) {
this . timeout ( 60000 ) ;
2019-05-28 21:12:59 +00:00
const appPath = path . join ( fixtures , 'api' , 'cookie-app' ) ;
2019-08-27 21:55:19 +00:00
const runAppWithPhase = ( phase : string ) = > {
2019-11-01 20:37:02 +00:00
return new Promise ( ( resolve ) = > {
2019-05-28 21:12:59 +00:00
let output = '' ;
const appProcess = ChildProcess . spawn (
2019-05-30 22:05:02 +00:00
process . execPath ,
2019-05-28 21:12:59 +00:00
[ appPath ] ,
{ env : { PHASE : phase , . . . process . env } }
) ;
appProcess . stdout . on ( 'data' , data = > { output += data ; } ) ;
2020-01-27 01:29:50 +00:00
appProcess . on ( 'exit' , ( ) = > {
2019-05-30 22:05:02 +00:00
resolve ( output . replace ( /(\r\n|\n|\r)/gm , '' ) ) ;
2019-05-28 21:12:59 +00:00
} ) ;
} ) ;
} ;
2019-05-30 22:05:02 +00:00
expect ( await runAppWithPhase ( 'one' ) ) . to . equal ( '011' ) ;
expect ( await runAppWithPhase ( 'two' ) ) . to . equal ( '110' ) ;
2019-05-28 21:12:59 +00:00
} ) ;
} ) ;
describe ( 'ses.clearStorageData(options)' , ( ) = > {
2019-08-29 06:50:14 +00:00
afterEach ( closeAllWindows ) ;
2019-05-28 21:12:59 +00:00
it ( 'clears localstorage data' , async ( ) = > {
2019-08-29 06:50:14 +00:00
const w = new BrowserWindow ( { show : false , webPreferences : { nodeIntegration : true } } ) ;
2019-05-28 21:12:59 +00:00
await w . loadFile ( path . join ( fixtures , 'api' , 'localstorage.html' ) ) ;
const options = {
origin : 'file://' ,
storages : [ 'localstorage' ] ,
quotas : [ 'persistent' ]
} ;
await w . webContents . session . clearStorageData ( options ) ;
while ( await w . webContents . executeJavaScript ( 'localStorage.length' ) !== 0 ) {
// The storage clear isn't instantly visible to the renderer, so keep
// trying until it is.
}
} ) ;
} ) ;
describe ( 'will-download event' , ( ) = > {
2019-08-29 06:50:14 +00:00
afterEach ( closeAllWindows ) ;
2019-05-30 22:05:02 +00:00
it ( 'can cancel default download behavior' , async ( ) = > {
2019-08-29 06:50:14 +00:00
const w = new BrowserWindow ( { show : false } ) ;
2019-05-28 21:12:59 +00:00
const mockFile = Buffer . alloc ( 1024 ) ;
const contentDisposition = 'inline; filename="mockFile.txt"' ;
const downloadServer = http . createServer ( ( req , res ) = > {
res . writeHead ( 200 , {
'Content-Length' : mockFile . length ,
'Content-Type' : 'application/plain' ,
'Content-Disposition' : contentDisposition
} ) ;
res . end ( mockFile ) ;
downloadServer . close ( ) ;
} ) ;
2023-02-20 11:30:57 +00:00
const url = ( await listen ( downloadServer ) ) . url ;
2019-05-28 21:12:59 +00:00
2019-09-25 21:38:50 +00:00
const downloadPrevented : Promise < { itemUrl : string , itemFilename : string , item : Electron.DownloadItem } > = new Promise ( resolve = > {
2019-05-28 21:12:59 +00:00
w . webContents . session . once ( 'will-download' , function ( e , item ) {
e . preventDefault ( ) ;
2019-11-01 20:37:02 +00:00
resolve ( { itemUrl : item.getURL ( ) , itemFilename : item.getFilename ( ) , item } ) ;
2019-05-28 21:12:59 +00:00
} ) ;
} ) ;
2019-05-30 22:05:02 +00:00
w . loadURL ( url ) ;
2019-11-01 20:37:02 +00:00
const { item , itemUrl , itemFilename } = await downloadPrevented ;
2023-02-20 11:30:57 +00:00
expect ( itemUrl ) . to . equal ( url + '/' ) ;
2019-09-25 21:38:50 +00:00
expect ( itemFilename ) . to . equal ( 'mockFile.txt' ) ;
2020-06-17 17:08:10 +00:00
// Delay till the next tick.
2023-02-17 18:32:39 +00:00
await new Promise ( setImmediate ) ;
2020-04-03 00:22:46 +00:00
expect ( ( ) = > item . getURL ( ) ) . to . throw ( 'DownloadItem used after being destroyed' ) ;
2019-05-28 21:12:59 +00:00
} ) ;
} ) ;
describe ( 'ses.protocol' , ( ) = > {
const partitionName = 'temp' ;
const protocolName = 'sp' ;
2019-08-27 21:55:19 +00:00
let customSession : Session ;
2019-05-28 21:12:59 +00:00
const protocol = session . defaultSession . protocol ;
2019-08-27 21:55:19 +00:00
const handler = ( ignoredError : any , callback : Function ) = > {
2020-03-20 15:12:18 +00:00
callback ( { data : '<script>require(\'electron\').ipcRenderer.send(\'hello\')</script>' , mimeType : 'text/html' } ) ;
2019-05-28 21:12:59 +00:00
} ;
2019-05-30 22:05:02 +00:00
beforeEach ( async ( ) = > {
2019-05-28 21:12:59 +00:00
customSession = session . fromPartition ( partitionName ) ;
2019-05-30 22:05:02 +00:00
await customSession . protocol . registerStringProtocol ( protocolName , handler ) ;
2019-05-28 21:12:59 +00:00
} ) ;
2019-05-30 22:05:02 +00:00
afterEach ( async ( ) = > {
await customSession . protocol . unregisterProtocol ( protocolName ) ;
2019-08-27 21:55:19 +00:00
customSession = null as any ;
2019-05-28 21:12:59 +00:00
} ) ;
2019-08-29 06:50:14 +00:00
afterEach ( closeAllWindows ) ;
2019-05-28 21:12:59 +00:00
2020-06-02 16:46:18 +00:00
it ( 'does not affect defaultSession' , ( ) = > {
const result1 = protocol . isProtocolRegistered ( protocolName ) ;
2019-05-28 21:12:59 +00:00
expect ( result1 ) . to . equal ( false ) ;
2020-06-02 16:46:18 +00:00
const result2 = customSession . protocol . isProtocolRegistered ( protocolName ) ;
2019-05-28 21:12:59 +00:00
expect ( result2 ) . to . equal ( true ) ;
} ) ;
it ( 'handles requests from partition' , async ( ) = > {
2019-08-29 06:50:14 +00:00
const w = new BrowserWindow ( {
show : false ,
webPreferences : {
partition : partitionName ,
2021-03-01 21:52:29 +00:00
nodeIntegration : true ,
contextIsolation : false
2019-08-29 06:50:14 +00:00
}
} ) ;
customSession = session . fromPartition ( partitionName ) ;
await customSession . protocol . registerStringProtocol ( protocolName , handler ) ;
2019-06-20 16:54:33 +00:00
w . loadURL ( ` ${ protocolName } ://fake-host ` ) ;
2023-02-23 23:53:53 +00:00
await once ( ipcMain , 'hello' ) ;
2019-05-28 21:12:59 +00:00
} ) ;
} ) ;
describe ( 'ses.setProxy(options)' , ( ) = > {
2019-08-27 21:55:19 +00:00
let server : http.Server ;
let customSession : Electron.Session ;
2020-10-27 17:53:27 +00:00
let created = false ;
2019-05-28 21:12:59 +00:00
2019-05-30 22:05:02 +00:00
beforeEach ( async ( ) = > {
2019-05-28 21:12:59 +00:00
customSession = session . fromPartition ( 'proxyconfig' ) ;
2020-10-27 17:53:27 +00:00
if ( ! created ) {
// Work around for https://github.com/electron/electron/issues/26166 to
// reduce flake
2023-02-23 23:53:53 +00:00
await setTimeout ( 100 ) ;
2020-10-27 17:53:27 +00:00
created = true ;
}
2019-05-28 21:12:59 +00:00
} ) ;
afterEach ( ( ) = > {
if ( server ) {
server . close ( ) ;
}
2020-05-19 17:18:12 +00:00
customSession = null as any ;
2019-05-28 21:12:59 +00:00
} ) ;
it ( 'allows configuring proxy settings' , async ( ) = > {
const config = { proxyRules : 'http=myproxy:80' } ;
await customSession . setProxy ( config ) ;
const proxy = await customSession . resolveProxy ( 'http://example.com/' ) ;
expect ( proxy ) . to . equal ( 'PROXY myproxy:80' ) ;
} ) ;
it ( 'allows removing the implicit bypass rules for localhost' , async ( ) = > {
const config = {
proxyRules : 'http=myproxy:80' ,
proxyBypassRules : '<-loopback>'
} ;
await customSession . setProxy ( config ) ;
const proxy = await customSession . resolveProxy ( 'http://localhost' ) ;
expect ( proxy ) . to . equal ( 'PROXY myproxy:80' ) ;
} ) ;
it ( 'allows configuring proxy settings with pacScript' , async ( ) = > {
server = http . createServer ( ( req , res ) = > {
const pac = `
function FindProxyForURL ( url , host ) {
return "PROXY myproxy:8132" ;
}
` ;
res . writeHead ( 200 , {
'Content-Type' : 'application/x-ns-proxy-autoconfig'
} ) ;
res . end ( pac ) ;
} ) ;
2023-02-20 11:30:57 +00:00
const { url } = await listen ( server ) ;
2020-10-27 06:50:06 +00:00
{
2023-02-20 11:30:57 +00:00
const config = { pacScript : url } ;
2020-10-27 06:50:06 +00:00
await customSession . setProxy ( config ) ;
const proxy = await customSession . resolveProxy ( 'https://google.com' ) ;
expect ( proxy ) . to . equal ( 'PROXY myproxy:8132' ) ;
}
{
2023-02-20 11:30:57 +00:00
const config = { mode : 'pac_script' as any , pacScript : url } ;
2020-10-27 06:50:06 +00:00
await customSession . setProxy ( config ) ;
const proxy = await customSession . resolveProxy ( 'https://google.com' ) ;
expect ( proxy ) . to . equal ( 'PROXY myproxy:8132' ) ;
}
2019-05-28 21:12:59 +00:00
} ) ;
it ( 'allows bypassing proxy settings' , async ( ) = > {
const config = {
proxyRules : 'http=myproxy:80' ,
proxyBypassRules : '<local>'
} ;
await customSession . setProxy ( config ) ;
const proxy = await customSession . resolveProxy ( 'http://example/' ) ;
expect ( proxy ) . to . equal ( 'DIRECT' ) ;
} ) ;
2020-10-27 06:50:06 +00:00
it ( 'allows configuring proxy settings with mode `direct`' , async ( ) = > {
const config = { mode : 'direct' as any , proxyRules : 'http=myproxy:80' } ;
await customSession . setProxy ( config ) ;
const proxy = await customSession . resolveProxy ( 'http://example.com/' ) ;
expect ( proxy ) . to . equal ( 'DIRECT' ) ;
} ) ;
it ( 'allows configuring proxy settings with mode `auto_detect`' , async ( ) = > {
const config = { mode : 'auto_detect' as any } ;
await customSession . setProxy ( config ) ;
} ) ;
it ( 'allows configuring proxy settings with mode `pac_script`' , async ( ) = > {
const config = { mode : 'pac_script' as any } ;
await customSession . setProxy ( config ) ;
const proxy = await customSession . resolveProxy ( 'http://example.com/' ) ;
expect ( proxy ) . to . equal ( 'DIRECT' ) ;
} ) ;
it ( 'allows configuring proxy settings with mode `fixed_servers`' , async ( ) = > {
const config = { mode : 'fixed_servers' as any , proxyRules : 'http=myproxy:80' } ;
await customSession . setProxy ( config ) ;
const proxy = await customSession . resolveProxy ( 'http://example.com/' ) ;
expect ( proxy ) . to . equal ( 'PROXY myproxy:80' ) ;
} ) ;
it ( 'allows configuring proxy settings with mode `system`' , async ( ) = > {
const config = { mode : 'system' as any } ;
await customSession . setProxy ( config ) ;
} ) ;
it ( 'disallows configuring proxy settings with mode `invalid`' , async ( ) = > {
const config = { mode : 'invalid' as any } ;
await expect ( customSession . setProxy ( config ) ) . to . eventually . be . rejectedWith ( /Invalid mode/ ) ;
} ) ;
it ( 'reload proxy configuration' , async ( ) = > {
let proxyPort = 8132 ;
server = http . createServer ( ( req , res ) = > {
const pac = `
function FindProxyForURL ( url , host ) {
return "PROXY myproxy:${proxyPort}" ;
}
` ;
res . writeHead ( 200 , {
'Content-Type' : 'application/x-ns-proxy-autoconfig'
} ) ;
res . end ( pac ) ;
} ) ;
2023-02-20 11:30:57 +00:00
const { url } = await listen ( server ) ;
const config = { mode : 'pac_script' as any , pacScript : url } ;
2020-10-27 06:50:06 +00:00
await customSession . setProxy ( config ) ;
{
const proxy = await customSession . resolveProxy ( 'https://google.com' ) ;
expect ( proxy ) . to . equal ( ` PROXY myproxy: ${ proxyPort } ` ) ;
}
{
proxyPort = 8133 ;
await customSession . forceReloadProxyConfig ( ) ;
const proxy = await customSession . resolveProxy ( 'https://google.com' ) ;
expect ( proxy ) . to . equal ( ` PROXY myproxy: ${ proxyPort } ` ) ;
}
} ) ;
2019-05-28 21:12:59 +00:00
} ) ;
2023-04-05 14:06:14 +00:00
describe ( 'ses.resolveHost(host)' , ( ) = > {
let customSession : Electron.Session ;
beforeEach ( async ( ) = > {
customSession = session . fromPartition ( 'resolvehost' ) ;
} ) ;
afterEach ( ( ) = > {
customSession = null as any ;
} ) ;
it ( 'resolves ipv4.localhost2' , async ( ) = > {
const { endpoints } = await customSession . resolveHost ( 'ipv4.localhost2' ) ;
expect ( endpoints ) . to . be . a ( 'array' ) ;
expect ( endpoints ) . to . have . lengthOf ( 1 ) ;
expect ( endpoints [ 0 ] . family ) . to . equal ( 'ipv4' ) ;
expect ( endpoints [ 0 ] . address ) . to . equal ( '10.0.0.1' ) ;
} ) ;
it ( 'fails to resolve AAAA record for ipv4.localhost2' , async ( ) = > {
await expect ( customSession . resolveHost ( 'ipv4.localhost2' , {
queryType : 'AAAA'
} ) )
. to . eventually . be . rejectedWith ( /net::ERR_NAME_NOT_RESOLVED/ ) ;
} ) ;
it ( 'resolves ipv6.localhost2' , async ( ) = > {
const { endpoints } = await customSession . resolveHost ( 'ipv6.localhost2' ) ;
expect ( endpoints ) . to . be . a ( 'array' ) ;
expect ( endpoints ) . to . have . lengthOf ( 1 ) ;
expect ( endpoints [ 0 ] . family ) . to . equal ( 'ipv6' ) ;
expect ( endpoints [ 0 ] . address ) . to . equal ( '::1' ) ;
} ) ;
it ( 'fails to resolve A record for ipv6.localhost2' , async ( ) = > {
await expect ( customSession . resolveHost ( 'notfound.localhost2' , {
queryType : 'A'
} ) )
. to . eventually . be . rejectedWith ( /net::ERR_NAME_NOT_RESOLVED/ ) ;
} ) ;
it ( 'fails to resolve notfound.localhost2' , async ( ) = > {
await expect ( customSession . resolveHost ( 'notfound.localhost2' ) )
. to . eventually . be . rejectedWith ( /net::ERR_NAME_NOT_RESOLVED/ ) ;
} ) ;
} ) ;
2019-09-03 22:54:14 +00:00
describe ( 'ses.getBlobData()' , ( ) = > {
2019-05-30 22:05:02 +00:00
const scheme = 'cors-blob' ;
const protocol = session . defaultSession . protocol ;
const url = ` ${ scheme } ://host ` ;
after ( async ( ) = > {
await protocol . unregisterProtocol ( scheme ) ;
} ) ;
2019-08-29 06:50:14 +00:00
afterEach ( closeAllWindows ) ;
2019-05-28 21:12:59 +00:00
2019-05-30 22:05:02 +00:00
it ( 'returns blob data for uuid' , ( done ) = > {
2019-05-28 21:12:59 +00:00
const postData = JSON . stringify ( {
type : 'blob' ,
value : 'hello'
} ) ;
const content = ` <html>
< script >
let fd = new FormData ( ) ;
fd . append ( 'file' , new Blob ( [ '${postData}' ] , { type : 'application/json' } ) ) ;
fetch ( '${url}' , { method : 'POST' , body : fd } ) ;
< / script >
< / html > ` ;
protocol . registerStringProtocol ( scheme , ( request , callback ) = > {
2020-06-30 22:10:36 +00:00
try {
if ( request . method === 'GET' ) {
callback ( { data : content , mimeType : 'text/html' } ) ;
} else if ( request . method === 'POST' ) {
const uuid = request . uploadData ! [ 1 ] . blobUUID ;
expect ( uuid ) . to . be . a ( 'string' ) ;
session . defaultSession . getBlobData ( uuid ! ) . then ( result = > {
try {
expect ( result . toString ( ) ) . to . equal ( postData ) ;
done ( ) ;
} catch ( e ) {
done ( e ) ;
}
} ) ;
}
2022-09-07 21:47:06 +00:00
} catch ( e ) {
done ( e ) ;
}
} ) ;
const w = new BrowserWindow ( { show : false } ) ;
w . loadURL ( url ) ;
} ) ;
} ) ;
describe ( 'ses.getBlobData2()' , ( ) = > {
const scheme = 'cors-blob' ;
const protocol = session . defaultSession . protocol ;
const url = ` ${ scheme } ://host ` ;
after ( async ( ) = > {
await protocol . unregisterProtocol ( scheme ) ;
} ) ;
afterEach ( closeAllWindows ) ;
it ( 'returns blob data for uuid' , ( done ) = > {
const content = ` <html>
< script >
let fd = new FormData ( ) ;
fd . append ( "data" , new Blob ( new Array ( 65 _537 ) . fill ( 'a' ) ) ) ;
fetch ( '${url}' , { method : 'POST' , body : fd } ) ;
< / script >
< / html > ` ;
protocol . registerStringProtocol ( scheme , ( request , callback ) = > {
try {
if ( request . method === 'GET' ) {
callback ( { data : content , mimeType : 'text/html' } ) ;
} else if ( request . method === 'POST' ) {
const uuid = request . uploadData ! [ 1 ] . blobUUID ;
expect ( uuid ) . to . be . a ( 'string' ) ;
session . defaultSession . getBlobData ( uuid ! ) . then ( result = > {
try {
const data = new Array ( 65 _537 ) . fill ( 'a' ) ;
expect ( result . toString ( ) ) . to . equal ( data . join ( '' ) ) ;
done ( ) ;
} catch ( e ) {
done ( e ) ;
}
} ) ;
}
2020-06-30 22:10:36 +00:00
} catch ( e ) {
done ( e ) ;
2019-05-28 21:12:59 +00:00
}
} ) ;
2020-06-02 16:46:18 +00:00
const w = new BrowserWindow ( { show : false } ) ;
w . loadURL ( url ) ;
2019-05-28 21:12:59 +00:00
} ) ;
} ) ;
2019-08-27 21:55:19 +00:00
describe ( 'ses.setCertificateVerifyProc(callback)' , ( ) = > {
let server : http.Server ;
2023-02-20 11:30:57 +00:00
let serverUrl : string ;
2019-05-28 21:12:59 +00:00
2023-02-20 11:30:57 +00:00
beforeEach ( async ( ) = > {
2019-05-28 21:12:59 +00:00
const certPath = path . join ( fixtures , 'certificates' ) ;
const options = {
key : fs.readFileSync ( path . join ( certPath , 'server.key' ) ) ,
cert : fs.readFileSync ( path . join ( certPath , 'server.pem' ) ) ,
ca : [
fs . readFileSync ( path . join ( certPath , 'rootCA.pem' ) ) ,
fs . readFileSync ( path . join ( certPath , 'intermediateCA.pem' ) )
] ,
rejectUnauthorized : false
} ;
server = https . createServer ( options , ( req , res ) = > {
res . writeHead ( 200 ) ;
res . end ( '<title>hello</title>' ) ;
} ) ;
2023-02-20 11:30:57 +00:00
serverUrl = ( await listen ( server ) ) . url ;
2019-05-28 21:12:59 +00:00
} ) ;
2019-05-30 22:05:02 +00:00
afterEach ( ( done ) = > {
server . close ( done ) ;
2019-05-28 21:12:59 +00:00
} ) ;
2019-08-29 06:50:14 +00:00
afterEach ( closeAllWindows ) ;
2019-05-28 21:12:59 +00:00
2019-05-30 22:05:02 +00:00
it ( 'accepts the request when the callback is called with 0' , async ( ) = > {
2020-11-17 19:12:50 +00:00
const ses = session . fromPartition ( ` ${ Math . random ( ) } ` ) ;
2021-06-17 21:17:25 +00:00
let validate : ( ) = > void ;
ses . setCertificateVerifyProc ( ( { hostname , verificationResult , errorCode } , callback ) = > {
2021-09-01 22:58:29 +00:00
if ( hostname !== '127.0.0.1' ) return callback ( - 3 ) ;
2021-06-17 21:17:25 +00:00
validate = ( ) = > {
expect ( verificationResult ) . to . be . oneOf ( [ 'net::ERR_CERT_AUTHORITY_INVALID' , 'net::ERR_CERT_COMMON_NAME_INVALID' ] ) ;
expect ( errorCode ) . to . be . oneOf ( [ - 202 , - 200 ] ) ;
} ;
2019-05-28 21:12:59 +00:00
callback ( 0 ) ;
} ) ;
2020-11-17 19:12:50 +00:00
const w = new BrowserWindow ( { show : false , webPreferences : { session : ses } } ) ;
2023-02-20 11:30:57 +00:00
await w . loadURL ( serverUrl ) ;
2019-05-30 22:05:02 +00:00
expect ( w . webContents . getTitle ( ) ) . to . equal ( 'hello' ) ;
2021-06-17 21:17:25 +00:00
expect ( validate ! ) . not . to . be . undefined ( ) ;
validate ! ( ) ;
2019-05-28 21:12:59 +00:00
} ) ;
2019-05-30 22:05:02 +00:00
it ( 'rejects the request when the callback is called with -2' , async ( ) = > {
2020-11-17 19:12:50 +00:00
const ses = session . fromPartition ( ` ${ Math . random ( ) } ` ) ;
2021-06-17 21:17:25 +00:00
let validate : ( ) = > void ;
2021-08-02 01:24:58 +00:00
ses . setCertificateVerifyProc ( ( { hostname , certificate , verificationResult , isIssuedByKnownRoot } , callback ) = > {
2021-09-01 22:58:29 +00:00
if ( hostname !== '127.0.0.1' ) return callback ( - 3 ) ;
2021-06-17 21:17:25 +00:00
validate = ( ) = > {
expect ( certificate . issuerName ) . to . equal ( 'Intermediate CA' ) ;
expect ( certificate . subjectName ) . to . equal ( 'localhost' ) ;
expect ( certificate . issuer . commonName ) . to . equal ( 'Intermediate CA' ) ;
expect ( certificate . subject . commonName ) . to . equal ( 'localhost' ) ;
expect ( certificate . issuerCert . issuer . commonName ) . to . equal ( 'Root CA' ) ;
expect ( certificate . issuerCert . subject . commonName ) . to . equal ( 'Intermediate CA' ) ;
expect ( certificate . issuerCert . issuerCert . issuer . commonName ) . to . equal ( 'Root CA' ) ;
expect ( certificate . issuerCert . issuerCert . subject . commonName ) . to . equal ( 'Root CA' ) ;
expect ( certificate . issuerCert . issuerCert . issuerCert ) . to . equal ( undefined ) ;
expect ( verificationResult ) . to . be . oneOf ( [ 'net::ERR_CERT_AUTHORITY_INVALID' , 'net::ERR_CERT_COMMON_NAME_INVALID' ] ) ;
2021-08-02 01:24:58 +00:00
expect ( isIssuedByKnownRoot ) . to . be . false ( ) ;
2021-06-17 21:17:25 +00:00
} ;
2019-05-28 21:12:59 +00:00
callback ( - 2 ) ;
} ) ;
2020-03-20 20:28:31 +00:00
2020-11-17 19:12:50 +00:00
const w = new BrowserWindow ( { show : false , webPreferences : { session : ses } } ) ;
2023-02-20 11:30:57 +00:00
await expect ( w . loadURL ( serverUrl ) ) . to . eventually . be . rejectedWith ( /ERR_FAILED/ ) ;
2021-06-17 21:17:25 +00:00
expect ( validate ! ) . not . to . be . undefined ( ) ;
validate ! ( ) ;
2019-05-28 21:12:59 +00:00
} ) ;
2019-06-28 22:22:23 +00:00
it ( 'saves cached results' , async ( ) = > {
2020-11-17 19:12:50 +00:00
const ses = session . fromPartition ( ` ${ Math . random ( ) } ` ) ;
2019-06-28 22:22:23 +00:00
let numVerificationRequests = 0 ;
2020-11-17 19:12:50 +00:00
ses . setCertificateVerifyProc ( ( e , callback ) = > {
2021-09-01 22:58:29 +00:00
if ( e . hostname !== '127.0.0.1' ) return callback ( - 3 ) ;
2019-06-28 22:22:23 +00:00
numVerificationRequests ++ ;
callback ( - 2 ) ;
} ) ;
2020-03-20 20:28:31 +00:00
2020-11-17 19:12:50 +00:00
const w = new BrowserWindow ( { show : false , webPreferences : { session : ses } } ) ;
2023-02-20 11:30:57 +00:00
await expect ( w . loadURL ( serverUrl ) , 'first load' ) . to . eventually . be . rejectedWith ( /ERR_FAILED/ ) ;
2023-02-23 23:53:53 +00:00
await once ( w . webContents , 'did-stop-loading' ) ;
2023-02-20 11:30:57 +00:00
await expect ( w . loadURL ( serverUrl + '/test' ) , 'second load' ) . to . eventually . be . rejectedWith ( /ERR_FAILED/ ) ;
2019-08-02 23:56:46 +00:00
expect ( numVerificationRequests ) . to . equal ( 1 ) ;
2019-06-28 22:22:23 +00:00
} ) ;
2020-11-17 19:12:50 +00:00
it ( 'does not cancel requests in other sessions' , async ( ) = > {
const ses1 = session . fromPartition ( ` ${ Math . random ( ) } ` ) ;
ses1 . setCertificateVerifyProc ( ( opts , cb ) = > cb ( 0 ) ) ;
const ses2 = session . fromPartition ( ` ${ Math . random ( ) } ` ) ;
2023-02-20 11:30:57 +00:00
const req = net . request ( { url : serverUrl , session : ses1 , credentials : 'include' } ) ;
2020-11-17 19:12:50 +00:00
req . end ( ) ;
2023-02-23 23:53:53 +00:00
setTimeout ( ) . then ( ( ) = > {
2020-11-17 19:12:50 +00:00
ses2 . setCertificateVerifyProc ( ( opts , callback ) = > callback ( 0 ) ) ;
} ) ;
2021-01-22 19:25:47 +00:00
await expect ( new Promise < void > ( ( resolve , reject ) = > {
2020-11-17 19:12:50 +00:00
req . on ( 'error' , ( err ) = > {
reject ( err ) ;
} ) ;
req . on ( 'response' , ( ) = > {
resolve ( ) ;
} ) ;
} ) ) . to . eventually . be . fulfilled ( ) ;
} ) ;
2019-05-28 21:12:59 +00:00
} ) ;
2020-02-21 18:40:45 +00:00
describe ( 'ses.clearAuthCache()' , ( ) = > {
2019-08-29 00:43:12 +00:00
it ( 'can clear http auth info from cache' , async ( ) = > {
2019-05-28 21:12:59 +00:00
const ses = session . fromPartition ( 'auth-cache' ) ;
const server = http . createServer ( ( req , res ) = > {
const credentials = auth ( req ) ;
if ( ! credentials || credentials . name !== 'test' || credentials . pass !== 'test' ) {
res . statusCode = 401 ;
res . setHeader ( 'WWW-Authenticate' , 'Basic realm="Restricted"' ) ;
res . end ( ) ;
} else {
res . end ( 'authenticated' ) ;
}
} ) ;
2023-02-20 11:30:57 +00:00
const { port } = await listen ( server ) ;
2019-08-29 00:43:12 +00:00
const fetch = ( url : string ) = > new Promise ( ( resolve , reject ) = > {
const request = net . request ( { url , session : ses } ) ;
request . on ( 'response' , ( response ) = > {
2019-11-25 22:34:25 +00:00
let data : string | null = null ;
2019-08-29 00:43:12 +00:00
response . on ( 'data' , ( chunk ) = > {
2019-11-25 22:34:25 +00:00
if ( ! data ) {
data = '' ;
}
2019-08-29 00:43:12 +00:00
data += chunk ;
2019-05-28 21:12:59 +00:00
} ) ;
2019-08-29 00:43:12 +00:00
response . on ( 'end' , ( ) = > {
2019-11-25 22:34:25 +00:00
if ( ! data ) {
reject ( new Error ( 'Empty response' ) ) ;
} else {
resolve ( data ) ;
}
2019-05-28 21:12:59 +00:00
} ) ;
2019-08-29 00:43:12 +00:00
response . on ( 'error' , ( error : any ) = > { reject ( new Error ( error ) ) ; } ) ;
2019-11-01 20:37:02 +00:00
} ) ;
2019-11-14 18:01:18 +00:00
request . on ( 'error' , ( error : any ) = > { reject ( new Error ( error ) ) ; } ) ;
2019-08-29 00:43:12 +00:00
request . end ( ) ;
2019-05-28 21:12:59 +00:00
} ) ;
2019-08-29 00:43:12 +00:00
// the first time should throw due to unauthenticated
await expect ( fetch ( ` http://127.0.0.1: ${ port } ` ) ) . to . eventually . be . rejected ( ) ;
// passing the password should let us in
expect ( await fetch ( ` http://test:test@127.0.0.1: ${ port } ` ) ) . to . equal ( 'authenticated' ) ;
// subsequently, the credentials are cached
expect ( await fetch ( ` http://127.0.0.1: ${ port } ` ) ) . to . equal ( 'authenticated' ) ;
2020-02-21 18:40:45 +00:00
await ses . clearAuthCache ( ) ;
2019-08-29 00:43:12 +00:00
// once the cache is cleared, we should get an error again
await expect ( fetch ( ` http://127.0.0.1: ${ port } ` ) ) . to . eventually . be . rejected ( ) ;
2019-05-28 21:12:59 +00:00
} ) ;
} ) ;
2019-05-30 22:05:02 +00:00
describe ( 'DownloadItem' , ( ) = > {
const mockPDF = Buffer . alloc ( 1024 * 1024 * 5 ) ;
const downloadFilePath = path . join ( __dirname , '..' , 'fixtures' , 'mock.pdf' ) ;
const protocolName = 'custom-dl' ;
const contentDisposition = 'inline; filename="mock.pdf"' ;
2023-02-20 11:30:57 +00:00
let port : number ;
2019-08-27 21:55:19 +00:00
let downloadServer : http.Server ;
2019-05-30 22:05:02 +00:00
before ( async ( ) = > {
downloadServer = http . createServer ( ( req , res ) = > {
res . writeHead ( 200 , {
'Content-Length' : mockPDF . length ,
'Content-Type' : 'application/pdf' ,
'Content-Disposition' : req . url === '/?testFilename' ? 'inline' : contentDisposition
} ) ;
res . end ( mockPDF ) ;
} ) ;
2023-02-20 11:30:57 +00:00
port = ( await listen ( downloadServer ) ) . port ;
2019-05-30 22:05:02 +00:00
} ) ;
after ( async ( ) = > {
await new Promise ( resolve = > downloadServer . close ( resolve ) ) ;
} ) ;
2019-08-29 06:50:14 +00:00
afterEach ( closeAllWindows ) ;
2019-05-30 22:05:02 +00:00
2019-08-27 21:55:19 +00:00
const isPathEqual = ( path1 : string , path2 : string ) = > {
2019-05-30 22:05:02 +00:00
return path . relative ( path1 , path2 ) === '' ;
} ;
2019-08-27 21:55:19 +00:00
const assertDownload = ( state : string , item : Electron.DownloadItem , isCustom = false ) = > {
2019-05-30 22:05:02 +00:00
expect ( state ) . to . equal ( 'completed' ) ;
expect ( item . getFilename ( ) ) . to . equal ( 'mock.pdf' ) ;
2019-06-20 17:04:57 +00:00
expect ( path . isAbsolute ( item . savePath ) ) . to . equal ( true ) ;
expect ( isPathEqual ( item . savePath , downloadFilePath ) ) . to . equal ( true ) ;
2019-05-30 22:05:02 +00:00
if ( isCustom ) {
expect ( item . getURL ( ) ) . to . equal ( ` ${ protocolName } ://item ` ) ;
} else {
2023-02-20 11:30:57 +00:00
expect ( item . getURL ( ) ) . to . be . equal ( ` ${ url } : ${ port } / ` ) ;
2019-05-30 22:05:02 +00:00
}
expect ( item . getMimeType ( ) ) . to . equal ( 'application/pdf' ) ;
expect ( item . getReceivedBytes ( ) ) . to . equal ( mockPDF . length ) ;
expect ( item . getTotalBytes ( ) ) . to . equal ( mockPDF . length ) ;
expect ( item . getContentDisposition ( ) ) . to . equal ( contentDisposition ) ;
expect ( fs . existsSync ( downloadFilePath ) ) . to . equal ( true ) ;
fs . unlinkSync ( downloadFilePath ) ;
} ;
2019-08-29 03:27:20 +00:00
it ( 'can download using session.downloadURL' , ( done ) = > {
session . defaultSession . once ( 'will-download' , function ( e , item ) {
item . savePath = downloadFilePath ;
item . on ( 'done' , function ( e , state ) {
2020-06-30 22:10:36 +00:00
try {
assertDownload ( state , item ) ;
done ( ) ;
} catch ( e ) {
done ( e ) ;
}
2019-08-29 03:27:20 +00:00
} ) ;
} ) ;
session . defaultSession . downloadURL ( ` ${ url } : ${ port } ` ) ;
} ) ;
2019-05-30 22:05:02 +00:00
it ( 'can download using WebContents.downloadURL' , ( done ) = > {
2019-08-29 06:50:14 +00:00
const w = new BrowserWindow ( { show : false } ) ;
2019-05-30 22:05:02 +00:00
w . webContents . session . once ( 'will-download' , function ( e , item ) {
2019-06-20 17:04:57 +00:00
item . savePath = downloadFilePath ;
2019-05-30 22:05:02 +00:00
item . on ( 'done' , function ( e , state ) {
2020-06-30 22:10:36 +00:00
try {
assertDownload ( state , item ) ;
done ( ) ;
} catch ( e ) {
done ( e ) ;
}
2019-05-30 22:05:02 +00:00
} ) ;
} ) ;
w . webContents . downloadURL ( ` ${ url } : ${ port } ` ) ;
} ) ;
it ( 'can download from custom protocols using WebContents.downloadURL' , ( done ) = > {
const protocol = session . defaultSession . protocol ;
2019-08-27 21:55:19 +00:00
const handler = ( ignoredError : any , callback : Function ) = > {
2019-05-30 22:05:02 +00:00
callback ( { url : ` ${ url } : ${ port } ` } ) ;
} ;
2020-06-02 16:46:18 +00:00
protocol . registerHttpProtocol ( protocolName , handler ) ;
const w = new BrowserWindow ( { show : false } ) ;
w . webContents . session . once ( 'will-download' , function ( e , item ) {
item . savePath = downloadFilePath ;
item . on ( 'done' , function ( e , state ) {
2020-06-30 22:10:36 +00:00
try {
assertDownload ( state , item , true ) ;
done ( ) ;
} catch ( e ) {
done ( e ) ;
}
2019-05-30 22:05:02 +00:00
} ) ;
} ) ;
2020-06-02 16:46:18 +00:00
w . webContents . downloadURL ( ` ${ protocolName } ://item ` ) ;
2019-05-30 22:05:02 +00:00
} ) ;
it ( 'can download using WebView.downloadURL' , async ( ) = > {
2019-08-29 06:50:14 +00:00
const w = new BrowserWindow ( { show : false , webPreferences : { webviewTag : true } } ) ;
2019-05-30 22:05:02 +00:00
await w . loadURL ( 'about:blank' ) ;
2019-11-01 20:37:02 +00:00
function webviewDownload ( { fixtures , url , port } : { fixtures : string , url : string , port : string } ) {
2019-08-27 21:55:19 +00:00
const webview = new ( window as any ) . WebView ( ) ;
2019-05-30 22:05:02 +00:00
webview . addEventListener ( 'did-finish-load' , ( ) = > {
webview . downloadURL ( ` ${ url } : ${ port } / ` ) ;
} ) ;
webview . src = ` file:// ${ fixtures } /api/blank.html ` ;
document . body . appendChild ( webview ) ;
}
2019-08-27 21:55:19 +00:00
const done : Promise < [ string , Electron . DownloadItem ] > = new Promise ( resolve = > {
2019-05-30 22:05:02 +00:00
w . webContents . session . once ( 'will-download' , function ( e , item ) {
2019-06-20 17:04:57 +00:00
item . savePath = downloadFilePath ;
2019-05-30 22:05:02 +00:00
item . on ( 'done' , function ( e , state ) {
resolve ( [ state , item ] ) ;
} ) ;
} ) ;
} ) ;
2019-11-01 20:37:02 +00:00
await w . webContents . executeJavaScript ( ` ( ${ webviewDownload } )( ${ JSON . stringify ( { fixtures , url , port } )}) ` ) ;
2019-05-30 22:05:02 +00:00
const [ state , item ] = await done ;
assertDownload ( state , item ) ;
} ) ;
it ( 'can cancel download' , ( done ) = > {
2019-08-29 06:50:14 +00:00
const w = new BrowserWindow ( { show : false } ) ;
2019-05-30 22:05:02 +00:00
w . webContents . session . once ( 'will-download' , function ( e , item ) {
2019-06-20 17:04:57 +00:00
item . savePath = downloadFilePath ;
2019-05-30 22:05:02 +00:00
item . on ( 'done' , function ( e , state ) {
2020-06-30 22:10:36 +00:00
try {
expect ( state ) . to . equal ( 'cancelled' ) ;
expect ( item . getFilename ( ) ) . to . equal ( 'mock.pdf' ) ;
expect ( item . getMimeType ( ) ) . to . equal ( 'application/pdf' ) ;
expect ( item . getReceivedBytes ( ) ) . to . equal ( 0 ) ;
expect ( item . getTotalBytes ( ) ) . to . equal ( mockPDF . length ) ;
expect ( item . getContentDisposition ( ) ) . to . equal ( contentDisposition ) ;
done ( ) ;
} catch ( e ) {
done ( e ) ;
}
2019-05-30 22:05:02 +00:00
} ) ;
item . cancel ( ) ;
} ) ;
w . webContents . downloadURL ( ` ${ url } : ${ port } / ` ) ;
} ) ;
it ( 'can generate a default filename' , function ( done ) {
if ( process . env . APPVEYOR === 'True' ) {
// FIXME(alexeykuzmin): Skip the test.
// this.skip()
return done ( ) ;
}
2019-08-29 06:50:14 +00:00
const w = new BrowserWindow ( { show : false } ) ;
2019-05-30 22:05:02 +00:00
w . webContents . session . once ( 'will-download' , function ( e , item ) {
2019-06-20 17:04:57 +00:00
item . savePath = downloadFilePath ;
2019-11-01 20:37:02 +00:00
item . on ( 'done' , function ( ) {
2020-06-30 22:10:36 +00:00
try {
expect ( item . getFilename ( ) ) . to . equal ( 'download.pdf' ) ;
done ( ) ;
} catch ( e ) {
done ( e ) ;
}
2019-05-30 22:05:02 +00:00
} ) ;
item . cancel ( ) ;
} ) ;
w . webContents . downloadURL ( ` ${ url } : ${ port } /?testFilename ` ) ;
} ) ;
it ( 'can set options for the save dialog' , ( done ) = > {
const filePath = path . join ( __dirname , 'fixtures' , 'mock.pdf' ) ;
const options = {
window : null ,
title : 'title' ,
message : 'message' ,
buttonLabel : 'buttonLabel' ,
nameFieldLabel : 'nameFieldLabel' ,
defaultPath : '/' ,
filters : [ {
name : '1' , extensions : [ '.1' , '.2' ]
} , {
name : '2' , extensions : [ '.3' , '.4' , '.5' ]
} ] ,
showsTagField : true ,
securityScopedBookmarks : true
} ;
2019-08-29 06:50:14 +00:00
const w = new BrowserWindow ( { show : false } ) ;
2019-05-30 22:05:02 +00:00
w . webContents . session . once ( 'will-download' , function ( e , item ) {
item . setSavePath ( filePath ) ;
item . setSaveDialogOptions ( options ) ;
2019-11-01 20:37:02 +00:00
item . on ( 'done' , function ( ) {
2020-06-30 22:10:36 +00:00
try {
expect ( item . getSaveDialogOptions ( ) ) . to . deep . equal ( options ) ;
done ( ) ;
} catch ( e ) {
done ( e ) ;
}
2019-05-30 22:05:02 +00:00
} ) ;
item . cancel ( ) ;
} ) ;
w . webContents . downloadURL ( ` ${ url } : ${ port } ` ) ;
} ) ;
describe ( 'when a save path is specified and the URL is unavailable' , ( ) = > {
it ( 'does not display a save dialog and reports the done state as interrupted' , ( done ) = > {
2019-08-29 06:50:14 +00:00
const w = new BrowserWindow ( { show : false } ) ;
2019-05-30 22:05:02 +00:00
w . webContents . session . once ( 'will-download' , function ( e , item ) {
2019-06-20 17:04:57 +00:00
item . savePath = downloadFilePath ;
2019-05-30 22:05:02 +00:00
if ( item . getState ( ) === 'interrupted' ) {
item . resume ( ) ;
}
item . on ( 'done' , function ( e , state ) {
2020-06-30 22:10:36 +00:00
try {
expect ( state ) . to . equal ( 'interrupted' ) ;
done ( ) ;
} catch ( e ) {
done ( e ) ;
}
2019-05-30 22:05:02 +00:00
} ) ;
} ) ;
w . webContents . downloadURL ( ` file:// ${ path . join ( __dirname , 'does-not-exist.txt' ) } ` ) ;
} ) ;
} ) ;
} ) ;
describe ( 'ses.createInterruptedDownload(options)' , ( ) = > {
2019-08-29 06:50:14 +00:00
afterEach ( closeAllWindows ) ;
2019-09-21 14:51:28 +00:00
it ( 'can create an interrupted download item' , async ( ) = > {
2019-05-30 22:05:02 +00:00
const downloadFilePath = path . join ( __dirname , '..' , 'fixtures' , 'mock.pdf' ) ;
const options = {
path : downloadFilePath ,
urlChain : [ 'http://127.0.0.1/' ] ,
mimeType : 'application/pdf' ,
offset : 0 ,
length : 5242880
} ;
2019-08-29 06:50:14 +00:00
const w = new BrowserWindow ( { show : false } ) ;
2023-02-23 23:53:53 +00:00
const p = once ( w . webContents . session , 'will-download' ) ;
2019-05-30 22:05:02 +00:00
w . webContents . session . createInterruptedDownload ( options ) ;
2019-09-21 14:51:28 +00:00
const [ , item ] = await p ;
expect ( item . getState ( ) ) . to . equal ( 'interrupted' ) ;
item . cancel ( ) ;
expect ( item . getURLChain ( ) ) . to . deep . equal ( options . urlChain ) ;
expect ( item . getMimeType ( ) ) . to . equal ( options . mimeType ) ;
expect ( item . getReceivedBytes ( ) ) . to . equal ( options . offset ) ;
expect ( item . getTotalBytes ( ) ) . to . equal ( options . length ) ;
expect ( item . savePath ) . to . equal ( downloadFilePath ) ;
2019-05-30 22:05:02 +00:00
} ) ;
it ( 'can be resumed' , async ( ) = > {
const downloadFilePath = path . join ( fixtures , 'logo.png' ) ;
const rangeServer = http . createServer ( ( req , res ) = > {
const options = { root : fixtures } ;
2019-08-27 21:55:19 +00:00
send ( req , req . url ! , options )
. on ( 'error' , ( error : any ) = > { throw error ; } ) . pipe ( res ) ;
2019-05-30 22:05:02 +00:00
} ) ;
try {
2023-02-20 11:30:57 +00:00
const { url } = await listen ( rangeServer ) ;
2019-08-29 06:50:14 +00:00
const w = new BrowserWindow ( { show : false } ) ;
2019-08-27 21:55:19 +00:00
const downloadCancelled : Promise < Electron.DownloadItem > = new Promise ( ( resolve ) = > {
2019-05-30 22:05:02 +00:00
w . webContents . session . once ( 'will-download' , function ( e , item ) {
item . setSavePath ( downloadFilePath ) ;
2019-11-01 20:37:02 +00:00
item . on ( 'done' , function ( ) {
2019-05-30 22:05:02 +00:00
resolve ( item ) ;
} ) ;
item . cancel ( ) ;
} ) ;
} ) ;
2023-02-20 11:30:57 +00:00
const downloadUrl = ` ${ url } /assets/logo.png ` ;
2019-05-30 22:05:02 +00:00
w . webContents . downloadURL ( downloadUrl ) ;
const item = await downloadCancelled ;
expect ( item . getState ( ) ) . to . equal ( 'cancelled' ) ;
const options = {
2019-06-20 17:04:57 +00:00
path : item.savePath ,
2019-05-30 22:05:02 +00:00
urlChain : item.getURLChain ( ) ,
mimeType : item.getMimeType ( ) ,
offset : item.getReceivedBytes ( ) ,
length : item.getTotalBytes ( ) ,
lastModified : item.getLastModifiedTime ( ) ,
2019-11-01 20:37:02 +00:00
eTag : item.getETag ( )
2019-05-30 22:05:02 +00:00
} ;
2019-08-27 21:55:19 +00:00
const downloadResumed : Promise < Electron.DownloadItem > = new Promise ( ( resolve ) = > {
2019-05-30 22:05:02 +00:00
w . webContents . session . once ( 'will-download' , function ( e , item ) {
expect ( item . getState ( ) ) . to . equal ( 'interrupted' ) ;
item . setSavePath ( downloadFilePath ) ;
item . resume ( ) ;
2019-11-01 20:37:02 +00:00
item . on ( 'done' , function ( ) {
2019-05-30 22:05:02 +00:00
resolve ( item ) ;
} ) ;
} ) ;
} ) ;
w . webContents . session . createInterruptedDownload ( options ) ;
const completedItem = await downloadResumed ;
expect ( completedItem . getState ( ) ) . to . equal ( 'completed' ) ;
expect ( completedItem . getFilename ( ) ) . to . equal ( 'logo.png' ) ;
2019-06-20 17:04:57 +00:00
expect ( completedItem . savePath ) . to . equal ( downloadFilePath ) ;
2019-05-30 22:05:02 +00:00
expect ( completedItem . getURL ( ) ) . to . equal ( downloadUrl ) ;
expect ( completedItem . getMimeType ( ) ) . to . equal ( 'image/png' ) ;
expect ( completedItem . getReceivedBytes ( ) ) . to . equal ( 14022 ) ;
expect ( completedItem . getTotalBytes ( ) ) . to . equal ( 14022 ) ;
expect ( fs . existsSync ( downloadFilePath ) ) . to . equal ( true ) ;
} finally {
rangeServer . close ( ) ;
}
} ) ;
} ) ;
describe ( 'ses.setPermissionRequestHandler(handler)' , ( ) = > {
2019-08-29 06:50:14 +00:00
afterEach ( closeAllWindows ) ;
2023-01-11 10:55:31 +00:00
// These tests are done on an http server because navigator.userAgentData
// requires a secure context.
let server : http.Server ;
let serverUrl : string ;
before ( async ( ) = > {
server = http . createServer ( ( req , res ) = > {
res . setHeader ( 'Content-Type' , 'text/html' ) ;
res . end ( '' ) ;
} ) ;
2023-02-20 11:30:57 +00:00
serverUrl = ( await listen ( server ) ) . url ;
2023-01-11 10:55:31 +00:00
} ) ;
after ( ( ) = > {
server . close ( ) ;
} ) ;
2019-05-30 22:05:02 +00:00
it ( 'cancels any pending requests when cleared' , async ( ) = > {
2019-08-29 06:50:14 +00:00
const w = new BrowserWindow ( {
2019-07-03 01:22:09 +00:00
show : false ,
webPreferences : {
2022-06-16 07:46:11 +00:00
partition : 'very-temp-permission-handler' ,
2021-03-01 21:52:29 +00:00
nodeIntegration : true ,
contextIsolation : false
2019-07-03 01:22:09 +00:00
}
} ) ;
2019-05-30 22:05:02 +00:00
const ses = w . webContents . session ;
ses . setPermissionRequestHandler ( ( ) = > {
ses . setPermissionRequestHandler ( null ) ;
} ) ;
2019-07-03 01:22:09 +00:00
ses . protocol . interceptStringProtocol ( 'https' , ( req , cb ) = > {
cb ( ` <html><script>( ${ remote } )()</script></html> ` ) ;
} ) ;
2023-02-23 23:53:53 +00:00
const result = once ( require ( 'electron' ) . ipcMain , 'message' ) ;
2019-05-30 22:05:02 +00:00
2019-11-01 20:37:02 +00:00
function remote ( ) {
( navigator as any ) . requestMIDIAccess ( { sysex : true } ) . then ( ( ) = > { } , ( err : any ) = > {
require ( 'electron' ) . ipcRenderer . send ( 'message' , err . name ) ;
} ) ;
2019-05-30 22:05:02 +00:00
}
2019-07-03 01:22:09 +00:00
await w . loadURL ( 'https://myfakesite' ) ;
2019-05-30 22:05:02 +00:00
2019-11-01 20:37:02 +00:00
const [ , name ] = await result ;
2019-05-30 22:05:02 +00:00
expect ( name ) . to . deep . equal ( 'SecurityError' ) ;
} ) ;
2023-01-11 10:55:31 +00:00
it ( 'successfully resolves when calling legacy getUserMedia' , async ( ) = > {
const ses = session . fromPartition ( '' + Math . random ( ) ) ;
ses . setPermissionRequestHandler (
( _webContents , _permission , callback ) = > {
callback ( true ) ;
}
) ;
const w = new BrowserWindow ( { show : false , webPreferences : { session : ses } } ) ;
await w . loadURL ( serverUrl ) ;
const { ok , message } = await w . webContents . executeJavaScript ( `
new Promise ( ( resolve , reject ) = > navigator . getUserMedia ( {
video : true ,
audio : true ,
} , x = > resolve ( { ok : x instanceof MediaStream } ) , e = > reject ( { ok : false , message : e.message } ) ) )
` );
expect ( ok ) . to . be . true ( message ) ;
} ) ;
it ( 'successfully rejects when calling legacy getUserMedia' , async ( ) = > {
const ses = session . fromPartition ( '' + Math . random ( ) ) ;
ses . setPermissionRequestHandler (
( _webContents , _permission , callback ) = > {
callback ( false ) ;
}
) ;
const w = new BrowserWindow ( { show : false , webPreferences : { session : ses } } ) ;
await w . loadURL ( serverUrl ) ;
await expect ( w . webContents . executeJavaScript ( `
new Promise ( ( resolve , reject ) = > navigator . getUserMedia ( {
video : true ,
audio : true ,
} , x = > resolve ( { ok : x instanceof MediaStream } ) , e = > reject ( { ok : false , message : e.message } ) ) )
` )).to.eventually.be.rejectedWith('Permission denied');
} ) ;
2019-05-30 22:05:02 +00:00
} ) ;
2019-08-29 06:50:14 +00:00
2022-08-23 01:25:57 +00:00
describe ( 'ses.setPermissionCheckHandler(handler)' , ( ) = > {
afterEach ( closeAllWindows ) ;
it ( 'details provides requestingURL for mainFrame' , async ( ) = > {
const w = new BrowserWindow ( {
show : false ,
webPreferences : {
partition : 'very-temp-permission-handler'
}
} ) ;
const ses = w . webContents . session ;
const loadUrl = 'https://myfakesite/' ;
let handlerDetails : Electron.PermissionCheckHandlerHandlerDetails ;
ses . protocol . interceptStringProtocol ( 'https' , ( req , cb ) = > {
cb ( '<html><script>console.log(\'test\');</script></html>' ) ;
} ) ;
ses . setPermissionCheckHandler ( ( wc , permission , requestingOrigin , details ) = > {
if ( permission === 'clipboard-read' ) {
handlerDetails = details ;
return true ;
}
return false ;
} ) ;
const readClipboardPermission : any = ( ) = > {
return w . webContents . executeJavaScript ( `
navigator . permissions . query ( { name : 'clipboard-read' } )
. then ( permission = > permission . state ) . catch ( err = > err . message ) ;
` , true);
} ;
await w . loadURL ( loadUrl ) ;
const state = await readClipboardPermission ( ) ;
expect ( state ) . to . equal ( 'granted' ) ;
expect ( handlerDetails ! . requestingUrl ) . to . equal ( loadUrl ) ;
} ) ;
it ( 'details provides requestingURL for cross origin subFrame' , async ( ) = > {
const w = new BrowserWindow ( {
show : false ,
webPreferences : {
partition : 'very-temp-permission-handler'
}
} ) ;
const ses = w . webContents . session ;
const loadUrl = 'https://myfakesite/' ;
let handlerDetails : Electron.PermissionCheckHandlerHandlerDetails ;
ses . protocol . interceptStringProtocol ( 'https' , ( req , cb ) = > {
cb ( '<html><script>console.log(\'test\');</script></html>' ) ;
} ) ;
ses . setPermissionCheckHandler ( ( wc , permission , requestingOrigin , details ) = > {
if ( permission === 'clipboard-read' ) {
handlerDetails = details ;
return true ;
}
return false ;
} ) ;
const readClipboardPermission : any = ( frame : WebFrameMain ) = > {
return frame . executeJavaScript ( `
navigator . permissions . query ( { name : 'clipboard-read' } )
. then ( permission = > permission . state ) . catch ( err = > err . message ) ;
` , true);
} ;
await w . loadFile ( path . join ( fixtures , 'api' , 'blank.html' ) ) ;
w . webContents . executeJavaScript ( `
var iframe = document . createElement ( 'iframe' ) ;
iframe . src = '${loadUrl}' ;
document . body . appendChild ( iframe ) ;
null ;
` );
2023-02-23 23:53:53 +00:00
const [ , , frameProcessId , frameRoutingId ] = await once ( w . webContents , 'did-frame-finish-load' ) ;
2022-08-23 01:25:57 +00:00
const state = await readClipboardPermission ( webFrameMain . fromId ( frameProcessId , frameRoutingId ) ) ;
expect ( state ) . to . equal ( 'granted' ) ;
expect ( handlerDetails ! . requestingUrl ) . to . equal ( loadUrl ) ;
expect ( handlerDetails ! . isMainFrame ) . to . be . false ( ) ;
expect ( handlerDetails ! . embeddingOrigin ) . to . equal ( 'file:///' ) ;
} ) ;
} ) ;
2020-03-11 07:02:22 +00:00
describe ( 'ses.isPersistent()' , ( ) = > {
afterEach ( closeAllWindows ) ;
it ( 'returns default session as persistent' , ( ) = > {
const w = new BrowserWindow ( {
show : false
} ) ;
const ses = w . webContents . session ;
expect ( ses . isPersistent ( ) ) . to . be . true ( ) ;
} ) ;
it ( 'returns persist: session as persistent' , ( ) = > {
const ses = session . fromPartition ( ` persist: ${ Math . random ( ) } ` ) ;
expect ( ses . isPersistent ( ) ) . to . be . true ( ) ;
} ) ;
it ( 'returns temporary session as not persistent' , ( ) = > {
const ses = session . fromPartition ( ` ${ Math . random ( ) } ` ) ;
expect ( ses . isPersistent ( ) ) . to . be . false ( ) ;
} ) ;
} ) ;
2019-08-29 06:50:14 +00:00
describe ( 'ses.setUserAgent()' , ( ) = > {
afterEach ( closeAllWindows ) ;
it ( 'can be retrieved with getUserAgent()' , ( ) = > {
const userAgent = 'test-agent' ;
2019-11-01 20:37:02 +00:00
const ses = session . fromPartition ( '' + Math . random ( ) ) ;
2019-08-29 06:50:14 +00:00
ses . setUserAgent ( userAgent ) ;
expect ( ses . getUserAgent ( ) ) . to . equal ( userAgent ) ;
} ) ;
it ( 'sets the User-Agent header for web requests made from renderers' , async ( ) = > {
const userAgent = 'test-agent' ;
2019-11-01 20:37:02 +00:00
const ses = session . fromPartition ( '' + Math . random ( ) ) ;
2020-06-04 11:05:37 +00:00
ses . setUserAgent ( userAgent , 'en-US,fr,de' ) ;
2019-08-29 06:50:14 +00:00
const w = new BrowserWindow ( { show : false , webPreferences : { session : ses } } ) ;
let headers : http.IncomingHttpHeaders | null = null ;
const server = http . createServer ( ( req , res ) = > {
headers = req . headers ;
res . end ( ) ;
server . close ( ) ;
} ) ;
2023-02-20 11:30:57 +00:00
const { url } = await listen ( server ) ;
await w . loadURL ( url ) ;
2019-08-29 06:50:14 +00:00
expect ( headers ! [ 'user-agent' ] ) . to . equal ( userAgent ) ;
2020-06-04 11:05:37 +00:00
expect ( headers ! [ 'accept-language' ] ) . to . equal ( 'en-US,fr;q=0.9,de;q=0.8' ) ;
2019-08-29 06:50:14 +00:00
} ) ;
} ) ;
2020-05-19 17:18:12 +00:00
describe ( 'session-created event' , ( ) = > {
it ( 'is emitted when a session is created' , async ( ) = > {
2023-02-23 23:53:53 +00:00
const sessionCreated = once ( app , 'session-created' ) ;
2020-10-07 13:59:27 +00:00
const session1 = session . fromPartition ( '' + Math . random ( ) ) ;
const [ session2 ] = await sessionCreated ;
expect ( session1 ) . to . equal ( session2 ) ;
2020-05-19 17:18:12 +00:00
} ) ;
} ) ;
2020-10-21 18:03:59 +00:00
2021-04-27 16:54:28 +00:00
describe ( 'session.storagePage' , ( ) = > {
it ( 'returns a string' , ( ) = > {
expect ( session . defaultSession . storagePath ) . to . be . a ( 'string' ) ;
} ) ;
it ( 'returns null for in memory sessions' , ( ) = > {
expect ( session . fromPartition ( 'in-memory' ) . storagePath ) . to . equal ( null ) ;
} ) ;
it ( 'returns different paths for partitions and the default session' , ( ) = > {
expect ( session . defaultSession . storagePath ) . to . not . equal ( session . fromPartition ( 'persist:two' ) . storagePath ) ;
} ) ;
it ( 'returns different paths for different partitions' , ( ) = > {
expect ( session . fromPartition ( 'persist:one' ) . storagePath ) . to . not . equal ( session . fromPartition ( 'persist:two' ) . storagePath ) ;
} ) ;
} ) ;
2022-03-15 20:34:53 +00:00
describe ( 'session.setCodeCachePath()' , ( ) = > {
it ( 'throws when relative or empty path is provided' , ( ) = > {
expect ( ( ) = > {
session . defaultSession . setCodeCachePath ( '../fixtures' ) ;
} ) . to . throw ( 'Absolute path must be provided to store code cache.' ) ;
expect ( ( ) = > {
session . defaultSession . setCodeCachePath ( '' ) ;
} ) . to . throw ( 'Absolute path must be provided to store code cache.' ) ;
expect ( ( ) = > {
2022-03-30 17:17:34 +00:00
session . defaultSession . setCodeCachePath ( path . join ( app . getPath ( 'userData' ) , 'electron-test-code-cache' ) ) ;
2022-03-15 20:34:53 +00:00
} ) . to . not . throw ( ) ;
} ) ;
} ) ;
2020-10-21 18:03:59 +00:00
describe ( 'ses.setSSLConfig()' , ( ) = > {
it ( 'can disable cipher suites' , async ( ) = > {
const ses = session . fromPartition ( '' + Math . random ( ) ) ;
2022-08-16 19:23:13 +00:00
const fixturesPath = path . resolve ( __dirname , 'fixtures' ) ;
2020-10-21 18:03:59 +00:00
const certPath = path . join ( fixturesPath , 'certificates' ) ;
const server = https . createServer ( {
key : fs.readFileSync ( path . join ( certPath , 'server.key' ) ) ,
cert : fs.readFileSync ( path . join ( certPath , 'server.pem' ) ) ,
ca : [
fs . readFileSync ( path . join ( certPath , 'rootCA.pem' ) ) ,
fs . readFileSync ( path . join ( certPath , 'intermediateCA.pem' ) )
] ,
minVersion : 'TLSv1.2' ,
maxVersion : 'TLSv1.2' ,
ciphers : 'AES128-GCM-SHA256'
} , ( req , res ) = > {
res . end ( 'hi' ) ;
} ) ;
2023-02-20 11:30:57 +00:00
const { port } = await listen ( server ) ;
2020-10-21 18:03:59 +00:00
defer ( ( ) = > server . close ( ) ) ;
function request ( ) {
return new Promise ( ( resolve , reject ) = > {
const r = net . request ( {
url : ` https://127.0.0.1: ${ port } ` ,
session : ses
} ) ;
r . on ( 'response' , ( res ) = > {
let data = '' ;
res . on ( 'data' , ( chunk ) = > {
data += chunk . toString ( 'utf8' ) ;
} ) ;
res . on ( 'end' , ( ) = > {
resolve ( data ) ;
} ) ;
} ) ;
r . on ( 'error' , ( err ) = > {
reject ( err ) ;
} ) ;
r . end ( ) ;
} ) ;
}
await expect ( request ( ) ) . to . be . rejectedWith ( /ERR_CERT_AUTHORITY_INVALID/ ) ;
ses . setSSLConfig ( {
disabledCipherSuites : [ 0x009C ]
} ) ;
await expect ( request ( ) ) . to . be . rejectedWith ( /ERR_SSL_VERSION_OR_CIPHER_MISMATCH/ ) ;
} ) ;
} ) ;
2019-05-28 21:12:59 +00:00
} ) ;