From 24fab19fc801a57fe714d4f9940edf608316fc5e Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Sat, 3 Apr 2021 00:52:06 +0800 Subject: [PATCH] support https (#462) * support https * tidy up, typecheck * clarify * changeset Co-authored-by: Rich Harris --- .changeset/blue-squids-march.md | 5 ++ packages/kit/package.json | 1 + packages/kit/src/cli.js | 22 ++++--- packages/kit/src/core/dev/index.js | 34 ++++++----- packages/kit/src/core/server/cert.js | 72 +++++++++++++++++++++++ packages/kit/src/core/server/index.js | 31 ++++++++++ packages/kit/src/core/start/index.js | 82 +++++++++++++-------------- pnpm-lock.yaml | 20 ++++++- 8 files changed, 197 insertions(+), 70 deletions(-) create mode 100644 .changeset/blue-squids-march.md create mode 100644 packages/kit/src/core/server/cert.js create mode 100644 packages/kit/src/core/server/index.js diff --git a/.changeset/blue-squids-march.md b/.changeset/blue-squids-march.md new file mode 100644 index 000000000000..2fea0eb34173 --- /dev/null +++ b/.changeset/blue-squids-march.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +Add --https flag to dev and start diff --git a/packages/kit/package.json b/packages/kit/package.json index c21e55d439ed..edfd18a126eb 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -22,6 +22,7 @@ "port-authority": "^1.1.2", "rimraf": "^3.0.2", "rollup": "^2.41.1", + "selfsigned": "^1.10.8", "sirv": "^1.0.11", "svelte": "^3.35.0", "tiny-glob": "^0.2.8", diff --git a/packages/kit/src/cli.js b/packages/kit/src/cli.js index 0830980f944b..6f826c412e7d 100644 --- a/packages/kit/src/cli.js +++ b/packages/kit/src/cli.js @@ -70,8 +70,9 @@ prog .describe('Start a development server') .option('-p, --port', 'Port', 3000) .option('-h, --host', 'Host (only use this on trusted networks)', 'localhost') + .option('--https', 'use self-signed HTTPS certificate', false) .option('-o, --open', 'Open a browser tab', false) - .action(async ({ port, host, open }) => { + .action(async ({ port, host, https, open }) => { await check_port(port); process.env.NODE_ENV = 'development'; @@ -80,7 +81,7 @@ prog const { dev } = await import('./core/dev/index.js'); try { - const watcher = await dev({ port, host, config }); + const watcher = await dev({ port, host, https, config }); watcher.on('stdout', (data) => { process.stdout.write(data); @@ -90,7 +91,7 @@ prog process.stderr.write(data); }); - welcome({ port, host, open }); + welcome({ port, host, https, open }); } catch (error) { handle_error(error); } @@ -131,8 +132,9 @@ prog .describe('Serve an already-built app') .option('-p, --port', 'Port', 3000) .option('-h, --host', 'Host (only use this on trusted networks)', 'localhost') + .option('--https', 'use self-signed HTTPS certificate', false) .option('-o, --open', 'Open a browser tab', false) - .action(async ({ port, host, open }) => { + .action(async ({ port, host, https, open }) => { await check_port(port); process.env.NODE_ENV = 'production'; @@ -141,9 +143,9 @@ prog const { start } = await import('./core/start/index.js'); try { - await start({ port, host, config }); + await start({ port, host, config, https }); - welcome({ port, host, open }); + welcome({ port, host, https, open }); } catch (error) { handle_error(error); } @@ -180,14 +182,16 @@ async function check_port(port) { * @param {{ * open: boolean; * host: string; + * https: boolean; * port: number; * }} param0 */ -function welcome({ port, host, open }) { +function welcome({ port, host, https, open }) { if (open) launch(port); console.log(colors.bold().cyan(`\n SvelteKit v${'__VERSION__'}\n`)); + const protocol = https ? 'https:' : 'http:'; const exposed = host !== 'localhost' && host !== '127.0.0.1'; Object.values(networkInterfaces()).forEach((interfaces) => { @@ -196,12 +200,12 @@ function welcome({ port, host, open }) { // prettier-ignore if (details.internal) { - console.log(` ${colors.gray('local: ')} http://${colors.bold(`localhost:${port}`)}`); + console.log(` ${colors.gray('local: ')} ${protocol}//${colors.bold(`localhost:${port}`)}`); } else { if (details.mac === '00:00:00:00:00:00') return; if (exposed) { - console.log(` ${colors.gray('network:')} http://${colors.bold(`${details.address}:${port}`)}`); + console.log(` ${colors.gray('network:')} ${protocol}//${colors.bold(`${details.address}:${port}`)}`); } else { console.log(` ${colors.gray('network: not exposed')}`); } diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js index 126477d43b9c..2a9ca1ab4905 100644 --- a/packages/kit/src/core/dev/index.js +++ b/packages/kit/src/core/dev/index.js @@ -1,4 +1,3 @@ -import http from 'http'; import fs from 'fs'; import path from 'path'; import { parse, URLSearchParams } from 'url'; @@ -13,8 +12,9 @@ import { ssr } from '../../runtime/server/index.js'; import { get_body } from '../http/index.js'; import { copy_assets } from '../utils.js'; import svelte from '@sveltejs/vite-plugin-svelte'; +import { get_server } from '../server/index.js'; -/** @typedef {{ cwd?: string, port: number, host: string, config: import('../../../types.internal').ValidatedConfig }} Options */ +/** @typedef {{ cwd?: string, port: number, host: string, https: boolean | import('https').ServerOptions, config: import('../../../types.internal').ValidatedConfig }} Options */ /** @typedef {import('../../../types.internal').SSRComponent} SSRComponent */ /** @param {Options} opts */ @@ -24,7 +24,7 @@ export function dev(opts) { class Watcher extends EventEmitter { /** @param {Options} opts */ - constructor({ cwd = process.cwd(), port, host, config }) { + constructor({ cwd = process.cwd(), port, host, https, config }) { super(); this.cwd = cwd; @@ -32,6 +32,7 @@ class Watcher extends EventEmitter { this.port = port; this.host = host; + this.https = https; this.config = config; process.env.NODE_ENV = 'development'; @@ -80,7 +81,7 @@ class Watcher extends EventEmitter { /** * @type {vite.ViteDevServer} */ - this.viteDevServer = await vite.createServer({ + this.vite = await vite.createServer({ ...user_config, configFile: false, root: this.cwd, @@ -113,8 +114,8 @@ class Watcher extends EventEmitter { const validator = this.config.kit.amp && (await amp_validator.getInstance()); - this.server = http.createServer((req, res) => { - this.viteDevServer.middlewares(req, res, async () => { + this.server = await get_server(this.port, this.host, this.https, (req, res) => { + this.vite.middlewares(req, res, async () => { try { const parsed = parse(req.url); @@ -123,15 +124,14 @@ class Watcher extends EventEmitter { // handle dynamic requests - i.e. pages and endpoints const template = fs.readFileSync(this.config.kit.files.template, 'utf-8'); - const hooks = /** @type {import('../../../types.internal').Hooks} */ (await this.viteDevServer + const hooks = /** @type {import('../../../types.internal').Hooks} */ (await this.vite .ssrLoadModule(`/${this.config.kit.files.hooks}`) .catch(() => ({}))); let root; try { - root = (await this.viteDevServer.ssrLoadModule(`/${this.dir}/generated/root.svelte`)) - .default; + root = (await this.vite.ssrLoadModule(`/${this.dir}/generated/root.svelte`)).default; } catch (e) { res.statusCode = 500; res.end(e.stack); @@ -219,7 +219,7 @@ class Watcher extends EventEmitter { only_render_prerenderable_pages: false, get_component_path: (id) => `/${id}?import`, get_stack: (error) => { - this.viteDevServer.ssrFixStacktrace(error); + this.vite.ssrFixStacktrace(error); return error.stack; }, get_static_file: (file) => @@ -239,13 +239,11 @@ class Watcher extends EventEmitter { res.end('Not found'); } } catch (e) { - this.viteDevServer.ssrFixStacktrace(e); + this.vite.ssrFixStacktrace(e); res.end(e.stack); } }); }); - - this.server.listen(this.port, this.host || '0.0.0.0'); } update() { @@ -273,8 +271,8 @@ class Watcher extends EventEmitter { const load = async (file) => { const url = path.resolve(this.cwd, file); - const mod = /** @type {SSRComponent} */ (await this.viteDevServer.ssrLoadModule(url)); - const node = await this.viteDevServer.moduleGraph.getModuleByUrl(url); + const mod = /** @type {SSRComponent} */ (await this.vite.ssrLoadModule(url)); + const node = await this.vite.moduleGraph.getModuleByUrl(url); const deps = new Set(); find_deps(node, deps); @@ -284,7 +282,7 @@ class Watcher extends EventEmitter { // TODO what about .scss files, etc? if (dep.file.endsWith('.css')) { try { - const mod = await this.viteDevServer.ssrLoadModule(dep.url); + const mod = await this.vite.ssrLoadModule(dep.url); css.add(mod.default); } catch { // this can happen with dynamically imported modules, I think @@ -367,7 +365,7 @@ class Watcher extends EventEmitter { params: get_params(route.params), load: async () => { const url = path.resolve(this.cwd, route.file); - return await this.viteDevServer.ssrLoadModule(url); + return await this.vite.ssrLoadModule(url); } }; }) @@ -378,7 +376,7 @@ class Watcher extends EventEmitter { if (this.closed) return; this.closed = true; - this.viteDevServer.close(); + this.vite.close(); this.server.close(); this.cheapwatch.close(); } diff --git a/packages/kit/src/core/server/cert.js b/packages/kit/src/core/server/cert.js new file mode 100644 index 000000000000..b49b30103fcf --- /dev/null +++ b/packages/kit/src/core/server/cert.js @@ -0,0 +1,72 @@ +// @ts-ignore +import { generate } from 'selfsigned'; + +/** + * https://github.com/webpack/webpack-dev-server/blob/master/lib/utils/createCertificate.js + * + * Copyright JS Foundation and other contributors + * This source code is licensed under the MIT license found in the + * LICENSE file at + * https://github.com/webpack/webpack-dev-server/blob/master/LICENSE + */ +export function createCertificate() { + const pems = generate(null, { + algorithm: 'sha256', + days: 30, + keySize: 2048, + extensions: [ + { + name: 'keyUsage', + keyCertSign: true, + digitalSignature: true, + nonRepudiation: true, + keyEncipherment: true, + dataEncipherment: true + }, + { + name: 'extKeyUsage', + serverAuth: true, + clientAuth: true, + codeSigning: true, + timeStamping: true + }, + { + name: 'subjectAltName', + altNames: [ + { + // type 2 is DNS + type: 2, + value: 'localhost' + }, + { + type: 2, + value: 'localhost.localdomain' + }, + { + type: 2, + value: 'lvh.me' + }, + { + type: 2, + value: '*.lvh.me' + }, + { + type: 2, + value: '[::1]' + }, + { + // type 7 is IP + type: 7, + ip: '127.0.0.1' + }, + { + type: 7, + ip: 'fe80::1' + } + ] + } + ] + }); + + return pems.private + pems.cert; +} diff --git a/packages/kit/src/core/server/index.js b/packages/kit/src/core/server/index.js new file mode 100644 index 000000000000..e1fb744ef5eb --- /dev/null +++ b/packages/kit/src/core/server/index.js @@ -0,0 +1,31 @@ +import http from 'http'; +import https from 'https'; + +/** + * + * @param {number} port + * @param {string} host + * @param {boolean | https.ServerOptions} https_options + * @param {(req: http.IncomingMessage, res: http.ServerResponse) => void} handler + * @returns {Promise} + */ +export async function get_server(port, host, https_options, handler) { + if (https_options) { + https_options = + typeof https_options === 'boolean' ? /** @type {https.ServerOptions} */ ({}) : https_options; + + if (!https_options.key || !https_options.cert) { + https_options.key = https_options.cert = (await import('./cert')).createCertificate(); + } + } + + return new Promise((fulfil) => { + const server = https_options + ? https.createServer(/** @type {https.ServerOptions} */ (https_options), handler) + : http.createServer(handler); + + server.listen(port, host || '0.0.0.0', () => { + fulfil(server); + }); + }); +} diff --git a/packages/kit/src/core/start/index.js b/packages/kit/src/core/start/index.js index d8950df6168b..738b0ae4fd5a 100644 --- a/packages/kit/src/core/start/index.js +++ b/packages/kit/src/core/start/index.js @@ -1,9 +1,9 @@ import fs from 'fs'; -import http from 'http'; import { parse, pathToFileURL, URLSearchParams } from 'url'; import sirv from 'sirv'; import { get_body } from '../http/index.js'; import { join, resolve } from 'path'; +import { get_server } from '../server/index.js'; /** @param {string} dir */ const mutable = (dir) => @@ -16,15 +16,21 @@ const mutable = (dir) => * @param {{ * port: number; * host: string; - * config: import('../../../types.internal').ValidatedConfig; + * config: import('types.internal').ValidatedConfig; + * https?: boolean | import('https').ServerOptions; * cwd?: string; * }} opts - * @returns {Promise} */ -export async function start({ port, host, config, cwd = process.cwd() }) { +export async function start({ + port, + host, + config, + https: https_options = false, + cwd = process.cwd() +}) { const app_file = resolve(cwd, '.svelte/output/server/app.js'); - /** @type {import('../../../types.internal').App} */ + /** @type {import('types.internal').App} */ const app = await import(pathToFileURL(app_file).href); /** @type {import('sirv').RequestHandler} */ @@ -37,47 +43,39 @@ export async function start({ port, host, config, cwd = process.cwd() }) { immutable: true }); - return new Promise((fulfil) => { - const server = http.createServer((req, res) => { - const parsed = parse(req.url || ''); + return get_server(port, host, https_options, (req, res) => { + const parsed = parse(req.url || ''); - assets_handler(req, res, () => { - static_handler(req, res, async () => { - const rendered = await app.render( - { - host: /** @type {string} */ (config.kit.host || - req.headers[config.kit.hostHeader || 'host']), - method: req.method, - headers: /** @type {import('../../../types.internal').Headers} */ (req.headers), - path: parsed.pathname, - body: await get_body(req), - query: new URLSearchParams(parsed.query || '') + assets_handler(req, res, () => { + static_handler(req, res, async () => { + const rendered = await app.render( + { + host: /** @type {string} */ (config.kit.host || + req.headers[config.kit.hostHeader || 'host']), + method: req.method, + headers: /** @type {import('types.internal').Headers} */ (req.headers), + path: parsed.pathname, + body: await get_body(req), + query: new URLSearchParams(parsed.query || '') + }, + { + paths: { + base: '', + assets: '/.' }, - { - paths: { - base: '', - assets: '/.' - }, - get_stack: (error) => error.stack, // TODO should this return a sourcemapped stacktrace? - get_static_file: (file) => fs.readFileSync(join(config.kit.files.assets, file)) - } - ); - - if (rendered) { - res.writeHead(rendered.status, rendered.headers); - res.end(rendered.body); - } else { - res.statusCode = 404; - res.end('Not found'); + get_stack: (error) => error.stack, // TODO should this return a sourcemapped stacktrace? + get_static_file: (file) => fs.readFileSync(join(config.kit.files.assets, file)) } - }); - }); - }); + ); - server.listen(port, host || '0.0.0.0', () => { - fulfil(server); + if (rendered) { + res.writeHead(rendered.status, rendered.headers); + res.end(rendered.body); + } else { + res.statusCode = 404; + res.end('Not found'); + } + }); }); - - return server; }); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 115b7d855778..8cea7e014c01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -180,6 +180,7 @@ importers: port-authority: 1.1.2 rimraf: 3.0.2 rollup: 2.41.1 + selfsigned: 1.10.8 sirv: 1.0.11 svelte: 3.35.0 tiny-glob: 0.2.8 @@ -204,6 +205,7 @@ importers: rimraf: ^3.0.2 rollup: ^2.41.1 sade: ^1.7.4 + selfsigned: ^1.10.8 sirv: ^1.0.11 svelte: ^3.35.0 tiny-glob: ^0.2.8 @@ -477,6 +479,10 @@ packages: node: '>= 8' resolution: integrity: sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== + /@polka/url/1.0.0-next.11: + dev: true + resolution: + integrity: sha512-3NsZsJIA/22P3QUyrEDNA2D133H4j224twJrdipXN38dpnIOzAbUDtOwkcJ5pXmn75w7LSQDjA4tO9dm1XlqlA== /@polka/url/1.0.0-next.12: dev: true resolution: @@ -2500,6 +2506,12 @@ packages: node: ^10.17 || >=12.3 resolution: integrity: sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg== + /node-forge/0.10.0: + dev: true + engines: + node: '>= 6.0.0' + resolution: + integrity: sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== /normalize-package-data/2.5.0: dependencies: hosted-git-info: 2.8.8 @@ -3092,6 +3104,12 @@ packages: dev: true resolution: integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + /selfsigned/1.10.8: + dependencies: + node-forge: 0.10.0 + dev: true + resolution: + integrity: sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w== /semver/5.7.1: dev: true hasBin: true @@ -3153,7 +3171,7 @@ packages: integrity: sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== /sirv/1.0.11: dependencies: - '@polka/url': 1.0.0-next.12 + '@polka/url': 1.0.0-next.11 mime: 2.5.2 totalist: 1.1.0 dev: true