From e6564a2423910628f7395028a108629fd53e67f6 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Thu, 6 Jan 2022 17:13:46 +0100 Subject: [PATCH 1/6] feat: convert to typescript (#76) Adds gh actions, autorelease, etc. BREAKING CHANGE: ESM only named exports no more CJS --- .aegir.cjs | 26 ++ .aegir.js | 30 -- .github/dependabot.yml | 8 + .github/workflows/main.yml | 103 +++++- LICENSE | 23 +- LICENSE-APACHE | 5 + LICENSE-MIT | 19 ++ README.md | 28 +- package.json | 117 +++---- src/constants.js | 12 - src/constants.ts | 10 + src/{filters.js => filters.ts} | 27 +- src/index.js | 168 --------- src/index.ts | 141 ++++++++ ...istener.browser.js => listener.browser.ts} | 3 +- src/listener.js | 100 ------ src/listener.ts | 138 ++++++++ src/socket-to-conn.js | 76 ----- src/socket-to-conn.ts | 71 ++++ test/browser.js | 78 ----- test/browser.ts | 90 +++++ ...{compliance.node.js => compliance.node.ts} | 40 +-- test/{node.js => node.ts} | 319 ++++++++++-------- tsconfig.json | 12 + 24 files changed, 897 insertions(+), 747 deletions(-) create mode 100644 .aegir.cjs delete mode 100644 .aegir.js create mode 100644 .github/dependabot.yml create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT delete mode 100644 src/constants.js create mode 100644 src/constants.ts rename src/{filters.js => filters.ts} (70%) delete mode 100644 src/index.js create mode 100644 src/index.ts rename src/{listener.browser.js => listener.browser.ts} (63%) delete mode 100644 src/listener.js create mode 100644 src/listener.ts delete mode 100644 src/socket-to-conn.js create mode 100644 src/socket-to-conn.ts delete mode 100644 test/browser.js create mode 100644 test/browser.ts rename test/{compliance.node.js => compliance.node.ts} (55%) rename test/{node.js => node.ts} (67%) create mode 100644 tsconfig.json diff --git a/.aegir.cjs b/.aegir.cjs new file mode 100644 index 0000000..66e8eed --- /dev/null +++ b/.aegir.cjs @@ -0,0 +1,26 @@ +'use strict' + +/** @type {import('aegir').PartialOptions} */ +module.exports = { + test: { + async before () { + const { Multiaddr } = await import('@multiformats/multiaddr') + const { mockUpgrader } = await import('@libp2p/interface-compliance-tests/transport/utils') + const { WebSockets } = await import('./dist/src/index.js') + const { pipe } = await import('it-pipe') + + const ws = new WebSockets({ upgrader: mockUpgrader() }) + const ma = new Multiaddr('/ip4/127.0.0.1/tcp/9095/ws') + const listener = ws.createListener(conn => pipe(conn, conn)) + await listener.listen(ma) + listener.on('error', console.error) + + return { + listener + } + }, + async after (_, before) { + await before.listener.close() + } + } +} diff --git a/.aegir.js b/.aegir.js deleted file mode 100644 index 08fd135..0000000 --- a/.aegir.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict' - -const { Multiaddr } = require('multiaddr') -const pipe = require('it-pipe') -const WS = require('./src') - -const mockUpgrader = { - upgradeInbound: maConn => maConn, - upgradeOutbound: maConn => maConn -} -let listener - -async function before () { - const ws = new WS({ upgrader: mockUpgrader }) - const ma = new Multiaddr('/ip4/127.0.0.1/tcp/9095/ws') - listener = ws.createListener(conn => pipe(conn, conn)) - await listener.listen(ma) - listener.on('error', console.error) -} - -function after () { - return listener.close() -} - -module.exports = { - test: { - before, - after - } -} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..de46e32 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: npm + directory: "/" + schedule: + interval: daily + time: "11:00" + open-pull-requests-limit: 10 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4811148..c9772b6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: ci +name: test & maybe release on: push: branches: @@ -8,41 +8,118 @@ on: - master jobs: + check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - run: npm install - - run: npx aegir lint - - run: npx aegir dep-check -- -i wrtc -i electron-webrtc - - run: npx aegir build --no-types + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present lint + - run: npm run --if-present dep-check + test-node: needs: check runs-on: ${{ matrix.os }} strategy: matrix: os: [windows-latest, ubuntu-latest, macos-latest] - node: [14, 16] + node: [16] fail-fast: true steps: - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v2 with: node-version: ${{ matrix.node }} - - run: npm install - - run: npx nyc --reporter=lcov aegir test -t node -- --bail + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:node - uses: codecov/codecov-action@v1 + test-chrome: needs: check runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - run: npm install - - run: npx aegir test -t browser -t webworker --bail + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:chrome + + test-chrome-webworker: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:chrome-webworker + test-firefox: needs: check runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - run: npm install - - run: npx aegir test -t browser -t webworker --bail -- --browsers FirefoxHeadless + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:firefox + + test-firefox-webworker: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:firefox-webworker + + test-electron-main: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npx xvfb-maybe npm run --if-present test:electron-main + + test-electron-renderer: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npx xvfb-maybe npm run --if-present test:electron-renderer + + release: + needs: [test-node, test-chrome, test-chrome-webworker, test-firefox, test-firefox-webworker, test-electron-main, test-electron-renderer] + runs-on: ubuntu-latest + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + steps: + - uses: actions/checkout@v2.4.0 + with: + fetch-depth: 0 + - uses: actions/setup-node@v2 + with: + node-version: lts/* + - uses: ipfs/aegir/actions/cache-node-modules@master + - uses: ipfs/aegir/actions/docker-login@master + with: + docker-token: ${{ secrets.DOCKER_USERNAME }} + docker-username: ${{ secrets.DOCKER_USERNAME }} + - run: npm run --if-present release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/LICENSE b/LICENSE index bb9cf40..b0b237f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,2 @@ -The MIT License (MIT) - -Copyright (c) 2016 David Dias - -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. +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 \ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..14478a3 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..749aa1e --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +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. \ No newline at end of file diff --git a/README.md b/README.md index 4f3d367..ef671ec 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# js-libp2p-websockets +# js-libp2p-websockets [![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io) [![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) @@ -16,9 +16,17 @@ > JavaScript implementation of the WebSockets module that libp2p uses and that implements the interface-transport interface -## Lead Maintainer +## Table of Contents -[Jacob Heun](https://github.com/jacobheun) +- [Description](#description) +- [Usage](#usage) +- [Install](#install) + - [npm](#npm) + - [Constructor properties](#constructor-properties) +- [Libp2p Usage Example](#libp2p-usage-example) +- [API](#api) + - [Transport](#transport) + - [Connection](#connection) ## Description @@ -31,13 +39,13 @@ ### npm ```sh -> npm i libp2p-websockets +> npm i @libp2p/websockets ``` ### Constructor properties ```js -const WS = require('libp2p-websockets') +import WS from '@libp2p/websockets' const properties = { upgrader, @@ -66,11 +74,11 @@ The available filters are: ## Libp2p Usage Example ```js -const Libp2p = require('libp2p') -const Websockets = require('libp2p-websockets') -const filters = require('libp2p-websockets/src/filters') -const MPLEX = require('libp2p-mplex') -const { NOISE } = require('libp2p-noise') +import Libp2p from 'libp2p' +import { Websockets } from '@libp2p/websockets' +import filters from 'libp2p-websockets/filters' +import { MPLEX } from 'libp2p-mplex' +import { NOISE } from 'libp2p-noise' const transportKey = Websockets.prototype[Symbol.toStringTag] const node = await Libp2p.create({ diff --git a/package.json b/package.json index 4cef846..22d2d5f 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,43 @@ { - "name": "libp2p-websockets", + "name": "@libp2p/websockets", "version": "0.16.2", "description": "JavaScript implementation of the WebSockets module that libp2p uses and that implements the interface-transport spec", - "leadMaintainer": "Jacob Heun ", - "main": "src/index.js", + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist/src", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, "scripts": { "lint": "aegir lint", - "build": "aegir build", - "test": "aegir test -t node -t browser ", - "test:node": "aegir test -t node", - "test:browser": "aegir test -t browser ", - "release": "aegir release -t node -t browser ", - "release-minor": "aegir release --type minor -t node -t browser", - "release-major": "aegir release --type major -t node -t browser", - "coverage": "nyc --reporter=lcov --reporter=text npm run test:node" + "dep-check": "aegir dep-check dist/src/**/*.js dist/test/**/*.js", + "build": "tsc", + "pretest": "npm run build", + "test": "aegir test", + "test:chrome": "npm run test -- -t browser -f ./dist/test/browser.js --cov", + "test:chrome-webworker": "npm run test -- -t webworker -f ./dist/test/browser.js", + "test:firefox": "npm run test -- -t browser -f ./dist/test/browser.js -- --browser firefox", + "test:firefox-webworker": "npm run test -- -t webworker -f ./dist/test/browser.js -- --browser firefox", + "test:node": "npm run test -- -t node -f ./dist/test/node.js --cov", + "test:electron-main": "npm run test -- -t electron-main -f ./dist/test/node.js --cov", + "release": "semantic-release" }, "browser": { - "./src/listener.js": "./src/listener.browser.js" + "./dist/src/listener.js": "./dist/src/listener.browser.js" }, - "files": [ - "src", - "dist" - ], - "pre-push": [ - "lint" - ], "repository": { "type": "git", "url": "git+https://github.com/libp2p/js-libp2p-websockets.git" @@ -38,51 +51,33 @@ }, "homepage": "https://github.com/libp2p/js-libp2p-websockets#readme", "dependencies": { - "abortable-iterator": "^3.0.0", - "class-is": "^1.1.0", + "@libp2p/utils": "^1.0.0", + "@multiformats/mafmt": "^11.0.1", + "@multiformats/multiaddr": "^10.0.0", + "@multiformats/multiaddr-to-uri": "^9.0.0", + "abortable-iterator": "^4.0.2", "debug": "^4.3.1", "err-code": "^3.0.1", - "ipfs-utils": "^9.0.1", - "it-ws": "^4.0.0", - "libp2p-utils": "^0.4.0", - "mafmt": "^10.0.0", - "multiaddr": "^10.0.0", - "multiaddr-to-uri": "^8.0.0", - "p-defer": "^3.0.0", - "p-timeout": "^4.1.0" + "it-ws": "^5.0.0", + "p-defer": "^4.0.0", + "p-timeout": "^5.0.2", + "wherearewe": "^1.0.0" }, "devDependencies": { - "abort-controller": "^3.0.0", - "aegir": "^33.0.0", - "bl": "^5.0.0", - "is-loopback-addr": "^1.0.1", - "it-goodbye": "^3.0.0", - "it-pipe": "^1.1.0", - "libp2p-interfaces": "^1.0.0", - "libp2p-interfaces-compliance-tests": "^1.0.0", - "streaming-iterables": "^6.0.0", - "uint8arrays": "^2.1.2", + "@libp2p/interface-compliance-tests": "^1.0.1", + "@libp2p/interfaces": "^1.0.0", + "@types/bl": "^5.0.2", + "@types/debug": "^4.1.7", + "@types/ws": "^8.2.2", + "aegir": "^36.1.3", + "is-loopback-addr": "^2.0.1", + "it-all": "^1.0.6", + "it-drain": "^1.0.5", + "it-goodbye": "^4.0.1", + "it-pipe": "^2.0.2", + "it-take": "^1.0.2", + "p-wait-for": "^4.1.0", + "uint8arrays": "^3.0.0", "util": "^0.12.3" - }, - "contributors": [ - "David Dias ", - "Vasco Santos ", - "Jacob Heun ", - "Friedel Ziegelmayer ", - "Alex Potsides ", - "Francisco Baio Dias ", - "Hugo Dias ", - "Dmitriy Ryajov ", - "Maciej Krüger ", - "ᴠɪᴄᴛᴏʀ ʙᴊᴇʟᴋʜᴏʟᴍ ", - "Chris Campbell ", - "Diogo Silva ", - "Irakli Gozalishvili ", - "Jack Kleeman ", - "Marcin Rataj ", - "Michael FIG ", - "Richard Littauer ", - "nikor ", - "Alan Shaw " - ] + } } diff --git a/src/constants.js b/src/constants.js deleted file mode 100644 index 28f2634..0000000 --- a/src/constants.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict' - -// p2p multi-address code -exports.CODE_P2P = 421 -exports.CODE_CIRCUIT = 290 - -exports.CODE_TCP = 6 -exports.CODE_WS = 477 -exports.CODE_WSS = 478 - -// Time to wait for a connection to close gracefully before destroying it manually -exports.CLOSE_TIMEOUT = 2000 diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..e8c3939 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,10 @@ +// p2p multi-address code +export const CODE_P2P = 421 +export const CODE_CIRCUIT = 290 + +export const CODE_TCP = 6 +export const CODE_WS = 477 +export const CODE_WSS = 478 + +// Time to wait for a connection to close gracefully before destroying it manually +export const CLOSE_TIMEOUT = 2000 diff --git a/src/filters.js b/src/filters.ts similarity index 70% rename from src/filters.js rename to src/filters.ts index 2aa0bc8..e01ffcf 100644 --- a/src/filters.js +++ b/src/filters.ts @@ -1,16 +1,15 @@ -'use strict' - -const mafmt = require('mafmt') -const { +import * as mafmt from '@multiformats/mafmt' +import type { Multiaddr } from '@multiformats/multiaddr' +import { CODE_CIRCUIT, CODE_P2P, CODE_TCP, CODE_WS, CODE_WSS -} = require('./constants') +} from './constants.js' -module.exports = { - all: (multiaddrs) => multiaddrs.filter((ma) => { +export function all (multiaddrs: Multiaddr[]) { + return multiaddrs.filter((ma) => { if (ma.protoCodes().includes(CODE_CIRCUIT)) { return false } @@ -19,8 +18,11 @@ module.exports = { return mafmt.WebSockets.matches(testMa) || mafmt.WebSocketsSecure.matches(testMa) - }), - dnsWss: (multiaddrs) => multiaddrs.filter((ma) => { + }) +} + +export function dnsWss (multiaddrs: Multiaddr[]) { + return multiaddrs.filter((ma) => { if (ma.protoCodes().includes(CODE_CIRCUIT)) { return false } @@ -29,8 +31,11 @@ module.exports = { return mafmt.WebSocketsSecure.matches(testMa) && mafmt.DNS.matches(testMa.decapsulateCode(CODE_TCP).decapsulateCode(CODE_WSS)) - }), - dnsWsOrWss: (multiaddrs) => multiaddrs.filter((ma) => { + }) +} + +export function dnsWsOrWss (multiaddrs: Multiaddr[]) { + return multiaddrs.filter((ma) => { if (ma.protoCodes().includes(CODE_CIRCUIT)) { return false } diff --git a/src/index.js b/src/index.js deleted file mode 100644 index e6f74e7..0000000 --- a/src/index.js +++ /dev/null @@ -1,168 +0,0 @@ -'use strict' - -const connect = require('it-ws/client') -const withIs = require('class-is') -const toUri = require('multiaddr-to-uri') -const { AbortError } = require('abortable-iterator') -const pDefer = require('p-defer') - -const debug = require('debug') -const log = debug('libp2p:websockets') -log.error = debug('libp2p:websockets:error') -const env = require('ipfs-utils/src/env') - -const createListener = require('./listener') -const toConnection = require('./socket-to-conn') -const filters = require('./filters') - -/** - * @typedef {import('multiaddr').Multiaddr} Multiaddr - */ - -/** - * @class WebSockets - */ -class WebSockets { - /** - * @class - * @param {object} options - * @param {Upgrader} options.upgrader - * @param {(multiaddrs: Array) => Array} options.filter - override transport addresses filter - */ - constructor ({ upgrader, filter }) { - if (!upgrader) { - throw new Error('An upgrader must be provided. See https://github.com/libp2p/interface-transport#upgrader.') - } - this._upgrader = upgrader - this._filter = filter - } - - /** - * @async - * @param {Multiaddr} ma - * @param {object} [options] - * @param {AbortSignal} [options.signal] - Used to abort dial requests - * @returns {Connection} An upgraded Connection - */ - async dial (ma, options = {}) { - log('dialing %s', ma) - - const socket = await this._connect(ma, options) - const maConn = toConnection(socket, { remoteAddr: ma, signal: options.signal }) - log('new outbound connection %s', maConn.remoteAddr) - - const conn = await this._upgrader.upgradeOutbound(maConn) - log('outbound connection %s upgraded', maConn.remoteAddr) - return conn - } - - /** - * @private - * @param {Multiaddr} ma - * @param {object} [options] - * @param {AbortSignal} [options.signal] - Used to abort dial requests - * @returns {Promise} Resolves a extended duplex iterable on top of a WebSocket - */ - async _connect (ma, options = {}) { - if (options.signal && options.signal.aborted) { - throw new AbortError() - } - const cOpts = ma.toOptions() - log('dialing %s:%s', cOpts.host, cOpts.port) - - const errorPromise = pDefer() - const errfn = (err) => { - const msg = `connection error: ${err.message}` - log.error(msg) - - errorPromise.reject(err) - } - - const rawSocket = connect(toUri(ma), Object.assign({ binary: true }, options)) - - if (rawSocket.socket.on) { - rawSocket.socket.on('error', errfn) - } else { - rawSocket.socket.onerror = errfn - } - - if (!options.signal) { - await Promise.race([rawSocket.connected(), errorPromise.promise]) - - log('connected %s', ma) - return rawSocket - } - - // Allow abort via signal during connect - let onAbort - const abort = new Promise((resolve, reject) => { - onAbort = () => { - reject(new AbortError()) - // FIXME: https://github.com/libp2p/js-libp2p-websockets/issues/121 - setTimeout(() => { - rawSocket.close() - }) - } - - // Already aborted? - if (options.signal.aborted) return onAbort() - options.signal.addEventListener('abort', onAbort) - }) - - try { - await Promise.race([abort, errorPromise.promise, rawSocket.connected()]) - } finally { - options.signal.removeEventListener('abort', onAbort) - } - - log('connected %s', ma) - 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`. - * - * @param {object} [options] - * @param {http.Server} [options.server] - A pre-created Node.js HTTP/S server. - * @param {function (Connection)} handler - * @returns {Listener} A Websockets listener - */ - createListener (options = {}, handler) { - if (typeof options === 'function') { - handler = options - options = {} - } - - return createListener({ handler, upgrader: this._upgrader }, 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. - * - * @param {Multiaddr[]} multiaddrs - * @returns {Multiaddr[]} Valid Websockets multiaddrs - */ - filter (multiaddrs) { - multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs] - - if (this._filter) { - return this._filter(multiaddrs) - } - - // Browser - if (env.isBrowser || env.isWebWorker) { - return filters.dnsWss(multiaddrs) - } - - return filters.all(multiaddrs) - } -} - -module.exports = withIs(WebSockets, { - className: 'WebSockets', - symbolName: '@libp2p/js-libp2p-websockets/websockets' -}) diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..2ad45ec --- /dev/null +++ b/src/index.ts @@ -0,0 +1,141 @@ +import { connect, WebSocketOptions } from 'it-ws/client' +import { multiaddrToUri as toUri } from '@multiformats/multiaddr-to-uri' +import { AbortError } from '@libp2p/interfaces/errors' +import pDefer from 'p-defer' +import debug from 'debug' +import env from 'wherearewe' +import { createListener } from './listener.js' +import { socketToMaConn } from './socket-to-conn.js' +import * as filters from './filters.js' +import type { Transport, Upgrader, MultiaddrFilter } from '@libp2p/interfaces/transport' +import type { AbortOptions } from '@libp2p/interfaces' +import type { WebSocketListenerOptions } from './listener.js' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { DuplexWebSocket } from 'it-ws/dist/src/duplex' + +const log = Object.assign(debug('libp2p:websockets'), { + error: debug('libp2p:websockets:error') +}) + +/** + * @class WebSockets + */ +export class WebSockets implements Transport { + private readonly upgrader: Upgrader + private readonly _filter?: MultiaddrFilter + + constructor (opts: { upgrader: Upgrader, filter?: MultiaddrFilter }) { + const { upgrader, filter } = opts + + if (upgrader == null) { + throw new Error('An upgrader must be provided. See https://github.com/libp2p/interface-transport#upgrader.') + } + + this.upgrader = upgrader + this._filter = filter + } + + async dial (ma: Multiaddr, options?: AbortOptions & WebSocketOptions) { + log('dialing %s', ma) + options = options ?? {} + + const socket = await this._connect(ma, options) + const maConn = socketToMaConn(socket, ma) + log('new outbound connection %s', maConn.remoteAddr) + + const conn = await this.upgrader.upgradeOutbound(maConn) + log('outbound connection %s upgraded', maConn.remoteAddr) + return conn + } + + async _connect (ma: Multiaddr, options: AbortOptions & WebSocketOptions): Promise { + if (options?.signal?.aborted === true) { + throw new AbortError() + } + const cOpts = ma.toOptions() + log('dialing %s:%s', cOpts.host, cOpts.port) + + const errorPromise = pDefer() + const errfn = (err: any) => { + log.error('connection error:', err) + + errorPromise.reject(err) + } + + const rawSocket = connect(toUri(ma), options) + + if (rawSocket.socket.on != null) { + rawSocket.socket.on('error', errfn) + } else { + rawSocket.socket.onerror = errfn + } + + if (options.signal == null) { + await Promise.race([rawSocket.connected(), errorPromise.promise]) + + log('connected %s', ma) + return rawSocket + } + + // Allow abort via signal during connect + let onAbort + const abort = new Promise((resolve, reject) => { + onAbort = () => { + reject(new AbortError()) + // FIXME: https://github.com/libp2p/js-libp2p-websockets/issues/121 + setTimeout(() => { + rawSocket.close().catch(err => { + log.error('error closing raw socket', err) + }) + }) + } + + // Already aborted? + if (options?.signal?.aborted === true) { + return onAbort() + } + + options?.signal?.addEventListener('abort', onAbort) + }) + + try { + await Promise.race([abort, errorPromise.promise, rawSocket.connected()]) + } finally { + if (onAbort != null) { + options?.signal?.removeEventListener('abort', onAbort) + } + } + + log('connected %s', ma) + 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?: WebSocketListenerOptions) { + return createListener(this.upgrader, 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. + */ + filter (multiaddrs: Multiaddr[]) { + multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs] + + if (this._filter != null) { + return this._filter(multiaddrs) + } + + // Browser + if (env.isBrowser || env.isWebWorker) { + return filters.dnsWss(multiaddrs) + } + + return filters.all(multiaddrs) + } +} diff --git a/src/listener.browser.js b/src/listener.browser.ts similarity index 63% rename from src/listener.browser.js rename to src/listener.browser.ts index 916c9a2..2d2457b 100644 --- a/src/listener.browser.js +++ b/src/listener.browser.ts @@ -1,5 +1,4 @@ -'use strict' -module.exports = function () { +export function createListener () { throw new Error('WebSocket Servers can not be created in the browser!') } diff --git a/src/listener.js b/src/listener.js deleted file mode 100644 index 4553635..0000000 --- a/src/listener.js +++ /dev/null @@ -1,100 +0,0 @@ -'use strict' - -const EventEmitter = require('events') -const os = require('os') -const { Multiaddr, protocols } = require('multiaddr') -const { createServer } = require('it-ws') -const debug = require('debug') -const log = debug('libp2p:websockets:listener') -log.error = debug('libp2p:websockets:listener:error') - -const toConnection = require('./socket-to-conn') - -module.exports = ({ handler, upgrader }, options = {}) => { - const listener = new EventEmitter() - - const server = createServer(options, async (stream) => { - let maConn, conn - - try { - maConn = toConnection(stream) - log('new inbound connection %s', maConn.remoteAddr) - conn = await upgrader.upgradeInbound(maConn) - } catch (err) { - log.error('inbound connection failed to upgrade', err) - return maConn && maConn.close() - } - - log('inbound connection %s upgraded', maConn.remoteAddr) - - trackConn(server, maConn) - - if (handler) handler(conn) - listener.emit('connection', conn) - }) - - server - .on('listening', () => listener.emit('listening')) - .on('error', err => listener.emit('error', err)) - .on('close', () => listener.emit('close')) - - // Keep track of open connections to destroy in case of timeout - server.__connections = [] - - let listeningMultiaddr - - listener.close = () => { - server.__connections.forEach(maConn => maConn.close()) - return server.close() - } - - listener.listen = (ma) => { - listeningMultiaddr = ma - - return server.listen(ma.toOptions()) - } - - listener.getAddrs = () => { - const multiaddrs = [] - const address = server.address() - - if (!address) { - throw new Error('Listener is not ready yet') - } - - const ipfsId = listeningMultiaddr.getPeerId() - const protos = listeningMultiaddr.protos() - - // Because TCP will only return the IPv6 version - // we need to capture from the passed multiaddr - if (protos.some(proto => proto.code === protocols('ip4').code)) { - const wsProto = protos.some(proto => proto.code === protocols('ws').code) ? '/ws' : '/wss' - let m = listeningMultiaddr.decapsulate('tcp') - m = m.encapsulate('/tcp/' + address.port + wsProto) - if (listeningMultiaddr.getPeerId()) { - m = m.encapsulate('/p2p/' + ipfsId) - } - - if (m.toString().indexOf('0.0.0.0') !== -1) { - const netInterfaces = os.networkInterfaces() - Object.keys(netInterfaces).forEach((niKey) => { - netInterfaces[niKey].forEach((ni) => { - if (ni.family === 'IPv4') { - multiaddrs.push(new Multiaddr(m.toString().replace('0.0.0.0', ni.address))) - } - }) - }) - } else { - multiaddrs.push(m) - } - } - - return multiaddrs - } - - return listener -} - -function trackConn (server, maConn) { - server.__connections.push(maConn) -} diff --git a/src/listener.ts b/src/listener.ts new file mode 100644 index 0000000..b98aa2a --- /dev/null +++ b/src/listener.ts @@ -0,0 +1,138 @@ +import { EventEmitter } from 'events' +import os from 'os' +import { Multiaddr, protocols } from '@multiformats/multiaddr' +import { createServer } from 'it-ws/server' +import debug from 'debug' +import { socketToMaConn } from './socket-to-conn.js' +import { ipPortToMultiaddr as toMultiaddr } from '@libp2p/utils/ip-port-to-multiaddr' +import type { ListenerOptions, Upgrader, Listener } from '@libp2p/interfaces/transport' +import type { Server } from 'http' +import type { WebSocketServer } from 'it-ws/server' +import type { DuplexWebSocket } from 'it-ws/duplex' + +const log = Object.assign(debug('libp2p:websockets:listener'), { + error: debug('libp2p:websockets:listener:error') +}) + +export interface WebSocketListenerOptions extends ListenerOptions { + server?: Server +} + +export function createListener (upgrader: Upgrader, options?: WebSocketListenerOptions): Listener { + options = options ?? {} + let server: WebSocketServer // eslint-disable-line prefer-const + let listeningMultiaddr: Multiaddr + // Keep track of open connections to destroy when the listener is closed + const connections = new Set() + + const listener: Listener = Object.assign(new EventEmitter(), { + close: async () => { + await Promise.all( + Array.from(connections).map(async maConn => await maConn.close()) + ) + + if (server.address() == null) { + // not listening, close will throw an error + return + } + + return await server.close() + }, + listen: async (ma: Multiaddr) => { + listeningMultiaddr = ma + + await server.listen(ma.toOptions()) + }, + getAddrs: () => { + const multiaddrs = [] + const address = server.address() + + if (address == null) { + throw new Error('Listener is not ready yet') + } + + if (typeof address === 'string') { + throw new Error('Wrong address type received - expected AddressInfo, got string - are you trying to listen on a unix socket?') + } + + const ipfsId = listeningMultiaddr.getPeerId() + const protos = listeningMultiaddr.protos() + + // Because TCP will only return the IPv6 version + // we need to capture from the passed multiaddr + if (protos.some(proto => proto.code === protocols('ip4').code)) { + const wsProto = protos.some(proto => proto.code === protocols('ws').code) ? '/ws' : '/wss' + let m = listeningMultiaddr.decapsulate('tcp') + m = m.encapsulate(`/tcp/${address.port}${wsProto}`) + if (ipfsId != null) { + m = m.encapsulate(`/p2p/${ipfsId}`) + } + + if (m.toString().includes('0.0.0.0')) { + const netInterfaces = os.networkInterfaces() + Object.values(netInterfaces).forEach(niInfos => { + if (niInfos == null) { + return + } + + niInfos.forEach(ni => { + if (ni.family === 'IPv4') { + multiaddrs.push(new Multiaddr(m.toString().replace('0.0.0.0', ni.address))) + } + }) + }) + } else { + multiaddrs.push(m) + } + } + + return multiaddrs + } + }) + + server = createServer({ + ...options, + onConnection: (stream: DuplexWebSocket) => { + const maConn = socketToMaConn(stream, toMultiaddr(stream.remoteAddress ?? '', stream.remotePort ?? 0)) + log('new inbound connection %s', maConn.remoteAddr) + + connections.add(stream) + + stream.socket.on('close', function () { + connections.delete(stream) + }) + + try { + void upgrader.upgradeInbound(maConn) + .then((conn) => { + log('inbound connection %s upgraded', maConn.remoteAddr) + + if (options?.handler != null) { + options?.handler(conn) + } + + listener.emit('connection', conn) + }) + .catch(async err => { + log.error('inbound connection failed to upgrade', err) + + await maConn.close().catch(err => { + log.error('inbound connection failed to close after upgrade failed', err) + }) + }) + } catch (err) { + log.error('inbound connection failed to upgrade', err) + maConn.close().catch(err => { + log.error('inbound connection failed to close after upgrade failed', err) + }) + } + } + }) + + server + .on('listening', () => listener.emit('listening')) + .on('error', (err: Error) => listener.emit('error', err)) + .on('close', () => listener.emit('close')) + + return listener +} diff --git a/src/socket-to-conn.js b/src/socket-to-conn.js deleted file mode 100644 index 0efda21..0000000 --- a/src/socket-to-conn.js +++ /dev/null @@ -1,76 +0,0 @@ -'use strict' - -const abortable = require('abortable-iterator') -const { CLOSE_TIMEOUT } = require('./constants') -const toMultiaddr = require('libp2p-utils/src/ip-port-to-multiaddr') - -const pTimeout = require('p-timeout') - -const debug = require('debug') -const log = debug('libp2p:websockets:socket') -log.error = debug('libp2p:websockets:socket:error') - -// Convert a stream into a MultiaddrConnection -// https://github.com/libp2p/interface-transport#multiaddrconnection -module.exports = (stream, options = {}) => { - const maConn = { - async sink (source) { - if (options.signal) { - source = abortable(source, options.signal) - } - - try { - await stream.sink((async function * () { - for await (const chunk of source) { - // Convert BufferList to Buffer - yield chunk instanceof Uint8Array ? chunk : chunk.slice() - } - })()) - } catch (err) { - if (err.type !== 'aborted') { - log.error(err) - } - } - }, - - source: options.signal ? abortable(stream.source, options.signal) : stream.source, - - conn: stream, - - localAddr: options.localAddr || (stream.localAddress && stream.localPort - ? toMultiaddr(stream.localAddress, stream.localPort) - : undefined), - - // If the remote address was passed, use it - it may have the peer ID encapsulated - remoteAddr: options.remoteAddr || toMultiaddr(stream.remoteAddress, stream.remotePort), - - timeline: { open: Date.now() }, - - async close () { - const start = Date.now() - - try { - await pTimeout(stream.close(), CLOSE_TIMEOUT) - } catch (err) { - const { host, port } = maConn.remoteAddr.toOptions() - log('timeout closing stream to %s:%s after %dms, destroying it manually', - host, port, Date.now() - start) - - stream.destroy() - } finally { - maConn.timeline.close = Date.now() - } - } - } - - stream.socket.once && stream.socket.once('close', () => { - // In instances where `close` was not explicitly called, - // such as an iterable stream ending, ensure we have set the close - // timeline - if (!maConn.timeline.close) { - maConn.timeline.close = Date.now() - } - }) - - return maConn -} diff --git a/src/socket-to-conn.ts b/src/socket-to-conn.ts new file mode 100644 index 0000000..989e4bd --- /dev/null +++ b/src/socket-to-conn.ts @@ -0,0 +1,71 @@ +import { abortableSource } from 'abortable-iterator' +import { CLOSE_TIMEOUT } from './constants.js' +import pTimeout from 'p-timeout' +import debug from 'debug' +import type { AbortOptions } from '@libp2p/interfaces' +import type { MultiaddrConnection } from '@libp2p/interfaces/transport' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { DuplexWebSocket } from 'it-ws/duplex' + +const log = Object.assign(debug('libp2p:websockets:socket'), { + error: debug('libp2p:websockets:socket:error') +}) + +export interface SocketToConnOptions extends AbortOptions { + localAddr?: Multiaddr +} + +// Convert a stream into a MultiaddrConnection +// https://github.com/libp2p/interface-transport#multiaddrconnection +export function socketToMaConn (stream: DuplexWebSocket, remoteAddr: Multiaddr, options?: SocketToConnOptions): MultiaddrConnection { + options = options ?? {} + + const maConn: MultiaddrConnection = { + async sink (source) { + if ((options?.signal) != null) { + source = abortableSource(source, options.signal) + } + + try { + await stream.sink(source) + } catch (err: any) { + if (err.type !== 'aborted') { + log.error(err) + } + } + }, + + source: (options.signal != null) ? abortableSource(stream.source, options.signal) : stream.source, + + remoteAddr, + + timeline: { open: Date.now() }, + + async close () { + const start = Date.now() + + try { + await pTimeout(stream.close(), CLOSE_TIMEOUT) + } catch (err) { + const { host, port } = maConn.remoteAddr.toOptions() + log('timeout closing stream to %s:%s after %dms, destroying it manually', + host, port, Date.now() - start) + + stream.destroy() + } finally { + maConn.timeline.close = Date.now() + } + } + } + + stream.socket.once != null && stream.socket.once('close', () => { // eslint-disable-line @typescript-eslint/prefer-optional-chain + // In instances where `close` was not explicitly called, + // such as an iterable stream ending, ensure we have set the close + // timeline + if (maConn.timeline.close == null) { + maConn.timeline.close = Date.now() + } + }) + + return maConn +} diff --git a/test/browser.js b/test/browser.js deleted file mode 100644 index 78c6cef..0000000 --- a/test/browser.js +++ /dev/null @@ -1,78 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') - -const { Multiaddr } = require('multiaddr') -const pipe = require('it-pipe') -const goodbye = require('it-goodbye') -const { collect, take } = require('streaming-iterables') -const uint8ArrayFromString = require('uint8arrays/from-string') - -const WS = require('../src') - -const mockUpgrader = { - upgradeInbound: maConn => maConn, - upgradeOutbound: maConn => maConn -} - -describe('libp2p-websockets', () => { - const ma = new Multiaddr('/ip4/127.0.0.1/tcp/9095/ws') - let ws - let conn - - beforeEach(async () => { - ws = new WS({ upgrader: mockUpgrader }) - conn = await ws.dial(ma) - }) - - it('echo', async () => { - const message = uint8ArrayFromString('Hello World!') - const s = goodbye({ source: [message], sink: collect }) - - const results = await pipe(s, conn, s) - expect(results).to.eql([message]) - }) - - it('should filter out no DNS websocket addresses', function () { - const ma1 = new Multiaddr('/ip4/127.0.0.1/tcp/80/ws') - const ma2 = new Multiaddr('/ip4/127.0.0.1/tcp/443/wss') - const ma3 = new Multiaddr('/ip6/::1/tcp/80/ws') - const ma4 = new Multiaddr('/ip6/::1/tcp/443/wss') - - const valid = ws.filter([ma1, ma2, ma3, ma4]) - expect(valid.length).to.equal(0) - }) - - describe('stress', () => { - it('one big write', async () => { - const rawMessage = new Uint8Array(1000000).fill('a') - - const s = goodbye({ source: [rawMessage], sink: collect }) - - const results = await pipe(s, conn, s) - expect(results).to.eql([rawMessage]) - }) - - it('many writes', async function () { - this.timeout(10000) - const s = goodbye({ - source: pipe( - { - [Symbol.iterator] () { return this }, - next: () => ({ done: false, value: uint8ArrayFromString(Math.random().toString()) }) - }, - take(20000) - ), - sink: collect - }) - - const result = await pipe(s, conn, s) - expect(result).to.have.length(20000) - }) - }) - - it('.createServer throws in browser', () => { - expect(new WS({ upgrader: mockUpgrader }).createListener).to.throw() - }) -}) diff --git a/test/browser.ts b/test/browser.ts new file mode 100644 index 0000000..eab58c9 --- /dev/null +++ b/test/browser.ts @@ -0,0 +1,90 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from '@multiformats/multiaddr' +import { pipe } from 'it-pipe' +import { goodbye } from 'it-goodbye' +import take from 'it-take' +import all from 'it-all' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { WebSockets } from '../src/index.js' +import { mockUpgrader } from '@libp2p/interface-compliance-tests/transport/utils' +import env from 'wherearewe' +import type { Connection } from '@libp2p/interfaces/connection' + +const upgrader = mockUpgrader() + +describe('libp2p-websockets', () => { + const ma = new Multiaddr('/ip4/127.0.0.1/tcp/9095/ws') + let ws: WebSockets + let conn: Connection + + beforeEach(async () => { + ws = new WebSockets({ upgrader }) + conn = await ws.dial(ma) + }) + + afterEach(async () => { + await conn.close() + }) + + it('echo', async () => { + const message = uint8ArrayFromString('Hello World!') + const s = goodbye({ source: [message], sink: all }) + const { stream } = await conn.newStream(['echo']) + + const results = await pipe(s, stream, s) + expect(results).to.eql([message]) + }) + + it('should filter out no DNS websocket addresses', function () { + const ma1 = new Multiaddr('/ip4/127.0.0.1/tcp/80/ws') + const ma2 = new Multiaddr('/ip4/127.0.0.1/tcp/443/wss') + const ma3 = new Multiaddr('/ip6/::1/tcp/80/ws') + const ma4 = new Multiaddr('/ip6/::1/tcp/443/wss') + + const valid = ws.filter([ma1, ma2, ma3, ma4]) + + if (env.isBrowser || env.isWebWorker) { + expect(valid.length).to.equal(0) + } else { + expect(valid.length).to.equal(4) + } + }) + + describe('stress', () => { + it('one big write', async () => { + const rawMessage = new Uint8Array(1000000).fill(5) + + const s = goodbye({ source: [rawMessage], sink: all }) + const { stream } = await conn.newStream(['echo']) + + const results = await pipe(s, stream, s) + expect(results).to.eql([rawMessage]) + }) + + it('many writes', async function () { + this.timeout(10000) + const s = goodbye({ + source: pipe( + (function * () { + while (true) { + yield uint8ArrayFromString(Math.random().toString()) + } + }()), + (source) => take(source, 20000) + ), + sink: all + }) + + const { stream } = await conn.newStream(['echo']) + + const results = await pipe(s, stream, s) + expect(results).to.have.length(20000) + }) + }) + + it('.createServer throws in browser', () => { + expect(new WebSockets({ upgrader }).createListener).to.throw() + }) +}) diff --git a/test/compliance.node.js b/test/compliance.node.ts similarity index 55% rename from test/compliance.node.js rename to test/compliance.node.ts index 970970f..e034cca 100644 --- a/test/compliance.node.js +++ b/test/compliance.node.ts @@ -1,16 +1,21 @@ /* eslint-env mocha */ -'use strict' -const tests = require('libp2p-interfaces-compliance-tests/src/transport') -const { Multiaddr } = require('multiaddr') -const http = require('http') -const WS = require('../src') -const filters = require('../src/filters') +import tests from '@libp2p/interface-compliance-tests/transport' +import { Multiaddr } from '@multiformats/multiaddr' +import http from 'http' +import { WebSockets } from '../src/index.js' +import * as filters from '../src/filters.js' +import type { WebSocketListenerOptions } from '../src/listener.js' describe('interface-transport compliance', () => { tests({ - async setup ({ upgrader }) { // eslint-disable-line require-await - const ws = new WS({ upgrader, filter: filters.all }) + async setup (args) { + if (args == null) { + throw new Error('No args') + } + + const { upgrader } = args + const ws = new WebSockets({ upgrader, filter: filters.all }) const addrs = [ new Multiaddr('/ip4/127.0.0.1/tcp/9091/ws'), new Multiaddr('/ip4/127.0.0.1/tcp/9092/ws'), @@ -19,41 +24,38 @@ describe('interface-transport compliance', () => { ] let delayMs = 0 - const delayedCreateListener = (options, handler) => { - if (typeof options === 'function') { - handler = options - options = {} - } - - options = options || {} + const delayedCreateListener = (options?: WebSocketListenerOptions) => { + options = options ?? {} // A server that will delay the upgrade event by delayMs options.server = new Proxy(http.createServer(), { get (server, prop) { if (prop === 'on') { - return (event, handler) => { + return (event: string, handler: (...args: any[]) => void) => { server.on(event, (...args) => { - if (event !== 'upgrade' || !delayMs) { + if (event !== 'upgrade' || delayMs === 0) { return handler(...args) } setTimeout(() => handler(...args), delayMs) }) } } + // @ts-expect-error cannot access props with a string return server[prop] } }) - return ws.createListener(options, handler) + return ws.createListener(options) } const wsProxy = new Proxy(ws, { + // @ts-expect-error cannot access props with a string get: (_, prop) => prop === 'createListener' ? delayedCreateListener : ws[prop] }) // Used by the dial tests to simulate a delayed connect const connector = { - delay (ms) { delayMs = ms }, + delay (ms: number) { delayMs = ms }, restore () { delayMs = 0 } } diff --git a/test/node.js b/test/node.ts similarity index 67% rename from test/node.js rename to test/node.ts index 6c551d5..dd87610 100644 --- a/test/node.js +++ b/test/node.ts @@ -1,191 +1,203 @@ /* eslint-env mocha */ /* eslint max-nested-callbacks: ["error", 6] */ -'use strict' -const https = require('https') -const fs = require('fs') - -const AbortController = require('abort-controller').default -const { expect } = require('aegir/utils/chai') -const { Multiaddr } = require('multiaddr') -const goodbye = require('it-goodbye') -const isLoopbackAddr = require('is-loopback-addr') -const { collect } = require('streaming-iterables') -const pipe = require('it-pipe') -const BufferList = require('bl/BufferList') -const uint8ArrayFromString = require('uint8arrays/from-string') - -const WS = require('../src') -const filters = require('../src/filters') - -require('./compliance.node') - -const mockUpgrader = { - upgradeInbound: maConn => maConn, - upgradeOutbound: maConn => maConn -} +import https from 'https' +import fs from 'fs' +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from '@multiformats/multiaddr' +import { goodbye } from 'it-goodbye' +import { isLoopbackAddr } from 'is-loopback-addr' +import all from 'it-all' +import { pipe } from 'it-pipe' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { mockUpgrader } from '@libp2p/interface-compliance-tests/transport/utils' +import defer from 'p-defer' +import waitFor from 'p-wait-for' +import { WebSockets } from '../src/index.js' +import * as filters from '../src/filters.js' +import type { Listener } from '@libp2p/interfaces/transport' + +import './compliance.node.js' + +const upgrader = mockUpgrader() describe('instantiate the transport', () => { it('create', () => { - const ws = new WS({ upgrader: mockUpgrader }) + const ws = new WebSockets({ upgrader }) expect(ws).to.exist() }) }) describe('listen', () => { + it('should close connections when stopping the listener', async () => { + const ma = new Multiaddr('/ip4/127.0.0.1/tcp/47382/ws') + + const ws = new WebSockets({ upgrader }) + const listener = ws.createListener({ + handler: (conn) => { + void conn.newStream(['echo']).then(async ({ stream }) => { + return await pipe(stream, stream) + }) + } + }) + await listener.listen(ma) + + const conn = await ws.dial(ma) + const { stream } = await conn.newStream(['echo']) + void pipe(stream, stream) + + await listener.close() + + await waitFor(() => conn.stat.timeline.close != null) + }) + describe('ip4', () => { - let ws - const ma = new Multiaddr('/ip4/127.0.0.1/tcp/9090/ws') + let ws: WebSockets + const ma = new Multiaddr('/ip4/127.0.0.1/tcp/47382/ws') + let listener: Listener beforeEach(() => { - ws = new WS({ upgrader: mockUpgrader }) + ws = new WebSockets({ upgrader }) + }) + + afterEach(async () => { + return await listener.close() }) it('listen, check for promise', async () => { - const listener = ws.createListener((conn) => { }) + listener = ws.createListener() await listener.listen(ma) - await listener.close() }) it('listen, check for listening event', (done) => { - const listener = ws.createListener((conn) => { }) + listener = ws.createListener() - listener.on('listening', async () => { - await listener.close() + listener.on('listening', () => { done() }) - listener.listen(ma) + void listener.listen(ma) }) it('listen, check for the close event', (done) => { - const listener = ws.createListener((conn) => { }) + const listener = ws.createListener() listener.on('listening', () => { listener.on('close', done) - listener.close() + void listener.close() }) - listener.listen(ma) + void listener.listen(ma) }) it('listen on addr with /ipfs/QmHASH', async () => { - const ma = new Multiaddr('/ip4/127.0.0.1/tcp/9090/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') - const listener = ws.createListener((conn) => { }) + const ma = new Multiaddr('/ip4/127.0.0.1/tcp/47382/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + listener = ws.createListener() await listener.listen(ma) - await listener.close() }) it('listen on port 0', async () => { const ma = new Multiaddr('/ip4/127.0.0.1/tcp/0/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') - const listener = ws.createListener((conn) => { }) + listener = ws.createListener() await listener.listen(ma) const addrs = await listener.getAddrs() expect(addrs.map((a) => a.toOptions().port)).to.not.include(0) - await listener.close() }) it('listen on any Interface', async () => { const ma = new Multiaddr('/ip4/0.0.0.0/tcp/0/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') - const listener = ws.createListener((conn) => { }) + listener = ws.createListener() await listener.listen(ma) const addrs = await listener.getAddrs() expect(addrs.map((a) => a.toOptions().host)).to.not.include('0.0.0.0') - await listener.close() }) it('getAddrs', async () => { - const listener = ws.createListener((conn) => { }) + listener = ws.createListener() await listener.listen(ma) const addrs = await listener.getAddrs() expect(addrs.length).to.equal(1) expect(addrs[0]).to.deep.equal(ma) - await listener.close() }) it('getAddrs on port 0 listen', async () => { const addr = new Multiaddr('/ip4/127.0.0.1/tcp/0/ws') - const listener = ws.createListener((conn) => { }) + listener = ws.createListener() await listener.listen(addr) const addrs = await listener.getAddrs() expect(addrs.length).to.equal(1) expect(addrs.map((a) => a.toOptions().port)).to.not.include('0') - await listener.close() }) it('getAddrs from listening on 0.0.0.0', async () => { - const addr = new Multiaddr('/ip4/0.0.0.0/tcp/9003/ws') - const listener = ws.createListener((conn) => { }) + const addr = new Multiaddr('/ip4/0.0.0.0/tcp/47382/ws') + listener = ws.createListener() await listener.listen(addr) const addrs = await listener.getAddrs() expect(addrs.map((a) => a.toOptions().host)).to.not.include('0.0.0.0') - await listener.close() }) it('getAddrs from listening on 0.0.0.0 and port 0', async () => { const addr = new Multiaddr('/ip4/0.0.0.0/tcp/0/ws') - const listener = ws.createListener((conn) => { }) + listener = ws.createListener() await listener.listen(addr) const addrs = await listener.getAddrs() expect(addrs.map((a) => a.toOptions().host)).to.not.include('0.0.0.0') expect(addrs.map((a) => a.toOptions().port)).to.not.include('0') - await listener.close() }) it('getAddrs preserves p2p Id', async () => { - const ma = new Multiaddr('/ip4/127.0.0.1/tcp/9090/ws/p2p/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') - const listener = ws.createListener((conn) => { }) + const ma = new Multiaddr('/ip4/127.0.0.1/tcp/47382/ws/p2p/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') + listener = ws.createListener() await listener.listen(ma) const addrs = await listener.getAddrs() expect(addrs.length).to.equal(1) expect(addrs[0]).to.deep.equal(ma) - await listener.close() }) }) describe('ip6', () => { - let ws + let ws: WebSockets const ma = new Multiaddr('/ip6/::1/tcp/9091/ws') beforeEach(() => { - ws = new WS({ upgrader: mockUpgrader }) + ws = new WebSockets({ upgrader }) }) it('listen, check for promise', async () => { - const listener = ws.createListener((conn) => { }) + const listener = ws.createListener() await listener.listen(ma) await listener.close() }) it('listen, check for listening event', (done) => { - const listener = ws.createListener((conn) => { }) + const listener = ws.createListener() - listener.on('listening', async () => { - await listener.close() - done() + listener.on('listening', () => { + void listener.close().then(done, done) }) - listener.listen(ma) + void listener.listen(ma) }) it('listen, check for the close event', (done) => { - const listener = ws.createListener((conn) => { }) + const listener = ws.createListener() listener.on('listening', () => { listener.on('close', done) - listener.close() + void listener.close() }) - listener.listen(ma) + void listener.listen(ma) }) it('listen on addr with /ipfs/QmHASH', async () => { const ma = new Multiaddr('/ip6/::1/tcp/9091/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') - const listener = ws.createListener((conn) => { }) + const listener = ws.createListener() await listener.listen(ma) await listener.close() }) @@ -194,35 +206,39 @@ describe('listen', () => { describe('dial', () => { describe('ip4', () => { - let ws - let listener + let ws: WebSockets + let listener: Listener const ma = new Multiaddr('/ip4/127.0.0.1/tcp/9091/ws') - beforeEach(() => { - ws = new WS({ upgrader: mockUpgrader }) - listener = ws.createListener(conn => pipe(conn, conn)) - return listener.listen(ma) + beforeEach(async () => { + ws = new WebSockets({ upgrader }) + listener = ws.createListener({ + handler: (conn) => { + void conn.newStream(['echo']).then(async ({ stream }) => { + return await pipe(stream, stream) + }) + } + }) + return await listener.listen(ma) }) - afterEach(() => listener.close()) + afterEach(async () => await listener.close()) it('dial', async () => { const conn = await ws.dial(ma) - const s = goodbye({ source: ['hey'], sink: collect }) - - const result = await pipe(s, conn, s) + const s = goodbye({ source: [uint8ArrayFromString('hey')], sink: all }) + const { stream } = await conn.newStream(['echo']) - expect(result).to.be.eql([uint8ArrayFromString('hey')]) + await expect(pipe(s, stream, s)).to.eventually.deep.equal([uint8ArrayFromString('hey')]) }) it('dial with p2p Id', async () => { const ma = new Multiaddr('/ip4/127.0.0.1/tcp/9091/ws/p2p/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') const conn = await ws.dial(ma) - const s = goodbye({ source: ['hey'], sink: collect }) + const s = goodbye({ source: [uint8ArrayFromString('hey')], sink: all }) + const { stream } = await conn.newStream(['echo']) - const result = await pipe(s, conn, s) - - expect(result).to.be.eql([uint8ArrayFromString('hey')]) + await expect(pipe(s, stream, s)).to.eventually.deep.equal([uint8ArrayFromString('hey')]) }) it('dial should throw on immediate abort', async () => { @@ -237,14 +253,12 @@ describe('dial', () => { it('should resolve port 0', async () => { const ma = new Multiaddr('/ip4/127.0.0.1/tcp/0/ws') - const ws = new WS({ upgrader: mockUpgrader }) + const ws = new WebSockets({ upgrader }) // Create a Promise that resolves when a connection is handled - let handled - const handlerPromise = new Promise(resolve => { handled = resolve }) - const handler = conn => handled(conn) + const deferred = defer() - const listener = ws.createListener(handler) + const listener = ws.createListener({ handler: deferred.resolve }) // Listen on the multiaddr await listener.listen(ma) @@ -256,7 +270,7 @@ describe('dial', () => { await ws.dial(localAddrs[0]) // Wait for the incoming dial to be handled - await handlerPromise + await deferred.promise // close the listener await listener.close() @@ -264,17 +278,23 @@ describe('dial', () => { }) describe('ip4 no loopback', () => { - let ws - let listener + let ws: WebSockets + let listener: Listener const ma = new Multiaddr('/ip4/0.0.0.0/tcp/0/ws') - beforeEach(() => { - ws = new WS({ upgrader: mockUpgrader }) - listener = ws.createListener(conn => pipe(conn, conn)) - return listener.listen(ma) + beforeEach(async () => { + ws = new WebSockets({ upgrader }) + listener = ws.createListener({ + handler: (conn) => { + void conn.newStream(['echo']).then(async ({ stream }) => { + return await pipe(stream, stream) + }) + } + }) + return await listener.listen(ma) }) - afterEach(() => listener.close()) + afterEach(async () => await listener.close()) it('dial', async () => { const addrs = listener.getAddrs().filter((ma) => { @@ -285,32 +305,41 @@ describe('dial', () => { // Dial first no loopback address const conn = await ws.dial(addrs[0]) - const s = goodbye({ source: ['hey'], sink: collect }) + const s = goodbye({ source: [uint8ArrayFromString('hey')], sink: all }) + const { stream } = await conn.newStream(['echo']) - const result = await pipe(s, conn, s) - - expect(result).to.be.eql([uint8ArrayFromString('hey')]) + await expect(pipe(s, stream, s)).to.eventually.deep.equal([uint8ArrayFromString('hey')]) }) }) describe('ip4 with wss', () => { - let ws - let listener - const ma = new Multiaddr('/ip4/127.0.0.1/tcp/9091/wss') - - const server = https.createServer({ - cert: fs.readFileSync('./test/fixtures/certificate.pem'), - key: fs.readFileSync('./test/fixtures/key.pem') + let ws: WebSockets + let listener: Listener + const ma = new Multiaddr('/ip4/127.0.0.1/tcp/37284/wss') + let server: https.Server + + beforeEach(async () => { + server = https.createServer({ + cert: fs.readFileSync('./test/fixtures/certificate.pem'), + key: fs.readFileSync('./test/fixtures/key.pem') + }) + ws = new WebSockets({ upgrader }) + listener = ws.createListener({ + server, + handler: (conn) => { + void conn.newStream(['echo']).then(async ({ stream }) => { + return await pipe(stream, stream) + }) + } + }) + return await listener.listen(ma) }) - beforeEach(() => { - ws = new WS({ upgrader: mockUpgrader }) - listener = ws.createListener({ server }, conn => pipe(conn, conn)) - return listener.listen(ma) + afterEach(async () => { + await listener.close() + await server.close() }) - afterEach(() => listener.close()) - it('should listen on wss address', () => { const addrs = listener.getAddrs() @@ -318,45 +347,43 @@ describe('dial', () => { expect(ma.equals(addrs[0])).to.eql(true) }) - it('dial', async () => { + it('dial ip4', async () => { const conn = await ws.dial(ma, { websocket: { rejectUnauthorized: false } }) - const s = goodbye({ source: ['hey'], sink: collect }) + const s = goodbye({ source: [uint8ArrayFromString('hey')], sink: all }) + const { stream } = await conn.newStream(['echo']) - const result = await pipe(s, conn, s) + const res = await pipe(s, stream, s) - expect(result).to.be.eql([uint8ArrayFromString('hey')]) + expect(res[0]).to.equalBytes(uint8ArrayFromString('hey')) + await conn.close() }) }) describe('ip6', () => { - let ws - let listener - const ma = new Multiaddr('/ip6/::1/tcp/9091') + let ws: WebSockets + let listener: Listener + const ma = new Multiaddr('/ip6/::1/tcp/9091/ws') - beforeEach(() => { - ws = new WS({ upgrader: mockUpgrader }) - listener = ws.createListener(conn => pipe(conn, conn)) - return listener.listen(ma) + beforeEach(async () => { + ws = new WebSockets({ upgrader }) + listener = ws.createListener({ + handler: (conn) => { + void conn.newStream(['echo']).then(async ({ stream }) => { + return await pipe(stream, stream) + }) + } + }) + return await listener.listen(ma) }) - afterEach(() => listener.close()) - - it('dial', async () => { - const conn = await ws.dial(ma) - const s = goodbye({ source: ['hey'], sink: collect }) - - const result = await pipe(s, conn, s) + afterEach(async () => await listener.close()) - expect(result).to.be.eql([uint8ArrayFromString('hey')]) - }) - - it('dial and use BufferList', async () => { + it('dial ip6', async () => { const conn = await ws.dial(ma) - const s = goodbye({ source: [new BufferList('hey')], sink: collect }) - - const result = await pipe(s, conn, s) + const s = goodbye({ source: [uint8ArrayFromString('hey')], sink: all }) + const { stream } = await conn.newStream(['echo']) - expect(result).to.be.eql([uint8ArrayFromString('hey')]) + await expect(pipe(s, stream, s)).to.eventually.deep.equal([uint8ArrayFromString('hey')]) }) it('dial with p2p Id', async () => { @@ -364,22 +391,22 @@ describe('dial', () => { const conn = await ws.dial(ma) const s = goodbye({ - source: ['hey'], - sink: collect + source: [uint8ArrayFromString('hey')], + sink: all }) + const { stream } = await conn.newStream(['echo']) - const result = await pipe(s, conn, s) - expect(result).to.be.eql([uint8ArrayFromString('hey')]) + await expect(pipe(s, stream, s)).to.eventually.deep.equal([uint8ArrayFromString('hey')]) }) }) }) describe('filter addrs', () => { - let ws + let ws: WebSockets describe('default filter addrs with only dns', () => { before(() => { - ws = new WS({ upgrader: mockUpgrader }) + ws = new WebSockets({ upgrader }) }) it('should filter out invalid WS addresses', function () { @@ -447,7 +474,7 @@ describe('filter addrs', () => { describe('custom filter addrs', () => { before(() => { - ws = new WS({ upgrader: mockUpgrader, filter: filters.all }) + ws = new WebSockets({ upgrader, filter: filters.all }) }) it('should fail invalid WS addresses', function () { @@ -569,7 +596,7 @@ describe('filter addrs', () => { it('filter a single addr for this transport', () => { const ma = new Multiaddr('/ip4/127.0.0.1/tcp/9090/ws/ipfs/Qmb6owHp6eaWArVbcJJbQSyifyJBttMMjYV76N2hMbf5Vw') - const valid = ws.filter(ma) + const valid = ws.filter([ma]) expect(valid.length).to.equal(1) expect(valid[0]).to.deep.equal(ma) }) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f296f99 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" + }, + "include": [ + "src", + "test" + ] +} From 89e62e3a14d0b9c3b143eab879325bf3c705a93e Mon Sep 17 00:00:00 2001 From: achingbrain Date: Sat, 29 Jan 2022 11:03:03 +0000 Subject: [PATCH 2/6] chore: update project config --- .github/dependabot.yml | 2 +- .../{main.yml => js-test-and-release.yml} | 39 +++++- LICENSE | 4 +- LICENSE-MIT | 2 +- package.json | 115 +++++++++++++++--- 5 files changed, 138 insertions(+), 24 deletions(-) rename .github/workflows/{main.yml => js-test-and-release.yml} (70%) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index de46e32..290ad02 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,5 +4,5 @@ updates: directory: "/" schedule: interval: daily - time: "11:00" + time: "10:00" open-pull-requests-limit: 10 diff --git a/.github/workflows/main.yml b/.github/workflows/js-test-and-release.yml similarity index 70% rename from .github/workflows/main.yml rename to .github/workflows/js-test-and-release.yml index c9772b6..8630dc5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/js-test-and-release.yml @@ -2,10 +2,10 @@ name: test & maybe release on: push: branches: - - master + - master # with #262 - ${{{ github.default_branch }}} pull_request: branches: - - master + - master # with #262 - ${{{ github.default_branch }}} jobs: @@ -35,7 +35,10 @@ jobs: node-version: ${{ matrix.node }} - uses: ipfs/aegir/actions/cache-node-modules@master - run: npm run --if-present test:node - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: node test-chrome: needs: check @@ -47,6 +50,10 @@ jobs: node-version: lts/* - uses: ipfs/aegir/actions/cache-node-modules@master - run: npm run --if-present test:chrome + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: chrome test-chrome-webworker: needs: check @@ -58,6 +65,10 @@ jobs: node-version: lts/* - uses: ipfs/aegir/actions/cache-node-modules@master - run: npm run --if-present test:chrome-webworker + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: chrome-webworker test-firefox: needs: check @@ -69,6 +80,10 @@ jobs: node-version: lts/* - uses: ipfs/aegir/actions/cache-node-modules@master - run: npm run --if-present test:firefox + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: firefox test-firefox-webworker: needs: check @@ -80,6 +95,10 @@ jobs: node-version: lts/* - uses: ipfs/aegir/actions/cache-node-modules@master - run: npm run --if-present test:firefox-webworker + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: firefox-webworker test-electron-main: needs: check @@ -91,6 +110,10 @@ jobs: node-version: lts/* - uses: ipfs/aegir/actions/cache-node-modules@master - run: npx xvfb-maybe npm run --if-present test:electron-main + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: electron-main test-electron-renderer: needs: check @@ -102,13 +125,17 @@ jobs: node-version: lts/* - uses: ipfs/aegir/actions/cache-node-modules@master - run: npx xvfb-maybe npm run --if-present test:electron-renderer + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: electron-renderer release: needs: [test-node, test-chrome, test-chrome-webworker, test-firefox, test-firefox-webworker, test-electron-main, test-electron-renderer] runs-on: ubuntu-latest - if: github.event_name == 'push' && github.ref == 'refs/heads/master' + if: github.event_name == 'push' && github.ref == 'refs/heads/master' # with #262 - 'refs/heads/${{{ github.default_branch }}}' steps: - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v2 with: fetch-depth: 0 - uses: actions/setup-node@v2 @@ -117,7 +144,7 @@ jobs: - uses: ipfs/aegir/actions/cache-node-modules@master - uses: ipfs/aegir/actions/docker-login@master with: - docker-token: ${{ secrets.DOCKER_USERNAME }} + docker-token: ${{ secrets.DOCKER_TOKEN }} docker-username: ${{ secrets.DOCKER_USERNAME }} - run: npm run --if-present release env: diff --git a/LICENSE b/LICENSE index b0b237f..20ce483 100644 --- a/LICENSE +++ b/LICENSE @@ -1,2 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + MIT: https://www.opensource.org/licenses/mit -Apache-2.0: https://www.apache.org/licenses/license-2.0 \ No newline at end of file +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/LICENSE-MIT b/LICENSE-MIT index 749aa1e..72dc60d 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -16,4 +16,4 @@ 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. \ No newline at end of file +THE SOFTWARE. diff --git a/package.json b/package.json index 22d2d5f..8046ce5 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,22 @@ "name": "@libp2p/websockets", "version": "0.16.2", "description": "JavaScript implementation of the WebSockets module that libp2p uses and that implements the interface-transport spec", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p-websockets#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-websockets.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-websockets/issues" + }, + "keywords": [ + "IPFS" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, "type": "module", "types": "./dist/src/index.d.ts", "files": [ @@ -21,6 +37,87 @@ "sourceType": "module" } }, + "release": { + "branches": [ + "master" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "chore", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Trivial Changes" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, "scripts": { "lint": "aegir lint", "dep-check": "aegir dep-check dist/src/**/*.js dist/test/**/*.js", @@ -35,21 +132,6 @@ "test:electron-main": "npm run test -- -t electron-main -f ./dist/test/node.js --cov", "release": "semantic-release" }, - "browser": { - "./dist/src/listener.js": "./dist/src/listener.browser.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/libp2p/js-libp2p-websockets.git" - }, - "keywords": [ - "IPFS" - ], - "license": "MIT", - "bugs": { - "url": "https://github.com/libp2p/js-libp2p-websockets/issues" - }, - "homepage": "https://github.com/libp2p/js-libp2p-websockets#readme", "dependencies": { "@libp2p/utils": "^1.0.0", "@multiformats/mafmt": "^11.0.1", @@ -79,5 +161,8 @@ "p-wait-for": "^4.1.0", "uint8arrays": "^3.0.0", "util": "^0.12.3" + }, + "browser": { + "./dist/src/listener.js": "./dist/src/listener.browser.js" } } From 88a6ee4ca63e43ed4ec0c71335d39fe8842b7a00 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Tue, 1 Feb 2022 06:24:09 +0000 Subject: [PATCH 3/6] chore: update upgrader url --- README.md | 2 +- src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ef671ec..516e80c 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ const ws = new WS(properties) | Name | Type | Description | Default | |------|------|-------------|---------| -| upgrader | [`Upgrader`](https://github.com/libp2p/interface-transport#upgrader) | connection upgrader object with `upgradeOutbound` and `upgradeInbound` | **REQUIRED** | +| upgrader | [`Upgrader`](https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces/src/transport#upgrader) | connection upgrader object with `upgradeOutbound` and `upgradeInbound` | **REQUIRED** | | filter | `(multiaddrs: Array) => Array` | override transport addresses filter | **Browser:** DNS+WSS multiaddrs / **Node.js:** DNS+{WS, WSS} multiaddrs | You can create your own address filters for this transports, or rely in the filters [provided](./src/filters.js). diff --git a/src/index.ts b/src/index.ts index 2ad45ec..6dd5bf0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,7 +28,7 @@ export class WebSockets implements Transport Date: Sun, 6 Feb 2022 15:43:19 +0000 Subject: [PATCH 4/6] chore: update readme --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 516e80c..98c4baa 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,7 @@ [![](https://img.shields.io/badge/freenode-%23ipfs-blue.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23ipfs) [![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) [![Coverage Status](https://coveralls.io/repos/github/libp2p/js-libp2p-websockets/badge.svg?branch=master)](https://coveralls.io/github/libp2p/js-libp2p-websockets?branch=master) -[![Travis CI](https://travis-ci.org/libp2p/js-libp2p-websockets.svg?branch=master)](https://travis-ci.org/libp2p/js-libp2p-websockets) -[![Circle CI](https://circleci.com/gh/libp2p/js-libp2p-websockets.svg?style=svg)](https://circleci.com/gh/libp2p/js-libp2p-websockets) +[![Build Status](https://github.com/libp2p/js-libp2p-websockets/actions/workflows/js-test-and-release.yml/badge.svg?branch=main)](https://github.com/libp2p/js-libp2p-websockets/actions/workflows/js-test-and-release.yml) [![Dependency Status](https://david-dm.org/libp2p/js-libp2p-websockets.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-websockets) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) ![](https://img.shields.io/badge/Node.js-%3E%3D14.0.0-orange.svg?style=flat-square) @@ -27,6 +26,8 @@ - [API](#api) - [Transport](#transport) - [Connection](#connection) +- [License](#license) + - [Contribution](#contribution) ## Description @@ -108,3 +109,14 @@ For more information see [libp2p/js-libp2p/doc/CONFIGURATION.md#customizing-tran ### Connection [![](https://raw.githubusercontent.com/libp2p/interface-connection/master/img/badge.png)](https://github.com/libp2p/interface-connection) + +## License + +Licensed under either of + + * Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / http://www.apache.org/licenses/LICENSE-2.0) + * MIT ([LICENSE-MIT](LICENSE-MIT) / http://opensource.org/licenses/MIT) + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. From c85a8bc33aff7c9b089e220e5ed2654523eb3a8d Mon Sep 17 00:00:00 2001 From: achingbrain Date: Sun, 6 Feb 2022 15:43:37 +0000 Subject: [PATCH 5/6] chore: add automerge --- .github/workflows/automerge.yml | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/automerge.yml diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 0000000..13da9c1 --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,50 @@ +# Automatically merge pull requests opened by web3-bot, as soon as (and only if) all tests pass. +# This reduces the friction associated with updating with our workflows. + +on: [ pull_request ] +name: Automerge + +jobs: + automerge-check: + if: github.event.pull_request.user.login == 'web3-bot' + runs-on: ubuntu-latest + outputs: + status: ${{ steps.should-automerge.outputs.status }} + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Check if we should automerge + id: should-automerge + run: | + for commit in $(git rev-list --first-parent origin/${{ github.event.pull_request.base.ref }}..${{ github.event.pull_request.head.sha }}); do + committer=$(git show --format=$'%ce' -s $commit) + echo "Committer: $committer" + if [[ "$committer" != "web3-bot@users.noreply.github.com" ]]; then + echo "Commit $commit wasn't committed by web3-bot, but by $committer." + echo "::set-output name=status::false" + exit + fi + done + echo "::set-output name=status::true" + automerge: + needs: automerge-check + runs-on: ubuntu-latest + # The check for the user is redundant here, as this job depends on the automerge-check job, + # but it prevents this job from spinning up, just to be skipped shortly after. + if: github.event.pull_request.user.login == 'web3-bot' && needs.automerge-check.outputs.status == 'true' + steps: + - name: Wait on tests + uses: lewagon/wait-on-check-action@bafe56a6863672c681c3cf671f5e10b20abf2eaa # v0.2 + with: + ref: ${{ github.event.pull_request.head.sha }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + wait-interval: 10 + running-workflow-name: 'automerge' # the name of this job + - name: Merge PR + uses: pascalgn/automerge-action@741c311a47881be9625932b0a0de1b0937aab1ae # v0.13.1 + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + MERGE_LABELS: "" + MERGE_METHOD: "squash" + MERGE_DELETE_BRANCH: true From 48c68b75ae147a992158f2a953080b1b8082366d Mon Sep 17 00:00:00 2001 From: achingbrain Date: Thu, 10 Feb 2022 09:20:09 +0200 Subject: [PATCH 6/6] chore: update interfaces --- .aegir.cjs | 4 +- package.json | 8 +- src/index.ts | 6 +- src/listener.ts | 222 +++++++++++++++++++++++------------------- src/socket-to-conn.ts | 6 +- test/node.ts | 12 +-- 6 files changed, 138 insertions(+), 120 deletions(-) diff --git a/.aegir.cjs b/.aegir.cjs index 66e8eed..53f9c6f 100644 --- a/.aegir.cjs +++ b/.aegir.cjs @@ -13,7 +13,9 @@ module.exports = { const ma = new Multiaddr('/ip4/127.0.0.1/tcp/9095/ws') const listener = ws.createListener(conn => pipe(conn, conn)) await listener.listen(ma) - listener.on('error', console.error) + listener.addEventListener('error', (evt) => { + console.error(evt.detail) + }) return { listener diff --git a/package.json b/package.json index 8046ce5..4e853ef 100644 --- a/package.json +++ b/package.json @@ -133,12 +133,12 @@ "release": "semantic-release" }, "dependencies": { + "@libp2p/logger": "^1.0.2", "@libp2p/utils": "^1.0.0", "@multiformats/mafmt": "^11.0.1", "@multiformats/multiaddr": "^10.0.0", "@multiformats/multiaddr-to-uri": "^9.0.0", "abortable-iterator": "^4.0.2", - "debug": "^4.3.1", "err-code": "^3.0.1", "it-ws": "^5.0.0", "p-defer": "^4.0.0", @@ -146,10 +146,8 @@ "wherearewe": "^1.0.0" }, "devDependencies": { - "@libp2p/interface-compliance-tests": "^1.0.1", - "@libp2p/interfaces": "^1.0.0", - "@types/bl": "^5.0.2", - "@types/debug": "^4.1.7", + "@libp2p/interface-compliance-tests": "^1.1.2", + "@libp2p/interfaces": "^1.3.2", "@types/ws": "^8.2.2", "aegir": "^36.1.3", "is-loopback-addr": "^2.0.1", diff --git a/src/index.ts b/src/index.ts index 6dd5bf0..7ec5349 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { connect, WebSocketOptions } from 'it-ws/client' import { multiaddrToUri as toUri } from '@multiformats/multiaddr-to-uri' import { AbortError } from '@libp2p/interfaces/errors' import pDefer from 'p-defer' -import debug from 'debug' +import { logger } from '@libp2p/logger' import env from 'wherearewe' import { createListener } from './listener.js' import { socketToMaConn } from './socket-to-conn.js' @@ -13,9 +13,7 @@ import type { WebSocketListenerOptions } from './listener.js' import type { Multiaddr } from '@multiformats/multiaddr' import type { DuplexWebSocket } from 'it-ws/dist/src/duplex' -const log = Object.assign(debug('libp2p:websockets'), { - error: debug('libp2p:websockets:error') -}) +const log = logger('libp2p:websockets') /** * @class WebSockets diff --git a/src/listener.ts b/src/listener.ts index b98aa2a..34958cc 100644 --- a/src/listener.ts +++ b/src/listener.ts @@ -1,138 +1,160 @@ -import { EventEmitter } from 'events' import os from 'os' import { Multiaddr, protocols } from '@multiformats/multiaddr' import { createServer } from 'it-ws/server' -import debug from 'debug' +import { logger } from '@libp2p/logger' import { socketToMaConn } from './socket-to-conn.js' import { ipPortToMultiaddr as toMultiaddr } from '@libp2p/utils/ip-port-to-multiaddr' -import type { ListenerOptions, Upgrader, Listener } from '@libp2p/interfaces/transport' +import type { ListenerOptions, Upgrader, Listener, ListenerEvents } from '@libp2p/interfaces/transport' import type { Server } from 'http' import type { WebSocketServer } from 'it-ws/server' import type { DuplexWebSocket } from 'it-ws/duplex' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces' -const log = Object.assign(debug('libp2p:websockets:listener'), { - error: debug('libp2p:websockets:listener:error') -}) +const log = logger('libp2p:websockets:listener') -export interface WebSocketListenerOptions extends ListenerOptions { - server?: Server -} +class WebSocketListener extends EventEmitter implements Listener { + private readonly connections: Set + private listeningMultiaddr?: Multiaddr + private readonly server: WebSocketServer -export function createListener (upgrader: Upgrader, options?: WebSocketListenerOptions): Listener { - options = options ?? {} - let server: WebSocketServer // eslint-disable-line prefer-const - let listeningMultiaddr: Multiaddr - // Keep track of open connections to destroy when the listener is closed - const connections = new Set() - - const listener: Listener = Object.assign(new EventEmitter(), { - close: async () => { - await Promise.all( - Array.from(connections).map(async maConn => await maConn.close()) - ) - - if (server.address() == null) { - // not listening, close will throw an error - return - } + constructor (upgrader: Upgrader, options: WebSocketListenerOptions) { + super() - return await server.close() - }, - listen: async (ma: Multiaddr) => { - listeningMultiaddr = ma + // Keep track of open connections to destroy when the listener is closed + this.connections = new Set() - await server.listen(ma.toOptions()) - }, - getAddrs: () => { - const multiaddrs = [] - const address = server.address() + const self = this // eslint-disable-line @typescript-eslint/no-this-alias - if (address == null) { - throw new Error('Listener is not ready yet') - } + this.server = createServer({ + ...options, + onConnection: (stream: DuplexWebSocket) => { + const maConn = socketToMaConn(stream, toMultiaddr(stream.remoteAddress ?? '', stream.remotePort ?? 0)) + log('new inbound connection %s', maConn.remoteAddr) - if (typeof address === 'string') { - throw new Error('Wrong address type received - expected AddressInfo, got string - are you trying to listen on a unix socket?') - } + this.connections.add(stream) - const ipfsId = listeningMultiaddr.getPeerId() - const protos = listeningMultiaddr.protos() - - // Because TCP will only return the IPv6 version - // we need to capture from the passed multiaddr - if (protos.some(proto => proto.code === protocols('ip4').code)) { - const wsProto = protos.some(proto => proto.code === protocols('ws').code) ? '/ws' : '/wss' - let m = listeningMultiaddr.decapsulate('tcp') - m = m.encapsulate(`/tcp/${address.port}${wsProto}`) - if (ipfsId != null) { - m = m.encapsulate(`/p2p/${ipfsId}`) - } + stream.socket.on('close', function () { + self.connections.delete(stream) + }) - if (m.toString().includes('0.0.0.0')) { - const netInterfaces = os.networkInterfaces() - Object.values(netInterfaces).forEach(niInfos => { - if (niInfos == null) { - return - } + try { + void upgrader.upgradeInbound(maConn) + .then((conn) => { + log('inbound connection %s upgraded', maConn.remoteAddr) - niInfos.forEach(ni => { - if (ni.family === 'IPv4') { - multiaddrs.push(new Multiaddr(m.toString().replace('0.0.0.0', ni.address))) + if (options?.handler != null) { + options?.handler(conn) } + + self.dispatchEvent(new CustomEvent('connection', { + detail: conn + })) + }) + .catch(async err => { + log.error('inbound connection failed to upgrade', err) + + await maConn.close().catch(err => { + log.error('inbound connection failed to close after upgrade failed', err) + }) }) + } catch (err) { + log.error('inbound connection failed to upgrade', err) + maConn.close().catch(err => { + log.error('inbound connection failed to close after upgrade failed', err) }) - } else { - multiaddrs.push(m) } } - - return multiaddrs + }) + + this.server.on('listening', () => { + this.dispatchEvent(new CustomEvent('listening')) + }) + this.server.on('error', (err: Error) => { + this.dispatchEvent(new CustomEvent('error', { + detail: err + })) + }) + this.server.on('close', () => { + this.dispatchEvent(new CustomEvent('close')) + }) + } + + async close () { + await Promise.all( + Array.from(this.connections).map(async maConn => await maConn.close()) + ) + + if (this.server.address() == null) { + // not listening, close will throw an error + return } - }) - server = createServer({ - ...options, - onConnection: (stream: DuplexWebSocket) => { - const maConn = socketToMaConn(stream, toMultiaddr(stream.remoteAddress ?? '', stream.remotePort ?? 0)) - log('new inbound connection %s', maConn.remoteAddr) + return await this.server.close() + } - connections.add(stream) + async listen (ma: Multiaddr) { + this.listeningMultiaddr = ma - stream.socket.on('close', function () { - connections.delete(stream) - }) + await this.server.listen(ma.toOptions()) + } - try { - void upgrader.upgradeInbound(maConn) - .then((conn) => { - log('inbound connection %s upgraded', maConn.remoteAddr) + getAddrs () { + const multiaddrs = [] + const address = this.server.address() - if (options?.handler != null) { - options?.handler(conn) - } + if (address == null) { + throw new Error('Listener is not ready yet') + } - listener.emit('connection', conn) - }) - .catch(async err => { - log.error('inbound connection failed to upgrade', err) + if (typeof address === 'string') { + throw new Error('Wrong address type received - expected AddressInfo, got string - are you trying to listen on a unix socket?') + } - await maConn.close().catch(err => { - log.error('inbound connection failed to close after upgrade failed', err) - }) + if (this.listeningMultiaddr == null) { + throw new Error('Listener is not ready yet') + } + + const ipfsId = this.listeningMultiaddr.getPeerId() + const protos = this.listeningMultiaddr.protos() + + // Because TCP will only return the IPv6 version + // we need to capture from the passed multiaddr + if (protos.some(proto => proto.code === protocols('ip4').code)) { + const wsProto = protos.some(proto => proto.code === protocols('ws').code) ? '/ws' : '/wss' + let m = this.listeningMultiaddr.decapsulate('tcp') + m = m.encapsulate(`/tcp/${address.port}${wsProto}`) + if (ipfsId != null) { + m = m.encapsulate(`/p2p/${ipfsId}`) + } + + if (m.toString().includes('0.0.0.0')) { + const netInterfaces = os.networkInterfaces() + Object.values(netInterfaces).forEach(niInfos => { + if (niInfos == null) { + return + } + + niInfos.forEach(ni => { + if (ni.family === 'IPv4') { + multiaddrs.push(new Multiaddr(m.toString().replace('0.0.0.0', ni.address))) + } }) - } catch (err) { - log.error('inbound connection failed to upgrade', err) - maConn.close().catch(err => { - log.error('inbound connection failed to close after upgrade failed', err) }) + } else { + multiaddrs.push(m) } } - }) - server - .on('listening', () => listener.emit('listening')) - .on('error', (err: Error) => listener.emit('error', err)) - .on('close', () => listener.emit('close')) + return multiaddrs + } +} + +export interface WebSocketListenerOptions extends ListenerOptions { + server?: Server +} + +export function createListener (upgrader: Upgrader, options?: WebSocketListenerOptions): Listener { + options = options ?? {} - return listener + return new WebSocketListener(upgrader, options) } diff --git a/src/socket-to-conn.ts b/src/socket-to-conn.ts index 989e4bd..18a854a 100644 --- a/src/socket-to-conn.ts +++ b/src/socket-to-conn.ts @@ -1,15 +1,13 @@ import { abortableSource } from 'abortable-iterator' import { CLOSE_TIMEOUT } from './constants.js' import pTimeout from 'p-timeout' -import debug from 'debug' +import { logger } from '@libp2p/logger' import type { AbortOptions } from '@libp2p/interfaces' import type { MultiaddrConnection } from '@libp2p/interfaces/transport' import type { Multiaddr } from '@multiformats/multiaddr' import type { DuplexWebSocket } from 'it-ws/duplex' -const log = Object.assign(debug('libp2p:websockets:socket'), { - error: debug('libp2p:websockets:socket:error') -}) +const log = logger('libp2p:websockets:socket') export interface SocketToConnOptions extends AbortOptions { localAddr?: Multiaddr diff --git a/test/node.ts b/test/node.ts index dd87610..162860e 100644 --- a/test/node.ts +++ b/test/node.ts @@ -72,7 +72,7 @@ describe('listen', () => { it('listen, check for listening event', (done) => { listener = ws.createListener() - listener.on('listening', () => { + listener.addEventListener('listening', () => { done() }) @@ -82,8 +82,8 @@ describe('listen', () => { it('listen, check for the close event', (done) => { const listener = ws.createListener() - listener.on('listening', () => { - listener.on('close', done) + listener.addEventListener('listening', () => { + listener.addEventListener('close', () => done()) void listener.close() }) @@ -177,7 +177,7 @@ describe('listen', () => { it('listen, check for listening event', (done) => { const listener = ws.createListener() - listener.on('listening', () => { + listener.addEventListener('listening', () => { void listener.close().then(done, done) }) @@ -187,8 +187,8 @@ describe('listen', () => { it('listen, check for the close event', (done) => { const listener = ws.createListener() - listener.on('listening', () => { - listener.on('close', done) + listener.addEventListener('listening', () => { + listener.addEventListener('close', () => done()) void listener.close() })