From d72e391319fa9e67654907c89947c54fe21d7254 Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Fri, 22 Mar 2024 19:34:09 +0100 Subject: [PATCH 01/16] feat: unix domain socket and windows named pipe support --- README.md | 6 ++++++ src/_utils.ts | 8 +++++++- src/cli.ts | 7 +++++++ src/listen.ts | 36 +++++++++++++++++++++++++++++++++--- src/types.ts | 5 +++++ 5 files changed, 58 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 05298ea..23783fe 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,12 @@ Option can be a function for Node.js `upgrade` handler (`(req, head) => void`) o When using dev server CLI, you can easily use `--ws` and a named export called `websocket` to define [CrossWS Hooks](https://github.com/unjs/crossws) with HMR support! +### `ipc [name]` + +- Default: `false [listhen]` + +Enables IPC (`--ipc`) through unix domain sockets on unixoid systems and named pipes on windows (unix: `/tmp/listhen.socket`; windows: `\\?\pipe\listhen`). + ## License diff --git a/src/_utils.ts b/src/_utils.ts index 6e355e7..1ddf46a 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -1,4 +1,4 @@ -import { networkInterfaces } from "node:os"; +import { networkInterfaces, platform } from "node:os"; import { relative } from "pathe"; import { colors } from "consola/utils"; import { consola } from "consola"; @@ -88,6 +88,12 @@ export function getDefaultHost(preferPublic?: boolean) { return preferPublic ? "" : "localhost"; } +export function getSocketPath(ipcSocketName: string) { + return platform() === "win32" + ? `\\\\?\\pipe\\${ipcSocketName || "listhen"}` + : `/tmp/${ipcSocketName || "listhen"}.socket`; +} + export function getPublicURL( listhenOptions: ListenOptions, baseURL?: string, diff --git a/src/cli.ts b/src/cli.ts index 879e99d..d7b47ac 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -141,6 +141,12 @@ export function getArgs() { description: "Open a tunnel using https://github.com/unjs/untun", required: false, }, + ipc: { + type: "string", + description: + "Listen on a Unix Domain Socket/Windows Pipe, optionally with custom name", + required: false, + }, } as const satisfies ArgsDef; } @@ -157,6 +163,7 @@ export function parseArgs(args: ParsedListhenArgs): Partial { qr: args.qr, publicURL: args.publicURL, public: args.public, + ipc: args.ipc, tunnel: args.tunnel, https: args.https ? { diff --git a/src/listen.ts b/src/listen.ts index f3e4d3c..ab2333a 100644 --- a/src/listen.ts +++ b/src/listen.ts @@ -27,6 +27,7 @@ import { isLocalhost, isAnyhost, getPublicURL, + getSocketPath, generateURL, getDefaultHost, validateHostname, @@ -62,6 +63,7 @@ export async function listen( isTest: _isTest, isProd: _isProd, public: _public, + socket: false, autoClose: true, }); @@ -112,20 +114,43 @@ export async function listen( let server: Server | HTTPServer; let https: Listener["https"] = false; const httpsOptions = listhenOptions.https as HTTPSOptions; + + const ipcSocket = getSocketPath(listhenOptions.socket); + + function constructServerListeningArgs() { + return listhenOptions.socket === "_____" + ? { + port, + hostname: listhenOptions.hostname, + *[Symbol.iterator]() { + yield this.port; + yield this.hostname; + }, + } + : { + ipcSocket, + *[Symbol.iterator]() { + yield this.ipcSocket; + }, + }; + } + let _addr: AddressInfo; if (httpsOptions) { https = await resolveCertificate(httpsOptions); server = createHTTPSServer(https, handle); addShutdown(server); + const args = constructServerListeningArgs(); // @ts-ignore - await promisify(server.listen.bind(server))(port, listhenOptions.hostname); + await promisify(server.listen.bind(server))(...args); _addr = server.address() as AddressInfo; listhenOptions.port = _addr.port; } else { server = createServer(handle); addShutdown(server); + const args = constructServerListeningArgs(); // @ts-ignore - await promisify(server.listen.bind(server))(port, listhenOptions.hostname); + await promisify(server.listen.bind(server))(...args); _addr = server.address() as AddressInfo; listhenOptions.port = _addr.port; } @@ -193,6 +218,11 @@ export async function listen( } }; + if (listhenOptions.ipc !== "_____") { + _addURL("local", ipcSocket); + return urls; + } + // Add public URL const publicURL = getURLOptions.publicURL || @@ -272,7 +302,7 @@ export async function listen( lines.push(`${label} ${formatURL(url.url)}${suffix}`); } - if (!firstPublicUrl) { + if (!firstPublicUrl && listhenOptions.ipc === "_____") { lines.push( colors.gray(` ➜ Network: use ${colors.white("--host")} to expose`), ); diff --git a/src/types.ts b/src/types.ts index d1316d5..2a2c4fe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -67,6 +67,11 @@ export interface ListenOptions { | boolean | CrossWSOptions | ((req: IncomingMessage, head: Buffer) => void); + * Listhen on a unix domain socket/windows pipe, optionally with custom name + * + * @default listhen + */ + ipc: string; } export type GetURLOptions = Pick< From 795fb57d5a754621b5ccb90e6d33728cb0784a1d Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Wed, 16 Aug 2023 21:33:54 +0200 Subject: [PATCH 02/16] test: http/https connection to socket created with handle/h3App --- test/index.test.ts | 157 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/test/index.test.ts b/test/index.test.ts index 3cf2142..77422ba 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,7 +1,36 @@ import { resolve } from "node:path"; import type { IncomingMessage, ServerResponse } from "node:http"; +import { request } from "node:http"; +import { request as httpsRequest } from "node:https"; +import { platform } from "node:os"; import { describe, afterEach, test, expect } from "vitest"; +import { toNodeListener, createApp, eventHandler, createRouter } from "h3"; import { listen, Listener } from "../src"; +import { getSocketPath } from "../src/_utils"; + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; + +function getApp() { + const app = createApp({}); + + const router = createRouter() + .get( + "/", + eventHandler(() => ({ hello: "world!" })), + ) + .get( + "/path", + eventHandler(() => ({ hello: "path!" })), + ) + .get( + "/unix", + eventHandler(() => ({ hello: "unix!" })), + ); + + app.use(router); + + return app; +} // eslint-disable-next-line no-console // console.log = fn() @@ -10,6 +39,34 @@ function handle(request: IncomingMessage, response: ServerResponse) { response.end(request.url); } +function ipcRequest(ipcSocket: string, path: string, https = false) { + return new Promise((resolve, reject) => { + (https ? httpsRequest : request)( + { + socketPath: ipcSocket, + path, + }, + (res) => { + const data: any[] = []; + res.setEncoding("utf8"); + res.on("data", (chunk) => { + data.push(chunk); + }); + res.on("error", (e) => { + reject(e); + }); + res.on("end", () => { + try { + resolve(JSON.parse(data.join(""))); + } catch { + resolve(data.join("")); + } + }); + }, + ).end(); + }); +} + describe("listhen", () => { let listener: Listener | undefined; @@ -19,6 +76,32 @@ describe("listhen", () => { listener = undefined; } }); + async function h3AppAssertions(ipcSocket: string, https: boolean) { + expect(listener!.url).toBe(ipcSocket); + + await expect(ipcRequest(ipcSocket, "/", https)).resolves.toEqual({ + hello: "world!", + }); + await expect(ipcRequest(ipcSocket, "/path", https)).resolves.toEqual({ + hello: "path!", + }); + await expect(ipcRequest(ipcSocket, "/unix", https)).resolves.toEqual({ + hello: "unix!", + }); + await expect(ipcRequest(ipcSocket, "/test", https)).resolves.toContain({ + statusCode: 404, + }); + } + + async function handleAssertions(ipcSocket: string, https: boolean) { + expect(listener!.url).toBe(ipcSocket); + + await expect(ipcRequest(ipcSocket, "/", https)).resolves.toEqual("/"); + await expect(ipcRequest(ipcSocket, "/path", https)).resolves.toEqual( + "/path", + ); + } + test("should listen to the next port in range (3000 -> 31000)", async () => { listener = await listen(handle, { port: { port: 3000 }, @@ -47,6 +130,30 @@ describe("listhen", () => { // expect(console.log).toHaveBeenCalledWith(expect.stringMatching('\n > Local: http://localhost:3000/foo/bar')) }); + test("listen on unix domain socket/windows named pipe (handle)", async () => { + const ipcSocketName = "listhen"; + const ipcSocket = getSocketPath(ipcSocketName); + + listener = await listen(handle, { + ipc: ipcSocketName, + }); + + await handleAssertions(ipcSocket, false); + }); + + test("listen on unix domain socket/windows named pipe (h3 app)", async () => { + const ipcSocketName = "listhen2"; + const ipcSocket = getSocketPath(ipcSocketName); + + listener = await listen(toNodeListener(getApp()), { + ipc: ipcSocketName, + }); + + expect(listener.url).toBe(ipcSocket); + + await h3AppAssertions(ipcSocket, false); + }); + describe("https", () => { test("listen (https - selfsigned)", async () => { listener = await listen(handle, { https: true, hostname: "localhost" }); @@ -66,6 +173,32 @@ describe("listhen", () => { expect(listener.url.startsWith("https://")).toBe(true); }); + test("listen (https - unix domain socket/windows named pipe - handle)", async () => { + const ipcSocketName = "listhen-https"; + const ipcSocket = getSocketPath(ipcSocketName); + + listener = await listen(handle, { + ipc: ipcSocketName, + https: true, + }); + + expect(listener.url).toBe(ipcSocket); + + await handleAssertions(ipcSocket, true); + }); + + test("listen (https - unix domain socket/windows named pipe - h3 app)", async () => { + const ipcSocketName = "listhen-https"; + const ipcSocket = getSocketPath(ipcSocketName); + + listener = await listen(toNodeListener(getApp()), { + ipc: ipcSocketName, + https: true, + }); + + await h3AppAssertions(ipcSocket, true); + }); + test("listen (https - custom - with private key passphrase)", async () => { listener = await listen(handle, { https: { @@ -173,4 +306,28 @@ describe("listhen", () => { expect(listener.url).toMatch(/:5\d{4}\/$/); }); }); + + describe("_utils", () => { + test("getSocketPath: empty ipcSocketName resolves to a 'listhen' named pipe/socket", () => { + if (platform() === "win32") { + expect(getSocketPath(undefined!)).toEqual("\\\\?\\pipe\\listhen"); + expect(getSocketPath("")).toEqual("\\\\?\\pipe\\listhen"); + } else { + expect(getSocketPath(undefined!)).toEqual("/tmp/listhen.socket"); + expect(getSocketPath("")).toEqual("/tmp/listhen.socket"); + } + }); + + test("getSocketPath: some string as ipcSocketName resolves to a pipe/socket named as this string", () => { + if (platform() === "win32") { + expect(getSocketPath("listhen-https")).toEqual( + "\\\\?\\pipe\\listhen-https", + ); + } else { + expect(getSocketPath("listhen-https")).toEqual( + "/tmp/listhen-https.socket", + ); + } + }); + }); }); From 32da9b953c48047e02ca1443a9139e092b867419 Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Sat, 19 Aug 2023 23:14:33 +0200 Subject: [PATCH 03/16] indicating if https or http is used with a domain socket --- src/_utils.ts | 6 ++++++ src/listen.ts | 24 ++++++++++++++---------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/_utils.ts b/src/_utils.ts index 1ddf46a..48f190c 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -94,6 +94,12 @@ export function getSocketPath(ipcSocketName: string) { : `/tmp/${ipcSocketName || "listhen"}.socket`; } +export const IPC_NOT_USED_NAME = "_____"; + +export function isSocketUsed(options: ListenOptions): boolean { + return options.ipc !== IPC_NOT_USED_NAME; +} + export function getPublicURL( listhenOptions: ListenOptions, baseURL?: string, diff --git a/src/listen.ts b/src/listen.ts index ab2333a..2b8e4cf 100644 --- a/src/listen.ts +++ b/src/listen.ts @@ -118,19 +118,19 @@ export async function listen( const ipcSocket = getSocketPath(listhenOptions.socket); function constructServerListeningArgs() { - return listhenOptions.socket === "_____" + return listhenOptions.socket ? { - port, - hostname: listhenOptions.hostname, + ipcSocket, *[Symbol.iterator]() { - yield this.port; - yield this.hostname; + yield this.ipcSocket; }, } : { - ipcSocket, + port, + hostname: listhenOptions.hostname, *[Symbol.iterator]() { - yield this.ipcSocket; + yield this.port; + yield this.hostname; }, }; } @@ -218,7 +218,7 @@ export async function listen( } }; - if (listhenOptions.ipc !== "_____") { + if (listhenOptions.socket) { _addURL("local", ipcSocket); return urls; } @@ -289,8 +289,12 @@ export async function listen( for (const url of urls) { const type = typeMap[url.type]; + let infix = ""; + if (listhenOptions.socket && url.type === "local") { + infix = listhenOptions.https ? " (https)" : " (http)"; + } const label = getColor(type[1])( - ` ➜ ${(type[0] + ":").padEnd(8, " ")}${nameSuffix} `, + ` ➜ ${(type[0] + infix + ":").padEnd(8, " ")}${nameSuffix} `, ); let suffix = ""; if (url === firstLocalUrl && listhenOptions.clipboard) { @@ -302,7 +306,7 @@ export async function listen( lines.push(`${label} ${formatURL(url.url)}${suffix}`); } - if (!firstPublicUrl && listhenOptions.ipc === "_____") { + if (!firstPublicUrl && !listhenOptions.socket) { lines.push( colors.gray(` ➜ Network: use ${colors.white("--host")} to expose`), ); From 7e534cce9fe6a0a744900e9bac744b56723f1534 Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Sun, 20 Aug 2023 05:59:22 +0200 Subject: [PATCH 04/16] use default tmp directory for constructing socket path --- src/_utils.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/_utils.ts b/src/_utils.ts index 48f190c..8f5d001 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -1,5 +1,5 @@ -import { networkInterfaces, platform } from "node:os"; -import { relative } from "pathe"; +import { networkInterfaces, platform, tmpdir } from "node:os"; +import { relative, join } from "pathe"; import { colors } from "consola/utils"; import { consola } from "consola"; import { provider } from "std-env"; @@ -89,9 +89,13 @@ export function getDefaultHost(preferPublic?: boolean) { } export function getSocketPath(ipcSocketName: string) { - return platform() === "win32" - ? `\\\\?\\pipe\\${ipcSocketName || "listhen"}` - : `/tmp/${ipcSocketName || "listhen"}.socket`; + if (platform() === "win32") { + return `\\\\?\\pipe\\${ipcSocketName || "listhen"}`; + } + return join( + tmpdir(), + ipcSocketName ? `${ipcSocketName}.socket` : "listhen.socket", + ); } export const IPC_NOT_USED_NAME = "_____"; From dc5b92cdc8776b4337f388f2170bd87c7db1d620 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 9 Sep 2023 01:25:23 +0200 Subject: [PATCH 05/16] small refactors --- README.md | 4 +--- src/_utils.ts | 16 ++++------------ src/cli.ts | 5 ++--- src/listen.ts | 33 +++++++++------------------------ src/types.ts | 3 +-- test/index.test.ts | 8 ++++---- 6 files changed, 21 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index 23783fe..9d90106 100644 --- a/README.md +++ b/README.md @@ -211,9 +211,7 @@ When using dev server CLI, you can easily use `--ws` and a named export called ` ### `ipc [name]` -- Default: `false [listhen]` - -Enables IPC (`--ipc`) through unix domain sockets on unixoid systems and named pipes on windows (unix: `/tmp/listhen.socket`; windows: `\\?\pipe\listhen`). +Enables IPC: Unix domain sockets are used on unixoid systems - instead of listening to network interfaces - and named pipes on windows (unix: `$PWD/listhen.sock`; windows: `\\?\pipe\listhen`). It's also possible to pass an absolute socket path and full pipe path. ## License diff --git a/src/_utils.ts b/src/_utils.ts index 8f5d001..1f412ba 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -88,20 +88,12 @@ export function getDefaultHost(preferPublic?: boolean) { return preferPublic ? "" : "localhost"; } -export function getSocketPath(ipcSocketName: string) { +export function getSocketPath(name: true | string) { + const _name = typeof name === "string" ? name : "listhen"; if (platform() === "win32") { - return `\\\\?\\pipe\\${ipcSocketName || "listhen"}`; + return `\\\\?\\pipe\\${_name}`; } - return join( - tmpdir(), - ipcSocketName ? `${ipcSocketName}.socket` : "listhen.socket", - ); -} - -export const IPC_NOT_USED_NAME = "_____"; - -export function isSocketUsed(options: ListenOptions): boolean { - return options.ipc !== IPC_NOT_USED_NAME; + return join(tmpdir(), `${_name}.socket`); } export function getPublicURL( diff --git a/src/cli.ts b/src/cli.ts index d7b47ac..506a38e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -141,8 +141,7 @@ export function getArgs() { description: "Open a tunnel using https://github.com/unjs/untun", required: false, }, - ipc: { - type: "string", + socket: { description: "Listen on a Unix Domain Socket/Windows Pipe, optionally with custom name", required: false, @@ -163,8 +162,8 @@ export function parseArgs(args: ParsedListhenArgs): Partial { qr: args.qr, publicURL: args.publicURL, public: args.public, - ipc: args.ipc, tunnel: args.tunnel, + socket: args.socket, https: args.https ? { cert: args["https.cert"], diff --git a/src/listen.ts b/src/listen.ts index 2b8e4cf..a318ba5 100644 --- a/src/listen.ts +++ b/src/listen.ts @@ -112,45 +112,30 @@ export async function listen( // --- Listen --- let server: Server | HTTPServer; - let https: Listener["https"] = false; - const httpsOptions = listhenOptions.https as HTTPSOptions; - const ipcSocket = getSocketPath(listhenOptions.socket); + const serverOptions = listhenOptions.socket + ? { path: getSocketPath(listhenOptions.socket) } + : { port, host: listhenOptions.hostname }; - function constructServerListeningArgs() { - return listhenOptions.socket - ? { - ipcSocket, - *[Symbol.iterator]() { - yield this.ipcSocket; - }, - } - : { - port, - hostname: listhenOptions.hostname, - *[Symbol.iterator]() { - yield this.port; - yield this.hostname; - }, - }; - } + let addr: { proto: "http" | "https"; addr: string; port: number } | null; + + 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); - const args = constructServerListeningArgs(); // @ts-ignore - await promisify(server.listen.bind(server))(...args); + await promisify(server.listen.bind(server))(serverOptions); _addr = server.address() as AddressInfo; listhenOptions.port = _addr.port; } else { server = createServer(handle); addShutdown(server); - const args = constructServerListeningArgs(); // @ts-ignore - await promisify(server.listen.bind(server))(...args); + await promisify(server.listen.bind(server))(serverOptions); _addr = server.address() as AddressInfo; listhenOptions.port = _addr.port; } diff --git a/src/types.ts b/src/types.ts index 2a2c4fe..bda3543 100644 --- a/src/types.ts +++ b/src/types.ts @@ -69,9 +69,8 @@ export interface ListenOptions { | ((req: IncomingMessage, head: Buffer) => void); * Listhen on a unix domain socket/windows pipe, optionally with custom name * - * @default listhen */ - ipc: string; + socket: boolean | string; } export type GetURLOptions = Pick< diff --git a/test/index.test.ts b/test/index.test.ts index 77422ba..da519ec 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -135,7 +135,7 @@ describe("listhen", () => { const ipcSocket = getSocketPath(ipcSocketName); listener = await listen(handle, { - ipc: ipcSocketName, + socket: ipcSocketName, }); await handleAssertions(ipcSocket, false); @@ -146,7 +146,7 @@ describe("listhen", () => { const ipcSocket = getSocketPath(ipcSocketName); listener = await listen(toNodeListener(getApp()), { - ipc: ipcSocketName, + socket: ipcSocketName, }); expect(listener.url).toBe(ipcSocket); @@ -178,7 +178,7 @@ describe("listhen", () => { const ipcSocket = getSocketPath(ipcSocketName); listener = await listen(handle, { - ipc: ipcSocketName, + socket: ipcSocketName, https: true, }); @@ -192,7 +192,7 @@ describe("listhen", () => { const ipcSocket = getSocketPath(ipcSocketName); listener = await listen(toNodeListener(getApp()), { - ipc: ipcSocketName, + socket: ipcSocketName, https: true, }); From ea07d22cd0e79790d224ab43f8c5710ac8da7249 Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Sun, 10 Sep 2023 00:50:36 +0200 Subject: [PATCH 06/16] add/remove lost changes (rebase) --- src/listen.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/listen.ts b/src/listen.ts index a318ba5..99eb662 100644 --- a/src/listen.ts +++ b/src/listen.ts @@ -117,8 +117,6 @@ export async function listen( ? { path: getSocketPath(listhenOptions.socket) } : { port, host: listhenOptions.hostname }; - let addr: { proto: "http" | "https"; addr: string; port: number } | null; - let https: Listener["https"] = false; const httpsOptions = listhenOptions.https as HTTPSOptions; @@ -160,7 +158,9 @@ export async function listen( // --- GetURL Utility --- const getURL = (host = listhenOptions.hostname, baseURL?: string) => - generateURL(host, listhenOptions, baseURL); + serverOptions.path + ? `unix+http${listhenOptions.https ? "s" : ""}://${serverOptions.path}` + : generateURL(host, listhenOptions, baseURL); // --- Start Tunnel --- let tunnel: Tunnel | undefined; @@ -203,8 +203,8 @@ export async function listen( } }; - if (listhenOptions.socket) { - _addURL("local", ipcSocket); + if (serverOptions.path) { + _addURL("local", serverOptions.path); return urls; } From 4b043a7060e73035f07fc67b904addbcf5c22568 Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Sun, 10 Sep 2023 01:25:07 +0200 Subject: [PATCH 07/16] fix: expected url in tests --- src/cli.ts | 2 +- test/index.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 506a38e..ee4cacc 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -163,7 +163,7 @@ export function parseArgs(args: ParsedListhenArgs): Partial { publicURL: args.publicURL, public: args.public, tunnel: args.tunnel, - socket: args.socket, + socket: args.socket as boolean | string | undefined, https: args.https ? { cert: args["https.cert"], diff --git a/test/index.test.ts b/test/index.test.ts index da519ec..9fc89fa 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -77,7 +77,7 @@ describe("listhen", () => { } }); async function h3AppAssertions(ipcSocket: string, https: boolean) { - expect(listener!.url).toBe(ipcSocket); + expect(listener!.url).toBe(`unix+http${https ? "s" : ""}://${ipcSocket}`); await expect(ipcRequest(ipcSocket, "/", https)).resolves.toEqual({ hello: "world!", @@ -94,7 +94,7 @@ describe("listhen", () => { } async function handleAssertions(ipcSocket: string, https: boolean) { - expect(listener!.url).toBe(ipcSocket); + expect(listener!.url).toBe(`unix+http${https ? "s" : ""}://${ipcSocket}`); await expect(ipcRequest(ipcSocket, "/", https)).resolves.toEqual("/"); await expect(ipcRequest(ipcSocket, "/path", https)).resolves.toEqual( @@ -149,7 +149,7 @@ describe("listhen", () => { socket: ipcSocketName, }); - expect(listener.url).toBe(ipcSocket); + expect(listener.url).toBe(`unix+http://${ipcSocket}`); await h3AppAssertions(ipcSocket, false); }); @@ -182,7 +182,7 @@ describe("listhen", () => { https: true, }); - expect(listener.url).toBe(ipcSocket); + expect(listener.url).toBe(`unix+https://${ipcSocket}`); await handleAssertions(ipcSocket, true); }); From be6ee87aa8bec2dbd8b68adce6d197b420d6b6f7 Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Sun, 10 Sep 2023 01:26:29 +0200 Subject: [PATCH 08/16] feat: also support absolute socket and full pipe paths --- src/_utils.ts | 9 ++++++--- src/cli.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/_utils.ts b/src/_utils.ts index 1f412ba..8dbf526 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -1,5 +1,5 @@ import { networkInterfaces, platform, tmpdir } from "node:os"; -import { relative, join } from "pathe"; +import { relative, join, isAbsolute } from "pathe"; import { colors } from "consola/utils"; import { consola } from "consola"; import { provider } from "std-env"; @@ -89,11 +89,14 @@ export function getDefaultHost(preferPublic?: boolean) { } export function getSocketPath(name: true | string) { - const _name = typeof name === "string" ? name : "listhen"; + const _name = typeof name === "string" && name.length > 0 ? name : "listhen"; if (platform() === "win32") { + if (_name.startsWith("\\\\?\\pipe\\")) { + return _name; + } return `\\\\?\\pipe\\${_name}`; } - return join(tmpdir(), `${_name}.socket`); + return isAbsolute(_name) ? _name : join(tmpdir(), `${_name}.socket`); } export function getPublicURL( diff --git a/src/cli.ts b/src/cli.ts index ee4cacc..202437a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -143,7 +143,7 @@ export function getArgs() { }, socket: { description: - "Listen on a Unix Domain Socket/Windows Pipe, optionally with custom name", + "Listen on a Unix Domain Socket/Windows Pipe, optionally with custom name or given absolute path", required: false, }, } as const satisfies ArgsDef; From 5664eae95a48d923c305198ad85fe75d2ab09819 Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Sun, 10 Sep 2023 01:27:07 +0200 Subject: [PATCH 09/16] test: absolute socket and full pipe paths --- test/index.test.ts | 65 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/test/index.test.ts b/test/index.test.ts index 9fc89fa..beaa50c 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -308,26 +308,53 @@ describe("listhen", () => { }); describe("_utils", () => { - test("getSocketPath: empty ipcSocketName resolves to a 'listhen' named pipe/socket", () => { - if (platform() === "win32") { - expect(getSocketPath(undefined!)).toEqual("\\\\?\\pipe\\listhen"); - expect(getSocketPath("")).toEqual("\\\\?\\pipe\\listhen"); - } else { - expect(getSocketPath(undefined!)).toEqual("/tmp/listhen.socket"); - expect(getSocketPath("")).toEqual("/tmp/listhen.socket"); - } - }); + describe("socket path", () => { + test("empty ipcSocketName resolves to a 'listhen' named pipe/socket", () => { + if (platform() === "win32") { + expect(getSocketPath(undefined!)).toEqual("\\\\?\\pipe\\listhen"); + expect(getSocketPath("")).toEqual("\\\\?\\pipe\\listhen"); + } else { + expect(getSocketPath(undefined!)).toEqual("/tmp/listhen.socket"); + expect(getSocketPath("")).toEqual("/tmp/listhen.socket"); + } + }); + + test("some string as ipcSocketName resolves to a pipe/socket named as this string", () => { + if (platform() === "win32") { + expect(getSocketPath("listhen-https")).toEqual( + "\\\\?\\pipe\\listhen-https", + ); + } else { + expect(getSocketPath("listhen-https")).toEqual( + "/tmp/listhen-https.socket", + ); + } + }); - test("getSocketPath: some string as ipcSocketName resolves to a pipe/socket named as this string", () => { - if (platform() === "win32") { - expect(getSocketPath("listhen-https")).toEqual( - "\\\\?\\pipe\\listhen-https", - ); - } else { - expect(getSocketPath("listhen-https")).toEqual( - "/tmp/listhen-https.socket", - ); - } + test("absolute path (or full pipe path) resolves to the exact same path", () => { + if (platform() === "win32") { + const pipe = "\\\\?\\pipe\\listhen"; + expect(getSocketPath(pipe)).toEqual(pipe); + } else { + const socket = "/tmp/listhen.socket"; + expect(getSocketPath(socket)).toEqual(socket); + } + }); + + test("relative path resolves to a socket named as this relative path", () => { + if (platform() === "win32") { + expect(getSocketPath("tmp\\listhen")).toEqual( + "\\\\?\\pipe\\tmp\\listhen", + ); + } else { + expect(getSocketPath("tmp/listhen.socket")).toEqual( + "/tmp/tmp/listhen.socket.socket", + ); + expect(getSocketPath("tmp/listhen")).toEqual( + "/tmp/tmp/listhen.socket", + ); + } + }); }); }); }); From 20c892e2b1133834028e54404a53c7d8d5e0ce0d Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Wed, 11 Oct 2023 21:19:10 +0200 Subject: [PATCH 10/16] fix: support relative paths for socket also on macOS --- src/_utils.ts | 11 ++++++++++- test/index.test.ts | 27 ++++++++++++++++----------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/_utils.ts b/src/_utils.ts index 8dbf526..939ddd0 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -1,4 +1,5 @@ import { networkInterfaces, platform, tmpdir } from "node:os"; +import { realpathSync } from "node:fs"; import { relative, join, isAbsolute } from "pathe"; import { colors } from "consola/utils"; import { consola } from "consola"; @@ -96,7 +97,15 @@ export function getSocketPath(name: true | string) { } return `\\\\?\\pipe\\${_name}`; } - return isAbsolute(_name) ? _name : join(tmpdir(), `${_name}.socket`); + + let socketPath = isAbsolute(_name) + ? _name + : join(realpathSync(tmpdir()), `${_name}`); + + if (!socketPath.endsWith(".socket")) { + socketPath += ".socket"; + } + return socketPath; } export function getPublicURL( diff --git a/test/index.test.ts b/test/index.test.ts index beaa50c..9e59961 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,8 +1,9 @@ -import { resolve } from "node:path"; import type { IncomingMessage, ServerResponse } from "node:http"; import { request } from "node:http"; import { request as httpsRequest } from "node:https"; -import { platform } from "node:os"; +import { platform, tmpdir } from "node:os"; +import { realpathSync } from "node:fs"; +import { resolve, join } from "pathe"; import { describe, afterEach, test, expect } from "vitest"; import { toNodeListener, createApp, eventHandler, createRouter } from "h3"; import { listen, Listener } from "../src"; @@ -314,8 +315,9 @@ describe("listhen", () => { expect(getSocketPath(undefined!)).toEqual("\\\\?\\pipe\\listhen"); expect(getSocketPath("")).toEqual("\\\\?\\pipe\\listhen"); } else { - expect(getSocketPath(undefined!)).toEqual("/tmp/listhen.socket"); - expect(getSocketPath("")).toEqual("/tmp/listhen.socket"); + const socketPath = join(realpathSync(tmpdir()), "listhen.socket"); + expect(getSocketPath(undefined!)).toEqual(socketPath); + expect(getSocketPath("")).toEqual(socketPath); } }); @@ -325,9 +327,11 @@ describe("listhen", () => { "\\\\?\\pipe\\listhen-https", ); } else { - expect(getSocketPath("listhen-https")).toEqual( - "/tmp/listhen-https.socket", + const socketPath = join( + realpathSync(tmpdir()), + "listhen-https.socket", ); + expect(getSocketPath("listhen-https")).toEqual(socketPath); } }); @@ -347,12 +351,13 @@ describe("listhen", () => { "\\\\?\\pipe\\tmp\\listhen", ); } else { - expect(getSocketPath("tmp/listhen.socket")).toEqual( - "/tmp/tmp/listhen.socket.socket", - ); - expect(getSocketPath("tmp/listhen")).toEqual( - "/tmp/tmp/listhen.socket", + const socketPath = join( + realpathSync(tmpdir()), + "tmp", + "listhen.socket", ); + expect(getSocketPath("tmp/listhen.socket")).toEqual(socketPath); + expect(getSocketPath("tmp/listhen")).toEqual(socketPath); } }); }); From 01620a5ae847eac672f6ad6ca82a14c967534cac Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Mon, 16 Oct 2023 20:32:39 +0200 Subject: [PATCH 11/16] removes prepending tmp directory and appending .socket --- src/_utils.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/_utils.ts b/src/_utils.ts index 939ddd0..63fc76a 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -1,6 +1,5 @@ -import { networkInterfaces, platform, tmpdir } from "node:os"; -import { realpathSync } from "node:fs"; -import { relative, join, isAbsolute } from "pathe"; +import { networkInterfaces, platform } from "node:os"; +import { relative } from "pathe"; import { colors } from "consola/utils"; import { consola } from "consola"; import { provider } from "std-env"; @@ -98,14 +97,7 @@ export function getSocketPath(name: true | string) { return `\\\\?\\pipe\\${_name}`; } - let socketPath = isAbsolute(_name) - ? _name - : join(realpathSync(tmpdir()), `${_name}`); - - if (!socketPath.endsWith(".socket")) { - socketPath += ".socket"; - } - return socketPath; + return _name === "listhen" ? "listhen.sock" : _name; } export function getPublicURL( From 07f097b986f7004e7c1c970fe4faf08d6fcc6392 Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Mon, 16 Oct 2023 20:37:00 +0200 Subject: [PATCH 12/16] test: splits windows test in separate suite --- test/index.test.ts | 96 +++++++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/test/index.test.ts b/test/index.test.ts index 9e59961..667988b 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,11 +1,10 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import { request } from "node:http"; import { request as httpsRequest } from "node:https"; -import { platform, tmpdir } from "node:os"; -import { realpathSync } from "node:fs"; -import { resolve, join } from "pathe"; -import { describe, afterEach, test, expect } from "vitest"; -import { toNodeListener, createApp, eventHandler, createRouter } from "h3"; +import { platform } from "node:os"; +import { resolve } from "pathe"; +import { afterEach, describe, expect, test } from "vitest"; +import { createApp, createRouter, eventHandler, toNodeListener } from "h3"; import { listen, Listener } from "../src"; import { getSocketPath } from "../src/_utils"; @@ -309,57 +308,66 @@ describe("listhen", () => { }); describe("_utils", () => { - describe("socket path", () => { - test("empty ipcSocketName resolves to a 'listhen' named pipe/socket", () => { - if (platform() === "win32") { - expect(getSocketPath(undefined!)).toEqual("\\\\?\\pipe\\listhen"); - expect(getSocketPath("")).toEqual("\\\\?\\pipe\\listhen"); - } else { - const socketPath = join(realpathSync(tmpdir()), "listhen.socket"); + describe.runIf(platform() !== "win32")( + "socket path (on unixoid systems)", + () => { + test("empty ipcSocketName resolves to a 'listhen' named pipe/socket", () => { + const socketPath = "listhen.sock"; expect(getSocketPath(undefined!)).toEqual(socketPath); expect(getSocketPath("")).toEqual(socketPath); - } - }); + }); + + test("some string as ipcSocketName resolves to a pipe/socket named as this string", () => { + const socketPath = "listhen-https"; + expect(getSocketPath("listhen-https")).toEqual(socketPath); + }); + + test("absolute path (or full pipe path) resolves to the exact same path", () => { + let socket = "/tmp/listhen.sock"; + expect(getSocketPath(socket)).toEqual(socket); + socket = "/tmp/listhen.sock"; + expect(getSocketPath(socket)).toEqual(socket); + socket = "/tmp/listhen"; + expect(getSocketPath(socket)).toEqual(socket); + }); + + test("relative path resolves to a socket named as this relative path", () => { + const socketPath = "frontend_run/listhen.sock"; + expect(getSocketPath("./frontend_run/listhen.sock")).toEqual( + "./" + socketPath, + ); + expect(getSocketPath("frontend_run/listhen.sock")).toEqual( + socketPath, + ); + }); + }, + ); - test("some string as ipcSocketName resolves to a pipe/socket named as this string", () => { - if (platform() === "win32") { + describe.runIf(platform() === "win32")( + "socket path (on windows systems)", + () => { + test("empty ipcSocketName resolves to a 'listhen' named pipe/socket", () => { + expect(getSocketPath(undefined!)).toEqual("\\\\?\\pipe\\listhen"); + expect(getSocketPath("")).toEqual("\\\\?\\pipe\\listhen"); + }); + + test("some string as ipcSocketName resolves to a pipe/socket named as this string", () => { expect(getSocketPath("listhen-https")).toEqual( "\\\\?\\pipe\\listhen-https", ); - } else { - const socketPath = join( - realpathSync(tmpdir()), - "listhen-https.socket", - ); - expect(getSocketPath("listhen-https")).toEqual(socketPath); - } - }); + }); - test("absolute path (or full pipe path) resolves to the exact same path", () => { - if (platform() === "win32") { + test("absolute path (or full pipe path) resolves to the exact same path", () => { const pipe = "\\\\?\\pipe\\listhen"; expect(getSocketPath(pipe)).toEqual(pipe); - } else { - const socket = "/tmp/listhen.socket"; - expect(getSocketPath(socket)).toEqual(socket); - } - }); + }); - test("relative path resolves to a socket named as this relative path", () => { - if (platform() === "win32") { + test("relative path resolves to a socket named as this relative path", () => { expect(getSocketPath("tmp\\listhen")).toEqual( "\\\\?\\pipe\\tmp\\listhen", ); - } else { - const socketPath = join( - realpathSync(tmpdir()), - "tmp", - "listhen.socket", - ); - expect(getSocketPath("tmp/listhen.socket")).toEqual(socketPath); - expect(getSocketPath("tmp/listhen")).toEqual(socketPath); - } - }); - }); + }); + }, + ); }); }); From bd79c01bdefd7fc84497fe2f51e87cc3b775002b Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Tue, 17 Oct 2023 20:42:56 +0200 Subject: [PATCH 13/16] make clear, that the socket/pipe is created in the CWD by prepending ./ if not present --- src/listen.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/listen.ts b/src/listen.ts index 99eb662..fd44692 100644 --- a/src/listen.ts +++ b/src/listen.ts @@ -12,6 +12,7 @@ import { ColorName, getColor, colors } from "consola/utils"; import { renderUnicodeCompact as renderQRCode } from "uqr"; import type { Tunnel } from "untun"; import type { AdapterOptions as CrossWSOptions } from "crossws"; +import { isAbsolute, sep } from "pathe"; import { open } from "./lib/open"; import type { ListenOptions, @@ -204,7 +205,13 @@ export async function listen( }; if (serverOptions.path) { - _addURL("local", serverOptions.path); + let _path = serverOptions.path + const currentDirPath = `.${sep}` + + if (!(isAbsolute(_path) || _path.startsWith(currentDirPath))) { + _path = currentDirPath + _path + } + _addURL("local", _path); return urls; } From 50225e0486540ea25a47aa816b13218e06434cab Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 18:43:36 +0000 Subject: [PATCH 14/16] chore: apply automated lint fixes --- src/listen.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/listen.ts b/src/listen.ts index fd44692..ae26e78 100644 --- a/src/listen.ts +++ b/src/listen.ts @@ -205,11 +205,11 @@ export async function listen( }; if (serverOptions.path) { - let _path = serverOptions.path - const currentDirPath = `.${sep}` + let _path = serverOptions.path; + const currentDirPath = `.${sep}`; if (!(isAbsolute(_path) || _path.startsWith(currentDirPath))) { - _path = currentDirPath + _path + _path = currentDirPath + _path; } _addURL("local", _path); return urls; From cdaafbef00f48932594d6354a0109dd6d25d99fd Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Fri, 22 Mar 2024 19:50:29 +0100 Subject: [PATCH 15/16] fix lost comment start --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index bda3543..c4982c6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -67,8 +67,8 @@ export interface ListenOptions { | boolean | CrossWSOptions | ((req: IncomingMessage, head: Buffer) => void); + /** * Listhen on a unix domain socket/windows pipe, optionally with custom name - * */ socket: boolean | string; } From 0b06e4955ca156158a9ba84b987dc8a0ae4786bc Mon Sep 17 00:00:00 2001 From: Armin Kunkel Date: Fri, 22 Mar 2024 20:15:04 +0100 Subject: [PATCH 16/16] fix: ipc-tests --- test/index.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/index.test.ts b/test/index.test.ts index 667988b..4716137 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -57,7 +57,7 @@ function ipcRequest(ipcSocket: string, path: string, https = false) { }); res.on("end", () => { try { - resolve(JSON.parse(data.join(""))); + resolve(JSON.parse(data)); } catch { resolve(data.join("")); } @@ -88,9 +88,11 @@ describe("listhen", () => { await expect(ipcRequest(ipcSocket, "/unix", https)).resolves.toEqual({ hello: "unix!", }); - await expect(ipcRequest(ipcSocket, "/test", https)).resolves.toContain({ - statusCode: 404, - }); + const response = await ipcRequest(ipcSocket, "/test", https); + expect(response.statusCode).toEqual(404); + expect(response.statusMessage).toEqual( + "Cannot find any path matching /test.", + ); } async function handleAssertions(ipcSocket: string, https: boolean) {