diff options
Diffstat (limited to 'ext/node/polyfills/internal_binding/udp_wrap.ts')
-rw-r--r-- | ext/node/polyfills/internal_binding/udp_wrap.ts | 504 |
1 files changed, 504 insertions, 0 deletions
diff --git a/ext/node/polyfills/internal_binding/udp_wrap.ts b/ext/node/polyfills/internal_binding/udp_wrap.ts new file mode 100644 index 000000000..54173e3ba --- /dev/null +++ b/ext/node/polyfills/internal_binding/udp_wrap.ts @@ -0,0 +1,504 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { + AsyncWrap, + providerType, +} from "internal:deno_node/polyfills/internal_binding/async_wrap.ts"; +import { GetAddrInfoReqWrap } from "internal:deno_node/polyfills/internal_binding/cares_wrap.ts"; +import { HandleWrap } from "internal:deno_node/polyfills/internal_binding/handle_wrap.ts"; +import { ownerSymbol } from "internal:deno_node/polyfills/internal_binding/symbols.ts"; +import { + codeMap, + errorMap, +} from "internal:deno_node/polyfills/internal_binding/uv.ts"; +import { notImplemented } from "internal:deno_node/polyfills/_utils.ts"; +import { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import type { ErrnoException } from "internal:deno_node/polyfills/internal/errors.ts"; +import { isIP } from "internal:deno_node/polyfills/internal/net.ts"; + +import { isLinux, isWindows } from "internal:deno_node/polyfills/_util/os.ts"; + +// @ts-ignore Deno[Deno.internal] is used on purpose here +const DenoListenDatagram = Deno[Deno.internal]?.nodeUnstable?.listenDatagram || + Deno.listenDatagram; + +type MessageType = string | Uint8Array | Buffer | DataView; + +const AF_INET = 2; +const AF_INET6 = 10; + +const UDP_DGRAM_MAXSIZE = 64 * 1024; + +export class SendWrap extends AsyncWrap { + list!: MessageType[]; + address!: string; + port!: number; + + callback!: (error: ErrnoException | null, bytes?: number) => void; + oncomplete!: (err: number | null, sent?: number) => void; + + constructor() { + super(providerType.UDPSENDWRAP); + } +} + +export class UDP extends HandleWrap { + [ownerSymbol]: unknown = null; + + #address?: string; + #family?: string; + #port?: number; + + #remoteAddress?: string; + #remoteFamily?: string; + #remotePort?: number; + + #listener?: Deno.DatagramConn; + #receiving = false; + + #recvBufferSize = UDP_DGRAM_MAXSIZE; + #sendBufferSize = UDP_DGRAM_MAXSIZE; + + onmessage!: ( + nread: number, + handle: UDP, + buf?: Buffer, + rinfo?: { + address: string; + family: "IPv4" | "IPv6"; + port: number; + size?: number; + }, + ) => void; + + lookup!: ( + address: string, + callback: ( + err: ErrnoException | null, + address: string, + family: number, + ) => void, + ) => GetAddrInfoReqWrap | Record<string, never>; + + constructor() { + super(providerType.UDPWRAP); + } + + addMembership(_multicastAddress: string, _interfaceAddress?: string): number { + notImplemented("udp.UDP.prototype.addMembership"); + } + + addSourceSpecificMembership( + _sourceAddress: string, + _groupAddress: string, + _interfaceAddress?: string, + ): number { + notImplemented("udp.UDP.prototype.addSourceSpecificMembership"); + } + + /** + * Bind to an IPv4 address. + * @param ip The hostname to bind to. + * @param port The port to bind to + * @return An error status code. + */ + bind(ip: string, port: number, flags: number): number { + return this.#doBind(ip, port, flags, AF_INET); + } + + /** + * Bind to an IPv6 address. + * @param ip The hostname to bind to. + * @param port The port to bind to + * @return An error status code. + */ + bind6(ip: string, port: number, flags: number): number { + return this.#doBind(ip, port, flags, AF_INET6); + } + + bufferSize( + size: number, + buffer: boolean, + ctx: Record<string, string | number>, + ): number | undefined { + let err: string | undefined; + + if (size > UDP_DGRAM_MAXSIZE) { + err = "EINVAL"; + } else if (!this.#address) { + err = isWindows ? "ENOTSOCK" : "EBADF"; + } + + if (err) { + ctx.errno = codeMap.get(err)!; + ctx.code = err; + ctx.message = errorMap.get(ctx.errno)![1]; + ctx.syscall = buffer ? "uv_recv_buffer_size" : "uv_send_buffer_size"; + + return; + } + + if (size !== 0) { + size = isLinux ? size * 2 : size; + + if (buffer) { + return (this.#recvBufferSize = size); + } + + return (this.#sendBufferSize = size); + } + + return buffer ? this.#recvBufferSize : this.#sendBufferSize; + } + + connect(ip: string, port: number): number { + return this.#doConnect(ip, port, AF_INET); + } + + connect6(ip: string, port: number): number { + return this.#doConnect(ip, port, AF_INET6); + } + + disconnect(): number { + this.#remoteAddress = undefined; + this.#remotePort = undefined; + this.#remoteFamily = undefined; + + return 0; + } + + dropMembership( + _multicastAddress: string, + _interfaceAddress?: string, + ): number { + notImplemented("udp.UDP.prototype.dropMembership"); + } + + dropSourceSpecificMembership( + _sourceAddress: string, + _groupAddress: string, + _interfaceAddress?: string, + ): number { + notImplemented("udp.UDP.prototype.dropSourceSpecificMembership"); + } + + /** + * Populates the provided object with remote address entries. + * @param peername An object to add the remote address entries to. + * @return An error status code. + */ + getpeername(peername: Record<string, string | number>): number { + if (this.#remoteAddress === undefined) { + return codeMap.get("EBADF")!; + } + + peername.address = this.#remoteAddress; + peername.port = this.#remotePort!; + peername.family = this.#remoteFamily!; + + return 0; + } + + /** + * Populates the provided object with local address entries. + * @param sockname An object to add the local address entries to. + * @return An error status code. + */ + getsockname(sockname: Record<string, string | number>): number { + if (this.#address === undefined) { + return codeMap.get("EBADF")!; + } + + sockname.address = this.#address; + sockname.port = this.#port!; + sockname.family = this.#family!; + + return 0; + } + + /** + * Opens a file descriptor. + * @param fd The file descriptor to open. + * @return An error status code. + */ + open(_fd: number): number { + // REF: https://github.com/denoland/deno/issues/6529 + notImplemented("udp.UDP.prototype.open"); + } + + /** + * Start receiving on the connection. + * @return An error status code. + */ + recvStart(): number { + if (!this.#receiving) { + this.#receiving = true; + this.#receive(); + } + + return 0; + } + + /** + * Stop receiving on the connection. + * @return An error status code. + */ + recvStop(): number { + this.#receiving = false; + + return 0; + } + + override ref() { + notImplemented("udp.UDP.prototype.ref"); + } + + send( + req: SendWrap, + bufs: MessageType[], + count: number, + ...args: [number, string, boolean] | [boolean] + ): number { + return this.#doSend(req, bufs, count, args, AF_INET); + } + + send6( + req: SendWrap, + bufs: MessageType[], + count: number, + ...args: [number, string, boolean] | [boolean] + ): number { + return this.#doSend(req, bufs, count, args, AF_INET6); + } + + setBroadcast(_bool: 0 | 1): number { + notImplemented("udp.UDP.prototype.setBroadcast"); + } + + setMulticastInterface(_interfaceAddress: string): number { + notImplemented("udp.UDP.prototype.setMulticastInterface"); + } + + setMulticastLoopback(_bool: 0 | 1): number { + notImplemented("udp.UDP.prototype.setMulticastLoopback"); + } + + setMulticastTTL(_ttl: number): number { + notImplemented("udp.UDP.prototype.setMulticastTTL"); + } + + setTTL(_ttl: number): number { + notImplemented("udp.UDP.prototype.setTTL"); + } + + override unref() { + notImplemented("udp.UDP.prototype.unref"); + } + + #doBind(ip: string, port: number, _flags: number, family: number): number { + // TODO(cmorten): use flags to inform socket reuse etc. + const listenOptions = { + port, + hostname: ip, + transport: "udp" as const, + }; + + let listener; + + try { + listener = DenoListenDatagram(listenOptions); + } catch (e) { + if (e instanceof Deno.errors.AddrInUse) { + return codeMap.get("EADDRINUSE")!; + } else if (e instanceof Deno.errors.AddrNotAvailable) { + return codeMap.get("EADDRNOTAVAIL")!; + } else if (e instanceof Deno.errors.PermissionDenied) { + throw e; + } + + // TODO(cmorten): map errors to appropriate error codes. + return codeMap.get("UNKNOWN")!; + } + + const address = listener.addr as Deno.NetAddr; + this.#address = address.hostname; + this.#port = address.port; + this.#family = family === AF_INET6 ? ("IPv6" as const) : ("IPv4" as const); + this.#listener = listener; + + return 0; + } + + #doConnect(ip: string, port: number, family: number): number { + this.#remoteAddress = ip; + this.#remotePort = port; + this.#remoteFamily = family === AF_INET6 + ? ("IPv6" as const) + : ("IPv4" as const); + + return 0; + } + + #doSend( + req: SendWrap, + bufs: MessageType[], + _count: number, + args: [number, string, boolean] | [boolean], + _family: number, + ): number { + let hasCallback: boolean; + + if (args.length === 3) { + this.#remotePort = args[0] as number; + this.#remoteAddress = args[1] as string; + hasCallback = args[2] as boolean; + } else { + hasCallback = args[0] as boolean; + } + + const addr: Deno.NetAddr = { + hostname: this.#remoteAddress!, + port: this.#remotePort!, + transport: "udp", + }; + + // Deno.DatagramConn.prototype.send accepts only one Uint8Array + const payload = new Uint8Array( + Buffer.concat( + bufs.map((buf) => { + if (typeof buf === "string") { + return Buffer.from(buf); + } + + return Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength); + }), + ), + ); + + (async () => { + let sent: number; + let err: number | null = null; + + try { + sent = await this.#listener!.send(payload, addr); + } catch (e) { + // TODO(cmorten): map errors to appropriate error codes. + if (e instanceof Deno.errors.BadResource) { + err = codeMap.get("EBADF")!; + } else if ( + e instanceof Error && + e.message.match(/os error (40|90|10040)/) + ) { + err = codeMap.get("EMSGSIZE")!; + } else { + err = codeMap.get("UNKNOWN")!; + } + + sent = 0; + } + + if (hasCallback) { + try { + req.oncomplete(err, sent); + } catch { + // swallow callback errors + } + } + })(); + + return 0; + } + + async #receive() { + if (!this.#receiving) { + return; + } + + const p = new Uint8Array(this.#recvBufferSize); + + let buf: Uint8Array; + let remoteAddr: Deno.NetAddr | null; + let nread: number | null; + + try { + [buf, remoteAddr] = (await this.#listener!.receive(p)) as [ + Uint8Array, + Deno.NetAddr, + ]; + + nread = buf.length; + } catch (e) { + // TODO(cmorten): map errors to appropriate error codes. + if ( + e instanceof Deno.errors.Interrupted || + e instanceof Deno.errors.BadResource + ) { + nread = 0; + } else { + nread = codeMap.get("UNKNOWN")!; + } + + buf = new Uint8Array(0); + remoteAddr = null; + } + + nread ??= 0; + + const rinfo = remoteAddr + ? { + address: remoteAddr.hostname, + port: remoteAddr.port, + family: isIP(remoteAddr.hostname) === 6 + ? ("IPv6" as const) + : ("IPv4" as const), + } + : undefined; + + try { + this.onmessage(nread, this, Buffer.from(buf), rinfo); + } catch { + // swallow callback errors. + } + + this.#receive(); + } + + /** Handle socket closure. */ + override _onClose(): number { + this.#receiving = false; + + this.#address = undefined; + this.#port = undefined; + this.#family = undefined; + + try { + this.#listener!.close(); + } catch { + // listener already closed + } + + this.#listener = undefined; + + return 0; + } +} |