diff --git a/spec/main-process/atom-application.test.js b/spec/main-process/atom-application.test.js index 7381a21bc..7180f2b2f 100644 --- a/spec/main-process/atom-application.test.js +++ b/spec/main-process/atom-application.test.js @@ -939,6 +939,49 @@ describe('AtomApplication', function () { }) } + it('reuses the main process between invocations', async () => { + const tempDirPath1 = makeTempDir() + const tempDirPath2 = makeTempDir() + + const options = { + pathsToOpen: [tempDirPath1] + } + + // Open the main application + const originalApplication = buildAtomApplication(options) + await originalApplication.initialize(options) + + // Wait until the first window gets opened + await conditionPromise( + () => originalApplication.getAllWindows().length === 1 + ) + + // Open another instance of the application on a different path. + AtomApplication.open({ + resourcePath: ATOM_RESOURCE_PATH, + atomHomeDirPath: process.env.ATOM_HOME, + pathsToOpen: [tempDirPath2] + }) + + await conditionPromise( + () => originalApplication.getAllWindows().length === 2 + ) + + // Check that the original application now has the two opened windows. + assert.notEqual( + originalApplication.getAllWindows().find( + window => window.loadSettings.initialPaths[0] === tempDirPath1 + ), + undefined + ) + assert.notEqual( + originalApplication.getAllWindows().find( + window => window.loadSettings.initialPaths[0] === tempDirPath2 + ), + undefined + ) + }) + function buildAtomApplication (params = {}) { const atomApplication = new AtomApplication( Object.assign( diff --git a/src/main-process/atom-application.js b/src/main-process/atom-application.js index 9a241c32b..dc5671ddc 100644 --- a/src/main-process/atom-application.js +++ b/src/main-process/atom-application.js @@ -34,6 +34,81 @@ const getDefaultPath = () => { } } +const getSocketSecretPath = (atomVersion) => { + const {username} = os.userInfo() + const atomHome = path.resolve(process.env.ATOM_HOME) + + return path.join(atomHome, `.atom-socket-secret-${username}-${atomVersion}`) +} + +const getSocketPath = (socketSecret) => { + if (!socketSecret) { + return null + } + + // Hash the secret to create the socket name to not expose it. + const socketName = crypto + .createHmac('sha256', socketSecret) + .update('socketName') + .digest('hex') + .substr(0, 12) + + if (process.platform === 'win32') { + return `\\\\.\\pipe\\atom-${socketName}-sock` + } else { + return path.join(os.tmpdir(), `atom-${socketName}.sock`) + } +} + +const getExistingSocketSecret = (atomVersion) => { + const socketSecretPath = getSocketSecretPath(atomVersion) + + if (!fs.existsSync(socketSecretPath)) { + return null + } + + return fs.readFileSync(socketSecretPath, 'utf8') +} + +const createSocketSecret = (atomVersion) => { + const socketSecret = crypto.randomBytes(16).toString('hex') + + fs.writeFileSync(getSocketSecretPath(atomVersion), socketSecret, {encoding: 'utf8', mode: 0o600}) + + return socketSecret +} + +const encryptOptions = (options, secret) => { + const message = JSON.stringify(options) + + const initVector = crypto.randomBytes(16) + + const cipher = crypto.createCipheriv('aes-256-gcm', secret, initVector) + + let content = cipher.update(message, 'utf8', 'hex') + content += cipher.final('hex') + + const authTag = cipher.getAuthTag().toString('hex') + + return JSON.stringify({ + authTag, + content, + initVector: initVector.toString('hex') + }) +} + +const decryptOptions = (optionsMessage, secret) => { + const {authTag, content, initVector} = JSON.parse(optionsMessage) + + const decipher = crypto.createDecipheriv('aes-256-gcm', secret, Buffer.from(initVector, 'hex')) + decipher.setAuthTag(Buffer.from(authTag, 'hex')) + + let message = decipher.update(content, 'hex', 'utf8') + message += decipher.final('utf8') + + return JSON.parse(message) +} + // The application's singleton class. // // It's the entry point into the Atom application and maintains the global state @@ -43,50 +118,23 @@ module.exports = class AtomApplication extends EventEmitter { // Public: The entry point into the Atom application. static open (options) { - if (!options.socketPath) { - const {username} = os.userInfo() - - // Lowercasing the ATOM_HOME to make sure that we don't get multiple sockets - // on case-insensitive filesystems due to arbitrary case differences in paths. - const atomHomeUnique = path.resolve(process.env.ATOM_HOME).toLowerCase() - const hash = crypto - .createHash('sha1') - .update(options.version) - .update('|') - .update(process.arch) - .update('|') - .update(username || '') - .update('|') - .update(atomHomeUnique) - - // We only keep the first 12 characters of the hash as not to have excessively long - // socket file. Note that macOS/BSD limit the length of socket file paths (see #15081). - // The replace calls convert the digest into "URL and Filename Safe" encoding (see RFC 4648). - const atomInstanceDigest = hash - .digest('base64') - .substring(0, 12) - .replace(/\+/g, '-') - .replace(/\//g, '_') - - if (process.platform === 'win32') { - options.socketPath = `\\\\.\\pipe\\atom-${atomInstanceDigest}-sock` - } else { - options.socketPath = path.join(os.tmpdir(), `atom-${atomInstanceDigest}.sock`) - } - } + const socketSecret = getExistingSocketSecret(options.version) + const socketPath = getSocketPath(socketSecret) // FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely // take a few seconds to trigger 'error' event, it could be a bug of node // or electron, before it's fixed we check the existence of socketPath to // speedup startup. - if ((process.platform !== 'win32' && !fs.existsSync(options.socketPath)) || - options.test || options.benchmark || options.benchmarkTest) { + if ( + !socketPath || options.test || options.benchmark || options.benchmarkTest || + (process.platform !== 'win32' && !fs.existsSync(socketPath)) + ) { new AtomApplication(options).initialize(options) return } - const client = net.connect({path: options.socketPath}, () => { - client.write(JSON.stringify(options), () => { + const client = net.connect({path: socketPath}, () => { + client.write(encryptOptions(options, socketSecret), () => { client.end() app.quit() }) @@ -110,11 +158,14 @@ class AtomApplication extends EventEmitter { this.version = options.version this.devMode = options.devMode this.safeMode = options.safeMode - this.socketPath = options.socketPath this.logFile = options.logFile this.userDataDir = options.userDataDir this._killProcess = options.killProcess || process.kill.bind(process) - if (options.test || options.benchmark || options.benchmarkTest) this.socketPath = null + + if (!options.test && !options.benchmark && !options.benchmarkTest) { + this.socketSecret = createSocketSecret(this.version) + this.socketPath = getSocketPath(this.socketSecret) + } this.waitSessionsByWindow = new Map() this.windowStack = new WindowStack() @@ -351,7 +402,15 @@ class AtomApplication extends EventEmitter { const server = net.createServer(connection => { let data = '' connection.on('data', chunk => { data += chunk }) - connection.on('end', () => this.openWithOptions(JSON.parse(data))) + connection.on('end', () => { + try { + const options = decryptOptions(data, this.socketSecret) + this.openWithOptions(options) + } catch (e) { + // Error while parsing/decrypting the options passed by the client. + // We cannot trust the client, aborting. + } + }) }) server.listen(this.socketPath) @@ -373,6 +432,24 @@ class AtomApplication extends EventEmitter { } } + deleteSocketSecretFile () { + if (!this.socketSecret) { + return + } + + const socketSecretPath = getSocketSecretPath(this.version) + + if (fs.existsSync(socketSecretPath)) { + try { + fs.unlinkSync(socketSecretPath) + } catch (error) { + // Ignore ENOENT errors in case the file was deleted between the exists + // check and the call to unlink sync. + if (error.code !== 'ENOENT') throw error + } + } + } + // Registers basic application commands, non-idempotent. handleEvents () { const getLoadSettings = () => { @@ -480,6 +557,7 @@ class AtomApplication extends EventEmitter { this.disposable.add(ipcHelpers.on(app, 'will-quit', () => { this.killAllProcesses() this.deleteSocketFile() + this.deleteSocketSecretFile() })) this.disposable.add(ipcHelpers.on(app, 'open-file', (event, pathToOpen) => {