Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for HTTPS on local server #201

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/generator-teams/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 52 additions & 1 deletion packages/generator-teams/src/app/GeneratorTeamsApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import validate = require('uuid-validate');
import EmptyGuid = require('./EmptyGuid');
import { CoreFilesUpdaterFactory } from './coreFilesUpdater/CoreFilesUpdaterFactory';


let yosay = require('yosay');
let pkg: any = require('../../package.json');

Expand Down Expand Up @@ -101,6 +100,10 @@ export class GeneratorTeamsApp extends Generator {
showLoadingIndicator: boolean;
isFullScreen: boolean;
quickScaffolding: boolean;
useHttps: boolean;
useSelfSignedSSL: boolean;
pfxFilePath: string;
pfxFilePassword: string;
};
// find out what manifest versions we can use
const manifestGeneratorFactory = new ManifestGeneratorFactory();
Expand Down Expand Up @@ -295,6 +298,32 @@ export class GeneratorTeamsApp extends Generator {
default: false, // set to false until the 20 second timeout bug is fixed in Teams
when: () => !this.options.existingManifest
},
{
type: 'confirm',
name: 'useHttps',
message: 'Would you like to host your local server on HTTPS (notice that end-to-end TLS tunnel with ngrok is available only with Pro & Business plans)?',
default: false,
when: (answers: IAnswers) => !answers.quickScaffolding && !this.options.existingManifest
},
{
type: 'confirm',
name: 'useSelfSignedSSL',
message: 'Would you like to generate a self-signed SSL certificate?',
default: true,
when: (answers: IAnswers) => !answers.quickScaffolding && answers.useHttps
},
{
type: 'input',
name: 'pfxFilePath',
message: 'Provide the path of a valid PFX file:',
when: (answers: IAnswers) => !answers.quickScaffolding && answers.useHttps && !answers.useSelfSignedSSL
},
{
type: 'password',
name: 'pfxFilePassword',
message: 'Provide the password for the PFX file:',
when: (answers: IAnswers) => !answers.quickScaffolding && answers.useHttps && !answers.useSelfSignedSSL
},
{
type: 'confirm',
name: 'isFullScreen',
Expand Down Expand Up @@ -371,6 +400,10 @@ export class GeneratorTeamsApp extends Generator {
this.options.unitTestsEnabled = answers.unitTestsEnabled;
this.options.useAzureAppInsights = answers.useAzureAppInsights;
this.options.azureAppInsightsKey = answers.azureAppInsightsKey;
this.options.useHttps = answers.useHttps;
this.options.useSelfSignedSSL = answers.useSelfSignedSSL;
this.options.pfxFilePath = answers.pfxFilePath;
this.options.pfxFilePassword = answers.pfxFilePassword;
} else {
// when updating projects
this.options.developer = this.options.existingManifest.developer.name;
Expand Down Expand Up @@ -438,6 +471,7 @@ export class GeneratorTeamsApp extends Generator {
"src/public/assets/icon.png",
"src/public/styles/main.scss",
"src/server/TeamsAppsComponents.ts",
"src/server/selfSignedLogic.ts",
"Dockerfile"
]

Expand Down Expand Up @@ -485,6 +519,23 @@ export class GeneratorTeamsApp extends Generator {
this.templatePath(t),
Yotilities.fixFileNames(t, this.options));
});

