summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYoshiya Hinosawa <stibium121@gmail.com>2023-10-04 11:37:39 +0900
committerGitHub <noreply@github.com>2023-10-04 11:37:39 +0900
commitda0b945804f19903beac71b23ff1040ebdb9b554 (patch)
tree8ec0508fbc04839f113b2d58169090d14b474cdd
parent8c1677ecbcbb474fc6a5ac9b5f73b562677bb829 (diff)
feat(unstable): add unix domain socket support to Deno.serve (#20759)
-rw-r--r--cli/tests/unit/net_test.ts7
-rw-r--r--cli/tests/unit/serve_test.ts35
-rw-r--r--cli/tests/unit/test_util.ts7
-rw-r--r--cli/tsc/dts/lib.deno.unstable.d.ts121
-rw-r--r--ext/http/00_serve.js44
-rw-r--r--ext/net/01_net.js9
-rw-r--r--ext/net/ops_unix.rs8
7 files changed, 216 insertions, 15 deletions
diff --git a/cli/tests/unit/net_test.ts b/cli/tests/unit/net_test.ts
index 54edf31fc..2a98b5e26 100644
--- a/cli/tests/unit/net_test.ts
+++ b/cli/tests/unit/net_test.ts
@@ -9,8 +9,8 @@ import {
delay,
execCode,
execCode2,
+ tmpUnixSocketPath,
} from "./test_util.ts";
-import { join } from "../../../test_util/std/path/mod.ts";
// Since these tests may run in parallel, ensure this port is unique to this file
const listenPort = 4503;
@@ -49,11 +49,6 @@ Deno.test(
},
);
-function tmpUnixSocketPath(): string {
- const folder = Deno.makeTempDirSync();
- return join(folder, "socket");
-}
-
Deno.test(
{
ignore: Deno.build.os === "windows",
diff --git a/cli/tests/unit/serve_test.ts b/cli/tests/unit/serve_test.ts
index 193b04ed1..6f58db006 100644
--- a/cli/tests/unit/serve_test.ts
+++ b/cli/tests/unit/serve_test.ts
@@ -15,6 +15,7 @@ import {
deferred,
execCode,
fail,
+ tmpUnixSocketPath,
} from "./test_util.ts";
// Since these tests may run in parallel, ensure this port is unique to this file
@@ -3715,3 +3716,37 @@ async function curlRequestWithStdErr(args: string[]) {
assert(success);
return [new TextDecoder().decode(stdout), new TextDecoder().decode(stderr)];
}
+
+Deno.test(
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { run: true, read: true, write: true },
+ },
+ async function httpServerUnixDomainSocket() {
+ const d = deferred();
+ const ac = new AbortController();
+ const filePath = tmpUnixSocketPath();
+ const server = Deno.serve(
+ {
+ signal: ac.signal,
+ path: filePath,
+ onListen(info) {
+ d.resolve(info);
+ },
+ onError: createOnErrorCb(ac),
+ },
+ (_req, { remoteAddr }) => {
+ assertEquals(remoteAddr, { path: filePath, transport: "unix" });
+ return new Response("hello world!");
+ },
+ );
+
+ assertEquals(await d, { path: filePath });
+ assertEquals(
+ "hello world!",
+ await curlRequest(["--unix-socket", filePath, "http://localhost"]),
+ );
+ ac.abort();
+ await server.finished;
+ },
+);
diff --git a/cli/tests/unit/test_util.ts b/cli/tests/unit/test_util.ts
index 23713faf4..de1e8e8c5 100644
--- a/cli/tests/unit/test_util.ts
+++ b/cli/tests/unit/test_util.ts
@@ -2,7 +2,7 @@
import * as colors from "../../../test_util/std/fmt/colors.ts";
export { colors };
-import { resolve } from "../../../test_util/std/path/mod.ts";
+import { join, resolve } from "../../../test_util/std/path/mod.ts";
export {
assert,
assertEquals,
@@ -81,3 +81,8 @@ export function execCode2(code: string) {
},
};
}
+
+export function tmpUnixSocketPath(): string {
+ const folder = Deno.makeTempDirSync();
+ return join(folder, "socket");
+}
diff --git a/cli/tsc/dts/lib.deno.unstable.d.ts b/cli/tsc/dts/lib.deno.unstable.d.ts
index 11510d144..4d909a789 100644
--- a/cli/tsc/dts/lib.deno.unstable.d.ts
+++ b/cli/tsc/dts/lib.deno.unstable.d.ts
@@ -1948,6 +1948,127 @@ declare namespace Deno {
shutdown(): Promise<void>;
}
+ export interface ServeUnixOptions {
+ /** The unix domain socket path to listen on. */
+ path: string;
+
+ /** An {@linkcode AbortSignal} to close the server and all connections. */
+ signal?: AbortSignal;
+
+ /** The handler to invoke when route handlers throw an error. */
+ onError?: (error: unknown) => Response | Promise<Response>;
+
+ /** The callback which is called when the server starts listening. */
+ onListen?: (params: { path: string }) => void;
+ }
+
+ /** Information for a unix domain socket HTTP request.
+ *
+ * @category HTTP Server
+ */
+ export interface ServeUnixHandlerInfo {
+ /** The remote address of the connection. */
+ remoteAddr: Deno.UnixAddr;
+ }
+
+ /** A handler for unix domain socket HTTP requests. Consumes a request and returns a response.
+ *
+ * If a handler throws, the server calling the handler will assume the impact
+ * of the error is isolated to the individual request. It will catch the error
+ * and if necessary will close the underlying connection.
+ *
+ * @category HTTP Server
+ */
+ export type ServeUnixHandler = (
+ request: Request,
+ info: ServeUnixHandlerInfo,
+ ) => Response | Promise<Response>;
+
+ /**
+ * @category HTTP Server
+ */
+ export interface ServeUnixInit {
+ /** The handler to invoke to process each incoming request. */
+ handler: ServeUnixHandler;
+ }
+
+ /** Serves HTTP requests with the given option bag and handler.
+ *
+ * You can specify the socket path with `path` option.
+ *
+ * ```ts
+ * Deno.serve(
+ * { path: "path/to/socket" },
+ * (_req) => new Response("Hello, world")
+ * );
+ * ```
+ *
+ * You can stop the server with an {@linkcode AbortSignal}. The abort signal
+ * needs to be passed as the `signal` option in the options bag. The server
+ * aborts when the abort signal is aborted. To wait for the server to close,
+ * await the promise returned from the `Deno.serve` API.
+ *
+ * ```ts
+ * const ac = new AbortController();
+ *
+ * const server = Deno.serve(
+ * { signal: ac.signal, path: "path/to/socket" },
+ * (_req) => new Response("Hello, world")
+ * );
+ * server.finished.then(() => console.log("Server closed"));
+ *
+ * console.log("Closing server...");
+ * ac.abort();
+ * ```
+ *
+ * By default `Deno.serve` prints the message
+ * `Listening on path/to/socket` on listening. If you like to
+ * change this behavior, you can specify a custom `onListen` callback.
+ *
+ * ```ts
+ * Deno.serve({
+ * onListen({ path }) {
+ * console.log(`Server started at ${path}`);
+ * // ... more info specific to your server ..
+ * },
+ * path: "path/to/socket",
+ * }, (_req) => new Response("Hello, world"));
+ * ```
+ *
+ * @category HTTP Server
+ */
+ export function serve(
+ options: ServeUnixOptions,
+ handler: ServeUnixHandler,
+ ): Server;
+ /** Serves HTTP requests with the given option bag.
+ *
+ * You can specify an object with the path option, which is the
+ * unix domain socket to listen on.
+ *
+ * ```ts
+ * const ac = new AbortController();
+ *
+ * const server = Deno.serve({
+ * path: "path/to/socket",
+ * handler: (_req) => new Response("Hello, world"),
+ * signal: ac.signal,
+ * onListen({ path }) {
+ * console.log(`Server started at ${path}`);
+ * },
+ * });
+ * server.finished.then(() => console.log("Server closed"));
+ *
+ * console.log("Closing server...");
+ * ac.abort();
+ * ```
+ *
+ * @category HTTP Server
+ */
+ export function serve(
+ options: ServeUnixInit & ServeUnixOptions,
+ ): Server;
+
/**
* A namespace containing runtime APIs available in Jupyter notebooks.
*
diff --git a/ext/http/00_serve.js b/ext/http/00_serve.js
index aeebca93d..e74e1e71f 100644
--- a/ext/http/00_serve.js
+++ b/ext/http/00_serve.js
@@ -34,11 +34,12 @@ import {
ReadableStreamPrototype,
resourceForReadableStream,
} from "ext:deno_web/06_streams.js";
-import { listen, TcpConn } from "ext:deno_net/01_net.js";
+import { listen, listenOptionApiName, TcpConn } from "ext:deno_net/01_net.js";
import { listenTls } from "ext:deno_net/02_tls.js";
const {
ArrayPrototypePush,
Error,
+ ObjectHasOwn,
ObjectPrototypeIsPrototypeOf,
PromisePrototypeCatch,
Symbol,
@@ -272,6 +273,13 @@ class InnerRequest {
}
get remoteAddr() {
+ const transport = this.#context.listener?.addr.transport;
+ if (transport === "unix" || transport === "unixpacket") {
+ return {
+ transport,
+ path: this.#context.listener.addr.path,
+ };
+ }
if (this.#methodAndUri === undefined) {
if (this.#slabId === undefined) {
throw new TypeError("request closed");
@@ -337,8 +345,9 @@ class CallbackContext {
serverRid;
closed;
closing;
+ listener;
- constructor(signal, args) {
+ constructor(signal, args, listener) {
// The abort signal triggers a non-graceful shutdown
signal?.addEventListener(
"abort",
@@ -352,6 +361,7 @@ class CallbackContext {
this.scheme = args[1];
this.fallbackHost = args[2];
this.closed = false;
+ this.listener = listener;
}
close() {
@@ -519,11 +529,29 @@ function serve(arg1, arg2) {
}
const wantsHttps = options.cert || options.key;
+ const wantsUnix = ObjectHasOwn(options, "path");
const signal = options.signal;
const onError = options.onError ?? function (error) {
console.error(error);
return internalServerError();
};
+
+ if (wantsUnix) {
+ const listener = listen({
+ transport: "unix",
+ path: options.path,
+ [listenOptionApiName]: "Deno.serve",
+ });
+ const path = listener.addr.path;
+ return serveHttpOnListener(listener, signal, handler, onError, () => {
+ if (options.onListen) {
+ options.onListen({ path });
+ } else {
+ console.log(`Listening on ${path}`);
+ }
+ });
+ }
+
const listenOpts = {
hostname: options.hostname ?? "0.0.0.0",
port: options.port ?? 8000,
@@ -581,7 +609,11 @@ function serve(arg1, arg2) {
* Serve HTTP/1.1 and/or HTTP/2 on an arbitrary listener.
*/
function serveHttpOnListener(listener, signal, handler, onError, onListen) {
- const context = new CallbackContext(signal, op_http_serve(listener.rid));
+ const context = new CallbackContext(
+ signal,
+ op_http_serve(listener.rid),
+ listener,
+ );
const callback = mapToCallback(context, handler, onError);
onListen(context.scheme);
@@ -593,7 +625,11 @@ function serveHttpOnListener(listener, signal, handler, onError, onListen) {
* Serve HTTP/1.1 and/or HTTP/2 on an arbitrary connection.
*/
function serveHttpOnConnection(connection, signal, handler, onError, onListen) {
- const context = new CallbackContext(signal, op_http_serve_on(connection.rid));
+ const context = new CallbackContext(
+ signal,
+ op_http_serve_on(connection.rid),
+ null,
+ );
const callback = mapToCallback(context, handler, onError);
onListen(context.scheme);
diff --git a/ext/net/01_net.js b/ext/net/01_net.js
index 9cdcdb78c..f2bf5e7df 100644
--- a/ext/net/01_net.js
+++ b/ext/net/01_net.js
@@ -19,6 +19,7 @@ const {
ObjectPrototypeIsPrototypeOf,
PromiseResolve,
SymbolAsyncIterator,
+ Symbol,
SymbolFor,
TypeError,
TypedArrayPrototypeSubarray,
@@ -416,6 +417,8 @@ class Datagram {
}
}
+const listenOptionApiName = Symbol("listenOptionApiName");
+
function listen(args) {
switch (args.transport ?? "tcp") {
case "tcp": {
@@ -427,7 +430,10 @@ function listen(args) {
return new Listener(rid, addr);
}
case "unix": {
- const { 0: rid, 1: path } = ops.op_net_listen_unix(args.path);
+ const { 0: rid, 1: path } = ops.op_net_listen_unix(
+ args.path,
+ args[listenOptionApiName] ?? "Deno.listen",
+ );
const addr = {
transport: "unix",
path,
@@ -505,6 +511,7 @@ export {
Datagram,
listen,
Listener,
+ listenOptionApiName,
resolveDns,
shutdown,
TcpConn,
diff --git a/ext/net/ops_unix.rs b/ext/net/ops_unix.rs
index beb41bb4a..7a5da9fa1 100644
--- a/ext/net/ops_unix.rs
+++ b/ext/net/ops_unix.rs
@@ -194,15 +194,17 @@ where
pub fn op_net_listen_unix<NP>(
state: &mut OpState,
#[string] path: String,
+ #[string] api_name: String,
) -> Result<(ResourceId, Option<String>), AnyError>
where
NP: NetPermissions + 'static,
{
let address_path = Path::new(&path);
- super::check_unstable(state, "Deno.listen");
+ super::check_unstable(state, &api_name);
let permissions = state.borrow_mut::<NP>();
- permissions.check_read(address_path, "Deno.listen()")?;
- permissions.check_write(address_path, "Deno.listen()")?;
+ let api_call_expr = format!("{}()", api_name);
+ permissions.check_read(address_path, &api_call_expr)?;
+ permissions.check_write(address_path, &api_call_expr)?;
let listener = UnixListener::bind(address_path)?;
let local_addr = listener.local_addr()?;
let pathname = local_addr.as_pathname().map(pathstring).transpose()?;