Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
25 changes: 18 additions & 7 deletions Composer/packages/electron-server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,18 @@ import { parseDeepLinkUrl } from './utility/url';
import { composerProtocol } from './constants';

const error = log.extend('error');
const baseUrl = isDevelopment ? 'http://localhost:3000/' : 'http://localhost:5000/';
let deeplinkUrl = '';
let serverPort;
// webpack dev server runs on :3000
const getBaseUrl = () => {
if (isDevelopment) {
return 'http://localhost:3000/';
}
if (!serverPort) {
throw new Error('getBaseUrl() called before serverPort is defined.');
}
return `http://localhost:${serverPort}`;
};

function processArgsForWindows(args: string[]): string {
const deepLinkUrl = args.find(arg => arg.startsWith(composerProtocol));
Expand Down Expand Up @@ -91,11 +101,12 @@ async function loadServer() {

log('Starting server...');
const { start } = await import('@bfc/server');
await start(pluginsDir);
log('Server started. Rendering application...');
serverPort = await start(pluginsDir);
log(`Server started at port: ${serverPort}`);
}

async function main() {
log('Rendering application...');
const mainWindow = ElectronWindow.getInstance().browserWindow;
if (mainWindow) {
if (process.env.COMPOSER_DEV_TOOLS) {
Expand All @@ -105,13 +116,14 @@ async function main() {
if (isWindows()) {
deeplinkUrl = processArgsForWindows(process.argv);
}
await mainWindow.webContents.loadURL(baseUrl + deeplinkUrl);
await mainWindow.webContents.loadURL(getBaseUrl() + deeplinkUrl);

mainWindow.show();

mainWindow.on('closed', function() {
ElectronWindow.destroy();
});
log('Rendered application.');
}
}

Expand All @@ -128,7 +140,7 @@ async function run() {

const mainWindow = ElectronWindow.getInstance().browserWindow;
if (mainWindow) {
await mainWindow.webContents.loadURL(baseUrl + deeplinkUrl);
await mainWindow.webContents.loadURL(getBaseUrl() + deeplinkUrl);
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
Expand All @@ -142,7 +154,6 @@ async function run() {
app.on('ready', async () => {
log('App ready');
await loadServer();
log('Server has been loaded');
await main();
initializeAppUpdater();
});
Expand Down Expand Up @@ -171,7 +182,7 @@ async function run() {
deeplinkUrl = parseDeepLinkUrl(url);
if (ElectronWindow.isBrowserWindowCreated) {
const mainWindow = ElectronWindow.getInstance().browserWindow;
mainWindow?.loadURL(baseUrl + deeplinkUrl);
mainWindow?.loadURL(getBaseUrl() + deeplinkUrl);
}
});
});
Expand Down
1 change: 1 addition & 0 deletions Composer/packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"morgan": "^1.9.1",
"passport": "^0.4.1",
"path-to-regexp": "^6.1.0",
"portfinder": "1.0.25",
"ts-md5": "^1.2.7",
"vscode-languageserver": "^5.3.0-next",
"vscode-ws-jsonrpc": "^0.1.1",
Expand Down
134 changes: 72 additions & 62 deletions Composer/packages/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'dotenv/config';
import path from 'path';
import crypto from 'crypto';

import { getPortPromise } from 'portfinder';
import express, { Express, Request, Response, NextFunction } from 'express';
import bodyParser from 'body-parser';
import morgan from 'morgan';
Expand All @@ -27,7 +28,7 @@ import log from './logger';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const session = require('express-session');

export async function start(pluginDir?: string) {
export async function start(pluginDir?: string): Promise<number | string> {
const clientDirectory = path.resolve(require.resolve('@bfc/client'), '..');
const app: Express = express();
app.set('view engine', 'ejs');
Expand All @@ -45,75 +46,82 @@ export async function start(pluginDir?: string) {

// load all the plugins that exist in the folder
pluginDir = pluginDir || path.resolve(__dirname, '../../../plugins');
pluginLoader.loadPluginsFromFolder(pluginDir).then(() => {
const { login, authorize } = getAuthProvider();

const CS_POLICIES = [
"default-src 'none';",
"font-src 'self' https:;",
"img-src 'self' data:;",
"base-uri 'none';",
"connect-src 'self';",
"frame-src 'self' bfemulator: https://login.microsoftonline.com https://*.botframework.com;",
"worker-src 'self';",
"form-action 'none';",
"frame-ancestors 'self';",
"manifest-src 'self';",
'upgrade-insecure-requests;',
];

app.all('*', function(req: Request, res: Response, next: NextFunction) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS,POST,PUT,DELETE');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');

if (process.env.ENABLE_CSP === 'true') {
req.__nonce__ = crypto.randomBytes(16).toString('base64');
res.header(
'Content-Security-Policy',
CS_POLICIES.concat([
`script-src 'self' 'nonce-${req.__nonce__}';`,
// TODO: use nonce strategy after addressing issues with monaco-editor pacakge
"style-src 'self' 'unsafe-inline'",
// `style-src 'self' 'nonce-${req.__nonce__}';`,
]).join(' ')
);
}
await pluginLoader.loadPluginsFromFolder(pluginDir);

const { login, authorize } = getAuthProvider();

const CS_POLICIES = [
"default-src 'none';",
"font-src 'self' https:;",
"img-src 'self' data:;",
"base-uri 'none';",
"connect-src 'self';",
"frame-src 'self' bfemulator: https://login.microsoftonline.com https://*.botframework.com;",
"worker-src 'self';",
"form-action 'none';",
"frame-ancestors 'self';",
"manifest-src 'self';",
'upgrade-insecure-requests;',
];

app.all('*', function(req: Request, res: Response, next: NextFunction) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS,POST,PUT,DELETE');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');

if (process.env.ENABLE_CSP === 'true') {
req.__nonce__ = crypto.randomBytes(16).toString('base64');
res.header(
'Content-Security-Policy',
CS_POLICIES.concat([
`script-src 'self' 'nonce-${req.__nonce__}';`,
// TODO: use nonce strategy after addressing issues with monaco-editor pacakge
"style-src 'self' 'unsafe-inline'",
// `style-src 'self' 'nonce-${req.__nonce__}';`,
]).join(' ')
);
}

next();
});
next();
});

app.use(`${BASEURL}/`, express.static(clientDirectory, { immutable: true, maxAge: 31536000 }));
app.use(morgan('dev'));
app.use(`${BASEURL}/`, express.static(clientDirectory, { immutable: true, maxAge: 31536000 }));
app.use(morgan('dev'));

// only register the login route if the auth provider defines one
if (login) {
app.get(`${BASEURL}/api/login`, login);
} else {
// register the route so that client that requires_auth knows not try repeatedly
app.get(`${BASEURL}/api/login`, (req, res) => {
res.redirect(`${BASEURL}#error=${encodeURIComponent('NoSupport')}`);
});
}
// only register the login route if the auth provider defines one
if (login) {
app.get(`${BASEURL}/api/login`, login);
} else {
// register the route so that client that requires_auth knows not try repeatedly
app.get(`${BASEURL}/api/login`, (req, res) => {
res.redirect(`${BASEURL}#error=${encodeURIComponent('NoSupport')}`);
});
}

// always authorize all api routes, it will be a no-op if no auth provider set
app.use(`${BASEURL}/api`, authorize, apiRouter);
// always authorize all api routes, it will be a no-op if no auth provider set
app.use(`${BASEURL}/api`, authorize, apiRouter);

// next needs to be an arg in order for express to recognize this as the error handler
// eslint-disable-next-line @typescript-eslint/no-unused-vars
app.use(function(err: Error, req: Request, res: Response, _next: NextFunction) {
if (err) {
log(err);
res.status(500).json({ message: err.message });
}
});
// next needs to be an arg in order for express to recognize this as the error handler
// eslint-disable-next-line @typescript-eslint/no-unused-vars
app.use(function(err: Error, req: Request, res: Response, _next: NextFunction) {
if (err) {
log(err);
res.status(500).json({ message: err.message });
}
});

app.get('*', function(req, res) {
res.render(path.resolve(clientDirectory, 'index.ejs'), { __nonce__: req.__nonce__ });
});
app.get('*', function(req, res) {
res.render(path.resolve(clientDirectory, 'index.ejs'), { __nonce__: req.__nonce__ });
});

const port = process.env.PORT || 5000;
const preferredPort = process.env.PORT || 5000;
let port = preferredPort;
if (process.env.NODE_ENV === 'production') {
// Dynamically search for an open PORT starting with PORT or 5000, so that
// the app doesn't crash if the port is already being used.
// (disabled in dev in order to avoid breaking the webpack dev server proxy)
port = await getPortPromise({ port: preferredPort as number });
}
let server;
await new Promise(resolve => {
server = app.listen(port, () => {
Expand Down Expand Up @@ -170,4 +178,6 @@ export async function start(pluginDir?: string) {
});
}
});

return port;
}
2 changes: 1 addition & 1 deletion Composer/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14792,7 +14792,7 @@ pnp-webpack-plugin@1.5.0:
dependencies:
ts-pnp "^1.1.2"

portfinder@^1.0.25:
portfinder@1.0.25, portfinder@^1.0.25:
version "1.0.25"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca"
integrity sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==
Expand Down