if (this.options.useHttps) {

if (this.options.telemetry) {
// Track the choice about HTTPS
AppInsights.defaultClient.trackEvent({ name: 'enable-https', properties: { selfSigned: this.options.useSelfSignedSSL } });
}

// Here we simply enable HTTPS in the .env file
Yotilities.addOrUpdateEnv(".env", "HTTPS", "true", this.fs);

// Here we configure the PFX file path and password in the .env file, if they were provided
if (!this.options.useSelfSignedSSL && this.options.pfxFilePath && this.options.pfxFilePassword) {
Yotilities.addOrUpdateEnv(".env", "HTTPS_PFX_PATH", this.options.pfxFilePath, this.fs);
Yotilities.addOrUpdateEnv(".env", "HTTPS_PFX_PASSWORD", this.options.pfxFilePassword, this.fs);
}
}
} else {
if (this.options.updateBuildSystem) {
let currentVersion = this.config.get("generator-version");
Expand Down
4 changes: 4 additions & 0 deletions packages/generator-teams/src/app/GeneratorTeamsAppOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export class GeneratorTeamsAppOptions {
showLoadingIndicator: boolean = false;
isFullScreen: boolean = false;
quickScaffolding: boolean = true;
useHttps: boolean = false;
useSelfSignedSSL: boolean = true;
pfxFilePath: string;
pfxFilePassword: string;

reactComponents: boolean;
websitePrefix: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/generator-teams/src/app/Yotilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class Yotilities {
const envFile = fs.read(fileName);
let added: boolean = false;
let output = envFile.split(os.EOL).map(line => {
if (line.startsWith(key)) {
if (line.startsWith(`${key}=`)) {
added = true;
return `${key}=${value}`;
}
Expand Down
8 changes: 8 additions & 0 deletions packages/generator-teams/src/app/templates/.env
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ PORT=3007
# Security token for the default outgoing webhook
SECURITY_TOKEN=

# HTTPS/SSL configuration
# Set this flag to true in order to enable HTTPS over SSL
HTTPS=false
# The file system path of a PFX file to use for HTTPS over SSL (leave empty to use a self-signed certificate)
HTTPS_PFX_PATH=
# The password to access a PFX file to use for HTTPS over SSL (leave empty to use a self-signed certificate)
HTTPS_PFX_PASSWORD=

# ID of the Outlook Connector
CONNECTOR_ID=<%= connectorId ? connectorId : '' %>

Expand Down
3 changes: 3 additions & 0 deletions packages/generator-teams/src/app/templates/_gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ connectors.json

# do not include dist files
dist

# do not include the self-signed certificate, if any
server.pem
3 changes: 2 additions & 1 deletion packages/generator-teams/src/app/templates/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@
"webpack": "5.0.0",
"yargs": "^16.0.3",
"yoteams-build-core": "^1.0.0",
"webpack-node-externals": "^2.5.2"
"webpack-node-externals": "^2.5.2",
"selfsigned": "^1.10.8"
},
"browserslist": [
"> 1%",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as https from "https";
import * as debug from "debug";

// Require SSL selfsigned certificate generation
const selfsigned = require("selfsigned");

// Require file system access
const fs = require("fs");

// Initialize debug logging module
const log = debug("msteams");

// Certificate lifetime (days)
const lifetime: number = 120;

// Define the certificate creation extensions for localhost
const selfSignedExtensions = [
{
name: "basicConstraints",
cA: true
},
{
name: "keyUsage",
keyCertSign: true,
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true,
dataEncipherment: true
},
{
name: "subjectAltName",
altNames: [
{
type: 2,
value: "localhost"
},
{
type: 2,
value: "localhost.localdomain"
},
{
type: 2,
value: "[::1]"
},
{
// type 7 is IP
type: 7,
ip: "127.0.0.1"
},
{
type: 7,
ip: "fe80::1"
}
]
}
];

export const selfSignedLogic = {
getSelfSignedOptions(): https.ServerOptions {

// Define the target self-signed certificate path
const certPath = `${__dirname}../../server.pem`;

// Let's see if the self-signed certificate exists
let certExists = fs.existsSync(certPath);
if (certExists) {

log(`Using self-signed certificate: ${certPath}`);

// And let's see if it is still valid
const certStat = fs.statSync(certPath);
const certTtl = 1000 * 60 * 60 * 24;
const now = new Date();

// cert is more than 120 days old, let's create a new one
if ((now.getTime() - certStat.ctime.getTime()) / certTtl > lifetime) {
log("SSL Certificate is more than 120 days old. Removing.");
fs.unlinkSync(certPath);
certExists = false;
}
}

// If the self-signed certificate doesn't exist
if (!certExists) {
// Let's create it from scratch
log("Generating a new SSL self-signed certificate");
const attrs = [{ name: "commonName", value: "localhost" }];
const pems = selfsigned.generate(attrs, {
algorithm: "sha256",
days: lifetime,
keySize: 2048,
extensions: selfSignedExtensions
});

fs.writeFileSync(certPath, pems.private + pems.cert, {
encoding: "utf-8"
});

log(`Self-signed certificate stored on: ${certPath}`);
}

const selfSignedCert = fs.readFileSync(certPath);

const sslOptions = {
key: selfSignedCert,
cert: selfSignedCert
};

return sslOptions;
}
};
47 changes: 42 additions & 5 deletions packages/generator-teams/src/app/templates/src/server/server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import * as Express from "express";
import * as http from "http";
import * as https from "https";
import { selfSignedLogic } from "./selfSignedLogic";
import * as path from "path";
import * as morgan from "morgan";
import { MsTeamsApiRouter, MsTeamsPageRouter } from "express-msteams-host";
import * as debug from "debug";
import * as compression from "compression";
<% if (useAzureAppInsights) { %>import * as appInsights from "applicationinsights";<% } %>
import * as compression from "compression";

// Require file system access
const fs = require("fs");

// Initialize debug logging module
const log = debug("msteams");

Expand Down Expand Up @@ -66,7 +72,38 @@ express.use("/", Express.static(path.join(__dirname, "web/"), {
// Set the port
express.set("port", port);

// Start the webserver
http.createServer(express).listen(port, () => {
log(`Server running on ${port}`);
});
if (process.env.HTTPS === "true") {

// Start the web server over HTTPS accordingly to the settings
if (process.env.HTTPS_PFX_PATH && process.env.HTTPS_PFX_PASSWORD) {

// Create the SSL options based on the provided PFX certificate
const sslOptions = {
pfx: fs.readFileSync(process.env.HTTPS_PFX_PATH),
passphrase: process.env.HTTPS_PFX_PASSWORD
};

// Start the webserver over HTTPS
https.createServer(sslOptions, express).listen(port, () => {
log(`Server running on ${port} over SSL with provided PFX`);
});

} else {

// Get or generate a self-signed certificate (will last 120 days)
const sslOptions = selfSignedLogic.getSelfSignedOptions();

// Start the webserver over HTTPS
https.createServer(sslOptions, express).listen(port, () => {
log(`Server running on ${port} over SSL with self-signed certificate`);
});

}
} else {

// Start the webserver over HTTP
http.createServer(express).listen(port, () => {
log(`Server running on ${port}`);
});

}
16 changes: 16 additions & 0 deletions packages/generator-teams/tests/generic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,22 @@ describe("teams:generic", function () {

async function genericTests(prompts: any) {

if (prompts.useHttps && prompts.useSelfSignedSSL) {
it("Should have .env settings for HTTPS with self-signed certificate", async () => {
assert.fileContent(".env", `HTTPS=${prompts.useHttps}`);
assert.fileContent(".env", /HTTPS_PFX_PATH=[\n|\r]/);
assert.fileContent(".env", /HTTPS_PFX_PASSWORD=[\n|\r]/);
});
}

if (prompts.useHttps && !prompts.useSelfSignedSSL) {
it("Should have .env settings for HTTPS with custom PFX", async () => {
assert.fileContent(".env", `HTTPS=${prompts.useHttps}`);
assert.fileContent(".env", `HTTPS_PFX_PATH=${prompts.pfxFilePath}`);
assert.fileContent(".env", `HTTPS_PFX_PASSWORD=${prompts.pfxFilePassword}`);
});
}

}

testHelper.runTests("generic", tests.generic, genericTests);
Expand Down
36 changes: 36 additions & 0 deletions packages/generator-teams/tests/tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,42 @@
"mpnId": "",
"showLoadingIndicator": true
}
},
{
"description": "HTTPS self-signed",
"manifestVersions": [
"v1.8",
"v1.9",
"devPreview"
],
"prompts": {
"parts": "tab",
"tabScopes": [
"team"
],
"quickScaffolding": false,
"useHttps": true,
"useSelfSignedSSL": true
}
},
{
"description": "HTTPS with PFX",
"manifestVersions": [
"v1.8",
"v1.9",
"devPreview"
],
"prompts": {
"parts": "tab",
"tabScopes": [
"team"
],
"quickScaffolding": false,
"useHttps": true,
"useSelfSignedSSL": false,
"pfxFilePath": "c:\\certificate.pfx",
"pfxFilePassword": "P@ssw0rd!"
}
}
]
}
Loading