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

feat: http2 support #106

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
638fc51
feat: http2 support
Mastercuber Aug 14, 2023
ea19891
test: http2 support
Mastercuber Sep 10, 2023
51a1782
update package name
joshmossas Dec 9, 2023
be54460
blah
joshmossas Dec 9, 2023
1d0f177
Merge branch 'main' into http2-support
joshmossas Dec 9, 2023
b1c38a1
feat: http2 support
Mastercuber Aug 14, 2023
187216b
test: http2 support
Mastercuber Sep 10, 2023
129a3f8
feat(cli): http2 switch
Mastercuber Dec 9, 2023
731f58e
Merge branch 'http2-support' of https://github.com/Mastercuber/listhe…
joshmossas Dec 13, 2023
33a0d63
allow listening to http1.1 and http2 with non-ssl
joshmossas Dec 13, 2023
35fe367
switching protocol (101) on Upgrade request
Mastercuber Dec 14, 2023
b8b1608
test: fix response from http2 client
Mastercuber Dec 14, 2023
1fb286a
Merge branch 'http2-support' of https://github.com/Mastercuber/listhe…
joshmossas Dec 14, 2023
55bb7fa
rename
joshmossas Dec 17, 2023
ed229de
more experiments
joshmossas Dec 17, 2023
d885b06
clean up
joshmossas Dec 21, 2023
0a78b07
Merge branch 'main' of https://github.com/unjs/listhen
joshmossas Feb 6, 2024
20da26a
update
joshmossas Feb 6, 2024
f6603fd
Merge branch 'main' of https://github.com/unjs/listhen into http2-sup…
joshmossas Mar 19, 2024
1ac005e
add any
joshmossas Mar 19, 2024
61af2ac
get websockets working with http2 and non-https
joshmossas Mar 19, 2024
0b5f80e
add ws support
joshmossas Mar 19, 2024
647d29b
remove unecessary cast
joshmossas Mar 19, 2024
b8ec179
fix(test): start listener in http2 mode
Mastercuber Mar 20, 2024
5b7bbf2
also log websocket URL
Mastercuber Mar 20, 2024
f6beab5
chore: apply automated updates
autofix-ci[bot] Mar 20, 2024
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,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)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,4 @@
"vitest": "^1.3.1"
},
"packageManager": "pnpm@8.15.4"
}
}
6 changes: 6 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,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)",
Expand Down Expand Up @@ -172,5 +177,6 @@ export function parseArgs(args: ParsedListhenArgs): Partial<ListenOptions> {
: undefined,
}
: false,
http2: args.http2,
};
}
133 changes: 105 additions & 28 deletions src/listen.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,65 @@
import { createServer } from "node:http";
import type { Server as HTTPServer } from "node:https";
import { createServer as createHTTPSServer } from "node:https";
import {
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";
import { promisify } from "node:util";
import type { RequestListener, Server } from "node:http";
import type { AddressInfo } from "node:net";
import { createServer as createRawTcpIpcServer, AddressInfo } from "node:net";
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 type { AdapterOptions as CrossWSOptions } from "crossws";
import type { Adapter as CrossWSOptions } from "crossws";
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";
import { isWsl } from "./lib/wsl";
import { isDocker } from "./lib/docker";

type RequestListenerHttp1x<
Request extends typeof IncomingMessage = typeof IncomingMessage,
Response extends
typeof ServerResponse<IncomingMessage> = typeof ServerResponse<IncomingMessage>,
> = (
req: InstanceType<Request>,
res: InstanceType<Response> & { req: InstanceType<Request> },
) => void;

type RequestListenerHttp2<
Request extends typeof Http2ServerRequest = typeof Http2ServerRequest,
Response extends typeof Http2ServerResponse = typeof Http2ServerResponse,
> = (request: InstanceType<Request>, response: InstanceType<Response>) => void;

type RequestListener = RequestListenerHttp1x | RequestListenerHttp2;

export async function listen(
handle: RequestListener,
_options: Partial<ListenOptions> = {},
Expand All @@ -53,6 +78,7 @@ export async function listen(
const listhenOptions = defu<ListenOptions, ListenOptions[]>(_options, {
name: "",
https: false,
http2: false,
port: process.env.PORT || 3000,
hostname: _hostname ?? getDefaultHost(_public),
showURL: true,
Expand Down Expand Up @@ -109,31 +135,67 @@ export async function listen(
}));

// --- Listen ---
let server: Server | HTTPServer;
let server: Server;
let wsTargetServer: Server | undefined;
let https: Listener["https"] = false;
const httpsOptions = listhenOptions.https as HTTPSOptions;
let _addr: AddressInfo;
if (httpsOptions) {
https = await resolveCertificate(httpsOptions);
server = createHTTPSServer(https, handle);
addShutdown(server);

async function bind() {
// @ts-ignore
await promisify(server.listen.bind(server))(port, listhenOptions.hostname);
_addr = server.address() as AddressInfo;
listhenOptions.port = _addr.port;
}
if (httpsOptions) {
https = await resolveCertificate(httpsOptions);
server = listhenOptions.http2
? createHttps2Server(
{
...https,
allowHTTP1: true,
},
handle as RequestListenerHttp2,
)
: createHttpsServer(https, handle as RequestListenerHttp1x);
addShutdown(server);
await bind();
} else if (listhenOptions.http2) {
const h1Server = createHttpServer(handle as RequestListenerHttp1x);
const h2Server = createHttp2Server(handle as RequestListenerHttp2);
server = createRawTcpIpcServer(async (socket) => {
const chunk = await new Promise((resolve) =>
socket.once("data", resolve),
);
// @ts-expect-error
socket._readableState.flowing = undefined;
socket.unshift(chunk);
if ((chunk as any).toString("utf8", 0, 3) === "PRI") {
h2Server.emit("connection", socket);
return;
}
h1Server.emit("connection", socket);
});

// websockets need to listen for upgrades here when both http1 and http2 and running without https
wsTargetServer = h1Server;

addShutdown(server);
await bind();
} else {
server = createServer(handle);
server = createHttpServer(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;
await bind();
}

// --- WebSocket ---
if (listhenOptions.ws) {
if (typeof listhenOptions.ws === "function") {
server.on("upgrade", listhenOptions.ws);
if (wsTargetServer) {
wsTargetServer.on("upgrade", listhenOptions.ws);
} else {
server.on("upgrade", listhenOptions.ws);
}
} else {
consola.warn(
"[listhen] Using experimental websocket API. Learn more: `https://crossws.unjs.io`",
Expand All @@ -142,9 +204,13 @@ export async function listen(
(r) => r.default || r,
);
const { handleUpgrade } = nodeWSAdapter({
...(listhenOptions.ws as CrossWSOptions),
...(listhenOptions.ws as CrossWSOptions<any, any>),
});
server.on("upgrade", handleUpgrade);
if (wsTargetServer) {
wsTargetServer.on("upgrade", handleUpgrade);
} else {
server.on("upgrade", handleUpgrade);
}
}
}

Expand Down Expand Up @@ -206,6 +272,16 @@ export async function listen(
_addURL("local", getURL(listhenOptions.hostname, getURLOptions.baseURL));
}

if (listhenOptions.ws) {
_addURL(
"local",
getURL(listhenOptions.hostname, getURLOptions.baseURL).replace(
"http",
"ws",
),
);
}

// Add tunnel URL
if (tunnel) {
_addURL("tunnel", await tunnel.getURL());
Expand Down Expand Up @@ -305,6 +381,7 @@ export async function listen(
url: getURL(),
https,
server,
// @ts-ignoref
address: _addr,
open: _open,
showURL,
Expand Down
11 changes: 9 additions & 2 deletions src/server/dev.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { existsSync, statSync } from "node:fs";
import { readFile, stat } from "node:fs/promises";
import { Http2ServerRequest, Http2ServerResponse } from "node:http2";
import { IncomingMessage, ServerResponse } from "node:http";
import { consola } from "consola";
import { dirname, join, resolve } from "pathe";
import type { ConsolaInstance } from "consola";
Expand All @@ -14,6 +16,11 @@ export interface DevServerOptions {
ws?: ListenOptions["ws"];
}

type NodeListener = (
req: IncomingMessage | Http2ServerRequest,
res: ServerResponse | Http2ServerResponse,
) => void;

export async function createDevServer(
entry: string,
options: DevServerOptions,
Expand Down Expand Up @@ -61,7 +68,7 @@ export async function createDevServer(
if (_ws && typeof _ws !== "function") {
_ws = {
...(options.ws as CrossWSOptions),
async resolve(info) {
async resolve(info: any) {
return {
...(await (options.ws as CrossWSOptions)?.resolve?.(info)),
...dynamicWS.hooks,
Expand Down Expand Up @@ -186,7 +193,7 @@ export async function createDevServer(
return {
cwd,
resolver,
nodeListener: toNodeListener(app),
nodeListener: toNodeListener(app) as NodeListener,
reload: (_initial?: boolean) => loadHandle(_initial),
_ws,
_entry: resolveEntry(),
Expand Down
17 changes: 13 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import type { IncomingMessage, Server } from "node:http";
import type { Server as HTTPServer } from "node:https";
import { AddressInfo } from "node:net";
import type { Server as HttpServer, IncomingMessage } from "node:http";
import type { Server as HttpsServer } from "node:https";
import type { Http2Server, Http2SecureServer } from "node:http2";
import type { AddressInfo, Server as RawTcpIpcServer } from "node:net";
import type { GetPortInput } from "get-port-please";
import type { NodeOptions } from "crossws/adapters/node";

export type CrossWSOptions = NodeOptions;

export type Server =
| HttpServer
| HttpsServer
| Http2Server
| Http2SecureServer
| RawTcpIpcServer;

export interface Certificate {
key: string;
cert: string;
Expand All @@ -29,6 +37,7 @@ export interface ListenOptions {
baseURL: string;
open: boolean;
https: boolean | HTTPSOptions;
http2: boolean;
clipboard: boolean;
isTest: boolean;
isProd: boolean;
Expand Down Expand Up @@ -87,7 +96,7 @@ export interface ListenURL {
export interface Listener {
url: string;
address: AddressInfo;
server: Server | HTTPServer;
server: Server;
https: false | Certificate;
close: () => Promise<void>;
open: () => Promise<void>;
Expand Down
Loading