diff --git a/.changeset/dev-server-https-http1.md b/.changeset/dev-server-https-http1.md new file mode 100644 index 0000000000..5e446f49ac --- /dev/null +++ b/.changeset/dev-server-https-http1.md @@ -0,0 +1,6 @@ +--- +'@web/dev-server-core': minor +'@web/dev-server': minor +--- + +Add an `https` option to serve over HTTPS with HTTP/1.1, without enabling HTTP/2. It uses `sslKey`/`sslCert`, falling back to the bundled self-signed certificate like `http2` does. diff --git a/docs/docs/dev-server/cli-and-configuration.md b/docs/docs/dev-server/cli-and-configuration.md index f34daff6e5..8752f5173e 100644 --- a/docs/docs/dev-server/cli-and-configuration.md +++ b/docs/docs/dev-server/cli-and-configuration.md @@ -120,6 +120,8 @@ interface DevServerConfig { // whether to run the server with HTTP2 http2?: boolean; + // whether to run the server with HTTPS over HTTP/1.1, ignored when http2 is set + https?: boolean; // path to SSL key sslKey?: string; // path to SSL certificate diff --git a/packages/dev-server-core/src/server/DevServerCoreConfig.ts b/packages/dev-server-core/src/server/DevServerCoreConfig.ts index 258e310a14..b7e6ca722d 100644 --- a/packages/dev-server-core/src/server/DevServerCoreConfig.ts +++ b/packages/dev-server-core/src/server/DevServerCoreConfig.ts @@ -48,6 +48,10 @@ export interface DevServerCoreConfig { * Whether to run the server with HTTP2 */ http2?: boolean; + /** + * Whether to run the server with HTTPS over HTTP/1.1. Ignored when http2 is set. + */ + https?: boolean; /** * Path to SSL key */ diff --git a/packages/dev-server-core/src/server/createServer.ts b/packages/dev-server-core/src/server/createServer.ts index 557e90d95c..962682c940 100644 --- a/packages/dev-server-core/src/server/createServer.ts +++ b/packages/dev-server-core/src/server/createServer.ts @@ -2,6 +2,7 @@ import { FSWatcher } from 'chokidar'; import fs from 'fs'; import httpServer, { IncomingMessage, ServerResponse } from 'http'; import http2Server from 'http2'; +import httpsServer from 'https'; import Koa from 'koa'; import net, { ListenOptions, Server, Socket } from 'net'; import path from 'path'; @@ -65,7 +66,7 @@ export function createServer( } let server: Server; - if (cfg.http2) { + if (cfg.http2 || cfg.https) { const dir = path.join(__dirname, '..'); const options = { key: fs.readFileSync( @@ -78,12 +79,15 @@ export function createServer( ? path.resolve(cfg.sslCert) : path.join(dir, '..', '.self-signed-dev-server-ssl.cert'), ), - allowHTTP1: true, - maxSessionMemory: 20, }; const httpsRedirectServer = httpServer.createServer(httpsRedirect); - server = http2Server.createSecureServer(options, app.callback()); + server = cfg.http2 + ? http2Server.createSecureServer( + { ...options, allowHTTP1: true, maxSessionMemory: 20 }, + app.callback(), + ) + : httpsServer.createServer(options, app.callback()); let appServerPort: number; let httpsRedirectServerPort: number; diff --git a/packages/dev-server-core/src/test-helpers.ts b/packages/dev-server-core/src/test-helpers.ts index fc34d83775..22d17d26b7 100644 --- a/packages/dev-server-core/src/test-helpers.ts +++ b/packages/dev-server-core/src/test-helpers.ts @@ -39,7 +39,7 @@ export async function createTestServer( await server.start(); const url = new URL('http://localhost'); - url.protocol = config.http2 ? 'https' : 'http'; + url.protocol = config.http2 || config.https ? 'https' : 'http'; url.port = port.toString(); return { server, port, host: url.toString().slice(0, -1) }; } diff --git a/packages/dev-server-core/test/server/DevServer.test.ts b/packages/dev-server-core/test/server/DevServer.test.ts index 02e5896085..95e2605556 100644 --- a/packages/dev-server-core/test/server/DevServer.test.ts +++ b/packages/dev-server-core/test/server/DevServer.test.ts @@ -1,10 +1,12 @@ import express from 'express'; import http from 'http'; +import https from 'https'; import Koa from 'koa'; import { Server } from 'net'; import assert from 'node:assert/strict'; import { after, afterEach, before, beforeEach, describe, it, mock } from 'node:test'; import portfinder from 'portfinder'; +import type { TLSSocket } from 'tls'; import { assertIncludes } from '../../../../test-helpers/node.js'; import type { ServerStartParams } from '../../dist/plugins/Plugin.js'; import type { DevServer } from '../../dist/server/DevServer.js'; @@ -90,6 +92,35 @@ describe('http2', () => { }); }); +describe('https', () => { + it('serves a website over HTTP/1.1 with TLS', async () => { + const { server, host } = await createTestServer({ hostname: 'localhost', https: true }); + assert.ok(host.startsWith('https://')); + + const { res, body, encrypted } = await new Promise<{ + res: http.IncomingMessage; + body: string; + encrypted: boolean; + }>((resolve, reject) => { + https + .get(`${host}/index.html`, { rejectUnauthorized: false }, res => { + const { encrypted } = res.socket as TLSSocket; + let body = ''; + res.setEncoding('utf8'); + res.on('data', chunk => (body += chunk)); + res.on('end', () => resolve({ res, body, encrypted })); + }) + .on('error', reject); + }); + + assert.ok(encrypted); + assert.equal(res.httpVersion, '1.1'); + assert.equal(res.statusCode, 200); + assertIncludes(body, '