diff --git a/resources/electron/electron-builder.mjs b/resources/electron/electron-builder.mjs index 604dc1df..dcb10b3b 100644 --- a/resources/electron/electron-builder.mjs +++ b/resources/electron/electron-builder.mjs @@ -85,12 +85,12 @@ export default { executableName: fileName, ...(azureEndpoint && azureCertificateProfileName && azureCodeSigningAccountName ? { - azureSignOptions: { - endpoint: azureEndpoint, - certificateProfileName: azureCertificateProfileName, - codeSigningAccountName: azureCodeSigningAccountName - }, - } + azureSignOptions: { + endpoint: azureEndpoint, + certificateProfileName: azureCertificateProfileName, + codeSigningAccountName: azureCodeSigningAccountName, + }, + } : {}), }, nsis: { diff --git a/resources/electron/electron-plugin/dist/server/api.js b/resources/electron/electron-plugin/dist/server/api.js index 21f586d3..b76f07b6 100644 --- a/resources/electron/electron-plugin/dist/server/api.js +++ b/resources/electron/electron-plugin/dist/server/api.js @@ -33,9 +33,11 @@ import settingsRoutes from './api/settings.js'; import shellRoutes from './api/shell.js'; import systemRoutes from './api/system.js'; import windowRoutes from './api/window.js'; +const API_HOST = '127.0.0.1'; function startAPIServer(randomSecret) { return __awaiter(this, void 0, void 0, function* () { const port = yield getPort({ + host: API_HOST, port: portNumbers(4000, 5000), }); return new Promise((resolve) => { @@ -66,7 +68,7 @@ function startAPIServer(randomSecret) { if (process.env.NODE_ENV === 'development') { httpServer.use('/api/debug', debugRoutes); } - const server = httpServer.listen(port, () => { + const server = httpServer.listen(port, API_HOST, () => { resolve({ server, port, diff --git a/resources/electron/electron-plugin/dist/server/api/childProcess.js b/resources/electron/electron-plugin/dist/server/api/childProcess.js index 8e5187c6..0d3eee2e 100644 --- a/resources/electron/electron-plugin/dist/server/api/childProcess.js +++ b/resources/electron/electron-plugin/dist/server/api/childProcess.js @@ -139,8 +139,13 @@ function stopProcess(alias) { } state.processes[alias].settings.persistent = false; console.log('Process [' + alias + '] stopping with PID [' + proc.pid + '].'); - killSync(proc.pid, 'SIGTERM', true); - proc.kill(); + try { + killSync(proc.pid, 'SIGTERM', true); + proc.kill(); + } + catch (_a) { + console.log('Process [' + alias + '] already exited — nothing to kill.'); + } } export function stopAllProcesses() { for (const alias in state.processes) { diff --git a/resources/electron/electron-plugin/dist/server/api/menuBar.js b/resources/electron/electron-plugin/dist/server/api/menuBar.js index 2ff952b4..a7ef6563 100644 --- a/resources/electron/electron-plugin/dist/server/api/menuBar.js +++ b/resources/electron/electron-plugin/dist/server/api/menuBar.js @@ -56,7 +56,7 @@ router.post('/create', (req, res) => { state.activeMenuBar.tray.destroy(); shouldSendCreatedEvent = false; } - const { width, height, url, label, alwaysOnTop, vibrancy, backgroundColor, transparency, icon, showDockIcon, onlyShowContextMenu, windowPosition, showOnAllWorkspaces, contextMenu, tooltip, resizable, webPreferences, } = req.body; + const { width, height, minWidth, minHeight, maxWidth, maxHeight, url, label, alwaysOnTop, vibrancy, backgroundColor, transparency, icon, showDockIcon, onlyShowContextMenu, windowPosition, showOnAllWorkspaces, contextMenu, tooltip, resizable, webPreferences, } = req.body; if (onlyShowContextMenu) { const tray = new Tray(icon || state.icon.replace('icon.png', 'IconTemplate.png')); tray.setContextMenu(buildMenu(contextMenu)); @@ -81,6 +81,10 @@ router.post('/create', (req, res) => { browserWindow: { width, height, + minWidth, + minHeight, + maxWidth, + maxHeight, resizable, alwaysOnTop, vibrancy, diff --git a/resources/electron/electron-plugin/dist/server/php.js b/resources/electron/electron-plugin/dist/server/php.js index ce2cc7f8..2ab03b94 100644 --- a/resources/electron/electron-plugin/dist/server/php.js +++ b/resources/electron/electron-plugin/dist/server/php.js @@ -39,20 +39,17 @@ function shouldOptimize() { return process.env.NODE_ENV !== 'development'; } function hasNightwatchInstalled(appPath) { - const candidateRoots = [ - appPath, - join(appPath, "build", "__nativephp_app_bundle") - ]; + const candidateRoots = [appPath, join(appPath, 'build', '__nativephp_app_bundle')]; for (const root of candidateRoots) { - if (existsSync(join(root, "vendor", "laravel", "nightwatch"))) { + if (existsSync(join(root, 'vendor', 'laravel', 'nightwatch'))) { return true; } - const composerLock = join(root, "composer.lock"); + const composerLock = join(root, 'composer.lock'); if (!existsSync(composerLock)) { continue; } try { - if (readFileSync(composerLock, "utf8").includes("\"name\": \"laravel/nightwatch\"")) { + if (readFileSync(composerLock, 'utf8').includes('"name": "laravel/nightwatch"')) { return true; } } @@ -65,20 +62,17 @@ function getNightwatchToken(appPath) { if (process.env.NIGHTWATCH_TOKEN) { return process.env.NIGHTWATCH_TOKEN; } - const candidateRoots = [ - appPath, - join(appPath, "build", "__nativephp_app_bundle") - ]; + const candidateRoots = [appPath, join(appPath, 'build', '__nativephp_app_bundle')]; for (const root of candidateRoots) { - const envPath = join(root, ".env"); + const envPath = join(root, '.env'); if (!existsSync(envPath)) { continue; } try { - const content = readFileSync(envPath, "utf8"); + const content = readFileSync(envPath, 'utf8'); const match = content.match(/^NIGHTWATCH_TOKEN=(.+)$/m); if (match && match[1]) { - return match[1].replace(/^['"]|['"]$/g, ""); + return match[1].replace(/^['"]|['"]$/g, ''); } } catch (_a) { @@ -248,7 +242,7 @@ function getDefaultEnvironmentVariables(secret, apiPort) { : join(process.env.APP_PATH, 'extras'), }; if (secret && apiPort) { - variables.NATIVEPHP_API_URL = `http://localhost:${apiPort}/api/`; + variables.NATIVEPHP_API_URL = `http://127.0.0.1:${apiPort}/api/`; variables.NATIVEPHP_SECRET = secret; } if (runningSecureBuild()) { @@ -282,11 +276,11 @@ function serveApp(secret, apiPort, phpIniSettings) { env.NIGHTWATCH_INGEST_URI = `127.0.0.1:${phpNightWatchPort}`; } else if (nightwatchToken) { - console.log("Skipping Nightwatch: package not installed."); + console.log('Skipping Nightwatch: package not installed.'); } const phpOptions = { cwd: appPath, - env + env, }; const store = new Store({ name: 'nativephp', diff --git a/resources/electron/electron-plugin/src/libs/positioner/index.ts b/resources/electron/electron-plugin/src/libs/positioner/index.ts index c3c751bb..eb554a5e 100644 --- a/resources/electron/electron-plugin/src/libs/positioner/index.ts +++ b/resources/electron/electron-plugin/src/libs/positioner/index.ts @@ -80,12 +80,8 @@ class Positioner { y: Math.floor((screenSize.height + screenSize.y) / 2 - windowSize[1] / 2), }, upperCenter: { - x: Math.floor( - screenSize.x + (screenSize.width / 2 - windowSize[0] / 2), - ), - y: Math.floor( - screenSize.y + (screenSize.height - windowSize[1]) / 3, - ), + x: Math.floor(screenSize.x + (screenSize.width / 2 - windowSize[0] / 2)), + y: Math.floor(screenSize.y + (screenSize.height - windowSize[1]) / 3), }, }; diff --git a/resources/electron/electron-plugin/src/server/api.ts b/resources/electron/electron-plugin/src/server/api.ts index 57f0942b..d69c4747 100644 --- a/resources/electron/electron-plugin/src/server/api.ts +++ b/resources/electron/electron-plugin/src/server/api.ts @@ -32,8 +32,11 @@ export interface APIProcess { port: number; } +const API_HOST = '127.0.0.1'; + async function startAPIServer(randomSecret: string): Promise { const port = await getPort({ + host: API_HOST, port: portNumbers(4000, 5000), }); @@ -67,7 +70,7 @@ async function startAPIServer(randomSecret: string): Promise { httpServer.use('/api/debug', debugRoutes); } - const server = httpServer.listen(port, () => { + const server = httpServer.listen(port, API_HOST, () => { resolve({ server, port, diff --git a/resources/electron/electron-plugin/src/server/api/childProcess.ts b/resources/electron/electron-plugin/src/server/api/childProcess.ts index ae5d2fb5..df2393fa 100644 --- a/resources/electron/electron-plugin/src/server/api/childProcess.ts +++ b/resources/electron/electron-plugin/src/server/api/childProcess.ts @@ -202,7 +202,7 @@ function stopProcess(alias) { // @ts-ignore killSync(proc.pid, 'SIGTERM', true); // Kill tree proc.kill(); // Does not work but just in case. (do not put before killSync) - } catch (e) { + } catch { console.log('Process [' + alias + '] already exited — nothing to kill.'); } } diff --git a/resources/electron/electron-plugin/src/server/api/window.ts b/resources/electron/electron-plugin/src/server/api/window.ts index 3b820358..8b6a1f54 100644 --- a/resources/electron/electron-plugin/src/server/api/window.ts +++ b/resources/electron/electron-plugin/src/server/api/window.ts @@ -412,7 +412,7 @@ router.post('/open', (req, res) => { if (state.noFocusOnRestart && window.isVisible()) { return; } - + window.show(); }); diff --git a/resources/electron/electron-plugin/src/server/php.ts b/resources/electron/electron-plugin/src/server/php.ts index 78fb7dc8..3bd37797 100644 --- a/resources/electron/electron-plugin/src/server/php.ts +++ b/resources/electron/electron-plugin/src/server/php.ts @@ -47,24 +47,21 @@ function shouldOptimize() { } function hasNightwatchInstalled(appPath: string) { - const candidateRoots = [ - appPath, - join(appPath, "build", "__nativephp_app_bundle") - ]; + const candidateRoots = [appPath, join(appPath, 'build', '__nativephp_app_bundle')]; for (const root of candidateRoots) { - if (existsSync(join(root, "vendor", "laravel", "nightwatch"))) { + if (existsSync(join(root, 'vendor', 'laravel', 'nightwatch'))) { return true; } - const composerLock = join(root, "composer.lock"); + const composerLock = join(root, 'composer.lock'); if (!existsSync(composerLock)) { continue; } try { - if (readFileSync(composerLock, "utf8").includes("\"name\": \"laravel/nightwatch\"")) { + if (readFileSync(composerLock, 'utf8').includes('"name": "laravel/nightwatch"')) { return true; } } catch { @@ -80,24 +77,21 @@ function getNightwatchToken(appPath: string) { return process.env.NIGHTWATCH_TOKEN; } - const candidateRoots = [ - appPath, - join(appPath, "build", "__nativephp_app_bundle") - ]; + const candidateRoots = [appPath, join(appPath, 'build', '__nativephp_app_bundle')]; for (const root of candidateRoots) { - const envPath = join(root, ".env"); + const envPath = join(root, '.env'); if (!existsSync(envPath)) { continue; } try { - const content = readFileSync(envPath, "utf8"); + const content = readFileSync(envPath, 'utf8'); const match = content.match(/^NIGHTWATCH_TOKEN=(.+)$/m); if (match && match[1]) { - return match[1].replace(/^['"]|['"]$/g, ""); + return match[1].replace(/^['"]|['"]$/g, ''); } } catch { // ignore and keep looking @@ -359,7 +353,7 @@ function getDefaultEnvironmentVariables(secret?: string, apiPort?: number): Envi // Only if the server has already started if (secret && apiPort) { - variables.NATIVEPHP_API_URL = `http://localhost:${apiPort}/api/`; + variables.NATIVEPHP_API_URL = `http://127.0.0.1:${apiPort}/api/`; variables.NATIVEPHP_SECRET = secret; } @@ -402,12 +396,12 @@ async function serveApp(secret, apiPort, phpIniSettings): Promise env.NIGHTWATCH_TOKEN = nightwatchToken; env.NIGHTWATCH_INGEST_URI = `127.0.0.1:${phpNightWatchPort}`; } else if (nightwatchToken) { - console.log("Skipping Nightwatch: package not installed."); + console.log('Skipping Nightwatch: package not installed.'); } const phpOptions = { cwd: appPath, - env + env, }; const store = new Store({ diff --git a/resources/electron/electron-plugin/tests/api.test.ts b/resources/electron/electron-plugin/tests/api.test.ts index 826c4852..e3112650 100644 --- a/resources/electron/electron-plugin/tests/api.test.ts +++ b/resources/electron/electron-plugin/tests/api.test.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import type { AddressInfo } from 'net'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import startAPIServer, { APIProcess } from '../src/server/api'; @@ -21,7 +22,7 @@ describe('API test', () => { beforeEach(async () => { vi.resetModules(); apiServer = await startAPIServer('randomSecret'); - axios.defaults.baseURL = `http://localhost:${apiServer.port}`; + axios.defaults.baseURL = `http://127.0.0.1:${apiServer.port}`; }); afterEach(async () => { @@ -44,6 +45,13 @@ describe('API test', () => { nextApiProcess.server.close(); }); + it('binds the API server to the loopback interface', async () => { + const address = apiServer.server.address() as AddressInfo; + + expect(address.address).toBe('127.0.0.1'); + expect(address.address).not.toBe('0.0.0.0'); + }); + it('protects API endpoints with a secret', async () => { try { await axios.get('/api/process');