UNPKG

@libp2p/websockets

Version:

JavaScript implementation of the WebSockets module that libp2p uses and that implements the interface-transport spec

219 lines (187 loc) • 6.96 kB
/** * @packageDocumentation * * A [libp2p transport](https://docs.libp2p.io/concepts/transports/overview/) based on [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API). * * @example * * ```TypeScript * import { createLibp2p } from 'libp2p' * import { webSockets } from '@libp2p/websockets' * import { multiaddr } from '@multiformats/multiaddr' * * const node = await createLibp2p({ * transports: [ * webSockets() * ] * //... other config * }) * await node.start() * * const ma = multiaddr('/dns4/example.com/tcp/9090/tls/ws') * await node.dial(ma) * ``` */ import { transportSymbol, serviceCapabilities, ConnectionFailedError } from '@libp2p/interface' import { multiaddrToUri as toUri } from '@multiformats/multiaddr-to-uri' import { connect } from 'it-ws/client' import pDefer from 'p-defer' import { CustomProgressEvent } from 'progress-events' import { raceSignal } from 'race-signal' import * as filters from './filters.js' import { createListener } from './listener.js' import { socketToMaConn } from './socket-to-conn.js' import type { Transport, MultiaddrFilter, CreateListenerOptions, DialTransportOptions, Listener, AbortOptions, ComponentLogger, Logger, Connection, OutboundConnectionUpgradeEvents, Metrics, CounterGroup, Libp2pEvents } from '@libp2p/interface' import type { Multiaddr } from '@multiformats/multiaddr' import type { WebSocketOptions } from 'it-ws/client' import type { DuplexWebSocket } from 'it-ws/duplex' import type { TypedEventTarget } from 'main-event' import type http from 'node:http' import type https from 'node:https' import type { ProgressEvent } from 'progress-events' import type { ClientOptions } from 'ws' export interface WebSocketsInit extends AbortOptions, WebSocketOptions { /** * @deprecated Use a ConnectionGater instead */ filter?: MultiaddrFilter /** * Options used to create WebSockets */ websocket?: ClientOptions /** * Options used to create the HTTP server */ http?: http.ServerOptions /** * Options used to create the HTTPs server. `options.http` will be used if * unspecified. */ https?: https.ServerOptions /** * Inbound connections must complete their upgrade within this many ms * * @deprecated Use the `connectionManager.inboundUpgradeTimeout` libp2p config key instead */ inboundConnectionUpgradeTimeout?: number } export interface WebSocketsComponents { logger: ComponentLogger events: TypedEventTarget<Libp2pEvents> metrics?: Metrics } export interface WebSocketsMetrics { dialerEvents: CounterGroup } export type WebSocketsDialEvents = OutboundConnectionUpgradeEvents | ProgressEvent<'websockets:open-connection'> class WebSockets implements Transport<WebSocketsDialEvents> { private readonly log: Logger private readonly init: WebSocketsInit private readonly logger: ComponentLogger private readonly metrics?: WebSocketsMetrics private readonly components: WebSocketsComponents constructor (components: WebSocketsComponents, init: WebSocketsInit = {}) { this.log = components.logger.forComponent('libp2p:websockets') this.logger = components.logger this.components = components this.init = init if (components.metrics != null) { this.metrics = { dialerEvents: components.metrics.registerCounterGroup('libp2p_websockets_dialer_events_total', { label: 'event', help: 'Total count of WebSockets dialer events by type' }) } } } readonly [transportSymbol] = true readonly [Symbol.toStringTag] = '@libp2p/websockets' readonly [serviceCapabilities]: string[] = [ '@libp2p/transport' ] async dial (ma: Multiaddr, options: DialTransportOptions<WebSocketsDialEvents>): Promise<Connection> { this.log('dialing %s', ma) options = options ?? {} const socket = await this._connect(ma, options) const maConn = socketToMaConn(socket, ma, { logger: this.logger, metrics: this.metrics?.dialerEvents }) this.log('new outbound connection %s', maConn.remoteAddr) const conn = await options.upgrader.upgradeOutbound(maConn, options) this.log('outbound connection %s upgraded', maConn.remoteAddr) return conn } async _connect (ma: Multiaddr, options: DialTransportOptions<WebSocketsDialEvents>): Promise<DuplexWebSocket> { options?.signal?.throwIfAborted() const cOpts = ma.toOptions() this.log('dialing %s:%s', cOpts.host, cOpts.port) const errorPromise = pDefer() const rawSocket = connect(toUri(ma), this.init) rawSocket.socket.addEventListener('error', () => { // the WebSocket.ErrorEvent type doesn't actually give us any useful // information about what happened // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event const err = new ConnectionFailedError(`Could not connect to ${ma.toString()}`) this.log.error('connection error:', err) this.metrics?.dialerEvents.increment({ error: true }) errorPromise.reject(err) }) try { options.onProgress?.(new CustomProgressEvent('websockets:open-connection')) await raceSignal(Promise.race([rawSocket.connected(), errorPromise.promise]), options.signal) } catch (err: any) { if (options.signal?.aborted) { this.metrics?.dialerEvents.increment({ abort: true }) } rawSocket.close() .catch(err => { this.log.error('error closing raw socket', err) }) throw err } this.log('connected %s', ma) this.metrics?.dialerEvents.increment({ connect: true }) return rawSocket } /** * Creates a WebSockets listener. The provided `handler` function will be called * anytime a new incoming Connection has been successfully upgraded via * `upgrader.upgradeInbound` */ createListener (options: CreateListenerOptions): Listener { return createListener({ logger: this.logger, events: this.components.events, metrics: this.components.metrics }, { ...this.init, ...options }) } /** * Takes a list of `Multiaddr`s and returns only valid WebSockets addresses. * By default, in a browser environment only DNS+WSS multiaddr is accepted, * while in a Node.js environment DNS+{WS, WSS} multiaddrs are accepted. */ listenFilter (multiaddrs: Multiaddr[]): Multiaddr[] { multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs] if (this.init?.filter != null) { return this.init?.filter(multiaddrs) } return filters.all(multiaddrs) } /** * Filter check for all Multiaddrs that this transport can dial */ dialFilter (multiaddrs: Multiaddr[]): Multiaddr[] { return this.listenFilter(multiaddrs) } } export function webSockets (init: WebSocketsInit = {}): (components: WebSocketsComponents) => Transport { return (components) => { return new WebSockets(components, init) } }