From 56896dd42f71bef2877c5c4dd36b4fec72418735 Mon Sep 17 00:00:00 2001 From: Yusuke Yamada Date: Fri, 26 Mar 2021 18:28:24 +0900 Subject: [PATCH] feat: Support serving web and api over https locally (#140) * feat(core): allows to specify protocol * feat: add useHttps config entry * feat(proxy): supports serving traffic over https ref: https://github.com/Azure/static-web-apps-cli/issues/4 * refactor: makes useSsl flags type boolean accepted these suggestions - https://github.com/Azure/static-web-apps-cli/pull/140#discussion_r600464301 - https://github.com/Azure/static-web-apps-cli/pull/140#discussion_r600467786 - https://github.com/Azure/static-web-apps-cli/pull/140#discussion_r600472647 * style: modifies test case and error mesasges - https://github.com/Azure/static-web-apps-cli/pull/140#discussion_r600469100 - https://github.com/Azure/static-web-apps-cli/pull/140#discussion_r600463074 * refactor: uses `ssl` insted of `useHttps` for making it suitable as a CLI option argument * fix: defines ssl related environment variables to types definitions * feat: adds ssl options to CLI flags * refactor: changes the timing when validating ssl related args * feat: changes in behavior when SSL cert or key are not specified * style: modifies cli options description messages - https://github.com/Azure/static-web-apps-cli/pull/140#discussion_r601308855 - https://github.com/Azure/static-web-apps-cli/pull/140#discussion_r601309135 --- src/cli/commands/start.ts | 9 +++++++++ src/cli/index.ts | 3 +++ src/config.ts | 3 +++ src/core/utils.test.ts | 5 +++++ src/core/utils.ts | 3 +-- src/proxy/server.ts | 31 ++++++++++++++++++++++++------- src/swa.d.ts | 6 ++++++ 7 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/cli/commands/start.ts b/src/cli/commands/start.ts index 7b4e3c41..4caa5501 100644 --- a/src/cli/commands/start.ts +++ b/src/cli/commands/start.ts @@ -84,6 +84,12 @@ export async function start(startContext: string, options: SWACLIConfig) { } } + if (options.ssl) { + if (options.sslCert === undefined || options.sslKey === undefined) { + logger.error(`SSL Key or SSL Cert are required when using HTTPS`, true); + } + } + // set env vars for current command const envVarsObj = { SWA_CLI_DEBUG: options.verbose, @@ -94,6 +100,9 @@ export async function start(startContext: string, options: SWACLIConfig) { SWA_CLI_HOST: options.host, SWA_CLI_PORT: `${options.port}`, SWA_WORKFLOW_FILES: userConfig?.files?.join(","), + SWA_CLI_APP_SSL: `${options.ssl}`, + SWA_CLI_APP_SSL_CERT: options.sslCert, + SWA_CLI_APP_SSL_KEY: options.sslKey, }; if (options.verbose?.includes("silly")) { diff --git a/src/cli/index.ts b/src/cli/index.ts index b97f0ed2..9cd089cb 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -34,6 +34,9 @@ import { start } from "./commands/start"; .option("--host ", "set the cli host address", DEFAULT_CONFIG.host) .option("--port ", "set the cli port", parsePort, DEFAULT_CONFIG.port) .option("--build", "build the app and API before starting the emulator", false) + .option("--ssl", "serving the app and API over HTTPS", DEFAULT_CONFIG.ssl) + .option("--ssl-cert ", "SSL certificate to use for serving HTTPS", DEFAULT_CONFIG.sslCert) + .option("--ssl-key ", "SSL key to use for serving HTTPS", DEFAULT_CONFIG.sslKey) .action(async (context: string = `.${path.sep}`, options: any) => { options = { ...options, diff --git a/src/config.ts b/src/config.ts index 935ace44..597bdd48 100644 --- a/src/config.ts +++ b/src/config.ts @@ -4,8 +4,11 @@ export const DEFAULT_CONFIG: SWACLIConfig = { host: "0.0.0.0", apiPort: 7071, apiPrefix: "api", + ssl: false, appLocation: `.${path.sep}`, appArtifactLocation: `.${path.sep}`, + sslCert: undefined, + sslKey: undefined, appBuildCommand: "npm run build --if-present", apiBuildCommand: "npm run build --if-present", swaConfigFilename: "staticwebapp.config.json", diff --git a/src/core/utils.test.ts b/src/core/utils.test.ts index 2a1d16e5..a2f0c8af 100644 --- a/src/core/utils.test.ts +++ b/src/core/utils.test.ts @@ -770,5 +770,10 @@ jobs: expect(address("127.0.0.1", "4200")).toBe("http://127.0.0.1:4200"); expect(address("[::1]", "4200")).toBe("http://[::1]:4200"); }); + + it("should accept protocol both HTTP and HTTPS protocols", () => { + expect(address("127.0.0.1", "4200", "http")).toBe("http://127.0.0.1:4200"); + expect(address("127.0.0.1", "4200", "https")).toBe("https://127.0.0.1:4200"); + }); }); }); diff --git a/src/core/utils.ts b/src/core/utils.ts index a5a721a9..8484070f 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -476,8 +476,7 @@ export async function findSWAConfigFile(folder: string) { return null; } -export const address = (host: string, port: number | string | undefined) => { - const protocol = `http`; +export const address = (host: string, port: number | string | undefined, protocol = `http`) => { if (!host) { throw new Error(`Host value is not set`); } diff --git a/src/proxy/server.ts b/src/proxy/server.ts index d050bba4..577bf112 100755 --- a/src/proxy/server.ts +++ b/src/proxy/server.ts @@ -3,6 +3,7 @@ import fs from "fs"; import chalk from "chalk"; import internalIp from "internal-ip"; import http from "http"; +import https from "https"; import httpProxy from "http-proxy"; import net from "net"; import path from "path"; @@ -18,8 +19,11 @@ const SWA_CLI_PORT = parseInt((process.env.SWA_CLI_PORT || DEFAULT_CONFIG.port) const SWA_CLI_API_URI = address(SWA_CLI_HOST, process.env.SWA_CLI_API_PORT); const SWA_CLI_APP_LOCATION = (process.env.SWA_CLI_APP_LOCATION || DEFAULT_CONFIG.appLocation) as string; const SWA_CLI_APP_ARTIFACT_LOCATION = (process.env.SWA_CLI_APP_ARTIFACT_LOCATION || DEFAULT_CONFIG.appArtifactLocation) as string; +const SWA_CLI_APP_SSL = process.env.SWA_CLI_APP_SSL === "true" || DEFAULT_CONFIG.ssl === true; +const SWA_CLI_APP_SSL_KEY = process.env.SWA_CLI_APP_SSL_KEY as string; +const SWA_CLI_APP_SSL_CERT = process.env.SWA_CLI_APP_SSL_CERT as string; -const PROTOCOL = `http://`; +const PROTOCOL = SWA_CLI_APP_SSL ? `https` : `http`; const proxyApi = httpProxy.createProxyServer({ autoRewrite: true }); const proxyApp = httpProxy.createProxyServer({ autoRewrite: true }); @@ -34,6 +38,13 @@ if (SWA_WORKFLOW_CONFIG_FILE) { logger.info(`\nFound workflow file:\n ${chalk.green(SWA_WORKFLOW_CONFIG_FILE)}`); } +const httpsServerOptions: Pick | null = SWA_CLI_APP_SSL + ? { + cert: fs.readFileSync(SWA_CLI_APP_SSL_CERT, "utf8"), + key: fs.readFileSync(SWA_CLI_APP_SSL_KEY, "utf8"), + } + : null; + const SWA_PUBLIC_DIR = path.resolve(__dirname, "..", "public"); const logRequest = (req: http.IncomingMessage, target: string | null = null, statusCode: number | null = null) => { @@ -41,7 +52,7 @@ const logRequest = (req: http.IncomingMessage, target: string | null = null, sta return; } - const host = target || `${PROTOCOL}${req.headers.host}`; + const host = target || `${PROTOCOL}://${req.headers.host}`; const url = req.url?.startsWith("/") ? req.url : `/${req.url}`; if (statusCode) { @@ -150,7 +161,7 @@ const requestHandler = (userConfig: SWAConfigFile | null) => res.statusCode = 404; serve(SWA_PUBLIC_DIR, req, res); - logRequest(req, PROTOCOL + req.headers.host + req.url, 404); + logRequest(req, PROTOCOL + "://" + req.headers.host + req.url, 404); } // proxy AUTH request to AUTH emulator @@ -258,8 +269,8 @@ const requestHandler = (userConfig: SWAConfigFile | null) => // prettier-ignore logger.log( `\nAvailable on:\n` + - ` ${chalk.green(address(`${localIpAdress}`, SWA_CLI_PORT))}\n` + - ` ${chalk.green(address(SWA_CLI_HOST, SWA_CLI_PORT))}\n\n` + + ` ${chalk.green(address(`${localIpAdress}`, SWA_CLI_PORT, PROTOCOL))}\n` + + ` ${chalk.green(address(SWA_CLI_HOST, SWA_CLI_PORT, PROTOCOL))}\n\n` + `Azure Static Web Apps emulator started. Press CTRL+C to exit.\n\n` ); @@ -272,11 +283,17 @@ const requestHandler = (userConfig: SWAConfigFile | null) => }; // load user custom rules if running in local mode (non-dev server) - let userConfig = null; + let userConfig: SWAConfigFile | null = null; if (!isStaticDevServer) { userConfig = await handleUserConfig(SWA_CLI_APP_LOCATION); } - const server = http.createServer(requestHandler(userConfig)); + const createServer = () => { + if (SWA_CLI_APP_SSL && httpsServerOptions !== null) { + return https.createServer(httpsServerOptions, requestHandler(userConfig)); + } + return http.createServer(requestHandler(userConfig)); + }; + const server = createServer(); server.listen(SWA_CLI_PORT, SWA_CLI_HOST, onServerStart); server.listen(SWA_CLI_PORT, localIpAdress); })(); diff --git a/src/swa.d.ts b/src/swa.d.ts index 1e226259..c0a9f45f 100644 --- a/src/swa.d.ts +++ b/src/swa.d.ts @@ -8,6 +8,9 @@ declare global { SWA_CLI_HOST: string; SWA_CLI_PORT: string; SWA_WORKFLOW_FILE: string; + SWA_CLI_APP_SSL: boolean; + SWA_CLI_APP_SSL_KEY: string; + SWA_CLI_APP_SSL_CERT: string; } } } @@ -56,7 +59,10 @@ declare type SWACLIConfig = GithubActionWorkflow & { port?: number; host?: string; apiPort?: number; + ssl?: boolean; apiPrefix?: "api"; + sslCert?: string; + sslKey?: string; swaConfigFilename?: "staticwebapp.config.json"; swaConfigFilenameLegacy?: "routes.json"; app?: string;