mirror of
https://github.com/denoland/deno.git
synced 2024-12-24 08:09:08 -05:00
feat(unstable): add unix domain socket support to Deno.serve (#20759)
This commit is contained in:
parent
8c1677ecbc
commit
da0b945804
7 changed files with 216 additions and 15 deletions
|
@ -9,8 +9,8 @@ import {
|
||||||
delay,
|
delay,
|
||||||
execCode,
|
execCode,
|
||||||
execCode2,
|
execCode2,
|
||||||
|
tmpUnixSocketPath,
|
||||||
} from "./test_util.ts";
|
} 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
|
// Since these tests may run in parallel, ensure this port is unique to this file
|
||||||
const listenPort = 4503;
|
const listenPort = 4503;
|
||||||
|
@ -49,11 +49,6 @@ Deno.test(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function tmpUnixSocketPath(): string {
|
|
||||||
const folder = Deno.makeTempDirSync();
|
|
||||||
return join(folder, "socket");
|
|
||||||
}
|
|
||||||
|
|
||||||
Deno.test(
|
Deno.test(
|
||||||
{
|
{
|
||||||
ignore: Deno.build.os === "windows",
|
ignore: Deno.build.os === "windows",
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
deferred,
|
deferred,
|
||||||
execCode,
|
execCode,
|
||||||
fail,
|
fail,
|
||||||
|
tmpUnixSocketPath,
|
||||||
} from "./test_util.ts";
|
} from "./test_util.ts";
|
||||||
|
|
||||||
// Since these tests may run in parallel, ensure this port is unique to this file
|
// 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);
|
assert(success);
|
||||||
return [new TextDecoder().decode(stdout), new TextDecoder().decode(stderr)];
|
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;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import * as colors from "../../../test_util/std/fmt/colors.ts";
|
import * as colors from "../../../test_util/std/fmt/colors.ts";
|
||||||
export { colors };
|
export { colors };
|
||||||
import { resolve } from "../../../test_util/std/path/mod.ts";
|
import { join, resolve } from "../../../test_util/std/path/mod.ts";
|
||||||
export {
|
export {
|
||||||
assert,
|
assert,
|
||||||
assertEquals,
|
assertEquals,
|
||||||
|
@ -81,3 +81,8 @@ export function execCode2(code: string) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function tmpUnixSocketPath(): string {
|
||||||
|
const folder = Deno.makeTempDirSync();
|
||||||
|
return join(folder, "socket");
|
||||||
|
}
|
||||||
|
|
121
cli/tsc/dts/lib.deno.unstable.d.ts
vendored
121
cli/tsc/dts/lib.deno.unstable.d.ts
vendored
|
@ -1948,6 +1948,127 @@ declare namespace Deno {
|
||||||
shutdown(): Promise<void>;
|
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.
|
* A namespace containing runtime APIs available in Jupyter notebooks.
|
||||||
*
|
*
|
||||||
|
|
|
@ -34,11 +34,12 @@ import {
|
||||||
ReadableStreamPrototype,
|
ReadableStreamPrototype,
|
||||||
resourceForReadableStream,
|
resourceForReadableStream,
|
||||||
} from "ext:deno_web/06_streams.js";
|
} 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";
|
import { listenTls } from "ext:deno_net/02_tls.js";
|
||||||
const {
|
const {
|
||||||
ArrayPrototypePush,
|
ArrayPrototypePush,
|
||||||
Error,
|
Error,
|
||||||
|
ObjectHasOwn,
|
||||||
ObjectPrototypeIsPrototypeOf,
|
ObjectPrototypeIsPrototypeOf,
|
||||||
PromisePrototypeCatch,
|
PromisePrototypeCatch,
|
||||||
Symbol,
|
Symbol,
|
||||||
|
@ -272,6 +273,13 @@ class InnerRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
get remoteAddr() {
|
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.#methodAndUri === undefined) {
|
||||||
if (this.#slabId === undefined) {
|
if (this.#slabId === undefined) {
|
||||||
throw new TypeError("request closed");
|
throw new TypeError("request closed");
|
||||||
|
@ -337,8 +345,9 @@ class CallbackContext {
|
||||||
serverRid;
|
serverRid;
|
||||||
closed;
|
closed;
|
||||||
closing;
|
closing;
|
||||||
|
listener;
|
||||||
|
|
||||||
constructor(signal, args) {
|
constructor(signal, args, listener) {
|
||||||
// The abort signal triggers a non-graceful shutdown
|
// The abort signal triggers a non-graceful shutdown
|
||||||
signal?.addEventListener(
|
signal?.addEventListener(
|
||||||
"abort",
|
"abort",
|
||||||
|
@ -352,6 +361,7 @@ class CallbackContext {
|
||||||
this.scheme = args[1];
|
this.scheme = args[1];
|
||||||
this.fallbackHost = args[2];
|
this.fallbackHost = args[2];
|
||||||
this.closed = false;
|
this.closed = false;
|
||||||
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
@ -519,11 +529,29 @@ function serve(arg1, arg2) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const wantsHttps = options.cert || options.key;
|
const wantsHttps = options.cert || options.key;
|
||||||
|
const wantsUnix = ObjectHasOwn(options, "path");
|
||||||
const signal = options.signal;
|
const signal = options.signal;
|
||||||
const onError = options.onError ?? function (error) {
|
const onError = options.onError ?? function (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return internalServerError();
|
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 = {
|
const listenOpts = {
|
||||||
hostname: options.hostname ?? "0.0.0.0",
|
hostname: options.hostname ?? "0.0.0.0",
|
||||||
port: options.port ?? 8000,
|
port: options.port ?? 8000,
|
||||||
|
@ -581,7 +609,11 @@ function serve(arg1, arg2) {
|
||||||
* Serve HTTP/1.1 and/or HTTP/2 on an arbitrary listener.
|
* Serve HTTP/1.1 and/or HTTP/2 on an arbitrary listener.
|
||||||
*/
|
*/
|
||||||
function serveHttpOnListener(listener, signal, handler, onError, onListen) {
|
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);
|
const callback = mapToCallback(context, handler, onError);
|
||||||
|
|
||||||
onListen(context.scheme);
|
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.
|
* Serve HTTP/1.1 and/or HTTP/2 on an arbitrary connection.
|
||||||
*/
|
*/
|
||||||
function serveHttpOnConnection(connection, signal, handler, onError, onListen) {
|
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);
|
const callback = mapToCallback(context, handler, onError);
|
||||||
|
|
||||||
onListen(context.scheme);
|
onListen(context.scheme);
|
||||||
|
|
|
@ -19,6 +19,7 @@ const {
|
||||||
ObjectPrototypeIsPrototypeOf,
|
ObjectPrototypeIsPrototypeOf,
|
||||||
PromiseResolve,
|
PromiseResolve,
|
||||||
SymbolAsyncIterator,
|
SymbolAsyncIterator,
|
||||||
|
Symbol,
|
||||||
SymbolFor,
|
SymbolFor,
|
||||||
TypeError,
|
TypeError,
|
||||||
TypedArrayPrototypeSubarray,
|
TypedArrayPrototypeSubarray,
|
||||||
|
@ -416,6 +417,8 @@ class Datagram {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const listenOptionApiName = Symbol("listenOptionApiName");
|
||||||
|
|
||||||
function listen(args) {
|
function listen(args) {
|
||||||
switch (args.transport ?? "tcp") {
|
switch (args.transport ?? "tcp") {
|
||||||
case "tcp": {
|
case "tcp": {
|
||||||
|
@ -427,7 +430,10 @@ function listen(args) {
|
||||||
return new Listener(rid, addr);
|
return new Listener(rid, addr);
|
||||||
}
|
}
|
||||||
case "unix": {
|
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 = {
|
const addr = {
|
||||||
transport: "unix",
|
transport: "unix",
|
||||||
path,
|
path,
|
||||||
|
@ -505,6 +511,7 @@ export {
|
||||||
Datagram,
|
Datagram,
|
||||||
listen,
|
listen,
|
||||||
Listener,
|
Listener,
|
||||||
|
listenOptionApiName,
|
||||||
resolveDns,
|
resolveDns,
|
||||||
shutdown,
|
shutdown,
|
||||||
TcpConn,
|
TcpConn,
|
||||||
|
|
|
@ -194,15 +194,17 @@ where
|
||||||
pub fn op_net_listen_unix<NP>(
|
pub fn op_net_listen_unix<NP>(
|
||||||
state: &mut OpState,
|
state: &mut OpState,
|
||||||
#[string] path: String,
|
#[string] path: String,
|
||||||
|
#[string] api_name: String,
|
||||||
) -> Result<(ResourceId, Option<String>), AnyError>
|
) -> Result<(ResourceId, Option<String>), AnyError>
|
||||||
where
|
where
|
||||||
NP: NetPermissions + 'static,
|
NP: NetPermissions + 'static,
|
||||||
{
|
{
|
||||||
let address_path = Path::new(&path);
|
let address_path = Path::new(&path);
|
||||||
super::check_unstable(state, "Deno.listen");
|
super::check_unstable(state, &api_name);
|
||||||
let permissions = state.borrow_mut::<NP>();
|
let permissions = state.borrow_mut::<NP>();
|
||||||
permissions.check_read(address_path, "Deno.listen()")?;
|
let api_call_expr = format!("{}()", api_name);
|
||||||
permissions.check_write(address_path, "Deno.listen()")?;
|
permissions.check_read(address_path, &api_call_expr)?;
|
||||||
|
permissions.check_write(address_path, &api_call_expr)?;
|
||||||
let listener = UnixListener::bind(address_path)?;
|
let listener = UnixListener::bind(address_path)?;
|
||||||
let local_addr = listener.local_addr()?;
|
let local_addr = listener.local_addr()?;
|
||||||
let pathname = local_addr.as_pathname().map(pathstring).transpose()?;
|
let pathname = local_addr.as_pathname().map(pathstring).transpose()?;
|
||||||
|
|
Loading…
Reference in a new issue