2023-02-14 11:38:45 -05:00
// 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.
2023-06-27 02:18:22 -04:00
// TODO(petamoriken): enable prefer-primordials for node polyfills
// deno-lint-ignore-file prefer-primordials
2023-07-02 14:19:30 -04:00
import { Buffer } from "node:buffer" ;
import { EventEmitter } from "node:events" ;
import { lookup as defaultLookup } from "node:dns" ;
2023-02-14 11:38:45 -05:00
import type {
ErrnoException ,
NodeSystemErrorCtx ,
2023-03-08 06:44:54 -05:00
} from "ext:deno_node/internal/errors.ts" ;
2023-02-14 11:38:45 -05:00
import {
ERR_BUFFER_OUT_OF_BOUNDS ,
ERR_INVALID_ARG_TYPE ,
ERR_INVALID_FD_TYPE ,
ERR_MISSING_ARGS ,
ERR_SOCKET_ALREADY_BOUND ,
ERR_SOCKET_BAD_BUFFER_SIZE ,
ERR_SOCKET_BUFFER_SIZE ,
ERR_SOCKET_DGRAM_IS_CONNECTED ,
ERR_SOCKET_DGRAM_NOT_CONNECTED ,
ERR_SOCKET_DGRAM_NOT_RUNNING ,
errnoException ,
exceptionWithHostPort ,
2023-03-08 06:44:54 -05:00
} from "ext:deno_node/internal/errors.ts" ;
import type { Abortable } from "ext:deno_node/_events.d.ts" ;
import { kStateSymbol , newHandle } from "ext:deno_node/internal/dgram.ts" ;
import type { SocketType } from "ext:deno_node/internal/dgram.ts" ;
2023-02-14 11:38:45 -05:00
import {
asyncIdSymbol ,
defaultTriggerAsyncIdScope ,
ownerSymbol ,
2023-03-08 06:44:54 -05:00
} from "ext:deno_node/internal/async_hooks.ts" ;
import { SendWrap , UDP } from "ext:deno_node/internal_binding/udp_wrap.ts" ;
2023-02-14 11:38:45 -05:00
import {
isInt32 ,
validateAbortSignal ,
validateNumber ,
validatePort ,
validateString ,
2023-03-08 06:44:54 -05:00
} from "ext:deno_node/internal/validators.mjs" ;
import { guessHandleType } from "ext:deno_node/internal_binding/util.ts" ;
import { os } from "ext:deno_node/internal_binding/constants.ts" ;
2023-07-02 14:19:30 -04:00
import { nextTick } from "node:process" ;
import { channel } from "node:diagnostics_channel" ;
2023-03-08 06:44:54 -05:00
import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts" ;
2023-02-14 11:38:45 -05:00
const { UV_UDP_REUSEADDR , UV_UDP_IPV6ONLY } = os ;
const udpSocketChannel = channel ( "udp.socket" ) ;
const BIND_STATE_UNBOUND = 0 ;
const BIND_STATE_BINDING = 1 ;
const BIND_STATE_BOUND = 2 ;
const CONNECT_STATE_DISCONNECTED = 0 ;
const CONNECT_STATE_CONNECTING = 1 ;
const CONNECT_STATE_CONNECTED = 2 ;
const RECV_BUFFER = true ;
const SEND_BUFFER = false ;
export interface AddressInfo {
address : string ;
family : number ;
port : number ;
}
export type MessageType = string | Uint8Array | Buffer | DataView ;
export type RemoteInfo = {
address : string ;
family : "IPv4" | "IPv6" ;
port : number ;
size? : number ;
} ;
export interface BindOptions {
port? : number ;
address? : string ;
exclusive? : boolean ;
fd? : number ;
}
export interface SocketOptions extends Abortable {
type : SocketType ;
reuseAddr? : boolean ;
/ * *
* @default false
* /
ipv6Only? : boolean ;
recvBufferSize? : number ;
sendBufferSize? : number ;
lookup? : typeof defaultLookup ;
}
interface SocketInternalState {
handle : UDP | null ;
receiving : boolean ;
bindState :
| typeof BIND_STATE_UNBOUND
| typeof BIND_STATE_BINDING
| typeof BIND_STATE_BOUND ;
connectState :
| typeof CONNECT_STATE_DISCONNECTED
| typeof CONNECT_STATE_CONNECTING
| typeof CONNECT_STATE_CONNECTED ;
queue? : Array < ( ) = > void > ;
reuseAddr? : boolean ;
ipv6Only? : boolean ;
recvBufferSize? : number ;
sendBufferSize? : number ;
}
const isSocketOptions = (
socketOption : unknown ,
) : socketOption is SocketOptions = >
socketOption !== null && typeof socketOption === "object" ;
const isUdpHandle = ( handle : unknown ) : handle is UDP = >
handle !== null &&
typeof handle === "object" &&
typeof ( handle as UDP ) . recvStart === "function" ;
const isBindOptions = ( options : unknown ) : options is BindOptions = >
options !== null && typeof options === "object" ;
/ * *
* Encapsulates the datagram functionality .
*
* New instances of ` dgram.Socket ` are created using ` createSocket ` .
* The ` new ` keyword is not to be used to create ` dgram.Socket ` instances .
* /
export class Socket extends EventEmitter {
[ asyncIdSymbol ] ! : number ;
[ kStateSymbol ] ! : SocketInternalState ;
type ! : SocketType ;
constructor (
type : SocketType | SocketOptions ,
listener ? : ( msg : Buffer , rinfo : RemoteInfo ) = > void ,
) {
super ( ) ;
let lookup ;
let recvBufferSize ;
let sendBufferSize ;
let options : SocketOptions | undefined ;
if ( isSocketOptions ( type ) ) {
options = type ;
type = options . type ;
lookup = options . lookup ;
recvBufferSize = options . recvBufferSize ;
sendBufferSize = options . sendBufferSize ;
}
const handle = newHandle ( type , lookup ) ;
handle [ ownerSymbol ] = this ;
this [ asyncIdSymbol ] = handle . getAsyncId ( ) ;
this . type = type ;
if ( typeof listener === "function" ) {
this . on ( "message" , listener ) ;
}
this [ kStateSymbol ] = {
handle ,
receiving : false ,
bindState : BIND_STATE_UNBOUND ,
connectState : CONNECT_STATE_DISCONNECTED ,
queue : undefined ,
reuseAddr : options && options . reuseAddr , // Use UV_UDP_REUSEADDR if true.
ipv6Only : options && options . ipv6Only ,
recvBufferSize ,
sendBufferSize ,
} ;
if ( options ? . signal !== undefined ) {
const { signal } = options ;
validateAbortSignal ( signal , "options.signal" ) ;
const onAborted = ( ) = > {
this . close ( ) ;
} ;
if ( signal . aborted ) {
onAborted ( ) ;
} else {
signal . addEventListener ( "abort" , onAborted ) ;
this . once (
"close" ,
( ) = > signal . removeEventListener ( "abort" , onAborted ) ,
) ;
}
}
if ( udpSocketChannel . hasSubscribers ) {
udpSocketChannel . publish ( {
socket : this ,
} ) ;
}
}
/ * *
* Tells the kernel to join a multicast group at the given ` multicastAddress `
* and ` multicastInterface ` using the ` IP_ADD_MEMBERSHIP ` socket option . If
* the ` multicastInterface ` argument is not specified , the operating system
* will choose one interface and will add membership to it . To add membership
* to every available interface , call ` addMembership ` multiple times , once
* per interface .
*
* When called on an unbound socket , this method will implicitly bind to a
* random port , listening on all interfaces .
*
* When sharing a UDP socket across multiple ` cluster ` workers , the
* ` socket.addMembership() ` function must be called only once or an
* ` EADDRINUSE ` error will occur :
*
* ` ` ` js
2023-03-08 06:44:54 -05:00
* import cluster from "ext:deno_node/cluster" ;
* import dgram from "ext:deno_node/dgram" ;
2023-02-14 11:38:45 -05:00
*
* if ( cluster . isPrimary ) {
* cluster . fork ( ) ; // Works ok.
* cluster . fork ( ) ; // Fails with EADDRINUSE.
* } else {
* const s = dgram . createSocket ( 'udp4' ) ;
* s . bind ( 1234 , ( ) = > {
* s . addMembership ( '224.0.0.114' ) ;
* } ) ;
* }
* ` ` `
* /
addMembership ( multicastAddress : string , interfaceAddress? : string ) {
healthCheck ( this ) ;
if ( ! multicastAddress ) {
throw new ERR_MISSING_ARGS ( "multicastAddress" ) ;
}
const { handle } = this [ kStateSymbol ] ;
const err = handle ! . addMembership ( multicastAddress , interfaceAddress ) ;
if ( err ) {
throw errnoException ( err , "addMembership" ) ;
}
}
/ * *
* Tells the kernel to join a source - specific multicast channel at the given
* ` sourceAddress ` and ` groupAddress ` , using the ` multicastInterface ` with
* the ` IP_ADD_SOURCE_MEMBERSHIP ` socket option . If the ` multicastInterface `
* argument is not specified , the operating system will choose one interface
* and will add membership to it . To add membership to every available
* interface , call ` socket.addSourceSpecificMembership() ` multiple times ,
* once per interface .
*
* When called on an unbound socket , this method will implicitly bind to a
* random port , listening on all interfaces .
* /
addSourceSpecificMembership (
sourceAddress : string ,
groupAddress : string ,
interfaceAddress? : string ,
) {
healthCheck ( this ) ;
validateString ( sourceAddress , "sourceAddress" ) ;
validateString ( groupAddress , "groupAddress" ) ;
const err = this [ kStateSymbol ] . handle ! . addSourceSpecificMembership (
sourceAddress ,
groupAddress ,
interfaceAddress ,
) ;
if ( err ) {
throw errnoException ( err , "addSourceSpecificMembership" ) ;
}
}
/ * *
* Returns an object containing the address information for a socket .
* For UDP sockets , this object will contain ` address ` , ` family ` and ` port ` properties .
*
* This method throws ` EBADF ` if called on an unbound socket .
* /
address ( ) : AddressInfo {
healthCheck ( this ) ;
const out = { } ;
const err = this [ kStateSymbol ] . handle ! . getsockname ( out ) ;
if ( err ) {
throw errnoException ( err , "getsockname" ) ;
}
return out as AddressInfo ;
}
/ * *
* For UDP sockets , causes the ` dgram.Socket ` to listen for datagram
* messages on a named ` port ` and optional ` address ` . If ` port ` is not
* specified or is ` 0 ` , the operating system will attempt to bind to a
* random port . If ` address ` is not specified , the operating system will
* attempt to listen on all addresses . Once binding is complete , a
* ` 'listening' ` event is emitted and the optional ` callback ` function is
* called .
*
* Specifying both a ` 'listening' ` event listener and passing a ` callback ` to
* the ` socket.bind() ` method is not harmful but not very useful .
*
* A bound datagram socket keeps the Node . js process running to receive
* datagram messages .
*
* If binding fails , an ` 'error' ` event is generated . In rare case ( e . g .
* attempting to bind with a closed socket ) , an ` Error ` may be thrown .
*
* Example of a UDP server listening on port 41234 :
*
* ` ` ` js
2023-03-08 06:44:54 -05:00
* import dgram from "ext:deno_node/dgram" ;
2023-02-14 11:38:45 -05:00
*
* const server = dgram . createSocket ( 'udp4' ) ;
*
* server . on ( 'error' , ( err ) = > {
* console . log ( ` server error: \ n ${ err . stack } ` ) ;
* server . close ( ) ;
* } ) ;
*
* server . on ( 'message' , ( msg , rinfo ) = > {
* console . log ( ` server got: ${ msg } from ${ rinfo . address } : ${ rinfo . port } ` ) ;
* } ) ;
*
* server . on ( 'listening' , ( ) = > {
* const address = server . address ( ) ;
* console . log ( ` server listening ${ address . address } : ${ address . port } ` ) ;
* } ) ;
*
* server . bind ( 41234 ) ;
* // Prints: server listening 0.0.0.0:41234
* ` ` `
*
* @param callback with no parameters . Called when binding is complete .
* /
bind ( port? : number , address? : string , callback ? : ( ) = > void ) : this ;
bind ( port : number , callback ? : ( ) = > void ) : this ;
bind ( callback : ( ) = > void ) : this ;
bind ( options : BindOptions , callback ? : ( ) = > void ) : this ;
bind ( port_? : unknown , address_? : unknown /* callback */ ) : this {
let port = typeof port_ === "function" ? null : port_ ;
healthCheck ( this ) ;
const state = this [ kStateSymbol ] ;
if ( state . bindState !== BIND_STATE_UNBOUND ) {
throw new ERR_SOCKET_ALREADY_BOUND ( ) ;
}
state . bindState = BIND_STATE_BINDING ;
const cb = arguments . length && arguments [ arguments . length - 1 ] ;
if ( typeof cb === "function" ) {
// deno-lint-ignore no-inner-declarations
function removeListeners ( this : Socket ) {
this . removeListener ( "error" , removeListeners ) ;
this . removeListener ( "listening" , onListening ) ;
}
// deno-lint-ignore no-inner-declarations
function onListening ( this : Socket ) {
removeListeners . call ( this ) ;
cb . call ( this ) ;
}
this . on ( "error" , removeListeners ) ;
this . on ( "listening" , onListening ) ;
}
if ( isUdpHandle ( port ) ) {
replaceHandle ( this , port ) ;
startListening ( this ) ;
return this ;
}
// Open an existing fd instead of creating a new one.
if ( isBindOptions ( port ) && isInt32 ( port . fd ! ) && port . fd ! > 0 ) {
const fd = port . fd ! ;
const state = this [ kStateSymbol ] ;
// TODO(cmorten): here we deviate somewhat from the Node implementation which
// makes use of the https://nodejs.org/api/cluster.html module to run servers
// across a "cluster" of Node processes to take advantage of multi-core
// systems.
//
// Though Deno has has a Worker capability from which we could simulate this,
// for now we assert that we are _always_ on the primary process.
const type = guessHandleType ( fd ) ;
if ( type !== "UDP" ) {
throw new ERR_INVALID_FD_TYPE ( type ) ;
}
const err = state . handle ! . open ( fd ) ;
if ( err ) {
throw errnoException ( err , "open" ) ;
}
startListening ( this ) ;
return this ;
}
let address : string ;
if ( isBindOptions ( port ) ) {
address = port . address || "" ;
port = port . port ;
} else {
address = typeof address_ === "function" ? "" : ( address_ as string ) ;
}
// Defaulting address for bind to all interfaces
if ( ! address ) {
if ( this . type === "udp4" ) {
address = "0.0.0.0" ;
} else {
address = "::" ;
}
}
// Resolve address first
state . handle ! . lookup ( address , ( lookupError , ip ) = > {
if ( lookupError ) {
state . bindState = BIND_STATE_UNBOUND ;
this . emit ( "error" , lookupError ) ;
return ;
}
let flags : number | undefined = 0 ;
if ( state . reuseAddr ) {
flags |= UV_UDP_REUSEADDR ;
}
if ( state . ipv6Only ) {
flags |= UV_UDP_IPV6ONLY ! ;
}
// TODO(cmorten): here we deviate somewhat from the Node implementation which
// makes use of the https://nodejs.org/api/cluster.html module to run servers
// across a "cluster" of Node processes to take advantage of multi-core
// systems.
//
// Though Deno has has a Worker capability from which we could simulate this,
// for now we assert that we are _always_ on the primary process.
if ( ! state . handle ) {
return ; // Handle has been closed in the mean time
}
const err = state . handle . bind ( ip , port as number || 0 , flags ) ;
if ( err ) {
const ex = exceptionWithHostPort ( err , "bind" , ip , port as number ) ;
state . bindState = BIND_STATE_UNBOUND ;
this . emit ( "error" , ex ) ;
// Todo(@bartlomieju): close?
return ;
}
startListening ( this ) ;
} ) ;
return this ;
}
/ * *
* Close the underlying socket and stop listening for data on it . If a
* callback is provided , it is added as a listener for the ` 'close' ` event .
*
* @param callback Called when the socket has been closed .
* /
close ( callback ? : ( ) = > void ) : this {
const state = this [ kStateSymbol ] ;
const queue = state . queue ;
if ( typeof callback === "function" ) {
this . on ( "close" , callback ) ;
}
if ( queue !== undefined ) {
queue . push ( this . close . bind ( this ) ) ;
return this ;
}
healthCheck ( this ) ;
stopReceiving ( this ) ;
state . handle ! . close ( ) ;
state . handle = null ;
defaultTriggerAsyncIdScope (
this [ asyncIdSymbol ] ,
nextTick ,
socketCloseNT ,
this ,
) ;
return this ;
}
/ * *
* Associates the ` dgram.Socket ` to a remote address and port . Every
* message sent by this handle is automatically sent to that destination .
* Also , the socket will only receive messages from that remote peer .
* Trying to call ` connect() ` on an already connected socket will result
* in an ` ERR_SOCKET_DGRAM_IS_CONNECTED ` exception . If ` address ` is not
* provided , ` '127.0.0.1' ` ( for ` udp4 ` sockets ) or ` '::1' ` ( for ` udp6 ` sockets )
* will be used by default . Once the connection is complete , a ` 'connect' ` event
* is emitted and the optional ` callback ` function is called . In case of failure ,
* the ` callback ` is called or , failing this , an ` 'error' ` event is emitted .
*
* @param callback Called when the connection is completed or on error .
* /
connect (
port : number ,
address? : string ,
callback ? : ( err? : ErrnoException ) = > void ,
) : void ;
connect ( port : number , callback : ( err? : ErrnoException ) = > void ) : void ;
connect ( port : number , address? : unknown , callback? : unknown ) {
port = validatePort ( port , "Port" , false ) ;
if ( typeof address === "function" ) {
callback = address ;
address = "" ;
} else if ( address === undefined ) {
address = "" ;
}
validateString ( address , "address" ) ;
const state = this [ kStateSymbol ] ;
if ( state . connectState !== CONNECT_STATE_DISCONNECTED ) {
throw new ERR_SOCKET_DGRAM_IS_CONNECTED ( ) ;
}
state . connectState = CONNECT_STATE_CONNECTING ;
if ( state . bindState === BIND_STATE_UNBOUND ) {
this . bind ( { port : 0 , exclusive : true } ) ;
}
if ( state . bindState !== BIND_STATE_BOUND ) {
enqueue (
this ,
_connect . bind (
this ,
port ,
address as string ,
callback as ( err? : ErrnoException ) = > void ,
) ,
) ;
return ;
}
Reflect . apply ( _connect , this , [ port , address , callback ] ) ;
}
/ * *
* A synchronous function that disassociates a connected ` dgram.Socket ` from
* its remote address . Trying to call ` disconnect() ` on an unbound or already
* disconnected socket will result in an ` ERR_SOCKET_DGRAM_NOT_CONNECTED `
* exception .
* /
disconnect() {
const state = this [ kStateSymbol ] ;
if ( state . connectState !== CONNECT_STATE_CONNECTED ) {
throw new ERR_SOCKET_DGRAM_NOT_CONNECTED ( ) ;
}
const err = state . handle ! . disconnect ( ) ;
if ( err ) {
throw errnoException ( err , "connect" ) ;
} else {
state . connectState = CONNECT_STATE_DISCONNECTED ;
}
}
/ * *
* Instructs the kernel to leave a multicast group at ` multicastAddress `
* using the ` IP_DROP_MEMBERSHIP ` socket option . This method is automatically
* called by the kernel when the socket is closed or the process terminates ,
* so most apps will never have reason to call this .
*
* If ` multicastInterface ` is not specified , the operating system will
* attempt to drop membership on all valid interfaces .
* /
dropMembership ( multicastAddress : string , interfaceAddress? : string ) {
healthCheck ( this ) ;
if ( ! multicastAddress ) {
throw new ERR_MISSING_ARGS ( "multicastAddress" ) ;
}
const err = this [ kStateSymbol ] . handle ! . dropMembership (
multicastAddress ,
interfaceAddress ,
) ;
if ( err ) {
throw errnoException ( err , "dropMembership" ) ;
}
}
/ * *
* Instructs the kernel to leave a source - specific multicast channel at the
* given ` sourceAddress ` and ` groupAddress ` using the
* ` IP_DROP_SOURCE_MEMBERSHIP ` socket option . This method is automatically
* called by the kernel when the socket is closed or the process terminates ,
* so most apps will never have reason to call this .
*
* If ` multicastInterface ` is not specified , the operating system will
* attempt to drop membership on all valid interfaces .
* /
dropSourceSpecificMembership (
sourceAddress : string ,
groupAddress : string ,
interfaceAddress? : string ,
) {
healthCheck ( this ) ;
validateString ( sourceAddress , "sourceAddress" ) ;
validateString ( groupAddress , "groupAddress" ) ;
const err = this [ kStateSymbol ] . handle ! . dropSourceSpecificMembership (
sourceAddress ,
groupAddress ,
interfaceAddress ,
) ;
if ( err ) {
throw errnoException ( err , "dropSourceSpecificMembership" ) ;
}
}
/ * *
* This method throws ` ERR_SOCKET_BUFFER_SIZE ` if called on an unbound
* socket .
*
* @return the ` SO_RCVBUF ` socket receive buffer size in bytes .
* /
getRecvBufferSize ( ) : number {
return bufferSize ( this , 0 , RECV_BUFFER ) ;
}
/ * *
* This method throws ` ERR_SOCKET_BUFFER_SIZE ` if called on an unbound
* socket .
*
* @return the ` SO_SNDBUF ` socket send buffer size in bytes .
* /
getSendBufferSize ( ) : number {
return bufferSize ( this , 0 , SEND_BUFFER ) ;
}
/ * *
* By default , binding a socket will cause it to block the Node . js process
* from exiting as long as the socket is open . The ` socket.unref() ` method
* can be used to exclude the socket from the reference counting that keeps
* the Node . js process active . The ` socket.ref() ` method adds the socket back
* to the reference counting and restores the default behavior .
*
* Calling ` socket.ref() ` multiples times will have no additional effect .
*
* The ` socket.ref() ` method returns a reference to the socket so calls can
* be chained .
* /
ref ( ) : this {
const handle = this [ kStateSymbol ] . handle ;
if ( handle ) {
handle . ref ( ) ;
}
return this ;
}
/ * *
* Returns an object containing the ` address ` , ` family ` , and ` port ` of the
* remote endpoint . This method throws an ` ERR_SOCKET_DGRAM_NOT_CONNECTED `
* exception if the socket is not connected .
* /
remoteAddress ( ) : AddressInfo {
healthCheck ( this ) ;
const state = this [ kStateSymbol ] ;
if ( state . connectState !== CONNECT_STATE_CONNECTED ) {
throw new ERR_SOCKET_DGRAM_NOT_CONNECTED ( ) ;
}
const out = { } ;
const err = state . handle ! . getpeername ( out ) ;
if ( err ) {
throw errnoException ( err , "getpeername" ) ;
}
return out as AddressInfo ;
}
/ * *
* Broadcasts a datagram on the socket .
* For connectionless sockets , the destination ` port ` and ` address ` must be
* specified . Connected sockets , on the other hand , will use their associated
* remote endpoint , so the ` port ` and ` address ` arguments must not be set .
*
* The ` msg ` argument contains the message to be sent .
* Depending on its type , different behavior can apply . If ` msg ` is a
* ` Buffer ` , any ` TypedArray ` or a ` DataView ` ,
* the ` offset ` and ` length ` specify the offset within the ` Buffer ` where the
* message begins and the number of bytes in the message , respectively .
* If ` msg ` is a ` String ` , then it is automatically converted to a ` Buffer `
* with ` 'utf8' ` encoding . With messages that contain multi - byte characters ,
* ` offset ` and ` length ` will be calculated with respect to ` byte length ` and
* not the character position . If ` msg ` is an array , ` offset ` and ` length `
* must not be specified .
*
* The ` address ` argument is a string . If the value of ` address ` is a host
* name , DNS will be used to resolve the address of the host . If ` address `
* is not provided or otherwise nullish , ` '127.0.0.1' ` ( for ` udp4 ` sockets )
* or ` '::1' ` ( for ` udp6 ` sockets ) will be used by default .
*
* If the socket has not been previously bound with a call to ` bind ` , the
* socket is assigned a random port number and is bound to the " all
* interfaces " address ( ` '0.0.0.0' ` for ` udp4 ` sockets , ` '::0' ` for ` udp6 `
* sockets . )
*
* An optional ` callback ` function may be specified to as a way of
* reporting DNS errors or for determining when it is safe to reuse the ` buf `
* object . DNS lookups delay the time to send for at least one tick of the
* Node . js event loop .
*
* The only way to know for sure that the datagram has been sent is by using
* a ` callback ` . If an error occurs and a ` callback ` is given , the error will
* be passed as the first argument to the ` callback ` . If a ` callback ` is not
* given , the error is emitted as an ` 'error' ` event on the ` socket ` object .
*
* Offset and length are optional but both _must_ be set if either are used .
* They are supported only when the first argument is a ` Buffer ` , a
* ` TypedArray ` , or a ` DataView ` .
*
* This method throws ` ERR_SOCKET_BAD_PORT ` if called on an unbound socket .
*
* Example of sending a UDP packet to a port on ` localhost ` ;
*
* ` ` ` js
2023-03-08 06:44:54 -05:00
* import dgram from "ext:deno_node/dgram" ;
* import { Buffer } from "ext:deno_node/buffer" ;
2023-02-14 11:38:45 -05:00
*
* const message = Buffer . from ( 'Some bytes' ) ;
* const client = dgram . createSocket ( 'udp4' ) ;
* client . send ( message , 41234 , 'localhost' , ( err ) = > {
* client . close ( ) ;
* } ) ;
* ` ` `
*
* Example of sending a UDP packet composed of multiple buffers to a port on
* ` 127.0.0.1 ` ;
*
* ` ` ` js
2023-03-08 06:44:54 -05:00
* import dgram from "ext:deno_node/dgram" ;
* import { Buffer } from "ext:deno_node/buffer" ;
2023-02-14 11:38:45 -05:00
*
* const buf1 = Buffer . from ( 'Some ' ) ;
* const buf2 = Buffer . from ( 'bytes' ) ;
* const client = dgram . createSocket ( 'udp4' ) ;
* client . send ( [ buf1 , buf2 ] , 41234 , ( err ) = > {
* client . close ( ) ;
* } ) ;
* ` ` `
*
* Sending multiple buffers might be faster or slower depending on the
* application and operating system . Run benchmarks to
* determine the optimal strategy on a case - by - case basis . Generally
* speaking , however , sending multiple buffers is faster .
*
* Example of sending a UDP packet using a socket connected to a port on
* ` localhost ` :
*
* ` ` ` js
2023-03-08 06:44:54 -05:00
* import dgram from "ext:deno_node/dgram" ;
* import { Buffer } from "ext:deno_node/buffer" ;
2023-02-14 11:38:45 -05:00
*
* const message = Buffer . from ( 'Some bytes' ) ;
* const client = dgram . createSocket ( 'udp4' ) ;
* client . connect ( 41234 , 'localhost' , ( err ) = > {
* client . send ( message , ( err ) = > {
* client . close ( ) ;
* } ) ;
* } ) ;
* ` ` `
*
* @param msg Message to be sent .
* @param offset Offset in the buffer where the message starts .
* @param length Number of bytes in the message .
* @param port Destination port .
* @param address Destination host name or IP address .
* @param callback Called when the message has been sent .
* /
send (
msg : MessageType | ReadonlyArray < MessageType > ,
port? : number ,
address? : string ,
callback ? : ( error : ErrnoException | null , bytes? : number ) = > void ,
) : void ;
send (
msg : MessageType | ReadonlyArray < MessageType > ,
port? : number ,
callback ? : ( error : ErrnoException | null , bytes? : number ) = > void ,
) : void ;
send (
msg : MessageType | ReadonlyArray < MessageType > ,
callback ? : ( error : ErrnoException | null , bytes? : number ) = > void ,
) : void ;
send (
msg : MessageType ,
offset : number ,
length : number ,
port? : number ,
address? : string ,
callback ? : ( error : ErrnoException | null , bytes? : number ) = > void ,
) : void ;
send (
msg : MessageType ,
offset : number ,
length : number ,
port? : number ,
callback ? : ( error : ErrnoException | null , bytes? : number ) = > void ,
) : void ;
send (
msg : MessageType ,
offset : number ,
length : number ,
callback ? : ( error : ErrnoException | null , bytes? : number ) = > void ,
) : void ;
send (
buffer : unknown ,
offset? : unknown ,
length? : unknown ,
port? : unknown ,
address? : unknown ,
callback? : unknown ,
) {
let list : MessageType [ ] | null ;
const state = this [ kStateSymbol ] ;
const connected = state . connectState === CONNECT_STATE_CONNECTED ;
if ( ! connected ) {
if ( address || ( port && typeof port !== "function" ) ) {
buffer = sliceBuffer (
buffer as MessageType ,
offset as number ,
length as number ,
) ;
} else {
callback = port ;
port = offset ;
address = length ;
}
} else {
if ( typeof length === "number" ) {
buffer = sliceBuffer ( buffer as MessageType , offset as number , length ) ;
if ( typeof port === "function" ) {
callback = port ;
port = null ;
}
} else {
callback = offset ;
}
if ( port || address ) {
throw new ERR_SOCKET_DGRAM_IS_CONNECTED ( ) ;
}
}
if ( ! Array . isArray ( buffer ) ) {
if ( typeof buffer === "string" ) {
list = [ Buffer . from ( buffer ) ] ;
} else if ( ! isArrayBufferView ( buffer ) ) {
throw new ERR_INVALID_ARG_TYPE (
"buffer" ,
[ "Buffer" , "TypedArray" , "DataView" , "string" ] ,
buffer ,
) ;
} else {
list = [ buffer as MessageType ] ;
}
} else if ( ! ( list = fixBufferList ( buffer ) ) ) {
throw new ERR_INVALID_ARG_TYPE (
"buffer list arguments" ,
[ "Buffer" , "TypedArray" , "DataView" , "string" ] ,
buffer ,
) ;
}
if ( ! connected ) {
port = validatePort ( port , "Port" , false ) ;
}
// Normalize callback so it's either a function or undefined but not anything
// else.
if ( typeof callback !== "function" ) {
callback = undefined ;
}
if ( typeof address === "function" ) {
callback = address ;
address = undefined ;
} else if ( address && typeof address !== "string" ) {
throw new ERR_INVALID_ARG_TYPE ( "address" , [ "string" , "falsy" ] , address ) ;
}
healthCheck ( this ) ;
if ( state . bindState === BIND_STATE_UNBOUND ) {
this . bind ( { port : 0 , exclusive : true } ) ;
}
if ( list . length === 0 ) {
list . push ( Buffer . alloc ( 0 ) ) ;
}
// If the socket hasn't been bound yet, push the outbound packet onto the
// send queue and send after binding is complete.
if ( state . bindState !== BIND_STATE_BOUND ) {
// @ts-ignore mapping unknowns back onto themselves doesn't type nicely
enqueue ( this , this . send . bind ( this , list , port , address , callback ) ) ;
return ;
}
const afterDns = ( ex : ErrnoException | null , ip : string ) = > {
defaultTriggerAsyncIdScope (
this [ asyncIdSymbol ] ,
doSend ,
ex ,
this ,
ip ,
list ,
address ,
port ,
callback ,
) ;
} ;
if ( ! connected ) {
state . handle ! . lookup ( address as string , afterDns ) ;
} else {
afterDns ( null , "" ) ;
}
}
/ * *
* Sets or clears the ` SO_BROADCAST ` socket option . When set to ` true ` , UDP
* packets may be sent to a local interface ' s broadcast address .
*
* This method throws ` EBADF ` if called on an unbound socket .
* /
setBroadcast ( arg : boolean ) {
const err = this [ kStateSymbol ] . handle ! . setBroadcast ( arg ? 1 : 0 ) ;
if ( err ) {
throw errnoException ( err , "setBroadcast" ) ;
}
}
/ * *
* _All references to scope in this section are referring to [ IPv6 Zone Indices ] ( https : //en.wikipedia.org/wiki/IPv6_address#Scoped_literal_IPv6_addresses), which are defined by [RFC
* 4007 ] ( https : //tools.ietf.org/html/rfc4007). In string form, an IP_
* _with a scope index is written as ` 'IP%scope' ` where scope is an interface name_
* _or interface number . _
*
* Sets the default outgoing multicast interface of the socket to a chosen
* interface or back to system interface selection . The ` multicastInterface ` must
* be a valid string representation of an IP from the socket ' s family .
*
* For IPv4 sockets , this should be the IP configured for the desired physical
* interface . All packets sent to multicast on the socket will be sent on the
* interface determined by the most recent successful use of this call .
*
* For IPv6 sockets , ` multicastInterface ` should include a scope to indicate the
* interface as in the examples that follow . In IPv6 , individual ` send ` calls can
* also use explicit scope in addresses , so only packets sent to a multicast
* address without specifying an explicit scope are affected by the most recent
* successful use of this call .
*
* This method throws ` EBADF ` if called on an unbound socket .
*
* # # # # Example : IPv6 outgoing multicast interface
*
* On most systems , where scope format uses the interface name :
*
* ` ` ` js
* const socket = dgram . createSocket ( 'udp6' ) ;
*
* socket . bind ( 1234 , ( ) = > {
* socket . setMulticastInterface ( '::%eth1' ) ;
* } ) ;
* ` ` `
*
* On Windows , where scope format uses an interface number :
*
* ` ` ` js
* const socket = dgram . createSocket ( 'udp6' ) ;
*
* socket . bind ( 1234 , ( ) = > {
* socket . setMulticastInterface ( '::%2' ) ;
* } ) ;
* ` ` `
*
* # # # # Example : IPv4 outgoing multicast interface
*
* All systems use an IP of the host on the desired physical interface :
*
* ` ` ` js
* const socket = dgram . createSocket ( 'udp4' ) ;
*
* socket . bind ( 1234 , ( ) = > {
* socket . setMulticastInterface ( '10.0.0.2' ) ;
* } ) ;
* ` ` `
* /
setMulticastInterface ( interfaceAddress : string ) {
healthCheck ( this ) ;
validateString ( interfaceAddress , "interfaceAddress" ) ;
const err = this [ kStateSymbol ] . handle ! . setMulticastInterface (
interfaceAddress ,
) ;
if ( err ) {
throw errnoException ( err , "setMulticastInterface" ) ;
}
}
/ * *
* Sets or clears the ` IP_MULTICAST_LOOP ` socket option . When set to ` true ` ,
* multicast packets will also be received on the local interface .
*
* This method throws ` EBADF ` if called on an unbound socket .
* /
setMulticastLoopback ( arg : boolean ) : typeof arg {
const err = this [ kStateSymbol ] . handle ! . setMulticastLoopback ( arg ? 1 : 0 ) ;
if ( err ) {
throw errnoException ( err , "setMulticastLoopback" ) ;
}
return arg ; // 0.4 compatibility
}
/ * *
* Sets the ` IP_MULTICAST_TTL ` socket option . While TTL generally stands for
* "Time to Live" , in this context it specifies the number of IP hops that a
* packet is allowed to travel through , specifically for multicast traffic . Each
* router or gateway that forwards a packet decrements the TTL . If the TTL is
* decremented to 0 by a router , it will not be forwarded .
*
* The ` ttl ` argument may be between 0 and 255 \ . The default on most systems is ` 1 ` .
*
* This method throws ` EBADF ` if called on an unbound socket .
* /
setMulticastTTL ( ttl : number ) : typeof ttl {
validateNumber ( ttl , "ttl" ) ;
const err = this [ kStateSymbol ] . handle ! . setMulticastTTL ( ttl ) ;
if ( err ) {
throw errnoException ( err , "setMulticastTTL" ) ;
}
return ttl ;
}
/ * *
* Sets the ` SO_RCVBUF ` socket option . Sets the maximum socket receive buffer
* in bytes .
*
* This method throws ` ERR_SOCKET_BUFFER_SIZE ` if called on an unbound socket .
* /
setRecvBufferSize ( size : number ) {
bufferSize ( this , size , RECV_BUFFER ) ;
}
/ * *
* Sets the ` SO_SNDBUF ` socket option . Sets the maximum socket send buffer
* in bytes .
*
* This method throws ` ERR_SOCKET_BUFFER_SIZE ` if called on an unbound socket .
* /
setSendBufferSize ( size : number ) {
bufferSize ( this , size , SEND_BUFFER ) ;
}
/ * *
* Sets the ` IP_TTL ` socket option . While TTL generally stands for "Time to Live" ,
* in this context it specifies the number of IP hops that a packet is allowed to
* travel through . Each router or gateway that forwards a packet decrements the
* TTL . If the TTL is decremented to 0 by a router , it will not be forwarded .
* Changing TTL values is typically done for network probes or when multicasting .
*
* The ` ttl ` argument may be between between 1 and 255 \ . The default on most systems
* is 64 .
*
* This method throws ` EBADF ` if called on an unbound socket .
* /
setTTL ( ttl : number ) : typeof ttl {
validateNumber ( ttl , "ttl" ) ;
const err = this [ kStateSymbol ] . handle ! . setTTL ( ttl ) ;
if ( err ) {
throw errnoException ( err , "setTTL" ) ;
}
return ttl ;
}
/ * *
* By default , binding a socket will cause it to block the Node . js process from
* exiting as long as the socket is open . The ` socket.unref() ` method can be used
* to exclude the socket from the reference counting that keeps the Node . js
* process active , allowing the process to exit even if the socket is still
* listening .
*
* Calling ` socket.unref() ` multiple times will have no addition effect .
*
* The ` socket.unref() ` method returns a reference to the socket so calls can be
* chained .
* /
unref ( ) : this {
const handle = this [ kStateSymbol ] . handle ;
if ( handle ) {
handle . unref ( ) ;
}
return this ;
}
}
/ * *
* Creates a ` dgram.Socket ` object . Once the socket is created , calling
* ` socket.bind() ` will instruct the socket to begin listening for datagram
* messages . When ` address ` and ` port ` are not passed to ` socket.bind() ` the
* method will bind the socket to the "all interfaces" address on a random port
* ( it does the right thing for both ` udp4 ` and ` udp6 ` sockets ) . The bound
* address and port can be retrieved using ` socket.address().address ` and
* ` socket.address().port ` .
*
* If the ` signal ` option is enabled , calling ` .abort() ` on the corresponding
* ` AbortController ` is similar to calling ` .close() ` on the socket :
*
* ` ` ` js
* const controller = new AbortController ( ) ;
* const { signal } = controller ;
* const server = dgram . createSocket ( { type : 'udp4' , signal } ) ;
* server . on ( 'message' , ( msg , rinfo ) = > {
* console . log ( ` server got: ${ msg } from ${ rinfo . address } : ${ rinfo . port } ` ) ;
* } ) ;
* // Later, when you want to close the server.
* controller . abort ( ) ;
* ` ` `
*
* @param options
* @param callback Attached as a listener for ` 'message' ` events . Optional .
* /
export function createSocket (
type : SocketType ,
listener ? : ( msg : Buffer , rinfo : RemoteInfo ) = > void ,
) : Socket ;
export function createSocket (
type : SocketOptions ,
listener ? : ( msg : Buffer , rinfo : RemoteInfo ) = > void ,
) : Socket ;
export function createSocket (
type : SocketType | SocketOptions ,
listener ? : ( msg : Buffer , rinfo : RemoteInfo ) = > void ,
) : Socket {
return new Socket ( type , listener ) ;
}
function startListening ( socket : Socket ) {
const state = socket [ kStateSymbol ] ;
state . handle ! . onmessage = onMessage ;
// Todo(@bartlomieju): handle errors
state . handle ! . recvStart ( ) ;
state . receiving = true ;
state . bindState = BIND_STATE_BOUND ;
if ( state . recvBufferSize ) {
bufferSize ( socket , state . recvBufferSize , RECV_BUFFER ) ;
}
if ( state . sendBufferSize ) {
bufferSize ( socket , state . sendBufferSize , SEND_BUFFER ) ;
}
socket . emit ( "listening" ) ;
}
function replaceHandle ( self : Socket , newHandle : UDP ) {
const state = self [ kStateSymbol ] ;
const oldHandle = state . handle ! ;
// Set up the handle that we got from primary.
newHandle . lookup = oldHandle . lookup ;
newHandle . bind = oldHandle . bind ;
newHandle . send = oldHandle . send ;
newHandle [ ownerSymbol ] = self ;
// Replace the existing handle by the handle we got from primary.
oldHandle . close ( ) ;
state . handle = newHandle ;
}
function bufferSize ( self : Socket , size : number , buffer : boolean ) : number {
if ( size >>> 0 !== size ) {
throw new ERR_SOCKET_BAD_BUFFER_SIZE ( ) ;
}
const ctx = { } ;
const ret = self [ kStateSymbol ] . handle ! . bufferSize ( size , buffer , ctx ) ;
if ( ret === undefined ) {
throw new ERR_SOCKET_BUFFER_SIZE ( ctx as NodeSystemErrorCtx ) ;
}
return ret ;
}
function socketCloseNT ( self : Socket ) {
self . emit ( "close" ) ;
}
function healthCheck ( socket : Socket ) {
if ( ! socket [ kStateSymbol ] . handle ) {
// Error message from dgram_legacy.js.
throw new ERR_SOCKET_DGRAM_NOT_RUNNING ( ) ;
}
}
function stopReceiving ( socket : Socket ) {
const state = socket [ kStateSymbol ] ;
if ( ! state . receiving ) {
return ;
}
state . handle ! . recvStop ( ) ;
state . receiving = false ;
}
function onMessage (
nread : number ,
handle : UDP ,
buf? : Buffer ,
rinfo? : RemoteInfo ,
) {
const self = handle [ ownerSymbol ] as Socket ;
if ( nread < 0 ) {
self . emit ( "error" , errnoException ( nread , "recvmsg" ) ) ;
return ;
}
rinfo ! . size = buf ! . length ; // compatibility
self . emit ( "message" , buf , rinfo ) ;
}
function sliceBuffer ( buffer : MessageType , offset : number , length : number ) {
if ( typeof buffer === "string" ) {
buffer = Buffer . from ( buffer ) ;
} else if ( ! isArrayBufferView ( buffer ) ) {
throw new ERR_INVALID_ARG_TYPE (
"buffer" ,
[ "Buffer" , "TypedArray" , "DataView" , "string" ] ,
buffer ,
) ;
}
offset = offset >>> 0 ;
length = length >>> 0 ;
if ( offset > buffer . byteLength ) {
throw new ERR_BUFFER_OUT_OF_BOUNDS ( "offset" ) ;
}
if ( offset + length > buffer . byteLength ) {
throw new ERR_BUFFER_OUT_OF_BOUNDS ( "length" ) ;
}
return Buffer . from ( buffer . buffer , buffer . byteOffset + offset , length ) ;
}
function fixBufferList (
list : ReadonlyArray < MessageType > ,
) : Array < MessageType > | null {
const newList = new Array ( list . length ) ;
for ( let i = 0 , l = list . length ; i < l ; i ++ ) {
const buf = list [ i ] ;
if ( typeof buf === "string" ) {
newList [ i ] = Buffer . from ( buf ) ;
} else if ( ! isArrayBufferView ( buf ) ) {
return null ;
} else {
newList [ i ] = Buffer . from ( buf . buffer , buf . byteOffset , buf . byteLength ) ;
}
}
return newList ;
}
function enqueue ( self : Socket , toEnqueue : ( ) = > void ) {
const state = self [ kStateSymbol ] ;
// If the send queue hasn't been initialized yet, do it, and install an
// event handler that flushes the send queue after binding is done.
if ( state . queue === undefined ) {
state . queue = [ ] ;
self . once ( EventEmitter . errorMonitor , onListenError ) ;
self . once ( "listening" , onListenSuccess ) ;
}
state . queue . push ( toEnqueue ) ;
}
function onListenSuccess ( this : Socket ) {
this . removeListener ( EventEmitter . errorMonitor , onListenError ) ;
clearQueue . call ( this ) ;
}
function onListenError ( this : Socket ) {
this . removeListener ( "listening" , onListenSuccess ) ;
this [ kStateSymbol ] . queue = undefined ;
}
function clearQueue ( this : Socket ) {
const state = this [ kStateSymbol ] ;
const queue = state . queue ;
state . queue = undefined ;
// Flush the send queue.
for ( const queueEntry of queue ! ) {
queueEntry ( ) ;
}
}
function _connect (
this : Socket ,
port : number ,
address : string ,
callback : ( err? : ErrnoException ) = > void ,
) {
const state = this [ kStateSymbol ] ;
if ( callback ) {
this . once ( "connect" , callback ) ;
}
const afterDns = ( ex : ErrnoException | null , ip : string ) = > {
defaultTriggerAsyncIdScope (
this [ asyncIdSymbol ] ,
doConnect ,
ex ,
this ,
ip ,
address ,
port ,
callback ,
) ;
} ;
state . handle ! . lookup ( address , afterDns ) ;
}
function doConnect (
ex : ErrnoException | null ,
self : Socket ,
ip : string ,
address : string ,
port : number ,
callback : ( err? : ErrnoException ) = > void ,
) {
const state = self [ kStateSymbol ] ;
if ( ! state . handle ) {
return ;
}
if ( ! ex ) {
const err = state . handle . connect ( ip , port ) ;
if ( err ) {
ex = exceptionWithHostPort ( err , "connect" , address , port ) ;
}
}
if ( ex ) {
state . connectState = CONNECT_STATE_DISCONNECTED ;
return nextTick ( ( ) = > {
if ( callback ) {
self . removeListener ( "connect" , callback ) ;
callback ( ex ! ) ;
} else {
self . emit ( "error" , ex ) ;
}
} ) ;
}
state . connectState = CONNECT_STATE_CONNECTED ;
nextTick ( ( ) = > self . emit ( "connect" ) ) ;
}
function doSend (
ex : ErrnoException | null ,
self : Socket ,
ip : string ,
list : MessageType [ ] ,
address : string ,
port : number ,
callback ? : ( error : ErrnoException | null , bytes? : number ) = > void ,
) {
const state = self [ kStateSymbol ] ;
if ( ex ) {
if ( typeof callback === "function" ) {
nextTick ( callback , ex ) ;
return ;
}
nextTick ( ( ) = > self . emit ( "error" , ex ) ) ;
return ;
} else if ( ! state . handle ) {
return ;
}
const req = new SendWrap ( ) ;
req . list = list ; // Keep reference alive.
req . address = address ;
req . port = port ;
if ( callback ) {
req . callback = callback ;
req . oncomplete = afterSend ;
}
let err ;
if ( port ) {
err = state . handle . send ( req , list , list . length , port , ip , ! ! callback ) ;
} else {
err = state . handle . send ( req , list , list . length , ! ! callback ) ;
}
if ( err >= 1 ) {
// Synchronous finish. The return code is msg_length + 1 so that we can
// distinguish between synchronous success and asynchronous success.
if ( callback ) {
nextTick ( callback , null , err - 1 ) ;
}
return ;
}
if ( err && callback ) {
// Don't emit as error, dgram_legacy.js compatibility
const ex = exceptionWithHostPort ( err , "send" , address , port ) ;
nextTick ( callback , ex ) ;
}
}
function afterSend ( this : SendWrap , err : number | null , sent? : number ) {
let ex : ErrnoException | null ;
if ( err ) {
ex = exceptionWithHostPort ( err , "send" , this . address , this . port ) ;
} else {
ex = null ;
}
this . callback ( ex , sent ) ;
}
export type { SocketType } ;
export default {
createSocket ,
Socket ,
} ;