bare-pipe
Native I/O pipes for JavaScript
v4.4.0bare-pipe — Native I/O pipes for JavaScript. It is a native addon and requires Bare >=1.16.0.
npm i bare-pipeUsage
const Pipe = require('bare-pipe')
const [read, write] = Pipe.pipe()
const reader = new Pipe(read)
const writer = new Pipe(write)
reader.on('data', (data) => console.log(data.toString()))
writer.end('Hello world!\n')API
Pipe
new Pipe(path: string | number, opts?: PipeOptions)
Create a new pipe. If path is a number, it is treated as a file descriptor to open. If it is a string, it is treated as a path to connect to.
Overloads:
new Pipe(path: string | number, opts?: PipeOptions)
new Pipe(opts?: PipeOptions)Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
path | string | number | — | A file descriptor to open (number), or a path to connect to (string). |
opts? | PipeOptions | — | Options; readBufferSize defaults to 65536, allowHalfOpen and eagerOpen to true, and ipc to false (set ipc: true to enable handle passing over the pipe). |
accept<T extends IPCAcceptable>(target: T): T
Accept a pending handle into target. target must implement the IPCAcceptable protocol. Call this synchronously from the 'handle' event listener.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
target | T | — | The object to accept the pending handle into; must implement the IPCAcceptable protocol. Call synchronously from the 'handle' event listener. |
Returns T — target, for chaining the accepted handle into an expression.
Throws
INVALID_IPC_TARGET—targetdoes not implement the IPC handle protocol.
connect(path: string, opts?: PipeConnectOptions, onconnect?: () => void): this
Connect the pipe to path. onconnect is called when the connection is established.
Overloads:
connect(path: string, opts?: PipeConnectOptions, onconnect?: () => void): this
connect(path: string, onconnect: () => void): this
connect(opts: PipeConnectOptions, onconnect?: () => void): thisParameters
| Parameter | Type | Default | Description |
|---|---|---|---|
path | string | — | The path to connect to. |
opts? | PipeConnectOptions | — | Options; path may be given here instead of as the first argument. |
onconnect? | () => void | — | Called when the connection is established. |
Throws
PIPE_ALREADY_CONNECTED— the pipe is already connecting or connected.
connecting: boolean
Whether the pipe is currently connecting.
open(fd: number, opts?: PipeOpenOptions, onconnect?: () => void): this
Open the pipe on the given file descriptor.
Overloads:
open(fd: number, opts?: PipeOpenOptions, onconnect?: () => void): this
open(fd: number, onconnect: () => void): this
open(opts: PipeOpenOptions & { fd: number }, onconnect?: () => void): thisParameters
| Parameter | Type | Default | Description |
|---|---|---|---|
fd | number | — | The file descriptor to open the pipe on. |
opts? | PipeOpenOptions | — | — |
onconnect? | () => void | — | Called once when the pipe emits 'connect'. |
pending: boolean
Whether the pipe has not yet connected.
Pipe.constants
Pipe.constants: {
state: {
CONNECTING: number
CONNECTED: number
BINDING: number
BOUND: number
READING: number
CLOSING: number
CLOSED: number
UNREFED: number
READABLE: number
WRITABLE: number
}
path: {
MAX_LENGTH: number
}
handle: {
NAMED_PIPE: number
TCP: number
UDP: number
}
}Object containing internal state constants and handle type constants.
Pipe.createConnection
Pipe.createConnection(path: string, opts?: CreateConnectionOptions, onconnect?: () => void): PipeCreate a new pipe and connect it to path. Shorthand for new Pipe(options).connect(path, options, onconnect).
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
path | string | — | The path to connect to. |
opts? | CreateConnectionOptions | — | Options passed to both the Pipe constructor and connect(). |
onconnect? | () => void | — | Called when the connection is established. |
Pipe.createServer(opts?: PipeServerOptions, onconnection?: (pipe: Pipe) => void): PipeServer
Create a new pipe server. The server extends EventEmitter.
Overloads:
Pipe.createServer(opts?: PipeServerOptions, onconnection?: (pipe: Pipe) => void): PipeServer
Pipe.createServer(onconnection: (pipe: Pipe) => void): PipeServerParameters
| Parameter | Type | Default | Description |
|---|---|---|---|
opts? | PipeServerOptions | — | Options applied to each incoming pipe; readBufferSize defaults to 65536, allowHalfOpen to true, pauseOnConnect to false, and ipc to false. |
onconnection? | (pipe: Pipe) => void | — | Called on each 'connection' event. |
Pipe.pipe(): [read: number, write: number]
Returns [read: number, write: number] — A [read, write] pair of file descriptors connected to each other.
readyState: 'open' | 'opening' | 'readOnly' | 'writeOnly' | 'closed'
The current state of the pipe. One of 'open', 'opening', 'readOnly', 'writeOnly', or 'closed'.
Pipe.ref(): this
Ref the pipe, preventing the process from exiting.
remoteCredentials(): PipeCredentials
The operating system's account of the process on the other end of the pipe, read from the kernel rather than from anything the peer sent.
Added in v4.4.0. See Local access control for what each platform reports.
Returns PipeCredentials — The peer's credentials.
Throws
PIPE_IS_CLOSED— the pipe is closed.PIPE_NOT_CONNECTED— the pipe is not connected.ENOTSOCK— the pipe is not a socket, on Unix.
Pipe.unref(): this
Unref the pipe, allowing the process to exit.
write
write(chunk: Buffer | string, encoding: BufferEncoding, handle?: IPCAcceptable, cb?: (err: Error | null) => void): booleanWrite chunk to the pipe. If handle is given and the pipe was created with ipc: true, the handle is transferred to the receiver alongside the chunk. handle must implement the IPCAcceptable protocol.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
chunk | Buffer | string | — | The data to write. |
encoding | BufferEncoding | — | The encoding of chunk when it is a string. |
handle? | IPCAcceptable | — | A handle to transfer to the receiver alongside the chunk; requires the pipe to have been created with ipc: true and handle to implement the IPCAcceptable protocol. |
cb? | (err: Error | null) => void | — | Called when the chunk has been processed. |
PipeServer
new PipeServer(opts?: PipeServerOptions, onconnection?: (pipe: Pipe) => void)
Overloads:
new PipeServer(opts?: PipeServerOptions, onconnection?: (pipe: Pipe) => void)
new PipeServer(onconnection: (pipe: Pipe) => void)Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
opts? | PipeServerOptions | — | Options applied to each incoming pipe; readBufferSize defaults to 65536, allowHalfOpen to true, pauseOnConnect to false, and ipc to false. |
onconnection? | (pipe: Pipe) => void | — | Called on each 'connection' event. |
address(): string | null
Returns string | null — The bound path, or null if the server is not listening.
close(onclose?: () => void): this
Close the server. No new connections will be accepted. The server emits close after all existing connections have ended.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
onclose? | () => void | — | Called once when the server emits 'close', after all existing connections have ended. |
closing: boolean
listen
listen(path: string, backlog?: number, opts?: PipeServerListenOptions, onlistening?: () => void): thisStart listening for connections on path. backlog defaults to 511.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
path | string | — | The path to listen on. |
backlog? | number | — | The maximum length of the queue of pending connections (default 511). |
opts? | PipeServerListenOptions | — | path and backlog may be given here instead of as positional arguments. |
onlistening? | () => void | — | Called once when the server emits 'listening'. |
Throws
SERVER_ALREADY_LISTENING— the server is already listening.SERVER_IS_CLOSED— the server has been closed.INVALID_ARGUMENT—ownerOnlyis not a boolean, or was given for an abstract socket.
listening: boolean
Whether the server is listening.
PipeServer.ref(): this
Ref the pipe, preventing the process from exiting.
PipeServer.unref(): this
Unref the pipe, allowing the process to exit.
Types
CreateConnectionOptions
interface CreateConnectionOptions {
allowHalfOpen?: boolean
eagerOpen?: boolean
ipc?: boolean
readBufferSize?: number
path?: string
}IPCAcceptable
interface IPCAcceptable {
readonly [ipcHandle]: unknown
[ipcAccept]?(): void
}The protocol an object must implement to be passed to pipe.accept(target) or received via pipe.write(chunk, handle, ...); see IPCAcceptable protocol.
PipeConnectOptions
interface PipeConnectOptions {
path?: string
}PipeCredentials
interface PipeCredentials {
pid: number | null
uid: number | null
gid: number | null
sid: string | null
}The credentials that remoteCredentials() returns, with null for anything the platform cannot report.
PipeEvents
interface PipeEvents {
connect: []
handle: [type: number]
data: [data: unknown]
end: []
readable: []
piping: [dest: Writable]
close: []
error: [err: Error]
drain: []
finish: []
pipe: [src: Readable]
}PipeOpenOptions
interface PipeOpenOptions {
fd?: number
}PipeOptions
interface PipeOptions {
allowHalfOpen?: boolean
eagerOpen?: boolean
ipc?: boolean
readBufferSize?: number
}PipeServerEvents
interface PipeServerEvents {
close: []
connection: [pipe: Pipe]
error: [err: Error]
listening: []
}PipeServerListenOptions
interface PipeServerListenOptions {
backlog?: number
ownerOnly?: boolean
path?: string
}ownerOnly restricts the pipe to the current user before any client can connect, and disconnects clients that the restriction alone would admit. It is not supported for an abstract socket. Added in v4.4.0; see Local access control.
PipeServerOptions
interface PipeServerOptions {
allowHalfOpen?: boolean
ipc?: boolean
pauseOnConnect?: boolean
readBufferSize?: number
}Classes
PipeError
class PipeError {
code: string
}bare-pipe/constants
Constants and variables
constants
constants: {
state: {
CONNECTING: number
CONNECTED: number
BINDING: number
BOUND: number
READING: number
CLOSING: number
CLOSED: number
UNREFED: number
READABLE: number
WRITABLE: number
}
path: {
MAX_LENGTH: number
}
handle: {
NAMED_PIPE: number
TCP: number
UDP: number
}
}Object containing internal state constants and handle type constants.
bare-pipe/errors
Classes
errors.PipeError
IPC handle passing
Pipes created with ipc: true can transfer libuv handles (named pipes, TCP sockets, UDP sockets) to a peer alongside the byte stream. The peer receives a 'handle' event for each transferred handle, in arrival order, before the corresponding 'data' event.
Handle passing needs a bidirectional socket on both ends, so the descriptors must come from a socket pair, such as bare-tcp's socketpair(). The descriptors from Pipe.pipe() are a unidirectional pipe and cannot carry handles.
Handle passing needs a bidirectional socket on both ends, so the descriptors must come from a socket pair, such as bare-tcp's socketpair(). The descriptors from Pipe.pipe() are a unidirectional pipe and cannot carry handles.
Sender:
const left = new Pipe(fd, { ipc: true })
const socket = tcp.createConnection(port)
socket.on('connect', () => {
left.write(Buffer.from('here'), socket)
})Receiver:
const right = new Pipe(fd, { ipc: true })
right.on('handle', (type) => {
if (type === Pipe.constants.handle.TCP) {
const socket = new tcp.Socket()
right.accept(socket)
socket.on('data', console.log)
}
})IPCAcceptable protocol
Any object passed to pipe.accept(target) or pipe.write(chunk, handle, ...) must implement two well-known symbols:
const ipcHandle = Symbol.for('bare.ipc.handle')
const ipcAccept = Symbol.for('bare.ipc.accept')
class MyTarget {
get [ipcHandle]() {
return this._handle // An ArrayBuffer backing a libuv `uv_*_t` struct
}
[ipcAccept]() {
// Optional: Called synchronously after the handle has been transferred
}
}Symbol.for('bare.ipc.handle')(required): A getter returning the underlying libuv handle (typically anArrayBufferwhose first bytes are auv_stream_t/uv_udp_t).Symbol.for('bare.ipc.accept')(optional): A method invoked synchronously after the handle has been transferred. Use it to initialize per-handle state (for example address lookup).
Pipe, bare-tcp's Socket, bare-dgram's Socket, and any compatible package implement this protocol natively, so a bare-tcp socket can be passed and received via bare-pipe IPC without any glue code.
TypeScript users can import the IPCAcceptable interface from bare-pipe to type the protocol.
Local access control
Added in v4.4.0.
A server can restrict its pipe to the current user, and either end can ask the operating system who is on the other end.
const server = Pipe.createServer((pipe) => {
const credentials = pipe.remoteCredentials()
if (/* not valid credentials */) return pipe.destroy()
pipe.end('hello\n')
})
server.listen({ path, ownerOnly: true })What counts as valid credentials is up to the application, such as a uid matching the current user on Unix or a sid matching it on Windows.
ownerOnly applies the restriction before the server listens, and disconnects some clients as soon as they are accepted, without a connection event.
On Unix the socket file is made 0600, without following a symbolic link that replaced it, which on Linux requires Linux 6.6 or later or a mounted /proc. A client whose user is neither that of the server nor root is disconnected, which the mode alone admits when the client may bypass it, such as a process with CAP_DAC_OVERRIDE on Linux.
On Windows the pipe gets a protected DACL that allows only the current user and SYSTEM, any client that opened the pipe before the DACL was applied is disconnected, and so is:
- A remote client.
- A client whose process runs as a user other than that of the server or
SYSTEM. The DACL alone admits these when they connect while impersonating the current user. - A client running at a lower integrity level than the server, such as an unelevated process seen from an elevated server or a sandboxed low integrity process. The DACL alone admits these, as they run as the same user.
- A client whose process the server may not query, such as one running as
SYSTEMseen from an unelevated server, or one that has exited.
An elevated process of the current user is admitted by an unelevated server, as it runs as the same user at a higher integrity level.
Keep the socket in a directory only the current user can write to, so that its path cannot be replaced. An abstract socket has no file to restrict, so ownerOnly is rejected for one.
pipe.remoteCredentials() returns { pid, uid, gid, sid }, with null for anything the platform cannot report. On Linux it reads SO_PEERCRED; on macOS and the BSDs getpeereid(), plus LOCAL_PEERPID on macOS. Read the credentials while the peer is connected: on macOS the process ID is no longer available once the peer has closed its end.
On Windows the process ID comes from the pipe. A remote peer reports no process ID or SID, since its process ID names a process on another machine.
On the server end, the SID is the user of the client process with that ID: the user the process runs as, not one it impersonated while connecting. It does not tell an elevated process from an unelevated one of the same user. The SID is null:
- For a client that has exited, once nothing holds its process open. Read the credentials while the client is alive.
- For a process created after the server accepted the connection, as it has reused the ID of a client that exited.
- For a client process the server may not query, such as one running as
SYSTEMseen from an unelevated server. - For a server end opened from a handle or received over IPC, as it may have connected at any time.
The check for a reused ID, which ownerOnly also relies on, has two gaps. A client that exits and has its ID reused while its connection waits to be accepted goes unnoticed, and so does a reuse after the system clock is set back, as process creation times are read from it.
On the client end, the process ID names the process that created the pipe, which may since have exited and had its ID reused. The SID is instead the owner of the pipe, which the kernel records when the server creates it, and which is the user of the server process, except for an elevated administrator, whose pipes are owned by the Administrators group (S-1-5-32-544). Only a process holding SeRestorePrivilege, such as an elevated administrator, can make another user the owner.
See also
- Builds on
bare-eventsandbare-stream. bare-subprocess— uses pipes for child-process stdio.- Bare modules — the full
bare-*catalog. - Bare runtime API — the runtime these modules extend.
Last updated on