From 129a3f8aff5517be2eb4b1a33789fe5cf1cb072f Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Sun, 10 Dec 2023 00:54:41 +0100 Subject: [PATCH] feat(cli): http2 switch --- README.md | 7 +++++++ src/cli.ts | 6 ++++++ src/listen.ts | 49 ++++++++++++++++++++++++++++------------------ src/types.ts | 5 +++-- test/index.test.ts | 15 +++++++++++++- 5 files changed, 60 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 2f4dc72..8ef994b 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,13 @@ When the keystore is password protected also set `passphrase`. You can also provide an inline cert and key instead of reading from the filesystem. In this case, they should start with `--`. +### `http2` + +- Type: Boolean +- Default: `false` + +HTTP-Versions 1 and 2 will be used when enabled; otherwise only HTTP 1 is used. + ### `showURL` - Default: `true` (force disabled on a test environment) diff --git a/src/cli.ts b/src/cli.ts index c72de18..2359396 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -116,6 +116,11 @@ export function getArgs() { description: "Comma seperated list of domains and IPs, the autogenerated certificate should be valid for (https: true)", }, + http2: { + type: "boolean", + description: "Enable serving HTTP Protocol Version 2 requests", + required: false, + }, publicURL: { type: "string", description: "Displayed public URL (used for QR code)", @@ -167,5 +172,6 @@ export function parseArgs(args: ParsedListhenArgs): Partial { : undefined, } : false, + http2: args.http2, }; } diff --git a/src/listen.ts b/src/listen.ts index 3382a32..6754203 100644 --- a/src/listen.ts +++ b/src/listen.ts @@ -1,6 +1,12 @@ -import { createServer, IncomingMessage, ServerResponse } from "node:http"; import { - createSecureServer, + createServer as createHttpServer, + IncomingMessage, + ServerResponse, +} from "node:http"; +import { createServer as createHttpsServer } from "node:https"; +import { + createSecureServer as createHttps2Server, + createServer as createHttp2Server, Http2ServerRequest, Http2ServerResponse, } from "node:http2"; @@ -10,27 +16,27 @@ import { getPort } from "get-port-please"; import addShutdown from "http-shutdown"; import consola from "consola"; import { defu } from "defu"; -import { ColorName, getColor, colors } from "consola/utils"; +import { ColorName, colors, getColor } from "consola/utils"; import { renderUnicodeCompact as renderQRCode } from "uqr"; import type { Tunnel } from "untun"; import { open } from "./lib/open"; import type { - ListenOptions, - Listener, - ShowURLOptions, + GetURLOptions, HTTPSOptions, + Listener, + ListenOptions, ListenURL, - GetURLOptions, Server, + ShowURLOptions, } from "./types"; import { formatURL, - getNetworkInterfaces, - isLocalhost, - isAnyhost, - getPublicURL, generateURL, getDefaultHost, + getNetworkInterfaces, + getPublicURL, + isAnyhost, + isLocalhost, validateHostname, } from "./_utils"; import { resolveCertificate } from "./_cert"; @@ -71,6 +77,7 @@ export async function listen( const listhenOptions = defu(_options, { name: "", https: false, + http2: false, port: process.env.PORT || 3000, hostname: _hostname ?? getDefaultHost(_public), showURL: true, @@ -133,20 +140,24 @@ export async function listen( let _addr: AddressInfo; if (httpsOptions) { https = await resolveCertificate(httpsOptions); - server = createSecureServer( - { - ...https, - allowHTTP1: true, - }, - handle as RequestListenerHttp2, - ); + server = listhenOptions.http2 + ? createHttps2Server( + { + ...https, + allowHTTP1: true, + }, + handle as RequestListenerHttp2, + ) + : createHttpsServer(https, handle as RequestListenerHttp1x); addShutdown(server); // @ts-ignore await promisify(server.listen.bind(server))(port, listhenOptions.hostname); _addr = server.address() as AddressInfo; listhenOptions.port = _addr.port; } else { - server = createServer(handle as RequestListenerHttp1x); + server = listhenOptions.http2 + ? createHttp2Server(handle as RequestListenerHttp2) + : createHttpServer(handle as RequestListenerHttp1x); addShutdown(server); // @ts-ignore await promisify(server.listen.bind(server))(port, listhenOptions.hostname); diff --git a/src/types.ts b/src/types.ts index 263ab98..fd98ec2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,10 +1,10 @@ import type { Server as HttpServer } from "node:http"; import type { Server as HttpsServer } from "node:https"; -import type { Http2SecureServer } from "node:http2"; +import type { Http2Server, Http2SecureServer } from "node:http2"; import type { AddressInfo } from "node:net"; import type { GetPortInput } from "get-port-please"; -export type Server = HttpServer | HttpsServer | Http2SecureServer; +export type Server = HttpServer | HttpsServer | Http2Server | Http2SecureServer; export interface Certificate { key: string; @@ -29,6 +29,7 @@ export interface ListenOptions { baseURL: string; open: boolean; https: boolean | HTTPSOptions; + http2: boolean; clipboard: boolean; isTest: boolean; isProd: boolean; diff --git a/test/index.test.ts b/test/index.test.ts index dd0ec62..6e4bc63 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -106,9 +106,21 @@ describe("listhen", () => { }); // see https://http2.github.io/faq/#does-http2-require-encryption - test("listen (http2)", async () => { + test("listen (http2): http1 client", async () => { listener = await listen(handle); expect(listener.url.startsWith("http://")).toBeTruthy(); + + const response = (await sendRequest(listener.url, false)) as string; + expect(JSON.parse(response)).toEqual({ + path: "/", + httpVersion: "1.1", + }); + }); + test("listhen (http2): http2 client", async () => { + listener = await listen(handle); + expect(listener.url.startsWith("http://")).toBeTruthy(); + + // Protocol HTTP 0.9 is used in firefox await expect(sendHttp2Request(listener.url)).rejects.toThrowError( "Protocol error", ); @@ -118,6 +130,7 @@ describe("listhen", () => { test("listen (http2)", async () => { listener = await listen(handle, { https: true, + http2: true, }); expect(listener.url.startsWith("https:")).toBeTruthy();