Bare Docs

bare-pipe

Native I/O pipes for JavaScript

Documented against v4.4.0
stable

bare-pipe — Native I/O pipes for JavaScript. It is a native addon and requires Bare >=1.16.0.

npm i bare-pipe

Usage

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

ParameterTypeDefaultDescription
pathstring | 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

ParameterTypeDefaultDescription
targetT—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 — target does 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): this

Parameters

ParameterTypeDefaultDescription
pathstring—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): this

Parameters

ParameterTypeDefaultDescription
fdnumber—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): Pipe

Create a new pipe and connect it to path. Shorthand for new Pipe(options).connect(path, options, onconnect).

Parameters

ParameterTypeDefaultDescription
pathstring—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): PipeServer

Parameters

ParameterTypeDefaultDescription
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): boolean

Write 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

ParameterTypeDefaultDescription
chunkBuffer | string—The data to write.
encodingBufferEncoding—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

ParameterTypeDefaultDescription
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

ParameterTypeDefaultDescription
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): this

Start listening for connections on path. backlog defaults to 511.

Parameters

ParameterTypeDefaultDescription
pathstring—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 — ownerOnly is 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 an ArrayBuffer whose first bytes are a uv_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 SYSTEM seen 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 SYSTEM seen 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

Last updated on

Was this helpful?

On this page