diff --git a/server.js b/server.js index 7d1cf160..ba5a6223 100644 --- a/server.js +++ b/server.js @@ -21,7 +21,7 @@ const Resolvers = require('./resolvers/') const Schema = fs.readFileSync(path.join(__dirname, './schema.graphql'), 'utf8') const spacesToCamelCase = require('./src/lib/spacesToCamelCase') const defaultPolicyServer = HOST -const IS_DEV = process.env.NODE_ENV === 'development' +const IS_DEV = process.env.STETHOSCOPE_ENV === 'development' const app = express() const http = require('http').Server(app) @@ -33,23 +33,23 @@ setKmdEnv({ }) function precompile () { - return glob(path.resolve(__dirname, `./sources/${process.platform}/*.sh`)).then(files => { - return files.map(file => { - const content = readFileSync(file, 'utf8') - const code = compile(content) - return code - }) - }) + const searchPath = path.resolve(__dirname, `./sources/${process.platform}/*.sh`) + return glob(searchPath) + .then(files => + files.map(file => + compile(readFileSync(file, 'utf8')) + ) + ) } // used to ensure that user is not shown multiple notifications for a login scan // sessionId is used as a key const alertCache = new Map() -module.exports = async function startServer (env, log, language, appActions) { +module.exports = async function startServer (env, log, language = 'en-US', appActions) { log.info('starting express server') const checks = await precompile() - const find = filePath => env === 'development' ? filePath : path.join(__dirname, filePath) + const find = filePath => IS_DEV ? filePath : path.join(__dirname, filePath) const settingsHandle = fs.readFileSync(find('./practices/config.yaml'), 'utf8') const defaultConfig = yaml.safeLoad(settingsHandle) @@ -69,9 +69,10 @@ module.exports = async function startServer (env, log, language, appActions) { policyServer = defaultPolicyServer } = defaultConfig + // wide open in dev, limited to hosts specified in './practices/config.yaml' in production const corsOptions = { origin (origin, callback) { - if (env === 'development') return callback(null, true) + if (IS_DEV) return callback(null, true) if (allowHosts.includes(origin)) return callback(null, true) if (hostLabels.length) { const isAllowed = hostLabels @@ -97,7 +98,7 @@ module.exports = async function startServer (env, log, language, appActions) { } } - if (env === 'development') { + if (IS_DEV) { const { graphiqlExpress } = require('graphql-server-express') app.use('/graphiql', cors(corsOptions), graphiqlExpress({ endpointURL: '/scan' })) } @@ -115,6 +116,7 @@ module.exports = async function startServer (env, log, language, appActions) { const remote = origin !== 'stethoscope://main' let remoteLabel + // Try to find the host label to display in app ("Last scanned by X") if (remote) { try { const matchHost = ({ pattern }) => (new RegExp(pattern)).test(origin) @@ -127,8 +129,13 @@ module.exports = async function startServer (env, log, language, appActions) { } let { query, variables: policy, sessionId = false } = req[key] + // native notifications are only shown for external requests and + // are throttled by the users's session id let showNotification = sessionId && !alertCache.has(sessionId) const start = performance.now() + // TODO each of these checks should probably be individually executed + // by relecvant resolvers. Since it is currently super fast, there is no + // real performance penalty for running all checks on each request const checkData = await Promise.all(checks.map(async script => { const response = await run(script) return response @@ -137,6 +144,8 @@ module.exports = async function startServer (env, log, language, appActions) { context.kmdResponse = extend(true, {}, ...checkData) + policy = policy || {} + if (sessionId && !alertCache.has(sessionId)) { alertCache.set(sessionId, true) } @@ -145,6 +154,7 @@ module.exports = async function startServer (env, log, language, appActions) { policy = JSON.parse(policy) } + // tell the app if a policy was passed to display scanning status if (Object.keys(policy).length) { // show the scan is happening in the UI io.sockets.emit('scan:init', { remote, remoteLabel }) @@ -154,6 +164,7 @@ module.exports = async function startServer (env, log, language, appActions) { const { data = {} } = result let scanResult = { noResults: true } + // update the tray icon if a policy result is in the response if (data.policy && data.policy.validate) { appActions.setScanStatus(data.policy.validate.status) scanResult = { result, remote, remoteLabel, policy, showNotification } @@ -227,7 +238,8 @@ module.exports = async function startServer (env, log, language, appActions) { }) const serverInstance = http.listen(PORT, '127.0.0.1', () => { - console.log(`local server listening on ${PORT}`) + console.log(`GraphQL server listening on ${PORT}`) + IS_DEV && console.log(`Explore the schema: http://127.0.0.1:${PORT}/graphiql`) serverInstance.emit('server:ready') }) diff --git a/src/start-react.js b/src/start-react.js index 8dd689bc..097a3c45 100644 --- a/src/start-react.js +++ b/src/start-react.js @@ -1,3 +1,7 @@ +/** + * Used in dev - polls for create-react-app HMR server readyness, + * triggers `npm run electron` once react is ready + */ const net = require('net') const { spawn } = require('child_process') const os = require('os') @@ -13,7 +17,8 @@ const tryConnection = () => { client.connect({ port }, () => { client.end() if (!startedElectron) { - console.log('starting electron') + console.log(`npm start react:start - react ready on http://127.0.0.1:${port}`) + console.log('npm start electron:start') startedElectron = true const cmd = os.platform() === 'win32' ? 'npm.cmd' : 'npm' const appServer = spawn(cmd, ['run', 'electron'], { diff --git a/src/start.js b/src/start.js index 83935ec0..c120906a 100644 --- a/src/start.js +++ b/src/start.js @@ -1,3 +1,19 @@ +/** + * main entry point for the electron app. This file configures and initializes + * the entire application which includes: + * - Handle launch/deeplink events + * - Initialize custom protocols used within the app (e.g. app://foo, ps://bar) + * - Initialize auto launch behavior + * - Create the BrowserWindow (stored as global.app for use throughout the app) + * - initialize app menus and dock/tray behavior + * - Start the GraphQL (express) server + * - handle server triggered events (e.g. changing app icon based on policy result) + * - Handle uncaught exceptions in any part of the app + * - Handle IPC calls from other parts of the application + * - 'scan:init' - Automatic update triggered (resizes app, displays progress) + * - 'app:loaded' - Notify when client side app is loaded + * - 'download:completed' - update has finished downloading + */ const { app, ipcMain, dialog, BrowserWindow, session, Tray, nativeImage } = require('electron') const path = require('path') const url = require('url') @@ -48,6 +64,7 @@ const windowPrefs = { } } +// use build/ assets in production, webpack HMR server in dev const BASE_URL = process.env.ELECTRON_START_URL || url.format({ pathname: path.join(__dirname, '/../build/index.html'), protocol: 'file:', @@ -69,13 +86,10 @@ const focusOrCreateWindow = () => { initMenu(mainWindow, app, focusOrCreateWindow, updater, log) mainWindow.loadURL(BASE_URL) } - - if (IS_DEV) { - loadReactDevTools(mainWindow) - } } async function createWindow () { + // used to show initial launch messages to user if (!settings.has('userHasLaunchedApp')) { isFirstLaunch = true settings.set('userHasLaunchedApp', true) @@ -105,7 +119,7 @@ async function createWindow () { // wait for process to load before hiding in dock, prevents the app // from flashing into view and then hiding - if (!IS_DEV && IS_MAC) setTimeout(() => app.dock.hide(), 0) + if (!IS_DEV && IS_MAC) setImmediate(() => app.dock.hide()) // windows detection of deep link path if (IS_WIN) deeplinkingUrl = process.argv.slice(1) // only allow resize if debugging production build @@ -114,12 +128,12 @@ async function createWindow () { mainWindow = new BrowserWindow(windowPrefs) if (IS_DEV) loadReactDevTools(BrowserWindow) - // open developer console if env vars or args request if (enableDebugger || DEBUG_MODE) { mainWindow.webContents.openDevTools() } + // required at run time so dependencies can be injected updater = require('./updater')(env, mainWindow, log, server) if (isLaunching) { @@ -268,6 +282,7 @@ app.on('ready', () => setTimeout(() => { }) if (launchIntoUpdater) { + // triggered via stethoscope://update app link log.info(`Launching into updater: ${launchIntoUpdater}`) updater.checkForUpdates(env, mainWindow).catch(err => log.error(`start:launch:check for updates exception${err}`)