1
0
Fork 0
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:
Yoshiya Hinosawa 2023-10-04 11:37:39 +09:00 committed by GitHub
parent 8c1677ecbc
commit da0b945804
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 216 additions and 15 deletions

View file

@ -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",

View file

@ -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;
},
);

View file

@ -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");
}

View file

@ -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.
* *

View file

@ -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);

View file

@ -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,

View file

@ -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()?;