From fb7950c5d688b62878af8e6538175eb1ff93ec22 Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Thu, 10 Feb 2022 10:14:02 +0200 Subject: [PATCH] feat: convert to typescript (#401) - Converts to typescript - Only named exports - No more CJS, only ESM - Runs tests on all supported environments - Adds auto-publish - Adds dependabot - Drops dependency on simple-peer as it has deps on Buffer, node streams, etc BREAKING CHANGE: switch to named exports, ESM only --- .github/dependabot.yml | 2 +- .github/workflows/automerge.yml | 50 +++ .github/workflows/js-test-and-release.yml | 152 +++++++++ .github/workflows/main.yml | 200 ------------ .gitignore | 1 + Dockerfile | 11 +- LICENSE | 4 + LICENSE-MIT | 2 +- lerna.json | 17 +- package.json | 51 +-- packages/signalling-server/package.json | 86 ----- packages/signalling-server/src/config.js | 19 -- packages/signalling-server/src/index.js | 50 --- .../signalling-server/src/routes-ws/index.js | 136 -------- packages/transport/.aegir.js | 46 --- packages/transport/package.json | 93 ------ packages/transport/src/index.js | 250 -------------- packages/transport/src/listener.js | 199 ------------ packages/transport/src/socket-to-conn.js | 110 ------- packages/transport/test/browser.js | 23 -- packages/transport/test/compliance.spec.js | 61 ---- packages/transport/test/node.js | 50 --- packages/transport/test/transport/dial.js | 157 --------- .../transport/test/transport/discovery.js | 91 ------ .../transport/test/transport/instance.spec.js | 22 -- .../test/transport/multiple-signal-servers.js | 121 ------- .../test/transport/reconnect.node.js | 84 ----- packages/transport/test/transport/track.js | 72 ---- packages/webrtc-star-protocol/LICENSE | 4 + .../LICENSE-APACHE | 0 .../LICENSE-MIT | 2 +- packages/webrtc-star-protocol/README.md | 34 ++ packages/webrtc-star-protocol/package.json | 135 ++++++++ packages/webrtc-star-protocol/src/index.ts | 54 +++ packages/webrtc-star-protocol/tsconfig.json | 12 + .../.aegir.cjs} | 13 +- .../CHANGELOG.md | 0 .../DEPLOYMENT.md | 0 .../webrtc-star-signalling-server/LICENSE | 4 + .../LICENSE-APACHE | 0 .../LICENSE-MIT | 2 +- .../README.md | 10 +- .../bin/index.js} | 9 +- .../package.json | 158 +++++++++ .../src/config.ts | 17 + .../src/index.html | 0 .../src/index.ts | 66 ++++ .../src/socket-server.ts | 123 +++++++ .../test/node.ts} | 21 +- .../test/sig-server.ts} | 110 ++++--- .../tsconfig.json | 20 ++ packages/webrtc-star-transport/.aegir.cjs | 49 +++ .../CHANGELOG.md | 0 .../DEPLOYMENT.md | 0 packages/webrtc-star-transport/LICENSE | 4 + packages/webrtc-star-transport/LICENSE-APACHE | 5 + packages/webrtc-star-transport/LICENSE-MIT | 19 ++ .../README.md | 30 +- packages/webrtc-star-transport/package.json | 166 ++++++++++ .../src/constants.ts} | 8 +- packages/webrtc-star-transport/src/index.ts | 275 ++++++++++++++++ .../webrtc-star-transport/src/listener.ts | 307 ++++++++++++++++++ .../webrtc-star-transport/src/peer/channel.ts | 101 ++++++ .../src/peer/handshake.ts | 75 +++++ .../webrtc-star-transport/src/peer/index.ts | 3 + .../src/peer/initiator.ts | 105 ++++++ .../src/peer/interface.ts | 25 ++ .../webrtc-star-transport/src/peer/peer.ts | 146 +++++++++ .../src/peer/receiver.ts | 89 +++++ .../src/socket-to-conn.ts | 88 +++++ .../src/utils.ts} | 29 +- .../webrtc-star-transport/test/browser.ts | 23 ++ .../test/compliance.spec.ts | 78 +++++ packages/webrtc-star-transport/test/node.ts | 55 ++++ .../test/transport/dial.ts | 236 ++++++++++++++ .../test/transport/discovery.ts | 105 ++++++ .../test/transport/filter.ts} | 11 +- .../test/transport/instance.spec.ts | 21 ++ .../test/transport/listen.ts} | 47 +-- .../test/transport/multiple-signal-servers.ts | 170 ++++++++++ .../test/transport/reconnect.node.ts | 93 ++++++ .../test/transport/track.ts | 99 ++++++ .../test/utils.spec.ts} | 9 +- packages/webrtc-star-transport/tsconfig.json | 20 ++ 84 files changed, 3379 insertions(+), 2066 deletions(-) create mode 100644 .github/workflows/automerge.yml create mode 100644 .github/workflows/js-test-and-release.yml delete mode 100644 .github/workflows/main.yml create mode 100644 LICENSE delete mode 100644 packages/signalling-server/package.json delete mode 100644 packages/signalling-server/src/config.js delete mode 100644 packages/signalling-server/src/index.js delete mode 100644 packages/signalling-server/src/routes-ws/index.js delete mode 100644 packages/transport/.aegir.js delete mode 100644 packages/transport/package.json delete mode 100644 packages/transport/src/index.js delete mode 100644 packages/transport/src/listener.js delete mode 100644 packages/transport/src/socket-to-conn.js delete mode 100644 packages/transport/test/browser.js delete mode 100644 packages/transport/test/compliance.spec.js delete mode 100644 packages/transport/test/node.js delete mode 100644 packages/transport/test/transport/dial.js delete mode 100644 packages/transport/test/transport/discovery.js delete mode 100644 packages/transport/test/transport/instance.spec.js delete mode 100644 packages/transport/test/transport/multiple-signal-servers.js delete mode 100644 packages/transport/test/transport/reconnect.node.js delete mode 100644 packages/transport/test/transport/track.js create mode 100644 packages/webrtc-star-protocol/LICENSE rename packages/{signalling-server => webrtc-star-protocol}/LICENSE-APACHE (100%) rename packages/{transport => webrtc-star-protocol}/LICENSE-MIT (98%) create mode 100644 packages/webrtc-star-protocol/README.md create mode 100644 packages/webrtc-star-protocol/package.json create mode 100644 packages/webrtc-star-protocol/src/index.ts create mode 100644 packages/webrtc-star-protocol/tsconfig.json rename packages/{signalling-server/.aegir.js => webrtc-star-signalling-server/.aegir.cjs} (72%) rename packages/{signalling-server => webrtc-star-signalling-server}/CHANGELOG.md (100%) rename packages/{signalling-server => webrtc-star-signalling-server}/DEPLOYMENT.md (100%) create mode 100644 packages/webrtc-star-signalling-server/LICENSE rename packages/{transport => webrtc-star-signalling-server}/LICENSE-APACHE (100%) rename packages/{signalling-server => webrtc-star-signalling-server}/LICENSE-MIT (98%) rename packages/{signalling-server => webrtc-star-signalling-server}/README.md (64%) rename packages/{signalling-server/src/bin.js => webrtc-star-signalling-server/bin/index.js} (82%) create mode 100644 packages/webrtc-star-signalling-server/package.json create mode 100644 packages/webrtc-star-signalling-server/src/config.ts rename packages/{signalling-server => webrtc-star-signalling-server}/src/index.html (100%) create mode 100644 packages/webrtc-star-signalling-server/src/index.ts create mode 100644 packages/webrtc-star-signalling-server/src/socket-server.ts rename packages/{signalling-server/test/node.js => webrtc-star-signalling-server/test/node.ts} (69%) rename packages/{signalling-server/test/sig-server.js => webrtc-star-signalling-server/test/sig-server.ts} (68%) create mode 100644 packages/webrtc-star-signalling-server/tsconfig.json create mode 100644 packages/webrtc-star-transport/.aegir.cjs rename packages/{transport => webrtc-star-transport}/CHANGELOG.md (100%) rename packages/{transport => webrtc-star-transport}/DEPLOYMENT.md (100%) create mode 100644 packages/webrtc-star-transport/LICENSE create mode 100644 packages/webrtc-star-transport/LICENSE-APACHE create mode 100644 packages/webrtc-star-transport/LICENSE-MIT rename packages/{transport => webrtc-star-transport}/README.md (56%) create mode 100644 packages/webrtc-star-transport/package.json rename packages/{transport/src/constants.js => webrtc-star-transport/src/constants.ts} (53%) create mode 100644 packages/webrtc-star-transport/src/index.ts create mode 100644 packages/webrtc-star-transport/src/listener.ts create mode 100644 packages/webrtc-star-transport/src/peer/channel.ts create mode 100644 packages/webrtc-star-transport/src/peer/handshake.ts create mode 100644 packages/webrtc-star-transport/src/peer/index.ts create mode 100644 packages/webrtc-star-transport/src/peer/initiator.ts create mode 100644 packages/webrtc-star-transport/src/peer/interface.ts create mode 100644 packages/webrtc-star-transport/src/peer/peer.ts create mode 100644 packages/webrtc-star-transport/src/peer/receiver.ts create mode 100644 packages/webrtc-star-transport/src/socket-to-conn.ts rename packages/{transport/src/utils.js => webrtc-star-transport/src/utils.ts} (54%) create mode 100644 packages/webrtc-star-transport/test/browser.ts create mode 100644 packages/webrtc-star-transport/test/compliance.spec.ts create mode 100644 packages/webrtc-star-transport/test/node.ts create mode 100644 packages/webrtc-star-transport/test/transport/dial.ts create mode 100644 packages/webrtc-star-transport/test/transport/discovery.ts rename packages/{transport/test/transport/filter.js => webrtc-star-transport/test/transport/filter.ts} (88%) create mode 100644 packages/webrtc-star-transport/test/transport/instance.spec.ts rename packages/{transport/test/transport/listen.js => webrtc-star-transport/test/transport/listen.ts} (67%) create mode 100644 packages/webrtc-star-transport/test/transport/multiple-signal-servers.ts create mode 100644 packages/webrtc-star-transport/test/transport/reconnect.node.ts create mode 100644 packages/webrtc-star-transport/test/transport/track.ts rename packages/{transport/test/utils.spec.js => webrtc-star-transport/test/utils.spec.ts} (94%) create mode 100644 packages/webrtc-star-transport/tsconfig.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index de46e326..290ad028 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/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 00000000..13da9c15 --- /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 diff --git a/.github/workflows/js-test-and-release.yml b/.github/workflows/js-test-and-release.yml new file mode 100644 index 00000000..8630dc5c --- /dev/null +++ b/.github/workflows/js-test-and-release.yml @@ -0,0 +1,152 @@ +name: test & maybe release +on: + push: + branches: + - master # with #262 - ${{{ github.default_branch }}} + pull_request: + branches: + - master # with #262 - ${{{ github.default_branch }}} + +jobs: + + 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 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: [16] + fail-fast: true + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node }} + - uses: ipfs/aegir/actions/cache-node-modules@master + - run: npm run --if-present test:node + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: node + + test-chrome: + 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 + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: 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 + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: chrome-webworker + + test-firefox: + 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 + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: 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 + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: 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 + - uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0 + with: + directory: ./.nyc_output + flags: 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 + - 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' # with #262 - 'refs/heads/${{{ github.default_branch }}}' + steps: + - uses: actions/checkout@v2 + 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_TOKEN }} + 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/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 764123db..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,200 +0,0 @@ -name: Test -on: - pull_request: - branches: - - master - -jobs: - - build: - name: Build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: 16 - - uses: actions/cache@v2 - id: cache - env: - CACHE_NAME: cache-node-modules - with: - path: | - ~/.npm - ./node_modules - ./packages/*/node_modules - ./packages/*/dist - key: ${{ runner.os }}-build-${{ env.CACHE_NAME }}-${{ github.event.pull_request.head.sha }} - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: | - npm install - npm run build --if-present - npm run link --if-present - - check: - name: Check - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: lts/* - - uses: actions/cache@v2 - id: cache - env: - CACHE_NAME: cache-node-modules - with: - path: | - ~/.npm - ./node_modules - ./packages/*/node_modules - ./packages/*/dist - key: ${{ runner.os }}-build-${{ env.CACHE_NAME }}-${{ github.event.pull_request.head.sha }} - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: | - npm install - npm run build --if-present - npm run link --if-present - - run: | - npm run lint - npm run dep-check -- -- -- -p - npm run dep-check -- -- -- -- --unused - - test-node: - name: Unit tests ${{ matrix.project }} node ${{ matrix.node }} ${{ matrix.os }} - needs: build - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [windows-latest, ubuntu-latest, macos-latest] - node: [16] - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node }} - - uses: actions/cache@v2 - id: cache - env: - CACHE_NAME: cache-node-modules - with: - path: | - ~/.npm - ./node_modules - ./packages/*/node_modules - ./packages/*/dist - key: ${{ runner.os }}-build-${{ env.CACHE_NAME }}-${{ github.event.pull_request.head.sha }} - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: | - npm install - npm run build --if-present - npm run link --if-present - - run: npm run test:node -- --since ${{ github.event.pull_request.base.sha }} --concurrency 1 - - test-browser: - name: Unit tests ${{ matrix.project }} ${{ matrix.browser }} ${{ matrix.type }} - needs: build - runs-on: ubuntu-latest - strategy: - matrix: - browser: - - chromium - - firefox - type: - - browser - - webworker - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - uses: actions/setup-node@v2 - with: - node-version: lts/* - - uses: actions/cache@v2 - id: cache - env: - CACHE_NAME: cache-node-modules - with: - path: | - ~/.npm - ./node_modules - ./packages/*/node_modules - ./packages/*/dist - key: ${{ runner.os }}-build-${{ env.CACHE_NAME }}-${{ github.event.pull_request.head.sha }} - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: | - npm install - npm run build --if-present - npm run link --if-present - - run: npm run test:browser -- --since ${{ github.event.pull_request.base.sha }} --concurrency 1 -- -- -- --browser ${{ matrix.browser }} - - test-electron-main: - name: Unit tests electron-main - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - uses: actions/setup-node@v2 - with: - node-version: lts/* - - uses: actions/cache@v2 - id: cache - env: - CACHE_NAME: cache-node-modules - with: - path: | - ~/.npm - ./node_modules - ./packages/*/node_modules - ./packages/*/dist - key: ${{ runner.os }}-build-${{ env.CACHE_NAME }}-${{ github.event.pull_request.head.sha }} - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: | - npm install - npm run build --if-present - npm run link --if-present - - uses: GabrielBB/xvfb-action@v1 - with: - run: npm run test:electron-main -- --since ${{ github.event.pull_request.base.sha }} --concurrency 1 -- -- --bail - - test-electron-renderer: - name: Unit tests electron-renderer - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - uses: actions/setup-node@v2 - with: - node-version: lts/* - - uses: actions/cache@v2 - id: cache - env: - CACHE_NAME: cache-node-modules - with: - path: | - ~/.npm - ./node_modules - ./packages/*/node_modules - ./packages/*/dist - key: ${{ runner.os }}-build-${{ env.CACHE_NAME }}-${{ github.event.pull_request.head.sha }} - - name: Install Dependencies - if: steps.cache.outputs.cache-hit != 'true' - run: | - npm install - npm run build --if-present - npm run link --if-present - - uses: GabrielBB/xvfb-action@v1 - with: - run: npm run test:electron-renderer -- --since ${{ github.event.pull_request.base.sha }} --concurrency 1 -- -- --bail diff --git a/.gitignore b/.gitignore index 4db4e026..2405c490 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ lib-cov # Coverage directory used by tools like istanbul coverage +.nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt diff --git a/Dockerfile b/Dockerfile index a88c8933..3c39ca76 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,13 +11,15 @@ RUN mkdir -p /home/node/app/webrtc-star/node_modules && chown -R node:node /home WORKDIR /home/node/app/webrtc-star # Install node modules -COPY packages/signalling-server/package.json ./ +COPY packages/webrtc-star-signalling-server/package.json ./ # Switch to the node user for installation RUN npm install --production # Copy over source files under the node user -COPY ./packages/signalling-server/src ./src -COPY ./packages/signalling-server/README.md ./ +COPY ./packages/webrtc-star-signalling-server/bin ./bin +COPY ./packages/webrtc-star-signalling-server/src ./src +COPY ./packages/webrtc-star-signalling-server/dist ./dist +COPY ./packages/webrtc-star-signalling-server/README.md ./ # Start from a clean node image FROM node as server @@ -36,4 +38,5 @@ EXPOSE 9090 # --port=9090 --host=0.0.0.0 --disableMetrics=false # Server logging can be enabled via the DEBUG environment variable: # DEBUG=signalling-server,signalling-server:error -CMD [ "node", "src/bin.js"] +RUN chmod +x bin/index.js +CMD [ "node", "bin/index.js"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/LICENSE @@ -0,0 +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 diff --git a/LICENSE-MIT b/LICENSE-MIT index 749aa1ec..72dc60d8 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/lerna.json b/lerna.json index 2b320986..aa6d0004 100644 --- a/lerna.json +++ b/lerna.json @@ -1,25 +1,12 @@ { - "version": "independent", + "lerna": "4.0.0", "packages": [ "packages/*" ], + "version": "independent", "command": { - "bootstrap": { - "hoist": true - }, "run": { "stream": true - }, - "publish": { - "message": "chore: publish", - "createRelease": "github", - "conventionalCommits": true, - "verifyAccess": false - }, - "version": { - "allowBranch": ["master", "release/*"], - "forcePublish": true, - "ignore-changes": [".github/**", "docs/**"] } } } diff --git a/package.json b/package.json index 0c7431db..475d8571 100644 --- a/package.json +++ b/package.json @@ -2,37 +2,40 @@ "name": "libp2p-webrtc-star", "version": "1.0.0", "description": "libp2p WebRTC transport that includes a discovery mechanism provided by the signalling-star", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p-webrtc-star#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-webrtc-star.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-webrtc-star/issues" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, "private": true, "scripts": { - "reset": "npm run clean && rimraf ./node_modules ./package-lock.json packages/*/node_modules packages/*/package-lock.json", - "link": "lerna link", - "clean": "lerna run clean", - "test": "lerna run test", - "test:node": "lerna run test:node", - "test:browser": "lerna run test:browser", - "test:webworker": "lerna run test:webworker", - "test:electron-main": "lerna run test:electron-main", - "test:electron-renderer": "lerna run test:electron-renderer", + "reset": "lerna run clean && rimraf ./node_modules ./package-lock.json packages/*/node_modules packages/*/package-lock.json packages/*/dist", + "test": "lerna run --concurrency 1 test -- --", + "test:node": "lerna run --concurrency 1 test:node -- --", + "test:chrome": "lerna run --concurrency 1 test:chrome -- --", + "test:chrome-webworker": "lerna --concurrency 1 run test:chrome-webworker -- --", + "test:firefox": "lerna run --concurrency 1 test:firefox -- --", + "test:firefox-webworker": "lerna run --concurrency 1 test:firefox-webworker -- --", + "test:electron-main": "lerna run --concurrency 1 test:electron-main -- --", + "test:electron-renderer": "lerna run --concurrency 1 test:electron-renderer -- --", "build": "lerna run build", "lint": "lerna run lint", - "release": "lerna run build && lerna publish", - "dep-check": "lerna run dep-check" - }, - "homepage": "https://github.com/ipfs/js-ipfs-interfaces/tree/master#readme", - "bugs": "https://github.com/ipfs/js-ipfs-interfaces/issues", - "license": "(Apache-2.0 OR MIT)", - "repository": { - "type": "git", - "url": "git+https://github.com/ipfs/js-ipfs-interfaces.git" + "dep-check": "lerna run dep-check", + "release": "lerna exec --concurrency 1 -- semantic-release -e semantic-release-monorepo" }, - "workspaces": [ - "packages/*" - ], "dependencies": { "lerna": "^4.0.0", "rimraf": "^3.0.2" }, - "engines": { - "npm": ">=7.0.0" - } + "workspaces": [ + "packages/*" + ] } diff --git a/packages/signalling-server/package.json b/packages/signalling-server/package.json deleted file mode 100644 index 96fbf007..00000000 --- a/packages/signalling-server/package.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "name": "libp2p-webrtc-star-signalling-server", - "version": "0.1.2", - "description": "signalling server to use with the libp2p WebRTC transport", - "main": "src/index.js", - "files": [ - "dist", - "src" - ], - "bin": { - "webrtc-star": "src/bin.js", - "star-sig": "src/bin.js", - "star-signal": "src/bin.js" - }, - "scripts": { - "star-signal": "node src/sig-server/bin.js", - "lint": "aegir lint", - "build": "aegir build --no-bundle", - "test": "npm run test:node", - "test:node": "aegir test -t node --timeout 20000 -- --exit", - "coverage": "aegir coverage", - "coverage-publish": "aegir coverage --provider coveralls", - "dep-check": "aegir dep-check" - }, - "engines": { - "node": ">=14.0.0" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/libp2p/js-libp2p-webrtc-star.git" - }, - "keywords": [ - "IPFS", - "libp2p" - ], - "license": "(Apache-2.0 OR MIT)", - "bugs": { - "url": "https://github.com/libp2p/js-libp2p-webrtc-star/issues" - }, - "homepage": "https://github.com/libp2p/js-libp2p-webrtc-star/tree/master/packages/signalling-server#readme", - "devDependencies": { - "aegir": "^36.0.2", - "socket.io-client-v2": "npm:socket.io-client@^2.3.0", - "socket.io-client-v3": "npm:socket.io-client@^3.1.2" - }, - "dependencies": { - "@hapi/hapi": "^20.0.0", - "@hapi/inert": "^6.0.3", - "debug": "^4.2.0", - "menoetius": "0.0.3", - "minimist": "^1.2.5", - "multiaddr": "^10.0.0", - "prom-client": "^14.0.0", - "socket.io": "^4.1.2", - "socket.io-client": "^4.1.2" - }, - "contributors": [ - "David Dias ", - "Vasco Santos ", - "Jacob Heun ", - "Friedel Ziegelmayer ", - "Yahya ", - "Alex Potsides ", - "Pedro Teixeira ", - "Maciej Krüger ", - "Dmitriy Ryajov ", - "ᴠɪᴄᴛᴏʀ ʙᴊᴇʟᴋʜᴏʟᴍ ", - "Alan Shaw ", - "Smite Chow ", - "Hugo Dias ", - "michaelfakhri ", - "Juan Benet ", - "Jacob Friedman ", - "Richard Littauer ", - "SeungWon ", - "Steverman ", - "Flarp ", - "anders ", - "Diogo Silva ", - "dmitriy ryajov ", - "Andrey Petrov ", - "Baris Gumustas ", - "interfect ", - "kumavis " - ] -} diff --git a/packages/signalling-server/src/config.js b/packages/signalling-server/src/config.js deleted file mode 100644 index ae885bfd..00000000 --- a/packages/signalling-server/src/config.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict' - -const debug = require('debug') -const log = debug('signalling-server') -log.error = debug('signalling-server:error') - -module.exports = { - log: log, - hapi: { - port: process.env.PORT || 13579, - host: '0.0.0.0', - options: { - routes: { - cors: true - } - } - }, - refreshPeerListIntervalMS: 10000 -} diff --git a/packages/signalling-server/src/index.js b/packages/signalling-server/src/index.js deleted file mode 100644 index 7df43be7..00000000 --- a/packages/signalling-server/src/index.js +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint no-unreachable: "warn" */ - -'use strict' - -const Hapi = require('@hapi/hapi') -const Inert = require('@hapi/inert') - -const config = require('./config') -const log = config.log -const menoetius = require('menoetius') -const path = require('path') - -module.exports = { - start: async (options = {}) => { - const port = options.port || config.hapi.port - const host = options.host || config.hapi.host - - const http = new Hapi.Server({ - ...config.hapi.options, - port, - host - }) - - await http.register(Inert) - await http.start() - - log('signaling server has started on: ' + http.info.uri) - - const peers = require('./routes-ws')(http, options.metrics).peers - - http.peers = () => ({ - ...peers() - }) - - http.route({ - method: 'GET', - path: '/', - handler: (request, reply) => reply.file(path.join(__dirname, 'index.html'), { - confine: false - }) - }) - - if (options.metrics) { - log('enabling metrics') - await menoetius.instrument(http) - } - - return http - } -} diff --git a/packages/signalling-server/src/routes-ws/index.js b/packages/signalling-server/src/routes-ws/index.js deleted file mode 100644 index d7dae1fb..00000000 --- a/packages/signalling-server/src/routes-ws/index.js +++ /dev/null @@ -1,136 +0,0 @@ -'use strict' - -const config = require('../config') -const log = config.log -const socketIO = require('socket.io') -const client = require('prom-client') - -const fake = { - gauge: { - set: () => {} - }, - counter: { - inc: () => {} - } -} - -module.exports = (http, hasMetrics) => { - const io = socketIO({ - allowEIO3: true // allow socket.io v2 clients to connect - }) - io.attach(http.listener, { - path: '/socket.io' // v2/v3/v4 clients can use this path - }) - io.attach(http.listener, { - path: '/socket.io-next' // v3/v4 clients might be using this path - }) - io.on('connection', handle) - - http.events.on('stop', () => io.close()) - - const peers = {} - - const peersMetric = hasMetrics ? new client.Gauge({ name: 'webrtc_star_peers', help: 'peers online now' }) : fake.gauge - const dialsSuccessTotal = hasMetrics ? new client.Counter({ name: 'webrtc_star_dials_total_success', help: 'successfully completed dials since server started' }) : fake.counter - const dialsFailureTotal = hasMetrics ? new client.Counter({ name: 'webrtc_star_dials_total_failure', help: 'failed dials since server started' }) : fake.counter - const dialsTotal = hasMetrics ? new client.Counter({ name: 'webrtc_star_dials_total', help: 'all dials since server started' }) : fake.counter - const joinsSuccessTotal = hasMetrics ? new client.Counter({ name: 'webrtc_star_joins_total_success', help: 'successfully completed joins since server started' }) : fake.counter - const joinsFailureTotal = hasMetrics ? new client.Counter({ name: 'webrtc_star_joins_total_failure', help: 'failed joins since server started' }) : fake.counter - const joinsTotal = hasMetrics ? new client.Counter({ name: 'webrtc_star_joins_total', help: 'all joins since server started' }) : fake.counter - - const refreshMetrics = () => peersMetric.set(Object.keys(peers).length) - - this.io = () => { - return io - } - - this.peers = () => { - return peers - } - - function safeEmit (addr, event, arg) { - const peer = peers[addr] - if (!peer) { - log('trying to emit %s but peer is gone', event) - return - } - - peer.emit(event, arg) - } - - function handle (socket) { - socket.on('ss-join', join.bind(socket)) - socket.on('ss-leave', leave.bind(socket)) - socket.on('disconnect', disconnect.bind(socket)) // socket.io own event - socket.on('ss-handshake', forwardHandshake) - } - - // join this signaling server network - function join (multiaddr) { - joinsTotal.inc() - if (!multiaddr) { return joinsFailureTotal.inc() } - const socket = peers[multiaddr] = this // socket - let refreshInterval = setInterval(sendPeers, config.refreshPeerListIntervalMS) - - socket.once('ss-leave', stopSendingPeers) - socket.once('disconnect', stopSendingPeers) - - sendPeers() - - function sendPeers () { - Object.keys(peers).forEach((mh) => { - if (mh === multiaddr) { - return - } - safeEmit(mh, 'ws-peer', multiaddr) - }) - } - - function stopSendingPeers () { - if (refreshInterval) { - clearInterval(refreshInterval) - refreshInterval = null - } - } - - joinsSuccessTotal.inc() - refreshMetrics() - } - - function leave (multiaddr) { - if (!multiaddr) { return } - if (peers[multiaddr]) { - delete peers[multiaddr] - refreshMetrics() - } - } - - function disconnect () { - Object.keys(peers).forEach((mh) => { - if (peers[mh].id === this.id) { - delete peers[mh] - } - refreshMetrics() - }) - } - - // forward an WebRTC offer to another peer - function forwardHandshake (offer) { - dialsTotal.inc() - if (offer == null || typeof offer !== 'object' || !offer.srcMultiaddr || !offer.dstMultiaddr) { return dialsFailureTotal.inc() } - if (offer.answer) { - dialsSuccessTotal.inc() - safeEmit(offer.srcMultiaddr, 'ws-handshake', offer) - } else { - if (peers[offer.dstMultiaddr]) { - safeEmit(offer.dstMultiaddr, 'ws-handshake', offer) - } else { - dialsFailureTotal.inc() - offer.err = 'peer is not available' - safeEmit(offer.srcMultiaddr, 'ws-handshake', offer) - } - } - } - - return this -} diff --git a/packages/transport/.aegir.js b/packages/transport/.aegir.js deleted file mode 100644 index 6d77bb7b..00000000 --- a/packages/transport/.aegir.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict' - -const sigServer = require('libp2p-webrtc-star-signalling-server') -let firstRun = true -let sigServers = [] - -async function boot () { - const options1 = { - port: 15555, - host: '127.0.0.1', - metrics: firstRun - } - - const options2 = { - port: 15556, - host: '127.0.0.1', - metrics: false - } - - const options3 = { - port: 15557, - host: '127.0.0.1', - metrics: false - } - - if (firstRun) { firstRun = false } - - sigServers.push(await sigServer.start(options1)) - sigServers.push(await sigServer.start(options2)) - sigServers.push(await sigServer.start(options3)) - - console.log('signalling on:') - sigServers.forEach((sig) => console.log(sig.info.uri)) -} - -async function stop () { - await Promise.all(sigServers.map(s => s.stop())) -} - -/** @type {import('aegir').PartialOptions} */ -module.exports = { - test: { - before: boot, - after: stop - } -} diff --git a/packages/transport/package.json b/packages/transport/package.json deleted file mode 100644 index aa901cfe..00000000 --- a/packages/transport/package.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "name": "libp2p-webrtc-star", - "version": "0.25.0", - "description": "libp2p WebRTC transport that includes a discovery mechanism provided by the signalling-star", - "main": "src/index.js", - "files": [ - "dist", - "src" - ], - "scripts": { - "lint": "aegir lint", - "build": "aegir build", - "test": "aegir test -t node -t browser --timeout 20000 -- --exit", - "test:node": "aegir test -t node --timeout 20000 -- --exit", - "test:browser": "aegir test -t browser --timeout 20000 -- --exit", - "test:dns": "WEBRTC_STAR_REMOTE_SIGNAL_DNS=1 aegir test -t browser --timeout 20000 -- --exit", - "test:ip": "WEBRTC_STAR_REMOTE_SIGNAL_IP=1 aegir test -t browser --timeout 20000 -- --exit", - "coverage": "aegir coverage", - "coverage-publish": "aegir coverage --provider coveralls", - "dep-check": "aegir dep-check -i @mapbox/node-pre-gyp -i util" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/libp2p/js-libp2p-webrtc-star.git" - }, - "keywords": [ - "IPFS", - "libp2p" - ], - "license": "(Apache-2.0 OR MIT)", - "bugs": { - "url": "https://github.com/libp2p/js-libp2p-webrtc-star/issues" - }, - "homepage": "https://github.com/libp2p/js-libp2p-webrtc-star/tree/master/packages/transport#readme", - "devDependencies": { - "@mapbox/node-pre-gyp": "^1.0.5", - "aegir": "^36.0.2", - "electron-webrtc": "~0.3.0", - "it-all": "^1.0.5", - "libp2p-interfaces-compliance-tests": "^2.0.1", - "libp2p-webrtc-star-signalling-server": "^0.1.2", - "p-wait-for": "^3.1.0", - "sinon": "^12.0.1", - "uint8arrays": "^3.0.0", - "util": "^0.12.4", - "wrtc": "^0.4.6" - }, - "dependencies": { - "abortable-iterator": "^3.0.0", - "class-is": "^1.1.0", - "debug": "^4.2.0", - "err-code": "^3.0.1", - "ipfs-utils": "^9.0.1", - "it-pipe": "^1.1.0", - "libp2p-utils": "^0.4.0", - "libp2p-webrtc-peer": "^10.0.1", - "mafmt": "^10.0.0", - "multiaddr": "^10.0.0", - "p-defer": "^3.0.0", - "peer-id": "^0.16.0", - "socket.io-client": "^4.1.2", - "stream-to-it": "^0.2.2" - }, - "contributors": [ - "David Dias ", - "Vasco Santos ", - "Jacob Heun ", - "Friedel Ziegelmayer ", - "Yahya ", - "Alex Potsides ", - "Pedro Teixeira ", - "Maciej Krüger ", - "Dmitriy Ryajov ", - "ᴠɪᴄᴛᴏʀ ʙᴊᴇʟᴋʜᴏʟᴍ ", - "Alan Shaw ", - "Smite Chow ", - "Hugo Dias ", - "michaelfakhri ", - "Juan Benet ", - "Jacob Friedman ", - "Richard Littauer ", - "SeungWon ", - "Steverman ", - "Flarp ", - "anders ", - "Diogo Silva ", - "dmitriy ryajov ", - "Andrey Petrov ", - "Baris Gumustas ", - "interfect ", - "kumavis " - ] -} diff --git a/packages/transport/src/index.js b/packages/transport/src/index.js deleted file mode 100644 index 53bc8d8a..00000000 --- a/packages/transport/src/index.js +++ /dev/null @@ -1,250 +0,0 @@ -'use strict' - -const debug = require('debug') -const log = debug('libp2p:webrtc-star') -log.error = debug('libp2p:webrtc-star:error') - -const { EventEmitter } = require('events') -const errcode = require('err-code') -const withIs = require('class-is') - -const { AbortError } = require('abortable-iterator') -const SimplePeer = require('libp2p-webrtc-peer') -const { supportsWebRTCDataChannels: webrtcSupport } = require('ipfs-utils/src/supports') - -const { Multiaddr } = require('multiaddr') -const mafmt = require('mafmt') -const PeerId = require('peer-id') - -const { CODE_CIRCUIT } = require('./constants') -const createListener = require('./listener') -const toConnection = require('./socket-to-conn') -const { cleanMultiaddr, cleanUrlSIO } = require('./utils') - -function noop () { } - -/** - * @class WebRTCStar - */ -class WebRTCStar { - /** - * @class - * @param {object} options - * @param {Upgrader} options.upgrader - */ - constructor (options = {}) { - if (!options.upgrader) { - throw new Error('An upgrader must be provided. See https://github.com/libp2p/interface-transport#upgrader.') - } - - this._upgrader = options.upgrader - - this.sioOptions = { - transports: ['websocket'], - 'force new connection': true - } - - if (options.wrtc) { - this.wrtc = options.wrtc - } - - // Keep Signalling references - this.sigReferences = new Map() - - // Discovery - this.discovery = new EventEmitter() - this.discovery.tag = 'webRTCStar' - this.discovery._isStarted = false - this.discovery.start = () => { - this.discovery._isStarted = true - } - this.discovery.stop = () => { - this.discovery._isStarted = false - } - this._peerDiscovered = this._peerDiscovered.bind(this) - } - - /** - * @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 = {}) { - const rawConn = await this._connect(ma, options) - const maConn = toConnection(rawConn, { 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 SimplePeer Webrtc channel - */ - _connect (ma, options = {}) { - if (options.signal && options.signal.aborted) { - throw new AbortError() - } - - const spOptions = { - initiator: true, - trickle: false, - ...options.spOptions || {} - } - - // Use custom WebRTC implementation - if (this.wrtc) { spOptions.wrtc = this.wrtc } - - const cOpts = ma.toOptions() - const intentId = (~~(Math.random() * 1e9)).toString(36) + Date.now() - - return new Promise((resolve, reject) => { - const sio = this.sigReferences.get(cleanUrlSIO(ma)) - - if (!sio || !sio.listener) { - return reject(errcode(new Error('unknown signal server to use'), 'ERR_UNKNOWN_SIGNAL_SERVER')) - } - - const sioClient = sio.listener.io - - const start = Date.now() - let connected - - log('dialing %s:%s', cOpts.host, cOpts.port) - const channel = new SimplePeer(spOptions) - - const onError = (err) => { - if (!connected) { - const msg = `connection error ${cOpts.host}:${cOpts.port}: ${err.message}` - log.error(msg) - done(err) - } - } - - const onTimeout = () => { - log('connnection timeout %s:%s', cOpts.host, cOpts.port) - const err = errcode(new Error(`connection timeout after ${Date.now() - start}ms`), 'ERR_CONNECT_TIMEOUT') - // Note: this will result in onError() being called - channel.emit('error', err) - } - - const onConnect = () => { - connected = true - - log('connection opened %s:%s', cOpts.host, cOpts.port) - done(null) - } - - const onAbort = () => { - log.error('connection aborted %s:%s', cOpts.host, cOpts.port) - channel.destroy() - done(new AbortError()) - } - - const done = (err) => { - channel.removeListener('timeout', onTimeout) - channel.removeListener('connect', onConnect) - options.signal && options.signal.removeEventListener('abort', onAbort) - - err ? reject(err) : resolve(channel) - } - - channel.on('error', onError) - channel.once('timeout', onTimeout) - channel.once('connect', onConnect) - channel.on('close', () => { - channel.removeListener('error', onError) - }) - options.signal && options.signal.addEventListener('abort', onAbort) - - channel.on('signal', (signal) => { - sioClient.emit('ss-handshake', { - intentId: intentId, - srcMultiaddr: sio.signallingAddr.toString(), - dstMultiaddr: ma.toString(), - signal: signal - }) - }) - - // NOTE: aegir segfaults if we do .once on the socket.io event emitter and we - // are clueless as to why. - sioClient.on('ws-handshake', (offer) => { - if (offer.intentId === intentId && offer.err) { - channel.destroy() - reject(errcode(offer.err instanceof Error ? offer.err : new Error(offer.err), 'ERR_SIGNALLING_FAILED')) - } - - if (offer.intentId !== intentId || !offer.answer || channel.destroyed) { - return - } - - channel.signal(offer.signal) - }) - }) - } - - /** - * Creates a WebrtcStar listener. The provided `handler` function will be called - * anytime a new incoming Connection has been successfully upgraded via - * `upgrader.upgradeInbound`. - * - * @param {object} [options] - simple-peer options for listener - * @param {function (Connection)} handler - * @returns {Listener} A WebrtcStar listener - */ - createListener (options = {}, handler) { - if (!webrtcSupport && !this.wrtc) { - throw errcode(new Error('no WebRTC support'), 'ERR_NO_WEBRTC_SUPPORT') - } - - if (typeof options === 'function') { - handler = options - options = {} - } - - handler = handler || noop - - return createListener({ handler, upgrader: this._upgrader }, this, options) - } - - /** - * Takes a list of `Multiaddr`s and returns only valid TCP addresses - * - * @param {Multiaddr[]} multiaddrs - * @returns {Multiaddr[]} Valid TCP multiaddrs - */ - filter (multiaddrs) { - multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs] - - return multiaddrs.filter((ma) => { - if (ma.protoCodes().includes(CODE_CIRCUIT)) { - return false - } - - return mafmt.WebRTCStar.matches(ma) - }) - } - - _peerDiscovered (maStr) { - if (!this.discovery._isStarted) return - - log('Peer Discovered:', maStr) - maStr = cleanMultiaddr(maStr) - - const ma = new Multiaddr(maStr) - const peerId = PeerId.createFromB58String(ma.getPeerId()) - - this.discovery.emit('peer', { - id: peerId, - multiaddrs: [ma] - }) - } -} - -module.exports = withIs(WebRTCStar, { className: 'WebRTCStar', symbolName: '@libp2p/js-libp2p-webrtc-star/webrtcstar' }) diff --git a/packages/transport/src/listener.js b/packages/transport/src/listener.js deleted file mode 100644 index a43d9c09..00000000 --- a/packages/transport/src/listener.js +++ /dev/null @@ -1,199 +0,0 @@ -'use strict' - -const EventEmitter = require('events') -const debug = require('debug') -const log = debug('libp2p:webrtc-star:listener') -log.error = debug('libp2p:webrtc-star:listener:error') - -const errCode = require('err-code') -const io = require('socket.io-client') -const SimplePeer = require('libp2p-webrtc-peer') -const pDefer = require('p-defer') - -const toConnection = require('./socket-to-conn') -const { cleanUrlSIO } = require('./utils') -const { CODE_P2P } = require('./constants') - -const sioOptions = { - transports: ['websocket'], - 'force new connection': true, - path: '/socket.io-next/' // This should be removed when socket.io@2 support is removed -} - -module.exports = ({ handler, upgrader }, WebRTCStar, options = {}) => { - const listener = new EventEmitter() - let listeningAddr - let signallingUrl - - listener.__connections = [] - listener.__spChannels = new Map() - listener.__pendingIntents = new Map() - listener.listen = (ma) => { - // Should only be used if not already listening - if (listeningAddr) { - throw errCode(new Error('listener already in use'), 'ERR_ALREADY_LISTENING') - } - - const defer = pDefer() - - // Should be kept unmodified - listeningAddr = ma - - let signallingAddr - if (!ma.protoCodes().includes(CODE_P2P) && upgrader.localPeer) { - signallingAddr = ma.encapsulate(`/p2p/${upgrader.localPeer.toB58String()}`) - } else { - signallingAddr = ma - } - - listener.on('error', () => defer.reject()) - - signallingUrl = cleanUrlSIO(ma) - - log('Dialing to Signalling Server on: ' + signallingUrl) - listener.io = io.connect(signallingUrl, sioOptions) - - const incomingDial = (offer) => { - if (offer.answer || offer.err || !offer.intentId) { - return - } - - const intentId = offer.intentId - let pendings = listener.__pendingIntents.get(intentId) - if (!pendings) { - pendings = [] - listener.__pendingIntents.set(intentId, pendings) - } - - let channel = listener.__spChannels.get(intentId) - if (channel) { - channel.signal(offer.signal) - return - } else if (offer.signal.type !== 'offer') { - pendings.push(offer) - return - } - - const spOptions = { - trickle: false, - ...options - } - - // Use custom WebRTC implementation - if (WebRTCStar.wrtc) { spOptions.wrtc = WebRTCStar.wrtc } - - channel = new SimplePeer(spOptions) - - const onError = (err) => { - log.error('incoming connection errored', err) - } - - channel.on('error', onError) - channel.once('close', (...args) => { - channel.removeListener('error', onError) - }) - - channel.on('signal', (signal) => { - offer.signal = signal - offer.answer = true - listener.io.emit('ss-handshake', offer) - }) - - channel.signal(offer.signal) - for (const pendingOffer of pendings) { - channel.signal(pendingOffer.signal) - } - listener.__pendingIntents.set(intentId, []) - - channel.once('connect', async () => { - const maConn = toConnection(channel) - log('new inbound connection %s', maConn.remoteAddr) - - let conn - try { - conn = await upgrader.upgradeInbound(maConn) - } catch (err) { - log.error('inbound connection failed to upgrade', err) - return maConn.close() - } - - if (!conn.remoteAddr) { - try { - conn.remoteAddr = ma.decapsulateCode(CODE_P2P).encapsulate(`/p2p/${conn.remotePeer.toB58String()}`) - } catch (err) { - log.error('could not determine remote address', err) - } - } - - log('inbound connection %s upgraded', maConn.remoteAddr) - - trackConn(listener, maConn, intentId) - - listener.emit('connection', conn) - handler(conn) - }) - listener.__spChannels.set(intentId, channel) - } - - listener.io.once('connect_error', (err) => defer.reject(err)) - listener.io.once('error', (err) => { - listener.emit('error', err) - listener.emit('close') - }) - - listener.io.on('ws-handshake', incomingDial) - listener.io.on('ws-peer', WebRTCStar._peerDiscovered) - - listener.io.on('connect', () => { - listener.io.emit('ss-join', signallingAddr.toString()) - }) - - listener.io.once('connect', () => { - listener.emit('listening') - defer.resolve() - }) - - // Store listen and signal reference addresses - WebRTCStar.sigReferences.set(signallingUrl, { - listener, - signallingAddr - }) - - return defer.promise - } - - listener.close = async () => { - // Close listener - const ref = WebRTCStar.sigReferences.get(signallingUrl) - if (ref && ref.listener.io) { - ref.listener.io.emit('ss-leave') - ref.listener.io.close() - } - - await Promise.all(listener.__connections.map(maConn => maConn.close())) - listener.emit('close') - listener.removeAllListeners() - - // Reset state - listeningAddr = undefined - WebRTCStar.sigReferences.delete(signallingUrl) - } - - listener.getAddrs = () => { - return [listeningAddr] - } - - return listener -} - -function trackConn (listener, maConn, intentId) { - listener.__connections.push(maConn) - - const untrackConn = () => { - listener.__connections = listener.__connections.filter(c => c !== maConn) - listener.__spChannels.delete(intentId) - listener.__pendingIntents.delete(intentId) - } - - maConn.conn.once('close', untrackConn) -} diff --git a/packages/transport/src/socket-to-conn.js b/packages/transport/src/socket-to-conn.js deleted file mode 100644 index 0ef48800..00000000 --- a/packages/transport/src/socket-to-conn.js +++ /dev/null @@ -1,110 +0,0 @@ -'use strict' - -const abortable = require('abortable-iterator') -const toIterable = require('stream-to-it') -const { CLOSE_TIMEOUT } = require('./constants') -const toMultiaddr = require('libp2p-utils/src/ip-port-to-multiaddr') - -const debug = require('debug') -const log = debug('libp2p:webrtc-star:socket') -log.error = debug('libp2p:webrtc-star:socket:error') - -const toWebrtcMultiaddr = (address, port) => { - if (!address || !port) return undefined - - try { - return toMultiaddr(address, port) - } catch (err) { - log.error(err) - // Account for mdns hostnames, just make it a local ip for now - return toMultiaddr('0.0.0.0', port) - } -} - -// Convert a socket into a MultiaddrConnection -// https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/transport#multiaddrconnection -module.exports = (socket, options = {}) => { - const { sink, source } = toIterable.duplex(socket) - - // If the remote address was passed, use it - it may have the peer ID encapsulated - const remoteAddr = options.remoteAddr || toWebrtcMultiaddr(socket.remoteAddress, socket.remotePort) - const localAddr = toWebrtcMultiaddr(socket.localAddress, socket.localPort) - - const maConn = { - async sink (source) { - if (options.signal) { - source = abortable(source, options.signal) - } - - try { - await sink((async function * () { - for await (const chunk of source) { - // Convert BufferList to Buffer - yield chunk instanceof Uint8Array ? chunk : chunk.slice() - } - })()) - } catch (err) { - // If aborted we can safely ignore - if (err.type !== 'aborted') { - // If the source errored the socket will already have been destroyed by - // toIterable.duplex(). If the socket errored it will already be - // destroyed. There's nothing to do here except log the error & return. - log.error(err) - } - } - }, - - source: options.signal ? abortable(source, options.signal) : source, - - conn: socket, - - localAddr, - remoteAddr, - - timeline: { open: Date.now() }, - - close () { - if (socket.destroyed) return - - return new Promise((resolve, reject) => { - const start = Date.now() - - // Attempt to end the socket. If it takes longer to close than the - // timeout, destroy it manually. - const timeout = setTimeout(() => { - if (maConn.remoteAddr) { - const { host, port } = maConn.remoteAddr.toOptions() - log('timeout closing socket to %s:%s after %dms, destroying it manually', - host, port, Date.now() - start) - } - - if (!socket.destroyed) { - socket.destroy() - } - }, CLOSE_TIMEOUT) - - socket.once('close', () => { - resolve() - }) - - socket.end(err => { - clearTimeout(timeout) - - maConn.timeline.close = Date.now() - if (err) return reject(err) - }) - }) - } - } - - 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/packages/transport/test/browser.js b/packages/transport/test/browser.js deleted file mode 100644 index 2de3760f..00000000 --- a/packages/transport/test/browser.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const WStar = require('..') -const PeerId = require('peer-id') - -describe('browser RTC', () => { - const create = async () => { - const localPeer = await PeerId.create() - return new WStar({ - upgrader: { - upgradeInbound: maConn => maConn, - upgradeOutbound: maConn => maConn, - localPeer - } - }) - } - - require('./transport/dial.js')(create) - require('./transport/listen.js')(create) - require('./transport/discovery.js')(create) - require('./transport/filter.js')(create) -}) diff --git a/packages/transport/test/compliance.spec.js b/packages/transport/test/compliance.spec.js deleted file mode 100644 index 1fb44ade..00000000 --- a/packages/transport/test/compliance.spec.js +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const wrtc = require('wrtc') - -const sinon = require('sinon') -const testsTransport = require('libp2p-interfaces-compliance-tests/src/transport') -const testsDiscovery = require('libp2p-interfaces-compliance-tests/src/peer-discovery') -const { Multiaddr } = require('multiaddr') - -const WStar = require('../src') - -describe('interface-transport compliance', function () { - testsTransport({ - setup ({ upgrader }) { - const ws = new WStar({ upgrader, wrtc: wrtc }) - - const base = (id) => { - return `/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/p2p/${id}` - } - - const addrs = [ - new Multiaddr(base('QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2a')), - new Multiaddr(base('QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2b')), - new Multiaddr(base('QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2c')) - ] - - // Used by the dial tests to simulate a delayed connect - const connector = { - delay () {}, - restore () { - sinon.restore() - } - } - - return { transport: ws, addrs, connector } - } - }) -}) - -describe('interface-discovery compliance', () => { - let intervalId - - testsDiscovery({ - setup () { - const mockUpgrader = { - upgradeInbound: maConn => maConn, - upgradeOutbound: maConn => maConn - } - const ws = new WStar({ upgrader: mockUpgrader, wrtc: wrtc }) - const maStr = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2d' - - intervalId = setInterval(() => ws._peerDiscovered(maStr), 1000) - - return ws.discovery - }, - teardown () { - clearInterval(intervalId) - } - }) -}) diff --git a/packages/transport/test/node.js b/packages/transport/test/node.js deleted file mode 100644 index 6932e0fc..00000000 --- a/packages/transport/test/node.js +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const wrtc = require('wrtc') -const electronWebRTC = require('electron-webrtc') -const PeerId = require('peer-id') -const WStar = require('..') - -const mockUpgrader = { - upgradeInbound: maConn => maConn, - upgradeOutbound: maConn => maConn -} - -describe('transport: with wrtc', () => { - const create = async () => { - const localPeer = await PeerId.create() - return new WStar({ - upgrader: { - upgradeInbound: maConn => maConn, - upgradeOutbound: maConn => maConn, - localPeer - }, - wrtc - }) - } - - require('./transport/dial.js')(create) - require('./transport/listen.js')(create) - require('./transport/multiple-signal-servers.js')(create) - require('./transport/track.js')(create) - require('./transport/discovery.js')(create) - require('./transport/filter.js')(create) - require('./transport/reconnect.node.js')(create) -}) - -// TODO: Electron-webrtc is currently unreliable on linux -describe.skip('transport: with electron-webrtc', () => { - const create = () => { - return new WStar({ upgrader: mockUpgrader, wrtc: electronWebRTC() }) - } - - require('./transport/dial.js')(create) - require('./transport/listen.js')(create) - require('./transport/multiple-signal-servers.js')(create) - require('./transport/track.js')(create) - require('./transport/discovery.js')(create) - require('./transport/filter.js')(create) - // TODO ensure that nodes from wrtc close properly (race issue in travis) - // require('./transport/reconnect.node.js')(create) -}) diff --git a/packages/transport/test/transport/dial.js b/packages/transport/test/transport/dial.js deleted file mode 100644 index 0bcf79de..00000000 --- a/packages/transport/test/transport/dial.js +++ /dev/null @@ -1,157 +0,0 @@ -/* eslint-env mocha */ -/* eslint-disable no-console */ - -'use strict' - -const { expect } = require('aegir/utils/chai') -const { Multiaddr } = require('multiaddr') -const pipe = require('it-pipe') -const all = require('it-all') -const { fromString: uint8ArrayFromString } = require('uint8arrays/from-string') -const SimplePeer = require('libp2p-webrtc-peer') -const sinon = require('sinon') - -function fire (socket, event) { - const args = [].slice.call(arguments, 2) - const callbacks = socket._callbacks[`$${event}`] - - for (const callback of callbacks) { - callback.apply(socket, args) - } -} - -module.exports = (create) => { - describe('dial', () => { - let ws1 - let ws2 - let ma1 - let ma2 - let listener1 - let listener2 - - const maHSDNS = new Multiaddr('/dns/star-signal.cloud.ipfs.team/wss/p2p-webrtc-star') - const maHSIP = new Multiaddr('/ip4/188.166.203.82/tcp/20000/wss/p2p-webrtc-star') - const maLS = new Multiaddr('/ip4/127.0.0.1/tcp/15555/wss/p2p-webrtc-star') - - if (process.env.WEBRTC_STAR_REMOTE_SIGNAL_DNS) { - // test with deployed signalling server using DNS - console.log('Using DNS:', maHSDNS) - ma1 = maHSDNS - ma2 = maHSDNS - } else if (process.env.WEBRTC_STAR_REMOTE_SIGNAL_IP) { - // test with deployed signalling server using IP - console.log('Using IP:', maHSIP) - ma1 = maHSIP - ma2 = maHSIP - } else { - ma1 = maLS - ma2 = maLS - } - - beforeEach(async () => { - // first - ws1 = await create() - listener1 = ws1.createListener((conn) => { - expect(conn.remoteAddr).to.exist() - pipe(conn, conn) - }) - - // second - ws2 = await create() - listener2 = ws2.createListener((conn) => pipe(conn, conn)) - - await Promise.all([listener1.listen(ma1), listener2.listen(ma2)]) - }) - - afterEach(async () => { - await Promise.all([listener1, listener2].map(l => l.close())) - }) - - it('dial on IPv4, check promise', async function () { - // Use one of the signal addresses - const [sigRefs] = ws2.sigReferences.values() - - const conn = await ws1.dial(sigRefs.signallingAddr) - const data = uint8ArrayFromString('some data') - const values = await pipe( - [data], - conn, - all - ) - - expect(values).to.eql([data]) - }) - - it('dial offline / non-exist()ent node on IPv4, check promise rejected', function () { - const maOffline = new Multiaddr('/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2f') - - return expect(ws1.dial(maOffline)).to.eventually.be.rejected().and.have.property('code', 'ERR_SIGNALLING_FAILED') - }) - - it('dial unknown signal server, check promise rejected', function () { - const maOffline = new Multiaddr('/ip4/127.0.0.1/tcp/15559/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2f') - - return expect(ws1.dial(maOffline)).to.eventually.be.rejected().and.have.property('code', 'ERR_UNKNOWN_SIGNAL_SERVER') - }) - - it.skip('dial on IPv6', (done) => { - // TODO IPv6 not supported yet - }) - - it('receive ws-handshake event withou intentId, check channel not created', () => { - fire(listener2.io, 'ws-handshake', { - intentId: null, - srcMultiaddr: ma1.toString(), - dstMultiaddr: ma2.toString(), - signal: {} - }) - expect(listener2.__spChannels.size).to.equal(0) - }) - - it('receive ws-handshake event but already exists channel, check channel.signal called', () => { - const channel = { signal: sinon.fake() } - listener2.__spChannels.set('itent-id', channel) - fire(listener2.io, 'ws-handshake', { - intentId: 'itent-id', - srcMultiaddr: ma1.toString(), - dstMultiaddr: ma2.toString(), - signal: {} - }) - expect(channel.signal.callCount).to.equal(1) - }) - - it('receive ws-handshake event but signal type is not offer, check message saved to peedingIntents', () => { - const message = { - intentId: 'itent-id', - srcMultiaddr: ma1.toString(), - dstMultiaddr: ma2.toString(), - signal: {} - } - fire(listener2.io, 'ws-handshake', message) - expect(listener2.__pendingIntents.get('itent-id')).to.deep.equal([message]) - }) - - it('receive ws-handshake event, the signal type is offer and exists peeding intents, check peeding intents consumed', () => { - const message = { - intentId: 'itent-id', - srcMultiaddr: ma1.toString(), - dstMultiaddr: ma2.toString(), - signal: {} - } - listener2.__pendingIntents.set('itent-id', [message]) - const fake = sinon.fake() - const stub = sinon.stub(SimplePeer.prototype, 'signal').callsFake(fake) - fire(listener2.io, 'ws-handshake', { - intentId: 'itent-id', - srcMultiaddr: ma1.toString(), - dstMultiaddr: ma2.toString(), - signal: { type: 'offer' } - }) - expect(listener2.__spChannels.size).to.equal(1) - expect(listener2.__pendingIntents.get('itent-id').length).to.equal(0) - // create the channel and consume the peeding intent - expect(fake.callCount).to.equal(2) - stub.restore() - }) - }) -} diff --git a/packages/transport/test/transport/discovery.js b/packages/transport/test/transport/discovery.js deleted file mode 100644 index 2d7e1fc5..00000000 --- a/packages/transport/test/transport/discovery.js +++ /dev/null @@ -1,91 +0,0 @@ -/* eslint-env mocha */ - -'use strict' - -const { expect } = require('aegir/utils/chai') -const { Multiaddr } = require('multiaddr') - -module.exports = (create) => { - describe('peer discovery', () => { - let ws1 - let ws2 - let ws3 - let ws4 - let ws1Listener - const signallerAddr = new Multiaddr('/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star') - - it('listen on the first', async () => { - ws1 = await create() - ws1Listener = ws1.createListener(() => { }) - ws1.discovery.start() - - await ws1Listener.listen(signallerAddr) - }) - - it('listen on the second, discover the first', async () => { - ws2 = await create() - const listener = ws2.createListener(() => { }) - ws2.discovery.start() - - const p = new Promise((resolve) => { - ws1.discovery.once('peer', ({ multiaddrs }) => { - // Check first of the signal addresses - const [sigRefs] = ws2.sigReferences.values() - - expect(multiaddrs.map(m => m.toString())).to.include(sigRefs.signallingAddr.toString()) - resolve() - }) - }) - - listener.listen(signallerAddr) - await p - }) - - // this test is mostly validating the non-discovery test mechanism works - it('listen on the third, verify ws-peer is discovered', async () => { - let discoveredPeer = false - - ws1.discovery.once('peer', () => { - discoveredPeer = true - }) - - // resolve on peer discovered - const p = new Promise((resolve) => { - ws1Listener.io.once('ws-peer', () => { - expect(discoveredPeer).to.equal(true) - resolve() - }) - }) - - ws3 = await create() - const listener = ws3.createListener(() => { }) - ws3.discovery.start() - - await listener.listen(signallerAddr) - await p - }) - - it('listen on the fourth, ws-peer is not discovered', async () => { - let discoveredPeer = false - - ws1.discovery.once('peer', () => { - discoveredPeer = true - }) - // resolve on peer discovered - const p = new Promise((resolve) => { - ws1Listener.io.once('ws-peer', () => { - expect(discoveredPeer).to.equal(false) - resolve() - }) - }) - - ws1.discovery.stop() - ws4 = await create() - const listener = ws4.createListener(() => { }) - ws4.discovery.start() - - await listener.listen(signallerAddr) - await p - }) - }) -} diff --git a/packages/transport/test/transport/instance.spec.js b/packages/transport/test/transport/instance.spec.js deleted file mode 100644 index 33cba9af..00000000 --- a/packages/transport/test/transport/instance.spec.js +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') - -const WebRTCStar = require('../../src') - -const mockUpgrader = { - upgradeInbound: maConn => maConn, - upgradeOutbound: maConn => maConn -} - -describe('instantiate the transport', () => { - it('create', () => { - const wstar = new WebRTCStar({ upgrader: mockUpgrader }) - expect(wstar).to.exist() - }) - - it('create without new', () => { - expect(() => WebRTCStar()).to.throw() - }) -}) diff --git a/packages/transport/test/transport/multiple-signal-servers.js b/packages/transport/test/transport/multiple-signal-servers.js deleted file mode 100644 index 24c22005..00000000 --- a/packages/transport/test/transport/multiple-signal-servers.js +++ /dev/null @@ -1,121 +0,0 @@ -/* eslint-env mocha */ - -'use strict' - -const { expect } = require('aegir/utils/chai') -const { Multiaddr } = require('multiaddr') -const pipe = require('it-pipe') - -const ma1 = new Multiaddr('/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star') -const ma2 = new Multiaddr('/ip4/127.0.0.1/tcp/15556/ws/p2p-webrtc-star') - -module.exports = (create) => { - describe('multiple signal servers', () => { - let ws1, ws2 - - beforeEach(async () => { - ws1 = await create() - ws2 = await create() - }) - - it('can listen on multiple signal servers with the same transport', async () => { - const listener1 = ws1.createListener(() => { }) - await listener1.listen(ma1) - - const listener2 = ws1.createListener(() => { }) - await listener2.listen(ma2) - - expect(Array.from(ws1.sigReferences.keys())).to.have.lengthOf(2) - - await Promise.all([ - listener1.close(), - listener2.close() - ]) - - expect(Array.from(ws1.sigReferences.keys())).to.have.lengthOf(0) - }) - - it('can dial the first listener using multiple signal servers in one listener', async function () { - // Listen on two signalling servers in one instance - const listener1m1 = ws1.createListener((conn) => pipe(conn, conn)) - await listener1m1.listen(ma1) - - const listener1m2 = ws1.createListener((conn) => pipe(conn, conn)) - await listener1m2.listen(ma2) - - expect(Array.from(ws1.sigReferences.keys())).to.have.lengthOf(2) - - // Create Listener 2 listening on the first signalling server - const listener2 = ws2.createListener((conn) => pipe(conn, conn)) - await listener2.listen(ma1) - - expect(Array.from(ws2.sigReferences.keys())).to.have.lengthOf(1) - - // // Use first of the signal addresses - const [sigRefs1] = ws1.sigReferences.values() - - await ws2.dial(sigRefs1.signallingAddr) - - await Promise.all([ - listener1m1.close(), - listener1m2.close(), - listener2.close() - ]) - }) - - it('can dial the last listener using multiple signal servers in one listener', async function () { - // Listen on two signalling servers in one instance - const listener1m1 = ws1.createListener((conn) => pipe(conn, conn)) - await listener1m1.listen(ma1) - - const listener1m2 = ws1.createListener((conn) => pipe(conn, conn)) - await listener1m2.listen(ma2) - - expect(Array.from(ws1.sigReferences.keys())).to.have.lengthOf(2) - - // Create Listener 2 listening on the last signalling server - const listener2 = ws2.createListener((conn) => pipe(conn, conn)) - await listener2.listen(ma2) - - expect(Array.from(ws2.sigReferences.keys())).to.have.lengthOf(1) - - // // Use last of the signal addresses - const [, sigRefs2] = ws1.sigReferences.values() - - await ws2.dial(sigRefs2.signallingAddr) - - await Promise.all([ - listener1m1.close(), - listener1m2.close(), - listener2.close() - ]) - }) - - it('can close a single listener', async function () { - const listener1m1 = ws1.createListener(() => { }) - await listener1m1.listen(ma1) - - const listener1m2 = ws1.createListener(() => { }) - await listener1m2.listen(ma2) - - expect(Array.from(ws1.sigReferences.keys())).to.have.lengthOf(2) - - await listener1m1.close() - expect(Array.from(ws1.sigReferences.keys())).to.have.lengthOf(1) - - // Use the second multiaddr to dial - const listener2 = ws2.createListener((conn) => pipe(conn, conn)) - await listener2.listen(ma2) - - // The first was cleaned up on close - const [sigRefs] = ws1.sigReferences.values() - - await ws2.dial(sigRefs.signallingAddr) - - await Promise.all([ - listener1m2.close(), - listener2.close() - ]) - }) - }) -} diff --git a/packages/transport/test/transport/reconnect.node.js b/packages/transport/test/transport/reconnect.node.js deleted file mode 100644 index 8e15d615..00000000 --- a/packages/transport/test/transport/reconnect.node.js +++ /dev/null @@ -1,84 +0,0 @@ -/* eslint-env mocha */ - -'use strict' - -const { expect } = require('aegir/utils/chai') -const { Multiaddr } = require('multiaddr') -const sigServer = require('libp2p-webrtc-star-signalling-server') - -const SERVER_PORT = 13580 - -module.exports = (create) => { - describe('reconnect to signaling server', () => { - let sigS - let ws1 - let ws2 - let ws3 - const signallerAddr = new Multiaddr('/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star') - - before(async () => { - sigS = await sigServer.start({ port: SERVER_PORT }) - }) - - after(async () => { - await sigS.stop() - }) - - it('listen on the first', async () => { - ws1 = await create() - - const listener = ws1.createListener(() => {}) - ws1.discovery.start() - - await listener.listen(signallerAddr) - }) - - it('listen on the second, discover the first', async () => { - ws2 = await create() - - const p = new Promise((resolve) => { - ws1.discovery.once('peer', ({ multiaddrs }) => { - // Check first of the signal addresses - const [sigRefs] = ws2.sigReferences.values() - - expect(multiaddrs.map(m => m.toString())).to.include(sigRefs.signallingAddr.toString()) - resolve() - }) - }) - - const listener = ws2.createListener(() => {}) - - await listener.listen(signallerAddr) - await p - }) - - it('stops the server', async () => { - await sigS.stop() - }) - - it('starts the server again', async () => { - sigS = await sigServer.start({ port: SERVER_PORT }) - }) - - it('wait a bit for clients to reconnect', (done) => { - setTimeout(done, 2000) - }) - - it('listen on the third, first discovers it', async () => { - ws3 = await create() - - const listener = ws3.createListener(() => {}) - await listener.listen(signallerAddr) - - await new Promise((resolve) => { - ws1.discovery.once('peer', ({ multiaddrs }) => { - // Check first of the signal addresses - const [sigRefs] = ws3.sigReferences.values() - - expect(multiaddrs.some((m) => m.equals(sigRefs.signallingAddr))).to.equal(true) - resolve() - }) - }) - }) - }) -} diff --git a/packages/transport/test/transport/track.js b/packages/transport/test/transport/track.js deleted file mode 100644 index 8b3ad471..00000000 --- a/packages/transport/test/transport/track.js +++ /dev/null @@ -1,72 +0,0 @@ -/* eslint-env mocha */ -/* eslint-disable no-console */ - -'use strict' - -const { expect } = require('aegir/utils/chai') -const { Multiaddr } = require('multiaddr') -const pipe = require('it-pipe') -const pWaitFor = require('p-wait-for') - -module.exports = (create) => { - describe('track connections', () => { - let ws1 - let ws2 - let ma - let listener - let remoteListener - - const maHSDNS = new Multiaddr('/dns/star-signal.cloud.ipfs.team/wss/p2p-webrtc-star') - const maHSIP = new Multiaddr('/ip4/188.166.203.82/tcp/20000/wss/p2p-webrtc-star') - const maLS = new Multiaddr('/ip4/127.0.0.1/tcp/15555/wss/p2p-webrtc-star') - - if (process.env.WEBRTC_STAR_REMOTE_SIGNAL_DNS) { - // test with deployed signalling server using DNS - console.log('Using DNS:', maHSDNS) - ma = maHSDNS - } else if (process.env.WEBRTC_STAR_REMOTE_SIGNAL_IP) { - // test with deployed signalling server using IP - console.log('Using IP:', maHSIP) - ma = maHSIP - } else { - ma = maLS - } - - beforeEach(async () => { - // first - ws1 = await create() - listener = ws1.createListener((conn) => pipe(conn, conn)) - - // second - ws2 = await create() - remoteListener = ws2.createListener((conn) => pipe(conn, conn)) - - await Promise.all([listener.listen(ma), remoteListener.listen(ma)]) - }) - - afterEach(async () => { - await Promise.all([listener, remoteListener].map(l => l.close())) - }) - - it('should untrack conn after being closed', async function () { - expect(listener.__connections).to.have.lengthOf(0) - - // Use one of the signal addresses - const [sigRefs] = ws2.sigReferences.values() - - const conn = await ws1.dial(sigRefs.signallingAddr) - - // Wait for the listener to begin tracking, this happens after signaling is complete - await pWaitFor(() => remoteListener.__connections.length === 1) - expect(remoteListener.__spChannels.size).to.equal(1) - expect(remoteListener.__pendingIntents.size).to.equal(1) - - await conn.close() - - // Wait for tracking to clear - await pWaitFor(() => remoteListener.__connections.length === 0) - expect(remoteListener.__spChannels.size).to.equal(0) - expect(remoteListener.__pendingIntents.size).to.equal(0) - }) - }) -} diff --git a/packages/webrtc-star-protocol/LICENSE b/packages/webrtc-star-protocol/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/webrtc-star-protocol/LICENSE @@ -0,0 +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 diff --git a/packages/signalling-server/LICENSE-APACHE b/packages/webrtc-star-protocol/LICENSE-APACHE similarity index 100% rename from packages/signalling-server/LICENSE-APACHE rename to packages/webrtc-star-protocol/LICENSE-APACHE diff --git a/packages/transport/LICENSE-MIT b/packages/webrtc-star-protocol/LICENSE-MIT similarity index 98% rename from packages/transport/LICENSE-MIT rename to packages/webrtc-star-protocol/LICENSE-MIT index 749aa1ec..72dc60d8 100644 --- a/packages/transport/LICENSE-MIT +++ b/packages/webrtc-star-protocol/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/packages/webrtc-star-protocol/README.md b/packages/webrtc-star-protocol/README.md new file mode 100644 index 00000000..8f06c098 --- /dev/null +++ b/packages/webrtc-star-protocol/README.md @@ -0,0 +1,34 @@ +# js-libp2p-webrtc-star-protocol + +[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) [![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) [![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p) [![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) [![](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star) [![Build Status](https://github.com/libp2p/js-libp2p-webrtc-star/actions/workflows/js-test-and-release.yml/badge.svg?branch=master)](https://github.com/libp2p/js-libp2p-webrtc-star/actions/workflows/js-test-and-release.yml) [![Dependency Status](https://david-dm.org/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-webrtc-star) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) + +> The protocol that allows WebRTC peers to find each other via a signalling server + +## Table of Contents + +- [Description](#description) +- [Install](#install) +- [Protocol](#protocol) + +## Description + +This module contains type definitions for the websocket events that are exchanged between peers during the handshake process. + +## Install + +```bash +> npm install -g @libp2p/webrtc-star-protocol +``` + +## Protocol + +1. Peers connect to the same signal server and send an `ss-join` event with their multiaddr as a string +2. Peers send one or more `ss-handshake` events with candidate signals +3. Peers receive one or more `ws-handshake` events with candidate signals +4. Peers send one `ss-handshake` event with an offer signal +5. Peers receive one `ws-handshake` events with an offer signal +6. Peers are now connected +7. Peers receive one or more `ws-peer` events with a multiaddr as a string for peer discovery +8. Peers send an `ss-leave` event or disconnect when hanging up + +See [./src/index.ts](src/index.ts) for definitions of `ss-handshake` and `ws-handshake` payloads. diff --git a/packages/webrtc-star-protocol/package.json b/packages/webrtc-star-protocol/package.json new file mode 100644 index 00000000..47ebe5b9 --- /dev/null +++ b/packages/webrtc-star-protocol/package.json @@ -0,0 +1,135 @@ +{ + "name": "@libp2p/webrtc-star-protocol", + "version": "0.0.0", + "description": "shared types used by the libp2p webrtc transport and signalling server", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p-webrtc-star/tree/master/packages/webrtc-star-protocol#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-webrtc-star.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-webrtc-star/issues" + }, + "keywords": [ + "IPFS", + "libp2p" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist/src", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "import": "./dist/src/index.js", + "types": "./dist/src/index.d.ts" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "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", + "build": "tsc" + }, + "dependencies": { + "@multiformats/multiaddr": "^10.1.2", + "socket.io-client": "^4.1.2" + }, + "devDependencies": { + "aegir": "^36.1.3" + } +} diff --git a/packages/webrtc-star-protocol/src/index.ts b/packages/webrtc-star-protocol/src/index.ts new file mode 100644 index 00000000..81166074 --- /dev/null +++ b/packages/webrtc-star-protocol/src/index.ts @@ -0,0 +1,54 @@ +import type { Socket } from 'socket.io-client' + +export interface OfferSignal { + type: 'offer' + sdp: string +} + +export interface AnswerSignal { + type: 'answer' + sdp: string +} + +export interface CandidateSignal { + type: 'candidate' + candidate: { + candidate: string + sdpMLineIndex?: number + sdpMid?: string + } +} + +export interface RenegotiateSignal { + type: 'renegotiate' +} + +export interface GoodbyeSignal { + type: 'goodbye' +} + +export type Signal = OfferSignal | AnswerSignal | CandidateSignal | RenegotiateSignal | GoodbyeSignal + +export interface HandshakeSignal { + srcMultiaddr: string + dstMultiaddr: string + intentId: string + signal: Signal + answer?: boolean + err?: string +} + +interface SocketEvents { + 'ss-handshake': (offer: HandshakeSignal) => void + 'ss-join': (maStr: string) => void + 'ss-leave': (maStr: string) => void + 'ws-peer': (maStr: string) => void + 'ws-handshake': (offer: HandshakeSignal) => void + 'error': (err: Error) => void + 'listening': () => void + 'close': () => void +} + +export interface WebRTCStarSocket extends Socket { + +} diff --git a/packages/webrtc-star-protocol/tsconfig.json b/packages/webrtc-star-protocol/tsconfig.json new file mode 100644 index 00000000..f296f994 --- /dev/null +++ b/packages/webrtc-star-protocol/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" + }, + "include": [ + "src", + "test" + ] +} diff --git a/packages/signalling-server/.aegir.js b/packages/webrtc-star-signalling-server/.aegir.cjs similarity index 72% rename from packages/signalling-server/.aegir.js rename to packages/webrtc-star-signalling-server/.aegir.cjs index aa4e641e..3fb87d3e 100644 --- a/packages/signalling-server/.aegir.js +++ b/packages/webrtc-star-signalling-server/.aegir.cjs @@ -1,6 +1,5 @@ 'use strict' -const sigServer = require('./src') let firstRun = true let sigServers = [] @@ -23,11 +22,15 @@ async function boot () { metrics: false } - if (firstRun) { firstRun = false } + if (firstRun) { + firstRun = false + } + + const { sigServer } = await import('./dist/src/index.js') - sigServers.push(await sigServer.start(options1)) - sigServers.push(await sigServer.start(options2)) - sigServers.push(await sigServer.start(options3)) + sigServers.push(await sigServer(options1)) + sigServers.push(await sigServer(options2)) + sigServers.push(await sigServer(options3)) console.log('signalling on:') sigServers.forEach((sig) => console.log(sig.info.uri)) diff --git a/packages/signalling-server/CHANGELOG.md b/packages/webrtc-star-signalling-server/CHANGELOG.md similarity index 100% rename from packages/signalling-server/CHANGELOG.md rename to packages/webrtc-star-signalling-server/CHANGELOG.md diff --git a/packages/signalling-server/DEPLOYMENT.md b/packages/webrtc-star-signalling-server/DEPLOYMENT.md similarity index 100% rename from packages/signalling-server/DEPLOYMENT.md rename to packages/webrtc-star-signalling-server/DEPLOYMENT.md diff --git a/packages/webrtc-star-signalling-server/LICENSE b/packages/webrtc-star-signalling-server/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/webrtc-star-signalling-server/LICENSE @@ -0,0 +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 diff --git a/packages/transport/LICENSE-APACHE b/packages/webrtc-star-signalling-server/LICENSE-APACHE similarity index 100% rename from packages/transport/LICENSE-APACHE rename to packages/webrtc-star-signalling-server/LICENSE-APACHE diff --git a/packages/signalling-server/LICENSE-MIT b/packages/webrtc-star-signalling-server/LICENSE-MIT similarity index 98% rename from packages/signalling-server/LICENSE-MIT rename to packages/webrtc-star-signalling-server/LICENSE-MIT index 749aa1ec..72dc60d8 100644 --- a/packages/signalling-server/LICENSE-MIT +++ b/packages/webrtc-star-signalling-server/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/packages/signalling-server/README.md b/packages/webrtc-star-signalling-server/README.md similarity index 64% rename from packages/signalling-server/README.md rename to packages/webrtc-star-signalling-server/README.md index 09fd1c74..49411e85 100644 --- a/packages/signalling-server/README.md +++ b/packages/webrtc-star-signalling-server/README.md @@ -1,8 +1,6 @@ # js-libp2p-webrtc-star-signalling-server -[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) [![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) [![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p) [![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) [![](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star) [![](https://img.shields.io/travis/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://travis-ci.com/libp2p/js-libp2p-webrtc-star) [![Dependency Status](https://david-dm.org/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-webrtc-star) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) - -[![](https://github.com/libp2p/js-libp2p-interfaces/raw/master/src/transport/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/transport) [![](https://github.com/libp2p/js-libp2p-interfaces/raw/master/src/connection/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/connection) [![](https://github.com/libp2p/js-libp2p-interfaces/raw/master/src/peer-discovery/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/peer-discovery) +[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) [![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) [![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p) [![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) [![](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star) [![Build Status](https://github.com/libp2p/js-libp2p-webrtc-star/actions/workflows/js-test-and-release.yml/badge.svg?branch=master)](https://github.com/libp2p/js-libp2p-webrtc-star/actions/workflows/js-test-and-release.yml) [![Dependency Status](https://david-dm.org/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-webrtc-star) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) > A webrtc-star signalling server that allows peer discovery between browsers @@ -20,7 +18,7 @@ Nodes using the `libp2p-webrtc-star` transport will connect to a known point in ## Install ```bash -> npm install -g libp2p-webrtc-star-signalling-server +> npm install -g @libp2p/webrtc-star-signalling-server ``` ## Usage @@ -39,9 +37,9 @@ Defaults: Or in JavaScript: ```js -import { start } from 'libp2p-webrtc-star-signalling-server' +import { signallingServer } from '@libp2p/webrtc-star-signalling-server' -const server = await start({ +const server = await signallingServer({ port: 24642, host: '0.0.0.0', metrics: false diff --git a/packages/signalling-server/src/bin.js b/packages/webrtc-star-signalling-server/bin/index.js similarity index 82% rename from packages/signalling-server/src/bin.js rename to packages/webrtc-star-signalling-server/bin/index.js index 16321083..4c6ff458 100755 --- a/packages/signalling-server/src/bin.js +++ b/packages/webrtc-star-signalling-server/bin/index.js @@ -1,12 +1,11 @@ #!/usr/bin/env node /* eslint-disable no-console */ -'use strict' - // Usage: $0 [--host ] [--port ] [--disable-metrics] -const signalling = require('./index') -const minimist = require('minimist') +import { sigServer } from '../dist/src/index.js' +import minimist from 'minimist' + const argv = minimist(process.argv.slice(2), { alias: { p: 'port', @@ -16,7 +15,7 @@ const argv = minimist(process.argv.slice(2), { }) ;(async () => { - const server = await signalling.start({ + const server = await sigServer({ port: argv.port || process.env.PORT || 9090, host: argv.host || process.env.HOST || '0.0.0.0', metrics: !(argv.disableMetrics || process.env.DISABLE_METRICS) diff --git a/packages/webrtc-star-signalling-server/package.json b/packages/webrtc-star-signalling-server/package.json new file mode 100644 index 00000000..c3469c66 --- /dev/null +++ b/packages/webrtc-star-signalling-server/package.json @@ -0,0 +1,158 @@ +{ + "name": "@libp2p/webrtc-star-signalling-server", + "version": "0.1.2", + "description": "signalling server to use with the libp2p WebRTC transport", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p-webrtc-star/tree/master/packages/webrtc-star-signalling-server#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-webrtc-star.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-webrtc-star/issues" + }, + "keywords": [ + "IPFS", + "libp2p" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "bin": { + "webrtc-star": "bin/index.js", + "star-sig": "bin/index.js", + "star-signal": "bin/index.js" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist/src", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "import": "./dist/src/index.js", + "types": "./dist/src/index.d.ts" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "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", + "build": "tsc", + "pretest": "npm run build", + "test": "aegir test -f ./dist/test/**/*.js", + "test:node": "npm run test -- -t node --cov", + "start": "node src/sig-server/bin.js" + }, + "dependencies": { + "@hapi/hapi": "^20.0.0", + "@hapi/inert": "^6.0.3", + "@libp2p/logger": "^1.0.2", + "@libp2p/webrtc-star-protocol": "^0.0.0", + "@multiformats/multiaddr": "^10.1.2", + "menoetius": "0.0.3", + "minimist": "^1.2.5", + "prom-client": "^14.0.0", + "socket.io": "^4.1.2", + "socket.io-client": "^4.1.2" + }, + "devDependencies": { + "@types/hapi__hapi": "^20.0.10", + "@types/hapi__inert": "^5.2.3", + "aegir": "^36.1.3", + "p-event": "^5.0.1", + "p-wait-for": "^4.1.0", + "socket.io-client-v2": "npm:socket.io-client@^2.3.0", + "socket.io-client-v3": "npm:socket.io-client@^3.1.2" + } +} diff --git a/packages/webrtc-star-signalling-server/src/config.ts b/packages/webrtc-star-signalling-server/src/config.ts new file mode 100644 index 00000000..cdee6378 --- /dev/null +++ b/packages/webrtc-star-signalling-server/src/config.ts @@ -0,0 +1,17 @@ +import { logger } from '@libp2p/logger' + +const log = logger('signalling-server') + +export const config = { + log: log, + hapi: { + port: process.env.PORT ?? 13579, + host: '0.0.0.0', + options: { + routes: { + cors: true + } + } + }, + refreshPeerListIntervalMS: 10000 +} diff --git a/packages/signalling-server/src/index.html b/packages/webrtc-star-signalling-server/src/index.html similarity index 100% rename from packages/signalling-server/src/index.html rename to packages/webrtc-star-signalling-server/src/index.html diff --git a/packages/webrtc-star-signalling-server/src/index.ts b/packages/webrtc-star-signalling-server/src/index.ts new file mode 100644 index 00000000..919ed11b --- /dev/null +++ b/packages/webrtc-star-signalling-server/src/index.ts @@ -0,0 +1,66 @@ +import { Server } from '@hapi/hapi' +import Inert from '@hapi/inert' + +import { config } from './config.js' +// @ts-expect-error no types +import menoetius from 'menoetius' +import path from 'path' +import { socketServer } from './socket-server.js' +import type { WebRTCStarSocket } from '@libp2p/webrtc-star-protocol' +import type { Server as SocketServer } from 'socket.io' + +const log = config.log + +interface Options { + port?: number + host?: string + metrics?: boolean +} + +export interface SigServer extends Server { + peers: Map + io: SocketServer +} + +export async function sigServer (options: Options = {}) { + const port = options.port ?? config.hapi.port + const host = options.host ?? config.hapi.host + const peers = new Map() + + const http: SigServer = Object.assign(new Server({ + ...config.hapi.options, + port, + host + }), { + peers, + io: socketServer(peers, options.metrics ?? false) + }) + + http.io.attach(http.listener, { + path: '/socket.io' // v2/v3/v4 clients can use this path + }) + http.io.attach(http.listener, { + path: '/socket.io-next' // v3/v4 clients might be using this path + }) + http.events.on('stop', () => http.io.close()) + + await http.register(Inert) + await http.start() + + log('signaling server has started on: ' + http.info.uri) + + http.route({ + method: 'GET', + path: '/', + handler: (request, reply) => reply.file(path.join(__dirname, 'index.html'), { + confine: false + }) + }) + + if (options.metrics === true) { + log('enabling metrics') + await menoetius.instrument(http) + } + + return http +} diff --git a/packages/webrtc-star-signalling-server/src/socket-server.ts b/packages/webrtc-star-signalling-server/src/socket-server.ts new file mode 100644 index 00000000..f01af7ac --- /dev/null +++ b/packages/webrtc-star-signalling-server/src/socket-server.ts @@ -0,0 +1,123 @@ +import { config } from './config.js' +import { Server } from 'socket.io' +import client from 'prom-client' +import type { HandshakeSignal, WebRTCStarSocket } from '@libp2p/webrtc-star-protocol' + +const log = config.log + +const fake = { + gauge: { + set: () => {} + }, + counter: { + inc: () => {} + } +} + +export function socketServer (peers: Map, hasMetrics: boolean) { + const io = new Server({ + allowEIO3: true // allow socket.io v2 clients to connect + }) + // @ts-expect-error types are different? + io.on('connection', (socket) => handle(socket)) + + const peersMetric = hasMetrics ? new client.Gauge({ name: 'webrtc_star_peers', help: 'peers online now' }) : fake.gauge + const dialsSuccessTotal = hasMetrics ? new client.Counter({ name: 'webrtc_star_dials_total_success', help: 'successfully completed dials since server started' }) : fake.counter + const dialsFailureTotal = hasMetrics ? new client.Counter({ name: 'webrtc_star_dials_total_failure', help: 'failed dials since server started' }) : fake.counter + const dialsTotal = hasMetrics ? new client.Counter({ name: 'webrtc_star_dials_total', help: 'all dials since server started' }) : fake.counter + const joinsSuccessTotal = hasMetrics ? new client.Counter({ name: 'webrtc_star_joins_total_success', help: 'successfully completed joins since server started' }) : fake.counter + const joinsFailureTotal = hasMetrics ? new client.Counter({ name: 'webrtc_star_joins_total_failure', help: 'failed joins since server started' }) : fake.counter + const joinsTotal = hasMetrics ? new client.Counter({ name: 'webrtc_star_joins_total', help: 'all joins since server started' }) : fake.counter + + const refreshMetrics = () => peersMetric.set(peers.size) + + function safeEmit (maStr: string, event: any, arg: any) { + const peer = peers.get(maStr) + + if (peer == null) { + log('trying to emit %s but peer is gone', event) + return + } + + peer.emit(event, arg) + } + + function handle (socket: WebRTCStarSocket) { + let multiaddr: string + + // join this signaling server network + socket.on('ss-join', (maStr: string) => { + joinsTotal.inc() + + if (maStr == null) { + return joinsFailureTotal.inc() + } + + multiaddr = maStr + + peers.set(multiaddr, socket) + + socket.once('ss-leave', stopSendingPeers) + socket.once('disconnect', stopSendingPeers) + + let refreshInterval: NodeJS.Timer | undefined = setInterval(sendPeers, config.refreshPeerListIntervalMS) + sendPeers() + + function sendPeers () { + for (const mh of peers.keys()) { + if (mh === multiaddr) { + continue + } + + safeEmit(mh, 'ws-peer', multiaddr) + } + } + + function stopSendingPeers () { + if (refreshInterval != null) { + clearInterval(refreshInterval) + refreshInterval = undefined + } + } + + joinsSuccessTotal.inc() + refreshMetrics() + }) + socket.on('ss-leave', () => { + peers.delete(multiaddr) + + refreshMetrics() + }) + + // socket.io own event + socket.on('disconnect', () => { + peers.delete(multiaddr) + + refreshMetrics() + }) + + // forward an WebRTC offer to another peer + socket.on('ss-handshake', (offer: HandshakeSignal) => { + dialsTotal.inc() + + if (offer == null || typeof offer !== 'object' || offer.srcMultiaddr == null || offer.dstMultiaddr == null) { + return dialsFailureTotal.inc() + } + + if (offer.answer === true) { + dialsSuccessTotal.inc() + safeEmit(offer.srcMultiaddr, 'ws-handshake', offer) + } else { + if (peers.has(offer.dstMultiaddr)) { + safeEmit(offer.dstMultiaddr, 'ws-handshake', offer) + } else { + dialsFailureTotal.inc() + offer.err = 'peer is not available' + safeEmit(offer.srcMultiaddr, 'ws-handshake', offer) + } + } + }) + } + + return io +} diff --git a/packages/signalling-server/test/node.js b/packages/webrtc-star-signalling-server/test/node.ts similarity index 69% rename from packages/signalling-server/test/node.js rename to packages/webrtc-star-signalling-server/test/node.ts index 01504b23..773f5158 100644 --- a/packages/signalling-server/test/node.js +++ b/packages/webrtc-star-signalling-server/test/node.ts @@ -1,13 +1,16 @@ /* eslint-env mocha */ -'use strict' -const sigServerTests = require('./sig-server') +import sigServerTests from './sig-server.js' +import { connect as socketClientV4 } from 'socket.io-client' +import { io as socketClientV3 } from 'socket.io-client-v3' +// @ts-expect-error no types +import { connect as socketClientV2 } from 'socket.io-client-v2' // Test v4, v3 and v2 clients against the socket server sigServerTests( 'socket.io-client@v4 (next path)', - require('socket.io-client'), { + socketClientV4, { transports: ['websocket'], forceNew: true, path: '/socket.io-next/' // TODO: This should be removed when socket.io@2 support is removed @@ -15,7 +18,8 @@ sigServerTests( ) sigServerTests( 'socket.io-client@v3 (next path)', - require('socket.io-client-v3'), { + // @ts-expect-error types are wrong + socketClientV3, { transports: ['websocket'], forceNew: true, path: '/socket.io-next/' // TODO: This should be removed when socket.io@2 support is removed @@ -23,7 +27,7 @@ sigServerTests( ) sigServerTests( 'socket.io-client@v2 (next path)', - require('socket.io-client-v2'), { + socketClientV2, { transports: ['websocket'], forceNew: true, path: '/socket.io-next/' // TODO: This should be removed when socket.io@2 support is removed @@ -31,21 +35,22 @@ sigServerTests( ) sigServerTests( 'socket.io-client@v4 (root path)', - require('socket.io-client'), { + socketClientV4, { transports: ['websocket'], forceNew: true } ) sigServerTests( 'socket.io-client@v3 (root path)', - require('socket.io-client-v3'), { + // @ts-expect-error types are wrong + socketClientV3, { transports: ['websocket'], forceNew: true } ) sigServerTests( 'socket.io-client@v2 (root path)', - require('socket.io-client-v2'), { + socketClientV2, { transports: ['websocket'], forceNew: true } diff --git a/packages/signalling-server/test/sig-server.js b/packages/webrtc-star-signalling-server/test/sig-server.ts similarity index 68% rename from packages/signalling-server/test/sig-server.js rename to packages/webrtc-star-signalling-server/test/sig-server.ts index 33e6ce0c..0526ece3 100644 --- a/packages/signalling-server/test/sig-server.js +++ b/packages/webrtc-star-signalling-server/test/sig-server.ts @@ -1,21 +1,22 @@ /* eslint-env mocha */ -'use strict' -const { expect } = require('aegir/utils/chai') -const { Multiaddr } = require('multiaddr') +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from '@multiformats/multiaddr' +import { SigServer, sigServer } from '../src/index.js' +import pWaitFor from 'p-wait-for' +import { pEvent } from 'p-event' +import type { WebRTCStarSocket } from '@libp2p/webrtc-star-protocol' -const sigServer = require('../src') - -module.exports = (clientName, io, sioOptions) => { +export default (clientName: string, io: (url: string, opts: any) => WebRTCStarSocket, sioOptions: any) => { describe(`signalling ${clientName}`, () => { - let sioUrl - let sigS - let c1 - let c2 - let c3 - let c4 - - const base = (id) => { + let sioUrl: string + let sigS: SigServer + let c1: WebRTCStarSocket + let c2: WebRTCStarSocket + let c3: WebRTCStarSocket + let c4: WebRTCStarSocket + + const base = (id: string) => { return `/ip4/127.0.0.1/tcp/9090/ws/p2p-webrtc-star/ipfs/${id}` } @@ -25,7 +26,7 @@ module.exports = (clientName, io, sioOptions) => { const c4mh = new Multiaddr(base('QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo4')) it('start and stop signalling server (default port)', async () => { - const server = await sigServer.start() + const server = await sigServer() expect(server.info.port).to.equal(13579) expect(server.info.protocol).to.equal('http') @@ -35,16 +36,20 @@ module.exports = (clientName, io, sioOptions) => { }) it('start and stop signalling server (default port) and spam it with invalid requests', (done) => { - sigServer.start().then(server => { + sigServer().then(server => { expect(server.info.port).to.equal(13579) expect(server.info.protocol).to.equal('http') expect(server.info.address).to.equal('0.0.0.0') const cl = io(server.info.uri, sioOptions) cl.on('connect', async () => { + // @ts-expect-error not a valid handler cl.emit('ss-handshake', null) + // @ts-expect-error not a valid handler cl.emit('ss-handshake', 1) + // @ts-expect-error not a valid handler cl.emit('ss-handshake', [1, 2, 3]) + // @ts-expect-error not a valid handler cl.emit('ss-handshake', {}) await server.stop() @@ -54,7 +59,7 @@ module.exports = (clientName, io, sioOptions) => { cl.on('disconnect', () => { cl.close() }) - }) + }, done) }) it('start and stop signalling server (custom port)', async () => { @@ -62,11 +67,12 @@ module.exports = (clientName, io, sioOptions) => { port: 12345 } - const server = await sigServer.start(options) + const server = await sigServer(options) expect(server.info.port).to.equal(12345) expect(server.info.protocol).to.equal('http') expect(server.info.address).to.equal('0.0.0.0') + await server.stop() }) @@ -75,7 +81,7 @@ module.exports = (clientName, io, sioOptions) => { port: 12345 } - const server = await sigServer.start(options) + const server = await sigServer(options) expect(server.info.port).to.equal(12345) expect(server.info.protocol).to.equal('http') @@ -109,45 +115,39 @@ module.exports = (clientName, io, sioOptions) => { } }) - it('ss-join first client', (done) => { + it('ss-join first client', async () => { c1.emit('ss-join', c1mh.toString()) - setTimeout(() => { - expect(Object.keys(sigS.peers()).length).to.equal(1) - done() - }, 10) + + await pWaitFor(() => sigS.peers.size === 1) }) - it('ss-join and ss-leave second client', (done) => { + it('ss-join and ss-leave second client', async () => { c2.emit('ss-join', c2mh.toString()) - setTimeout(() => { - expect(Object.keys(sigS.peers()).length).to.equal(2) - c2.emit('ss-leave', c2mh.toString()) - setTimeout(() => { - expect(Object.keys(sigS.peers()).length).to.equal(1) - done() - }, 10) - }, 10) + + await pWaitFor(() => sigS.peers.size === 2) + + c2.emit('ss-leave', c2mh.toString()) + + await pWaitFor(() => sigS.peers.size === 1) }) - it('ss-join and disconnect third client', (done) => { + it('ss-join and disconnect third client', async () => { c3.emit('ss-join', c3mh.toString()) - setTimeout(() => { - expect(Object.keys(sigS.peers()).length).to.equal(2) - c3.disconnect() - setTimeout(() => { - expect(Object.keys(sigS.peers()).length).to.equal(1) - done() - }, 10) - }, 10) + + await pWaitFor(() => sigS.peers.size === 2) + + c3.disconnect() + + await pWaitFor(() => sigS.peers.size === 1) }) - it('ss-join the fourth', (done) => { - c1.once('ws-peer', (multiaddr) => { - expect(multiaddr).to.equal(c4mh.toString()) - expect(Object.keys(sigS.peers()).length).to.equal(2) - done() - }) + it('ss-join the fourth', async () => { c4.emit('ss-join', c4mh.toString()) + + const multiaddr = await pEvent(c1, 'ws-peer') + + expect(multiaddr).to.equal(c4mh.toString()) + expect(sigS.peers.size).to.equal(2) }) it('c1 handshake c4', (done) => { @@ -164,7 +164,12 @@ module.exports = (clientName, io, sioOptions) => { c1.emit('ss-handshake', { srcMultiaddr: c1mh.toString(), - dstMultiaddr: c4mh.toString() + dstMultiaddr: c4mh.toString(), + intentId: 'intent-id', + signal: { + type: 'offer', + sdp: 'sdp' + } }) }) @@ -176,7 +181,12 @@ module.exports = (clientName, io, sioOptions) => { c1.emit('ss-handshake', { srcMultiaddr: c1mh.toString(), - dstMultiaddr: c2mh.toString() + dstMultiaddr: c2mh.toString(), + intentId: 'intent-id', + signal: { + type: 'offer', + sdp: 'sdp' + } }) }) diff --git a/packages/webrtc-star-signalling-server/tsconfig.json b/packages/webrtc-star-signalling-server/tsconfig.json new file mode 100644 index 00000000..42af6972 --- /dev/null +++ b/packages/webrtc-star-signalling-server/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../webrtc-star-protocol" + }, + { + "path": "../webrtc-star-transport" + } + ] +} diff --git a/packages/webrtc-star-transport/.aegir.cjs b/packages/webrtc-star-transport/.aegir.cjs new file mode 100644 index 00000000..3aa42b98 --- /dev/null +++ b/packages/webrtc-star-transport/.aegir.cjs @@ -0,0 +1,49 @@ +'use strict' + +let firstRun = true + +/** @type {import('aegir').PartialOptions} */ +module.exports = { + test: { + async before () { + const options1 = { + port: 15555, + host: '127.0.0.1', + metrics: firstRun + } + + const options2 = { + port: 15556, + host: '127.0.0.1', + metrics: false + } + + const options3 = { + port: 15557, + host: '127.0.0.1', + metrics: false + } + + if (firstRun) { + firstRun = false + } + + const { sigServer } = await import('@libp2p/webrtc-star-signalling-server') + const sigServers = [] + + sigServers.push(await sigServer(options1)) + sigServers.push(await sigServer(options2)) + sigServers.push(await sigServer(options3)) + + console.log('signalling on:') + sigServers.forEach((sig) => console.log(sig.info.uri)) + + return { + sigServers + } + }, + async after (_, before) { + await Promise.all(before.sigServers.map(s => s.stop())) + } + } +} diff --git a/packages/transport/CHANGELOG.md b/packages/webrtc-star-transport/CHANGELOG.md similarity index 100% rename from packages/transport/CHANGELOG.md rename to packages/webrtc-star-transport/CHANGELOG.md diff --git a/packages/transport/DEPLOYMENT.md b/packages/webrtc-star-transport/DEPLOYMENT.md similarity index 100% rename from packages/transport/DEPLOYMENT.md rename to packages/webrtc-star-transport/DEPLOYMENT.md diff --git a/packages/webrtc-star-transport/LICENSE b/packages/webrtc-star-transport/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/webrtc-star-transport/LICENSE @@ -0,0 +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 diff --git a/packages/webrtc-star-transport/LICENSE-APACHE b/packages/webrtc-star-transport/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/webrtc-star-transport/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/packages/webrtc-star-transport/LICENSE-MIT b/packages/webrtc-star-transport/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/packages/webrtc-star-transport/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. diff --git a/packages/transport/README.md b/packages/webrtc-star-transport/README.md similarity index 56% rename from packages/transport/README.md rename to packages/webrtc-star-transport/README.md index d3860c07..057427e9 100644 --- a/packages/transport/README.md +++ b/packages/webrtc-star-transport/README.md @@ -1,8 +1,10 @@ # js-libp2p-webrtc-star -[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) [![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) [![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p) [![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) [![](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star) [![](https://img.shields.io/travis/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://travis-ci.com/libp2p/js-libp2p-webrtc-star) [![Dependency Status](https://david-dm.org/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-webrtc-star) [![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/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://protocol.ai) [![](https://img.shields.io/badge/project-libp2p-yellow.svg?style=flat-square)](http://libp2p.io/) [![](https://img.shields.io/badge/freenode-%23libp2p-yellow.svg?style=flat-square)](http://webchat.freenode.net/?channels=%23libp2p) [![Discourse posts](https://img.shields.io/discourse/https/discuss.libp2p.io/posts.svg)](https://discuss.libp2p.io) [![](https://img.shields.io/codecov/c/github/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://codecov.io/gh/libp2p/js-libp2p-webrtc-star) [![Build Status](https://github.com/libp2p/js-libp2p-webrtc-star/actions/workflows/js-test-and-release.yml/badge.svg?branch=master)](https://github.com/libp2p/js-libp2p-webrtc-star/actions/workflows/js-test-and-release.yml) [![Dependency Status](https://david-dm.org/libp2p/js-libp2p-webrtc-star.svg?style=flat-square)](https://david-dm.org/libp2p/js-libp2p-webrtc-star) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://github.com/feross/standard) -[![](https://github.com/libp2p/js-libp2p-interfaces/raw/master/src/transport/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/transport) [![](https://github.com/libp2p/js-libp2p-interfaces/raw/master/src/connection/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/connection) [![](https://github.com/libp2p/js-libp2p-interfaces/raw/master/src/peer-discovery/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/peer-discovery) +[![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/blob/master/packages/libp2p-interfaces/src/transport/README.md) +[![](https://raw.githubusercontent.com/libp2p/interface-connection/master/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/blob/master/packages/libp2p-interfaces/src/connection/README.md) +[![](https://raw.githubusercontent.com/libp2p/interface-peer-discovery/master/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/blob/master/packages/libp2p-interfaces/src/peer-discovery/README.md) > libp2p WebRTC transport @@ -38,27 +40,27 @@ To use this module in Node.js, you have to BYOI of WebRTC, there are multiple op Instead of just creating the WebRTCStar instance without arguments, you need to pass an options object with the WebRTC implementation: ```JavaScript -const wrtc = require('wrtc') -const electronWebRTC = require('electron-webrtc') -const WStar = require('libp2p-webrtc-star') +import wrtc from 'wrtc' +import electronWebRTC from 'electron-webrtc' +import { WebRTCStar } from '@libp2p/webrtc-star' // Using wrtc -const ws1 = new WStar({ wrtc: wrtc }) +const ws1 = new WebRTCStar({ wrtc: wrtc }) // Using electron-webrtc -const ws2 = new WStar({ wrtc: electronWebRTC() }) +const ws2 = new WebRTCStar({ wrtc: electronWebRTC() }) ``` ### Using this module in the Browser ```JavaScript -const WStar = require('libp2p-webrtc-star') -const multiaddr = require('multiaddr') -const all = require('it-all') +import { WebRTCStar } from '@libp2p/webrtc-star' +import { Multiaddr } from '@multiformats/multiaddr' +import all from 'it-all' const addr = multiaddr('/ip4/188.166.203.82/tcp/20000/wss/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2a') -const ws = new WStar({ upgrader }) +const ws = new WebRTCStar({ upgrader }) const listener = ws.createListener((socket) => { console.log('new connection opened') @@ -90,12 +92,12 @@ Please see the [libp2p-webrtc-star-signalling-server](https://npmjs.com/package/ ### Transport -[![](https://github.com/libp2p/js-libp2p-interfaces/raw/master/src/transport/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/transport) +[![](https://raw.githubusercontent.com/libp2p/interface-transport/master/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/blob/master/packages/libp2p-interfaces/src/transport/README.md) ### Connection -[![](https://github.com/libp2p/js-libp2p-interfaces/raw/master/src/connection/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/connection) +[![](https://raw.githubusercontent.com/libp2p/interface-connection/master/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/blob/master/packages/libp2p-interfaces/src/connection/README.md) ### Peer Discovery - `ws.discovery` -[![](https://github.com/libp2p/js-libp2p-interfaces/raw/master/src/peer-discovery/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/peer-discovery) +[![](https://raw.githubusercontent.com/libp2p/interface-peer-discovery/master/img/badge.png)](https://github.com/libp2p/js-libp2p-interfaces/blob/master/packages/libp2p-interfaces/src/peer-discovery/README.md) diff --git a/packages/webrtc-star-transport/package.json b/packages/webrtc-star-transport/package.json new file mode 100644 index 00000000..d3e351b2 --- /dev/null +++ b/packages/webrtc-star-transport/package.json @@ -0,0 +1,166 @@ +{ + "name": "@libp2p/webrtc-star", + "version": "0.25.0", + "description": "libp2p WebRTC transport that includes a discovery mechanism provided by the signalling-star", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/libp2p/js-libp2p-webrtc-star/tree/master/packages/webrtc-star-transport#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/libp2p/js-libp2p-webrtc-star.git" + }, + "bugs": { + "url": "https://github.com/libp2p/js-libp2p-webrtc-star/issues" + }, + "keywords": [ + "IPFS", + "libp2p" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist/src", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "import": "./dist/src/index.js", + "types": "./dist/src/index.d.ts" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "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", + "build": "tsc", + "pretest": "npm run build", + "test": "aegir test -f ./dist/test/*.spec.js", + "test:chrome": "npm run test -- -t browser -f ./dist/test/browser.js ", + "test:firefox": "npm run test -- -t browser -- --browser firefox -f ./dist/test/browser.js", + "test:dns": "WEBRTC_STAR_REMOTE_SIGNAL_DNS=1 aegir test -t browser", + "test:ip": "WEBRTC_STAR_REMOTE_SIGNAL_IP=1 aegir test -t browser" + }, + "dependencies": { + "@libp2p/interfaces": "^1.3.2", + "@libp2p/logger": "^1.0.2", + "@libp2p/peer-id": "^1.0.4", + "@libp2p/utils": "^1.0.5", + "@libp2p/webrtc-star-protocol": "^0.0.0", + "@multiformats/mafmt": "^11.0.2", + "@multiformats/multiaddr": "^10.1.2", + "abortable-iterator": "^4.0.2", + "delay": "^5.0.0", + "err-code": "^3.0.1", + "iso-random-stream": "^2.0.2", + "it-pipe": "^2.0.3", + "it-pushable": "^2.0.1", + "multiformats": "^9.6.3", + "p-defer": "^4.0.0", + "p-event": "^5.0.1", + "socket.io-client": "^4.1.2" + }, + "devDependencies": { + "@libp2p/interface-compliance-tests": "^1.0.6", + "@libp2p/webrtc-star-signalling-server": "^0.1.2", + "@mapbox/node-pre-gyp": "^1.0.5", + "aegir": "^36.1.3", + "electron-webrtc": "~0.3.0", + "it-all": "^1.0.5", + "p-wait-for": "^4.1.0", + "sinon": "^13.0.0", + "uint8arrays": "^3.0.0", + "util": "^0.12.4", + "wrtc": "^0.4.6" + } +} diff --git a/packages/transport/src/constants.js b/packages/webrtc-star-transport/src/constants.ts similarity index 53% rename from packages/transport/src/constants.js rename to packages/webrtc-star-transport/src/constants.ts index b7ab8fe2..75362aea 100644 --- a/packages/transport/src/constants.js +++ b/packages/webrtc-star-transport/src/constants.ts @@ -1,8 +1,6 @@ -'use strict' - // p2p multi-address code -exports.CODE_P2P = 421 -exports.CODE_CIRCUIT = 290 +export const CODE_P2P = 421 +export const CODE_CIRCUIT = 290 // Time to wait for a connection to close gracefully before destroying it manually -exports.CLOSE_TIMEOUT = 2000 +export const CLOSE_TIMEOUT = 2000 diff --git a/packages/webrtc-star-transport/src/index.ts b/packages/webrtc-star-transport/src/index.ts new file mode 100644 index 00000000..c8e583e1 --- /dev/null +++ b/packages/webrtc-star-transport/src/index.ts @@ -0,0 +1,275 @@ +import { logger } from '@libp2p/logger' +import errcode from 'err-code' +import { AbortError } from 'abortable-iterator' +import { Multiaddr } from '@multiformats/multiaddr' +import * as mafmt from '@multiformats/mafmt' +import { PeerId } from '@libp2p/peer-id' +import { CODE_CIRCUIT } from './constants.js' +import { createListener } from './listener.js' +import { toMultiaddrConnection } from './socket-to-conn.js' +import { cleanMultiaddr, cleanUrlSIO } from './utils.js' +import { WebRTCInitiator } from './peer/initiator.js' +import randomBytes from 'iso-random-stream/src/random.js' +import { toString as uint8ArrayToString } from 'uint8arrays' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces' +import type { WRTC } from './peer/interface.js' +import type { Connection } from '@libp2p/interfaces/connection' +import type { Transport, Upgrader, ListenerOptions, MultiaddrConnection, Listener } from '@libp2p/interfaces/transport' +import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interfaces/peer-discovery' +import type { AbortOptions } from '@libp2p/interfaces' +import type { WebRTCInitiatorOptions } from './peer/initiator.js' +import type { WebRTCReceiver, WebRTCReceiverOptions } from './peer/receiver.js' +import type { WebRTCStarSocket, HandshakeSignal } from '@libp2p/webrtc-star-protocol' + +const webrtcSupport = 'RTCPeerConnection' in globalThis +const log = logger('libp2p:webrtc-star') + +const noop = () => {} + +class WebRTCStarDiscovery extends EventEmitter implements PeerDiscovery { + public tag = 'webRTCStar' + private started = false + + isStarted () { + return this.started + } + + async start () { + this.started = true + } + + async stop () { + this.started = false + } + + dispatchEvent (event: Event) { + if (!this.isStarted()) { + return false + } + + return super.dispatchEvent(event) + } +} + +export interface WebRTCStarOptions { + upgrader: Upgrader + peerId: PeerId + wrtc?: WRTC +} + +export interface WebRTCStarDialOptions extends AbortOptions { + channelOptions?: WebRTCInitiatorOptions +} + +export interface WebRTCStarListenerOptions extends ListenerOptions, WebRTCInitiatorOptions { + channelOptions?: WebRTCReceiverOptions +} + +export interface SignalServerServerEvents { + 'error': CustomEvent + 'listening': CustomEvent + 'peer': CustomEvent + 'connection': CustomEvent +} + +export interface SignalServer extends EventEmitter { + signallingAddr: Multiaddr + socket: WebRTCStarSocket + connections: MultiaddrConnection[] + channels: Map + pendingSignals: Map + close: () => Promise +} + +/** + * @class WebRTCStar + */ +export class WebRTCStar implements Transport { + public wrtc?: WRTC + public discovery: PeerDiscovery + public sigServers: Map + + private readonly upgrader: Upgrader + private readonly peerId: PeerId + + constructor (options: WebRTCStarOptions) { + if (options.upgrader == null) { + throw new Error('An upgrader must be provided. See https://github.com/libp2p/interface-transport#upgrader.') + } + + this.upgrader = options.upgrader + this.peerId = options.peerId + + if (options.wrtc != null) { + this.wrtc = options.wrtc + } + + // Keep Signalling references + this.sigServers = new Map() + + // Discovery + this.discovery = new WebRTCStarDiscovery() + this.peerDiscovered = this.peerDiscovered.bind(this) + } + + async dial (ma: Multiaddr, options?: WebRTCStarDialOptions) { + options = options ?? {} + const rawConn = await this._connect(ma, options) + const maConn = toMultiaddrConnection(rawConn, { 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 + } + + async _connect (ma: Multiaddr, options: WebRTCStarDialOptions) { + if (options.signal?.aborted === true) { + throw new AbortError() + } + + const channelOptions = { + ...(options.channelOptions ?? {}) + } + + // Use custom WebRTC implementation + if (this.wrtc != null) { + channelOptions.wrtc = this.wrtc + } + + const cOpts = ma.toOptions() + const intentId = uint8ArrayToString(randomBytes(36), 'hex') + + return await new Promise((resolve, reject) => { + const sio = this.sigServers.get(cleanUrlSIO(ma)) + + if (sio?.socket == null) { + return reject(errcode(new Error('unknown signal server to use'), 'ERR_UNKNOWN_SIGNAL_SERVER')) + } + + let connected: boolean = false + + log('dialing %s:%s', cOpts.host, cOpts.port) + const channel = new WebRTCInitiator(channelOptions) + + const onError = (err: Error) => { + if (!connected) { + const msg = `connection error ${cOpts.host}:${cOpts.port}: ${err.message}` + log.error(msg) + done(err) + } + } + + const onReady = () => { + connected = true + + log('connection opened %s:%s', cOpts.host, cOpts.port) + done() + } + + const onAbort = () => { + log.error('connection aborted %s:%s', cOpts.host, cOpts.port) + channel.close().finally(() => { + done(new AbortError()) + }) + } + + const done = (err?: Error) => { + channel.removeListener('ready', onReady) + options.signal?.removeEventListener('abort', onAbort) + + if (err == null) { + resolve(channel) + } else { + reject(err) + } + } + + channel.on('error', onError) + channel.once('ready', onReady) + channel.on('close', () => { + channel.removeListener('error', onError) + }) + options.signal?.addEventListener('abort', onAbort) + + channel.on('signal', (signal) => { + sio.socket.emit('ss-handshake', { + intentId: intentId, + srcMultiaddr: sio.signallingAddr.toString(), + dstMultiaddr: ma.toString(), + signal: signal + }) + }) + + sio.socket.on('ws-handshake', (offer) => { + if (offer.intentId === intentId && offer.err != null) { + channel.close().finally(() => { + reject(errcode(new Error(offer.err), 'ERR_SIGNALLING_FAILED')) + }) + } + + if (offer.intentId !== intentId || offer.answer == null || channel.closed) { + return + } + + channel.handleSignal(offer.signal) + }) + }) + } + + /** + * Creates a WebrtcStar listener. The provided `handler` function will be called + * anytime a new incoming Connection has been successfully upgraded via + * `upgrader.upgradeInbound`. + */ + createListener (options?: WebRTCStarListenerOptions): Listener { + if (!webrtcSupport && this.wrtc == null) { + throw errcode(new Error('no WebRTC support'), 'ERR_NO_WEBRTC_SUPPORT') + } + + options = options ?? {} + options.channelOptions = options.channelOptions ?? {} + + if (this.wrtc != null) { + options.channelOptions.wrtc = this.wrtc + } + + return createListener(this.upgrader, options.handler ?? noop, this.peerId, this, options) + } + + /** + * Takes a list of `Multiaddr`s and returns only valid TCP addresses + */ + filter (multiaddrs: Multiaddr[]) { + multiaddrs = Array.isArray(multiaddrs) ? multiaddrs : [multiaddrs] + + return multiaddrs.filter((ma) => { + if (ma.protoCodes().includes(CODE_CIRCUIT)) { + return false + } + + return mafmt.WebRTCStar.matches(ma) + }) + } + + peerDiscovered (maStr: string) { + log('peer discovered: %s', maStr) + maStr = cleanMultiaddr(maStr) + + const ma = new Multiaddr(maStr) + const peerIdStr = ma.getPeerId() + + if (peerIdStr == null) { + return + } + + const peerId = PeerId.fromString(peerIdStr) + + this.discovery.dispatchEvent(new CustomEvent('peer', { + detail: { + id: peerId, + multiaddrs: [ma], + protocols: [] + } + })) + } +} diff --git a/packages/webrtc-star-transport/src/listener.ts b/packages/webrtc-star-transport/src/listener.ts new file mode 100644 index 00000000..01ef6479 --- /dev/null +++ b/packages/webrtc-star-transport/src/listener.ts @@ -0,0 +1,307 @@ +import { logger } from '@libp2p/logger' +import errCode from 'err-code' +import { connect } from 'socket.io-client' +import pDefer from 'p-defer' +import { WebRTCReceiver } from './peer/receiver.js' +import { toMultiaddrConnection } from './socket-to-conn.js' +import { cleanUrlSIO } from './utils.js' +import { CODE_P2P } from './constants.js' +import { base58btc } from 'multiformats/bases/base58' +import type { Multiaddr } from '@multiformats/multiaddr' +import type { Upgrader, ConnectionHandler, Listener, MultiaddrConnection, ListenerEvents } from '@libp2p/interfaces/transport' +import type { WebRTCStar, WebRTCStarListenerOptions, SignalServer, SignalServerServerEvents } from './index.js' +import type { PeerId } from '@libp2p/peer-id' +import type { WebRTCReceiverOptions } from './peer/receiver' +import type { WebRTCStarSocket, HandshakeSignal, Signal } from '@libp2p/webrtc-star-protocol' +import { EventEmitter, CustomEvent } from '@libp2p/interfaces' + +const log = logger('libp2p:webrtc-star:listener') + +const sioOptions = { + transports: ['websocket'], + 'force new connection': true, + path: '/socket.io-next/' // This should be removed when socket.io@2 support is removed +} + +class SigServer extends EventEmitter implements SignalServer { + public signallingAddr: Multiaddr + public socket: WebRTCStarSocket + public connections: MultiaddrConnection[] + public channels: Map + public pendingSignals: Map + + private readonly upgrader: Upgrader + private readonly handler: ConnectionHandler + private readonly channelOptions?: WebRTCReceiverOptions + + constructor (signallingUrl: string, signallingAddr: Multiaddr, upgrader: Upgrader, handler: ConnectionHandler, channelOptions?: WebRTCReceiverOptions) { + super() + + this.signallingAddr = signallingAddr + this.socket = connect(signallingUrl, sioOptions) + this.connections = [] + this.channels = new Map() + this.pendingSignals = new Map() + + this.upgrader = upgrader + this.handler = handler + this.channelOptions = channelOptions + + this.handleWsHandshake = this.handleWsHandshake.bind(this) + + this.socket.once('connect_error', (err) => { + this.dispatchEvent(new CustomEvent('error', { + detail: err + })) + }) + this.socket.once('error', (err: Error) => { + this.dispatchEvent(new CustomEvent('error', { + detail: err + })) + }) + + this.socket.on('ws-handshake', this.handleWsHandshake) + this.socket.on('ws-peer', (maStr) => { + this.dispatchEvent(new CustomEvent('peer', { + detail: maStr + })) + }) + this.socket.on('connect', () => this.socket.emit('ss-join', signallingAddr.toString())) + this.socket.once('connect', () => { + this.dispatchEvent(new CustomEvent('listening')) + }) + } + + _createChannel (intentId: string, srcMultiaddr: string, dstMultiaddr: string) { + const channelOptions: WebRTCReceiverOptions = { + ...this.channelOptions + } + + const channel = new WebRTCReceiver(channelOptions) + + const onError = (err: Error) => { + log.error('incoming connection errored', err) + } + + channel.on('error', onError) + channel.once('close', () => { + channel.removeListener('error', onError) + }) + + channel.on('signal', (signal: Signal) => { + this.socket.emit('ss-handshake', { + intentId, + srcMultiaddr, + dstMultiaddr, + answer: true, + signal + }) + }) + + channel.once('ready', () => { + const maConn = toMultiaddrConnection(channel, { remoteAddr: this.signallingAddr }) + log('new inbound connection %s', maConn.remoteAddr) + + try { + this.upgrader.upgradeInbound(maConn) + .then(conn => { + log('inbound connection %s upgraded', maConn.remoteAddr) + + this.connections.push(maConn) + + const untrackConn = () => { + this.connections = this.connections.filter(c => c !== maConn) + this.channels.delete(intentId) + this.pendingSignals.delete(intentId) + } + + channel.once('close', untrackConn) + + this.dispatchEvent(new CustomEvent('connection', { + detail: conn + })) + this.handler(conn) + }) + .catch(err => { + log.error('inbound connection failed to upgrade', err) + maConn.close().catch(err => { + log.error('inbound connection failed to close after failing to upgrade', err) + }) + }) + } catch (err: any) { + log.error('inbound connection failed to upgrade', err) + maConn.close().catch(err => { + log.error('inbound connection failed to close after failing to upgrade', err) + }) + } + }) + + return channel + } + + handleWsHandshake (offer: HandshakeSignal) { + log('incoming handshake. signal type "%s" is answer %s', offer.signal.type, offer.answer) + + if (offer.answer === true || offer.err != null || offer.intentId == null) { + return + } + + const intentId = offer.intentId + let pendingSignals = this.pendingSignals.get(intentId) + + if (pendingSignals == null) { + pendingSignals = [] + this.pendingSignals.set(intentId, pendingSignals) + } + + pendingSignals.push(offer) + + let channel = this.channels.get(intentId) + + if (channel == null) { + if (offer.signal.type !== 'offer') { + log('handshake is not an offer and channel does not exist, buffering until we receive an offer') + return + } + + log('creating new channel to handle offer handshake') + channel = this._createChannel(offer.intentId, offer.srcMultiaddr, offer.dstMultiaddr) + this.channels.set(intentId, channel) + } else { + log('channel already exists, using it to handle handshake') + } + + while (pendingSignals.length > 0) { + const handshake = pendingSignals.shift() + + if (handshake?.signal != null) { + channel.handleSignal(handshake.signal) + } + } + } + + async close () { + // Close listener + this.socket.emit('ss-leave', this.signallingAddr.toString()) + this.socket.removeAllListeners() + this.socket.close() + + await Promise.all([ + ...this.connections.map(async maConn => await maConn.close()), + ...Array.from(this.channels.values()).map(async channel => await channel.close()) + ]) + + this.dispatchEvent(new CustomEvent('close')) + } +} + +class WebRTCListener extends EventEmitter implements Listener { + private listeningAddr?: Multiaddr + private signallingUrl?: string + private readonly upgrader: Upgrader + private readonly handler: ConnectionHandler + private readonly peerId: PeerId + private readonly transport: WebRTCStar + private readonly options: WebRTCStarListenerOptions + + constructor (upgrader: Upgrader, handler: ConnectionHandler, peerId: PeerId, transport: WebRTCStar, options: WebRTCStarListenerOptions) { + super() + + this.upgrader = upgrader + this.handler = handler + this.peerId = peerId + this.transport = transport + this.options = options + } + + async listen (ma: Multiaddr) { + // Should only be used if not already listening + if (this.listeningAddr != null) { + throw errCode(new Error('listener already in use'), 'ERR_ALREADY_LISTENING') + } + + const defer = pDefer() // eslint-disable-line @typescript-eslint/no-invalid-void-type + + // Should be kept unmodified + this.listeningAddr = ma + + let signallingAddr: Multiaddr + if (!ma.protoCodes().includes(CODE_P2P)) { + signallingAddr = ma.encapsulate(`/p2p/${this.peerId.toString(base58btc)}`) + } else { + signallingAddr = ma + } + + this.signallingUrl = cleanUrlSIO(ma) + + log('connecting to signalling server on: %s', this.signallingUrl) + const server: SignalServer = new SigServer(this.signallingUrl, signallingAddr, this.upgrader, this.handler, this.options.channelOptions) + server.addEventListener('error', (evt) => { + const err = evt.detail + + log('error connecting to signalling server %o', err) + server.close().catch(err => { + log.error('error closing server after error', err) + }) + defer.reject(err) + }) + server.addEventListener('listening', () => { + log('connected to signalling server') + this.dispatchEvent(new CustomEvent('listening')) + defer.resolve() + }) + server.addEventListener('peer', (evt) => { + this.transport.peerDiscovered(evt.detail) + }) + server.addEventListener('connection', (evt) => { + const conn = evt.detail + + if (conn.remoteAddr == null) { + try { + conn.remoteAddr = ma.decapsulateCode(CODE_P2P).encapsulate(`/p2p/${conn.remotePeer.toString(base58btc)}`) + } catch (err) { + log.error('could not determine remote address', err) + } + } + + this.dispatchEvent(new CustomEvent('connection', { + detail: conn + })) + }) + + // Store listen and signal reference addresses + this.transport.sigServers.set(this.signallingUrl, server) + + return await defer.promise + } + + async close () { + if (this.signallingUrl != null) { + const server = this.transport.sigServers.get(this.signallingUrl) + + if (server != null) { + await server.close() + this.transport.sigServers.delete(this.signallingUrl) + } + } + + this.dispatchEvent(new CustomEvent('close')) + + // Reset state + this.listeningAddr = undefined + } + + getAddrs () { + if (this.listeningAddr != null) { + return [ + this.listeningAddr + ] + } + + return [] + } +} + +export function createListener (upgrader: Upgrader, handler: ConnectionHandler, peerId: PeerId, transport: WebRTCStar, options: WebRTCStarListenerOptions) { + return new WebRTCListener(upgrader, handler, peerId, transport, options) +} diff --git a/packages/webrtc-star-transport/src/peer/channel.ts b/packages/webrtc-star-transport/src/peer/channel.ts new file mode 100644 index 00000000..f49b95fd --- /dev/null +++ b/packages/webrtc-star-transport/src/peer/channel.ts @@ -0,0 +1,101 @@ +import errCode from 'err-code' +import defer, { DeferredPromise } from 'p-defer' +import type { Logger } from './interface.js' + +const MAX_BUFFERED_AMOUNT = 64 * 1024 +const CHANNEL_CLOSING_TIMEOUT = 5 * 1000 + +export interface WebRTCDataChannelOptions { + onMessage: (event: MessageEvent) => void + onOpen: () => void + onClose: () => void + onError: (err: Error) => void + log: Logger +} + +export class WebRTCDataChannel { + public label: string + private readonly channel: RTCDataChannel + private readonly closingInterval: NodeJS.Timer + private open: DeferredPromise + private readonly log: Logger + + constructor (channel: RTCDataChannel, opts: WebRTCDataChannelOptions) { + this.label = channel.label + this.open = defer() + this.channel = channel + this.channel.binaryType = 'arraybuffer' + this.log = opts.log + + if (typeof this.channel.bufferedAmountLowThreshold === 'number') { + this.channel.bufferedAmountLowThreshold = MAX_BUFFERED_AMOUNT + } + + channel.addEventListener('message', event => { + opts.onMessage(event) + }) + channel.addEventListener('bufferedamountlow', () => { + this.log('stop backpressure: bufferedAmount %d', this.channel.bufferedAmount) + this.open.resolve() + }) + channel.addEventListener('open', () => { + this.open.resolve() + opts.onOpen() + }) + channel.addEventListener('close', () => { + opts.onClose() + }) + channel.addEventListener('error', event => { + // @ts-expect-error ChannelErrorEvent is just an Event in the types? + if (event.error?.message === 'Transport channel closed') { + return this.close() + } + + // @ts-expect-error ChannelErrorEvent is just an Event in the types? + opts.log.error('channel encounter an error in state "%s" message: "%s" detail: "%s', channel.readyState, event.error?.message, event.error?.errorDetail) // eslint-disable-line @typescript-eslint/restrict-template-expressions + + // @ts-expect-error ChannelErrorEvent is just an Event in the types? + const err = event.error instanceof Error + // @ts-expect-error ChannelErrorEvent is just an Event in the types? + ? event.error + // @ts-expect-error ChannelErrorEvent is just an Event in the types? + : new Error(`datachannel error: ${event.error?.message} ${event.error?.errorDetail}`) // eslint-disable-line @typescript-eslint/restrict-template-expressions + + opts.onError(errCode(err, 'ERR_DATA_CHANNEL')) + }) + + // HACK: Chrome will sometimes get stuck in readyState "closing", let's check for this condition + // https://bugs.chromium.org/p/chromium/issues/detail?id=882743 + let isClosing = false + this.closingInterval = setInterval(() => { // No "onclosing" event + if (channel.readyState === 'closing') { + if (isClosing) { + opts.onClose() // closing timed out: equivalent to onclose firing + } + isClosing = true + } else { + isClosing = false + } + }, CHANNEL_CLOSING_TIMEOUT) + } + + async send (data: Uint8Array) { + await this.open.promise + + this.channel.send(data) + + if (this.channel.bufferedAmount > MAX_BUFFERED_AMOUNT) { + this.log('start backpressure: bufferedAmount %d', this.channel.bufferedAmount) + this.open = defer() + } + } + + close () { + clearInterval(this.closingInterval) + this.channel.close() + } + + get bufferedAmount () { + return this.channel.bufferedAmount + } +} diff --git a/packages/webrtc-star-transport/src/peer/handshake.ts b/packages/webrtc-star-transport/src/peer/handshake.ts new file mode 100644 index 00000000..1fed1c63 --- /dev/null +++ b/packages/webrtc-star-transport/src/peer/handshake.ts @@ -0,0 +1,75 @@ +import { EventEmitter } from 'events' +import errCode from 'err-code' +import type { WRTC, Logger } from './interface.js' +import type { Signal, OfferSignal, AnswerSignal, CandidateSignal, RenegotiateSignal, GoodbyeSignal } from '@libp2p/webrtc-star-protocol' + +export interface WebRTCHandshakeOptions { + log: Logger + peerConnection: RTCPeerConnection + offerOptions?: RTCOfferOptions + wrtc: WRTC +} + +export class WebRTCHandshake extends EventEmitter { + protected log: Logger + protected peerConnection: RTCPeerConnection + protected status: 'idle' | 'negotiating' + protected wrtc: WRTC + + constructor (options: WebRTCHandshakeOptions) { + super() + + this.log = options.log + this.peerConnection = options.peerConnection + this.wrtc = options.wrtc + this.status = 'idle' + + this.peerConnection.addEventListener('negotiationneeded', () => { + this.log('peer connection negotiation needed') + + this.handleRenegotiate({ type: 'renegotiate' }).catch(err => { + this.log.error('could not renegotiate %o', err) + }) + }) + } + + async handleSignal (signal: Signal) { + this.log('incoming signal "%s"', signal.type) + + if (signal.type === 'offer') { + return await this.handleOffer(signal) + } else if (signal.type === 'answer') { + return await this.handleAnswer(signal) + } else if (signal.type === 'candidate') { + return await this.handleCandidate(signal) + } else if (signal.type === 'renegotiate') { + return await this.handleRenegotiate(signal) + } else if (signal.type === 'goodbye') { + return await this.handleGoodye(signal) + } else { + // @ts-expect-error all types are handled above + this.log(`Unknown signal type ${signal.type}`) // eslint-disable-line @typescript-eslint/restrict-template-expressions + } + } + + async handleOffer (signal: OfferSignal) {} + async handleAnswer (signal: AnswerSignal) {} + async handleRenegotiate (signal: RenegotiateSignal) {} + async handleGoodye (signal: GoodbyeSignal) { + this.peerConnection.close() + } + + async handleCandidate (signal: CandidateSignal) { + const iceCandidate = new this.wrtc.RTCIceCandidate(signal.candidate) + + try { + await this.peerConnection.addIceCandidate(iceCandidate) + } catch (err) { + if (iceCandidate.address == null || iceCandidate.address.endsWith('.local')) { + this.log('ignoring unsupported ICE candidate.') + } else { + throw errCode(err, 'ERR_ADD_ICE_CANDIDATE') + } + } + } +} diff --git a/packages/webrtc-star-transport/src/peer/index.ts b/packages/webrtc-star-transport/src/peer/index.ts new file mode 100644 index 00000000..c9a2ba13 --- /dev/null +++ b/packages/webrtc-star-transport/src/peer/index.ts @@ -0,0 +1,3 @@ + +export { WebRTCReceiver } from './receiver.js' +export { WebRTCInitiator } from './initiator.js' diff --git a/packages/webrtc-star-transport/src/peer/initiator.ts b/packages/webrtc-star-transport/src/peer/initiator.ts new file mode 100644 index 00000000..64521caa --- /dev/null +++ b/packages/webrtc-star-transport/src/peer/initiator.ts @@ -0,0 +1,105 @@ +import { WebRTCPeer } from './peer.js' +import { WebRTCHandshake } from './handshake.js' +import randombytes from 'iso-random-stream/src/random.js' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { pEvent } from 'p-event' +import delay from 'delay' +import type { WebRTCPeerOptions } from './peer.js' +import type { WebRTCHandshakeOptions } from './handshake.js' +import type { AnswerSignal, Signal } from '@libp2p/webrtc-star-protocol' +import type { WebRTCPeerEvents } from './interface.js' + +const ICECOMPLETE_TIMEOUT = 1000 + +export interface WebRTCInitiatorOptions extends WebRTCPeerOptions { + dataChannelLabel?: string + dataChannelInit?: RTCDataChannelInit + offerOptions?: RTCOfferOptions +} + +export class WebRTCInitiator extends WebRTCPeer implements WebRTCPeerEvents { + private readonly handshake: WebRTCInitiatorHandshake + + constructor (opts: WebRTCInitiatorOptions) { + super({ + ...opts, + logPrefix: 'initiator' + }) + + this.handleDataChannelEvent({ + channel: this.peerConnection.createDataChannel( + opts.dataChannelLabel ?? uint8ArrayToString(randombytes(20), 'hex').slice(0, 7), + opts.dataChannelInit + ) + }) + + this.handshake = new WebRTCInitiatorHandshake({ + log: this.log, + peerConnection: this.peerConnection, + wrtc: this.wrtc, + offerOptions: opts.offerOptions + }) + this.handshake.on('signal', event => this.emit('signal', event)) + } + + handleSignal (signal: Signal) { + this.handshake.handleSignal(signal).catch(err => { + this.log('error handling signal %o %o', signal, err) + }) + } +} + +interface WebRTCInitiatorHandshakeOptions extends WebRTCHandshakeOptions { + offerOptions?: RTCOfferOptions +} + +class WebRTCInitiatorHandshake extends WebRTCHandshake { + private readonly options: WebRTCInitiatorHandshakeOptions + + constructor (options: WebRTCInitiatorHandshakeOptions) { + super(options) + + this.options = options + this.status = 'idle' + + this.peerConnection.addEventListener('icecandidate', (event) => { + if (event.candidate == null) { + return + } + + this.emit('signal', { + type: 'candidate', + candidate: { + candidate: event.candidate.candidate, + sdpMLineIndex: event.candidate.sdpMLineIndex, + sdpMid: event.candidate.sdpMid + } + }) + this.emit('ice-candidate') + }) + } + + async handleRenegotiate () { + if (this.status === 'negotiating') { + this.log('already negotiating, queueing') + return + } + + this.status = 'negotiating' + + const offer = await this.peerConnection.createOffer(this.options.offerOptions) + + await this.peerConnection.setLocalDescription(offer) + + // wait for at least one candidate before sending the offer + await pEvent(this, 'ice-candidate') + await delay(ICECOMPLETE_TIMEOUT) + + this.emit('signal', this.peerConnection.localDescription) + } + + async handleAnswer (signal: AnswerSignal) { + await this.peerConnection.setRemoteDescription(new this.wrtc.RTCSessionDescription(signal)) + this.status = 'idle' + } +} diff --git a/packages/webrtc-star-transport/src/peer/interface.ts b/packages/webrtc-star-transport/src/peer/interface.ts new file mode 100644 index 00000000..af2d3b64 --- /dev/null +++ b/packages/webrtc-star-transport/src/peer/interface.ts @@ -0,0 +1,25 @@ +import type { EventEmitter } from 'events' +import type { Signal } from '@libp2p/webrtc-star-protocol' + +export interface Logger { + (...opts: any[]): void + error: (...opts: any[]) => void +} + +export interface WRTC { + RTCPeerConnection: typeof RTCPeerConnection + RTCSessionDescription: typeof RTCSessionDescription + RTCIceCandidate: typeof RTCIceCandidate +} + +interface WebRCTEvents { + 'signal': Signal + 'ready': never + 'close': never +} + +export interface WebRTCPeerEvents extends EventEmitter { + on: ( (event: U, listener: (event: WebRCTEvents[U]) => void) => this) + once: ( (event: U, listener: (event: WebRCTEvents[U]) => void) => this) + emit: ( (name: U, event: WebRCTEvents[U]) => boolean) +} diff --git a/packages/webrtc-star-transport/src/peer/peer.ts b/packages/webrtc-star-transport/src/peer/peer.ts new file mode 100644 index 00000000..033444e1 --- /dev/null +++ b/packages/webrtc-star-transport/src/peer/peer.ts @@ -0,0 +1,146 @@ +import { logger } from '@libp2p/logger' +import { EventEmitter } from 'events' +import errCode from 'err-code' +import randombytes from 'iso-random-stream/src/random.js' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { Pushable, pushable } from 'it-pushable' +import defer, { DeferredPromise } from 'p-defer' +import { WebRTCDataChannel } from './channel.js' +import delay from 'delay' +import type { WRTC } from './interface.js' +import type { Duplex } from 'it-stream-types' + +// const ICECOMPLETE_TIMEOUT = 5 * 1000 + +export interface Logger { + (...opts: any[]): void + error: (...opts: any[]) => void +} + +const DEFAULT_PEER_CONNECTION_CONFIG: RTCConfiguration = { + iceServers: [{ + urls: [ + 'stun:stun.l.google.com:19302', + 'stun:global.stun.twilio.com:3478' + ] + }] +} + +function getBrowserRTC (): WRTC { + if (typeof globalThis === 'undefined') { + throw errCode(new Error('No WebRTC support detected'), 'ERR_WEBRTC_SUPPORT') + } + + const wrtc = { + // @ts-expect-error browser prefixed property names + RTCPeerConnection: globalThis.RTCPeerConnection ?? globalThis.mozRTCPeerConnection ?? globalThis.webkitRTCPeerConnection, + // @ts-expect-error browser prefixed property names + RTCSessionDescription: globalThis.RTCSessionDescription ?? globalThis.mozRTCSessionDescription ?? globalThis.webkitRTCSessionDescription, + // @ts-expect-error browser prefixed property names + RTCIceCandidate: globalThis.RTCIceCandidate ?? globalThis.mozRTCIceCandidate ?? globalThis.webkitRTCIceCandidate + } + + if (wrtc.RTCPeerConnection == null) { + throw errCode(new Error('No WebRTC support detected'), 'ERR_WEBRTC_SUPPORT') + } + + return wrtc +} + +export interface WebRTCPeerOptions { + id?: string + wrtc?: WRTC + peerConnectionConfig?: RTCConfiguration +} + +export class WebRTCPeer extends EventEmitter implements Duplex { + public id: string + public source: Pushable + public sink: (source: AsyncIterable | Iterable) => Promise + public closed: boolean + protected wrtc: WRTC + protected peerConnection: RTCPeerConnection + protected channel?: WebRTCDataChannel + protected log: Logger + private readonly connected: DeferredPromise + + constructor (opts: WebRTCPeerOptions & { logPrefix: string }) { + super() + + this.id = opts.id ?? uint8ArrayToString(randombytes(4), 'hex').slice(0, 7) + this.log = logger(`libp2p:webrtc-star:peer:${opts.logPrefix}:${this.id}`) + this.wrtc = opts.wrtc ?? getBrowserRTC() + this.peerConnection = new this.wrtc.RTCPeerConnection( + Object.assign({}, DEFAULT_PEER_CONNECTION_CONFIG, opts.peerConnectionConfig) + ) + this.closed = false + this.connected = defer() + + // duplex properties + this.source = pushable() + this.sink = async (source) => { + await this.connected.promise + + if (this.channel == null) { + throw errCode(new Error('Connected but no channel?!'), 'ERR_DATA_CHANNEL') + } + + for await (const buf of source) { + await this.channel.send(buf) + } + + await this.close() + } + } + + protected handleDataChannelEvent (event: { channel?: RTCDataChannel}) { + if (event.channel == null) { + // In some situations `pc.createDataChannel()` returns `undefined` (in wrtc), + // which is invalid behavior. Handle it gracefully. + // See: https://github.com/feross/simple-peer/issues/163 + this.close(errCode(new Error('Data channel event is missing `channel` property'), 'ERR_DATA_CHANNEL')) + .catch(err => { + this.log('Error closing after event channel was found to be null', err) + }) + + return + } + + this.channel = new WebRTCDataChannel(event.channel, { + log: this.log, + onMessage: (event) => { + this.source.push(new Uint8Array(event.data)) + }, + onOpen: () => { + this.connected.resolve() + this.emit('ready') + }, + onClose: () => { + this.close().catch(err => { + this.log('error closing connection after channel close', err) + }) + }, + onError: (err) => { + this.close(err).catch(err => { + this.log('error closing connection after channel error', err) + }) + } + }) + } + + async close (err?: Error) { + this.closed = true + + if (err == null && this.channel != null) { + // wait for the channel to flush all data before closing the channel + while (this.channel.bufferedAmount > 0) { + await delay(100) + } + } + + this.channel?.close() + this.peerConnection.close() + this.source.end(err) + this.emit('close') + } +} diff --git a/packages/webrtc-star-transport/src/peer/receiver.ts b/packages/webrtc-star-transport/src/peer/receiver.ts new file mode 100644 index 00000000..396c0d65 --- /dev/null +++ b/packages/webrtc-star-transport/src/peer/receiver.ts @@ -0,0 +1,89 @@ +import { WebRTCPeer } from './peer.js' +import { WebRTCHandshake } from './handshake.js' +import type { WebRTCPeerOptions } from './peer.js' +import type { WebRTCHandshakeOptions } from './handshake.js' +import type { WebRTCPeerEvents } from './interface.js' +import type { OfferSignal, Signal, CandidateSignal } from '@libp2p/webrtc-star-protocol' + +export interface WebRTCReceiverOptions extends WebRTCPeerOptions { + answerOptions?: RTCAnswerOptions +} + +export class WebRTCReceiver extends WebRTCPeer implements WebRTCPeerEvents { + private readonly handshake: WebRTCReceiverHandshake + + constructor (opts: WebRTCReceiverOptions) { + super({ + ...opts, + logPrefix: 'receiver' + }) + + this.handshake = new WebRTCReceiverHandshake({ + log: this.log, + peerConnection: this.peerConnection, + wrtc: this.wrtc, + answerOptions: opts.answerOptions + }) + + this.handshake.on('signal', event => this.emit('signal', event)) + this.peerConnection.addEventListener('datachannel', (event) => { + this.handleDataChannelEvent(event) + }) + } + + handleSignal (signal: Signal) { + this.handshake.handleSignal(signal).catch(err => { + this.log('error handling signal %o %o', signal, err) + }) + } +} + +interface WebRTCReceiverHandshakeOptions extends WebRTCHandshakeOptions { + answerOptions?: RTCAnswerOptions +} + +class WebRTCReceiverHandshake extends WebRTCHandshake { + private readonly options: WebRTCReceiverHandshakeOptions + private iceCandidates: CandidateSignal[] + + constructor (options: WebRTCReceiverHandshakeOptions) { + super(options) + + this.options = options + this.status = 'idle' + this.iceCandidates = [] + } + + async handleRenegotiate () { + this.emit('signal', { + type: 'renegotiate' + }) + } + + async handleOffer (signal: OfferSignal) { + await this.peerConnection.setRemoteDescription(new this.wrtc.RTCSessionDescription(signal)) + + // add any candidates we were send before the offer arrived + for (const candidate of this.iceCandidates) { + await this.handleCandidate(candidate) + } + this.iceCandidates = [] + + const answer = await this.peerConnection.createAnswer(this.options.answerOptions) + + await this.peerConnection.setLocalDescription(answer) + + this.emit('signal', this.peerConnection.localDescription) + } + + async handleCandidate (signal: CandidateSignal) { + if (this.peerConnection.remoteDescription == null || this.peerConnection.remoteDescription.type == null) { + // we haven't been sent an offer yet, cache the remote ICE candidates + this.iceCandidates.push(signal) + + return + } + + await super.handleCandidate(signal) + } +} diff --git a/packages/webrtc-star-transport/src/socket-to-conn.ts b/packages/webrtc-star-transport/src/socket-to-conn.ts new file mode 100644 index 00000000..410b05d9 --- /dev/null +++ b/packages/webrtc-star-transport/src/socket-to-conn.ts @@ -0,0 +1,88 @@ +import { abortableSource } from 'abortable-iterator' +import { CLOSE_TIMEOUT } from './constants.js' +import { logger } from '@libp2p/logger' +import type { MultiaddrConnection } from '@libp2p/interfaces/transport' +import type { WebRTCPeer } from './peer/peer.js' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Multiaddr } from '@multiformats/multiaddr' + +const log = logger('libp2p:webrtc-star:socket') + +export interface ToMultiaddrConnectionOptions extends AbortOptions { + remoteAddr: Multiaddr +} + +/** + * Convert a socket into a MultiaddrConnection + * https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/transport#multiaddrconnection + */ +export function toMultiaddrConnection (socket: WebRTCPeer, options: ToMultiaddrConnectionOptions): MultiaddrConnection { + const { sink, source } = socket + + const maConn: MultiaddrConnection = { + remoteAddr: options.remoteAddr, + + async sink (source) { + if (options.signal != null) { + source = abortableSource(source, options.signal) + } + + try { + await sink(source) + } catch (err: any) { + // If aborted we can safely ignore + if (err.type !== 'aborted') { + // If the source errored the socket will already have been destroyed by + // toIterable.duplex(). If the socket errored it will already be + // destroyed. There's nothing to do here except log the error & return. + log.error(err) + } + } + }, + + source: (options.signal != null) ? abortableSource(source, options.signal) : source, + + timeline: { open: Date.now() }, + + async close () { + if (socket.closed) { + return + } + + const start = Date.now() + + // Attempt to end the socket. If it takes longer to close than the + // timeout, destroy it manually. + const timeout = setTimeout(() => { + if (maConn.remoteAddr != null) { + const { host, port } = maConn.remoteAddr.toOptions() + log('timeout closing socket to %s:%s after %dms, destroying it manually', + host, port, Date.now() - start) + } + + if (!socket.closed) { + socket.close().catch(err => { + log.error('could not close socket', err) + }) + } + }, CLOSE_TIMEOUT) + + try { + await socket.close() + } finally { + clearTimeout(timeout) + } + } + } + + 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 == null) { + maConn.timeline.close = Date.now() + } + }) + + return maConn +} diff --git a/packages/transport/src/utils.js b/packages/webrtc-star-transport/src/utils.ts similarity index 54% rename from packages/transport/src/utils.js rename to packages/webrtc-star-transport/src/utils.ts index 42278459..711a396a 100644 --- a/packages/transport/src/utils.js +++ b/packages/webrtc-star-transport/src/utils.ts @@ -1,40 +1,44 @@ -'use strict' +import { Multiaddr } from '@multiformats/multiaddr' -const { Multiaddr } = require('multiaddr') - -function cleanUrlSIO (ma) { +export function cleanUrlSIO (ma: Multiaddr) { const maStrSplit = ma.toString().split('/') const tcpProto = ma.protos()[1].name const wsProto = ma.protos()[2].name const tcpPort = ma.stringTuples()[1][1] if (tcpProto !== 'tcp' || (wsProto !== 'ws' && wsProto !== 'wss')) { - throw new Error('invalid multiaddr: ' + ma.toString()) + throw new Error(`invalid multiaddr: ${ma.toString()}`) } if (!Multiaddr.isName(ma)) { - return 'http://' + maStrSplit[2] + ':' + maStrSplit[4] + return `http://${maStrSplit[2]}:${maStrSplit[4]}` } if (wsProto === 'ws') { - return 'http://' + maStrSplit[2] + (tcpPort === '80' ? '' : ':' + tcpPort) + return `http://${maStrSplit[2]}${tcpPort == null || tcpPort === '80' ? '' : `:${tcpPort}`}` } if (wsProto === 'wss') { - return 'https://' + maStrSplit[2] + (tcpPort === '443' ? '' : ':' + tcpPort) + return `https://${maStrSplit[2]}${tcpPort == null || tcpPort === '443' ? '' : `:${tcpPort}`}` } + + throw new Error('invalid multiaddr: ' + ma.toString()) } -function cleanMultiaddr (maStr) { +export function cleanMultiaddr (maStr: string) { const legacy = '/libp2p-webrtc-star' - if (maStr.indexOf(legacy) !== -1) { + if (maStr.includes(legacy)) { maStr = maStr.substring(legacy.length, maStr.length) let ma = new Multiaddr(maStr) const tuppleIPFS = ma.stringTuples().filter((tupple) => { return tupple[0] === 421 // ipfs code })[0] + if (tuppleIPFS[1] == null) { + throw new Error('invalid multiaddr: ' + maStr) + } + ma = ma.decapsulate('p2p') ma = ma.encapsulate('/p2p-webrtc-star') ma = ma.encapsulate(`/p2p/${tuppleIPFS[1]}`) @@ -43,8 +47,3 @@ function cleanMultiaddr (maStr) { return maStr } - -module.exports = { - cleanUrlSIO, - cleanMultiaddr -} diff --git a/packages/webrtc-star-transport/test/browser.ts b/packages/webrtc-star-transport/test/browser.ts new file mode 100644 index 00000000..bdef6ddf --- /dev/null +++ b/packages/webrtc-star-transport/test/browser.ts @@ -0,0 +1,23 @@ +/* eslint-env mocha */ + +import { WebRTCStar } from '../src/index.js' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { mockUpgrader } from '@libp2p/interface-compliance-tests/transport/utils' +import dialTests from './transport/dial.js' +import listenTests from './transport/listen.js' +import discoveryTests from './transport/discovery.js' +import filterTests from './transport/filter.js' + +describe('browser RTC', () => { + const create = async () => { + return new WebRTCStar({ + peerId: await createEd25519PeerId(), + upgrader: mockUpgrader() + }) + } + + dialTests(create) + listenTests(create) + discoveryTests(create) + filterTests(create) +}) diff --git a/packages/webrtc-star-transport/test/compliance.spec.ts b/packages/webrtc-star-transport/test/compliance.spec.ts new file mode 100644 index 00000000..471efe3b --- /dev/null +++ b/packages/webrtc-star-transport/test/compliance.spec.ts @@ -0,0 +1,78 @@ +/* eslint-env mocha */ + +// @ts-expect-error no types +import wrtc from 'wrtc' +import sinon from 'sinon' +import { Multiaddr } from '@multiformats/multiaddr' +import testsTransport from '@libp2p/interface-compliance-tests/transport' +import testsDiscovery from '@libp2p/interface-compliance-tests/peer-discovery' +import { WebRTCStar } from '../src/index.js' +import { PeerId } from '@libp2p/peer-id' +import { mockUpgrader } from '@libp2p/interface-compliance-tests/transport/utils' +import pWaitFor from 'p-wait-for' + +describe('interface-transport compliance', function () { + testsTransport({ + async setup (args) { + if (args == null) { + throw new Error('No args') + } + + const { upgrader } = args + const peerId = PeerId.fromString('QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2a') + const ws = new WebRTCStar({ upgrader, wrtc, peerId }) + + const base = (id: string) => { + return `/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/p2p/${id}` + } + + const addrs = [ + new Multiaddr(base('QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2a')), + new Multiaddr(base('QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2b')), + new Multiaddr(base('QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2c')) + ] + + // Used by the dial tests to simulate a delayed connect + const connector = { + delay () {}, + restore () { + sinon.restore() + } + } + + return { transport: ws, addrs, connector } + }, + async teardown () {} + }) +}) + +describe('interface-discovery compliance', () => { + let intervalId: NodeJS.Timer + + testsDiscovery({ + async setup () { + const peerId = PeerId.fromString('QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2d') + const ws = new WebRTCStar({ upgrader: mockUpgrader(), wrtc, peerId }) + const maStr = '/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2d' + + // only discover peers while discovery is running + void pWaitFor(() => ws.discovery.isStarted()) + .then(() => { + intervalId = setInterval(() => { + if (ws.discovery.isStarted()) { + ws.peerDiscovered(maStr) + } + }, 1000) + + if (intervalId.unref != null) { + intervalId.unref() + } + }) + + return ws.discovery + }, + async teardown () { + clearInterval(intervalId) + } + }) +}) diff --git a/packages/webrtc-star-transport/test/node.ts b/packages/webrtc-star-transport/test/node.ts new file mode 100644 index 00000000..33c2521a --- /dev/null +++ b/packages/webrtc-star-transport/test/node.ts @@ -0,0 +1,55 @@ +/* eslint-env mocha */ + +// @ts-expect-error no types +import wrtc from 'wrtc' +// @ts-expect-error no types +import electronWebRTC from 'electron-webrtc' +import { createEd25519PeerId } from '@libp2p/peer-id-factory' +import { mockUpgrader } from '@libp2p/interface-compliance-tests/transport/utils' +import { WebRTCStar } from '../src/index.js' +import dialTests from './transport/dial.js' +import listenTests from './transport/listen.js' +import discoveryTests from './transport/discovery.js' +import filterTests from './transport/filter.js' +import multipleSignalServersTests from './transport/multiple-signal-servers.js' +import trackTests from './transport/track.js' +import reconnectTests from './transport/reconnect.node.js' + +describe('transport: with wrtc', () => { + const create = async () => { + const peerId = await createEd25519PeerId() + return new WebRTCStar({ + peerId, + upgrader: mockUpgrader(), + wrtc + }) + } + + dialTests(create) + listenTests(create) + multipleSignalServersTests(create) + trackTests(create) + discoveryTests(create) + filterTests(create) + reconnectTests(create) +}) + +// TODO: Electron-webrtc is currently unreliable on linux +describe.skip('transport: with electron-webrtc', () => { + const create = async () => { + const peerId = await createEd25519PeerId() + return new WebRTCStar({ + peerId, + upgrader: mockUpgrader(), + wrtc: electronWebRTC() + }) + } + + dialTests(create) + listenTests(create) + multipleSignalServersTests(create) + trackTests(create) + discoveryTests(create) + filterTests(create) + reconnectTests(create) +}) diff --git a/packages/webrtc-star-transport/test/transport/dial.ts b/packages/webrtc-star-transport/test/transport/dial.ts new file mode 100644 index 00000000..1e0923b8 --- /dev/null +++ b/packages/webrtc-star-transport/test/transport/dial.ts @@ -0,0 +1,236 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from '@multiformats/multiaddr' +import { pipe } from 'it-pipe' +import all from 'it-all' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import sinon from 'sinon' +import { WebRTCReceiver } from '../../src/peer/receiver.js' +import { cleanUrlSIO } from '../../src/utils.js' +import type { WebRTCStar } from '../../src/index.js' +import type { Listener } from '@libp2p/interfaces/src/transport' +import pWaitFor from 'p-wait-for' +import type { HandshakeSignal } from '@libp2p/webrtc-star-protocol' + +export default (create: () => Promise) => { + describe('dial', () => { + let ws1: WebRTCStar + let ws2: WebRTCStar + let ma1: Multiaddr + let ma2: Multiaddr + let listener1: Listener + let listener2: Listener + + const maHSDNS = new Multiaddr('/dns/star-signal.cloud.ipfs.team/wss/p2p-webrtc-star') + const maHSIP = new Multiaddr('/ip4/188.166.203.82/tcp/20000/wss/p2p-webrtc-star') + const maLS = new Multiaddr('/ip4/127.0.0.1/tcp/15555/wss/p2p-webrtc-star') + + if (process.env.WEBRTC_STAR_REMOTE_SIGNAL_DNS != null) { + // test with deployed signalling server using DNS + console.log('Using DNS:', maHSDNS) // eslint-disable-line no-console + ma1 = maHSDNS + ma2 = maHSDNS + } else if (process.env.WEBRTC_STAR_REMOTE_SIGNAL_IP != null) { + // test with deployed signalling server using IP + console.log('Using IP:', maHSIP) // eslint-disable-line no-console + ma1 = maHSIP + ma2 = maHSIP + } else { + ma1 = maLS + ma2 = maLS + } + + beforeEach(async () => { + // first + ws1 = await create() + listener1 = ws1.createListener({ + handler: (conn) => { + expect(conn.remoteAddr).to.exist() + + void conn.newStream(['echo']) + .then(({ stream }) => { + void pipe(stream, stream) + }) + } + }) + + // second + ws2 = await create() + listener2 = ws2.createListener({ + handler: (conn) => { + expect(conn.remoteAddr).to.exist() + + void conn.newStream(['echo']) + .then(({ stream }) => { + void pipe(stream, stream) + }) + } + }) + + await Promise.all([ + listener1.listen(ma1), + listener2.listen(ma2) + ]) + }) + + afterEach(async () => { + await Promise.all( + [listener1, listener2].map(async l => await l.close()) + ) + }) + + it('dial on IPv4, check promise', async function () { + // Use one of the signal addresses + const [sigRefs] = ws2.sigServers.values() + + const conn = await ws1.dial(sigRefs.signallingAddr) + const { stream } = await conn.newStream(['echo']) + const data = uint8ArrayFromString('some data') + const values = await pipe( + [data], + stream, + async (source) => await all(source) + ) + + expect(values).to.deep.equal([data]) + }) + + it('dial offline / non-exist()ent node on IPv4, check promise rejected', function () { + const maOffline = new Multiaddr('/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2f') + + return expect(ws1.dial(maOffline)).to.eventually.be.rejected().and.have.property('code', 'ERR_SIGNALLING_FAILED') + }) + + it('dial unknown signal server, check promise rejected', function () { + const maOffline = new Multiaddr('/ip4/127.0.0.1/tcp/15559/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooo2f') + + return expect(ws1.dial(maOffline)).to.eventually.be.rejected().and.have.property('code', 'ERR_UNKNOWN_SIGNAL_SERVER') + }) + + it.skip('dial on IPv6', (done) => { + // TODO IPv6 not supported yet + }) + + it('receive ws-handshake event without intentId, check channel not created', () => { + const server = ws2.sigServers.get(cleanUrlSIO(ma2)) + + if (server == null) { + throw new Error(`No sigserver found for ma ${ma2.toString()}`) + } + + server.socket.emit('ws-handshake', { + // @ts-expect-error invalid field + intentId: null, + srcMultiaddr: ma1.toString(), + dstMultiaddr: ma2.toString(), + // @ts-expect-error invalid field + signal: {} + }) + + expect(server.channels.size).to.equal(0) + }) + + it('receive ws-handshake event but channel already exists, check channel.handleSignal called', async () => { + const server = ws2.sigServers.get(cleanUrlSIO(ma2)) + + if (server == null) { + throw new Error(`No sigserver found for ma ${ma2.toString()}`) + } + + const channel = { handleSignal: sinon.fake(), close: () => {} } + // @ts-expect-error invalid field + server.channels.set('intent-id', channel) + + const listeners = server.socket.listeners('ws-handshake') + expect(listeners.length).to.equal(1) + + const message: HandshakeSignal = { + intentId: 'intent-id', + srcMultiaddr: ma1.toString(), + dstMultiaddr: ma2.toString(), + signal: { + type: 'candidate', + candidate: { + candidate: 'derp' + } + } + } + + listeners[0](message) + + await pWaitFor(() => channel.handleSignal.callCount === 1) + + expect(channel.handleSignal.callCount).to.equal(1) + }) + + it('receive ws-handshake event but signal type is not offer, check message saved to pendingIntents', () => { + const server = ws2.sigServers.get(cleanUrlSIO(ma2)) + + if (server == null) { + throw new Error(`No sigserver found for ma ${ma2.toString()}`) + } + + const message: HandshakeSignal = { + intentId: 'intent-id', + srcMultiaddr: ma1.toString(), + dstMultiaddr: ma2.toString(), + signal: { + type: 'candidate', + candidate: { + candidate: 'derp' + } + } + } + + const listeners = server.socket.listeners('ws-handshake') + expect(listeners.length).to.equal(1) + listeners[0](message) + + expect(server.pendingSignals.get('intent-id')).to.deep.equal([message]) + }) + + it('receive ws-handshake event, the signal type is offer and exists pending intents, check pending intents consumed', () => { + const server = ws2.sigServers.get(cleanUrlSIO(ma2)) + + if (server == null) { + throw new Error(`No sigserver found for ma ${ma2.toString()}`) + } + + const message1: HandshakeSignal = { + intentId: 'intent-id', + srcMultiaddr: ma1.toString(), + dstMultiaddr: ma2.toString(), + signal: { + type: 'candidate', + candidate: { + candidate: 'derp' + } + } + } + + server.pendingSignals.set('intent-id', [message1]) + + const fake = sinon.fake() + const stub = sinon.stub(WebRTCReceiver.prototype, 'handleSignal').callsFake(fake) + const message2: HandshakeSignal = { + intentId: 'intent-id', + srcMultiaddr: ma1.toString(), + dstMultiaddr: ma2.toString(), + signal: { + type: 'offer', + sdp: 'v=hello' + } + } + + const listeners = server.socket.listeners('ws-handshake') + expect(listeners.length).to.equal(1) + listeners[0](message2) + + expect(server.channels.size).to.equal(1) + expect(server.pendingSignals.get('intent-id')).to.have.lengthOf(0) + // create the channel and consume the pending intent + expect(fake.callCount).to.equal(2) + stub.restore() + }) + }) +} diff --git a/packages/webrtc-star-transport/test/transport/discovery.ts b/packages/webrtc-star-transport/test/transport/discovery.ts new file mode 100644 index 00000000..1f367935 --- /dev/null +++ b/packages/webrtc-star-transport/test/transport/discovery.ts @@ -0,0 +1,105 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from '@multiformats/multiaddr' +import { pEvent } from 'p-event' +import { cleanUrlSIO } from '../../src/utils.js' +import type { WebRTCStar } from '../../src/index.js' +import type { Listener } from '@libp2p/interfaces/src/transport' + +export default (create: () => Promise) => { + describe('peer discovery', () => { + let ws1: WebRTCStar + let ws2: WebRTCStar + let ws3: WebRTCStar + let ws4: WebRTCStar + let ws1Listener: Listener + const signallerAddr = new Multiaddr('/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star') + + after(async () => { + if (ws1Listener != null) { + await ws1Listener.close() + } + }) + + it('listen on the first', async () => { + ws1 = await create() + ws1Listener = ws1.createListener() + await ws1.discovery.start() + + await ws1Listener.listen(signallerAddr) + }) + + it('listen on the second, discover the first', async () => { + ws2 = await create() + const listener = ws2.createListener() + await ws2.discovery.start() + + await listener.listen(signallerAddr) + const { detail: { multiaddrs } } = await pEvent<'peer', { detail: { multiaddrs: Multiaddr[] } }>(ws1.discovery, 'peer') + + // Check first of the signal addresses + const [sigRefs] = ws2.sigServers.values() + + expect(multiaddrs.map(m => m.toString())).to.include(sigRefs.signallingAddr.toString()) + + await listener.close() + }) + + // this test is mostly validating the non-discovery test mechanism works + it('listen on the third, verify ws-peer is discovered', async () => { + const server = ws1.sigServers.get(cleanUrlSIO(signallerAddr)) + + if (server == null) { + throw new Error(`No sigserver found for ma ${signallerAddr.toString()}`) + } + + let discoveredPeer = false + + ws1.discovery.addEventListener('peer', () => { + discoveredPeer = true + }, { + once: true + }) + + ws3 = await create() + const listener = ws3.createListener() + await ws3.discovery.start() + + await listener.listen(signallerAddr) + await pEvent(server.socket, 'ws-peer') + + expect(discoveredPeer).to.equal(true) + + await listener.close() + }) + + it('listen on the fourth, ws-peer is not discovered', async () => { + const server = ws1.sigServers.get(cleanUrlSIO(signallerAddr)) + + if (server == null) { + throw new Error(`No sigserver found for ma ${signallerAddr.toString()}`) + } + + let discoveredPeer = false + + ws1.discovery.addEventListener('peer', () => { + discoveredPeer = true + }, { + once: true + }) + + void ws1.discovery.stop() + ws4 = await create() + const listener = ws4.createListener() + void ws4.discovery.start() + + await listener.listen(signallerAddr) + await pEvent(server.socket, 'ws-peer') + + expect(discoveredPeer).to.equal(false) + + await listener.close() + }) + }) +} diff --git a/packages/transport/test/transport/filter.js b/packages/webrtc-star-transport/test/transport/filter.ts similarity index 88% rename from packages/transport/test/transport/filter.js rename to packages/webrtc-star-transport/test/transport/filter.ts index 316faa47..ac046e11 100644 --- a/packages/transport/test/transport/filter.js +++ b/packages/webrtc-star-transport/test/transport/filter.ts @@ -1,11 +1,10 @@ /* eslint-env mocha */ -'use strict' -const { expect } = require('aegir/utils/chai') +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from '@multiformats/multiaddr' +import type { WebRTCStar } from '../../src/index.js' -const { Multiaddr } = require('multiaddr') - -module.exports = (create) => { +export default (create: () => Promise) => { describe('filter', () => { it('filters non valid webrtc-star multiaddrs', async () => { const ws = await create() @@ -33,7 +32,7 @@ module.exports = (create) => { const ws = await create() const ma = new Multiaddr('/ip4/127.0.0.1/tcp/9090/ws/p2p-webrtc-star/p2p/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSoooo1') - const filtered = ws.filter(ma) + const filtered = ws.filter([ma]) expect(filtered.length).to.equal(1) }) }) diff --git a/packages/webrtc-star-transport/test/transport/instance.spec.ts b/packages/webrtc-star-transport/test/transport/instance.spec.ts new file mode 100644 index 00000000..364fdae1 --- /dev/null +++ b/packages/webrtc-star-transport/test/transport/instance.spec.ts @@ -0,0 +1,21 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/utils/chai.js' +import { WebRTCStar } from '../../src/index.js' +import { mockUpgrader } from '@libp2p/interface-compliance-tests/transport/utils' +import { PeerId } from '@libp2p/peer-id' + +describe('instantiate the transport', () => { + it('create', () => { + const wstar = new WebRTCStar({ + peerId: PeerId.fromString('12D3KooWJKCJW8Y26pRFNv78TCMGLNTfyN8oKaFswMRYXTzSbSst'), + upgrader: mockUpgrader() + }) + expect(wstar).to.exist() + }) + + it('create without new', () => { + // @ts-expect-error WebRTCStar is a class and needs new + expect(() => WebRTCStar()).to.throw() + }) +}) diff --git a/packages/transport/test/transport/listen.js b/packages/webrtc-star-transport/test/transport/listen.ts similarity index 67% rename from packages/transport/test/transport/listen.js rename to packages/webrtc-star-transport/test/transport/listen.ts index 6003e416..d937efd6 100644 --- a/packages/transport/test/transport/listen.js +++ b/packages/webrtc-star-transport/test/transport/listen.ts @@ -1,14 +1,13 @@ /* eslint-env mocha */ -'use strict' +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from '@multiformats/multiaddr' +import { pEvent } from 'p-event' +import type { WebRTCStar } from '../../src/index.js' -const { expect } = require('aegir/utils/chai') -const { Multiaddr } = require('multiaddr') - -module.exports = (create) => { +export default (create: () => Promise) => { describe('listen', () => { - let ws - + let ws: WebRTCStar const ma = new Multiaddr('/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star') before(async () => { @@ -16,36 +15,26 @@ module.exports = (create) => { }) it('listen, check for promise', async () => { - const listener = ws.createListener(() => {}) + const listener = ws.createListener() await listener.listen(ma) await listener.close() }) it('listen, check for listening event', async () => { - const listener = ws.createListener(() => {}) - - const p = new Promise((resolve) => { - listener.once('listening', () => { - listener.close() - resolve() - }) - }) + const listener = ws.createListener() - await listener.listen(ma) - await p + void listener.listen(ma) + await pEvent(listener, 'listening') + await listener.close() }) it('listen, check for the close event', async () => { - const listener = ws.createListener(() => {}) + const listener = ws.createListener() await listener.listen(ma) - const p = new Promise((resolve) => { - listener.once('close', () => resolve()) - }) - - listener.close() - await p + void listener.close() + await pEvent(listener, 'close') }) it.skip('listen on IPv6 addr', () => { @@ -53,7 +42,7 @@ module.exports = (create) => { }) it('should throw an error if it cannot listen on the given multiaddr', async () => { - const listener = ws.createListener(() => { }) + const listener = ws.createListener() const ma = new Multiaddr('/ip4/127.0.0.1/tcp/15554/ws/p2p-webrtc-star') await expect(listener.listen(ma)) @@ -61,7 +50,7 @@ module.exports = (create) => { }) it('getAddrs', async () => { - const listener = ws.createListener(() => {}) + const listener = ws.createListener() const ma = new Multiaddr('/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star') await listener.listen(ma) @@ -73,7 +62,7 @@ module.exports = (create) => { }) it('getAddrs with peer id', async () => { - const listener = ws.createListener(() => {}) + const listener = ws.createListener() const ma = new Multiaddr('/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star/ipfs/QmcgpsyWgH8Y8ajJz1Cu72KnS5uo2Aa2LpzU7kinSooooA') await listener.listen(ma) @@ -85,7 +74,7 @@ module.exports = (create) => { }) it('can only listen on one address per listener', async () => { - const listener = ws.createListener(() => { }) + const listener = ws.createListener() await listener.listen(ma) diff --git a/packages/webrtc-star-transport/test/transport/multiple-signal-servers.ts b/packages/webrtc-star-transport/test/transport/multiple-signal-servers.ts new file mode 100644 index 00000000..9286d86d --- /dev/null +++ b/packages/webrtc-star-transport/test/transport/multiple-signal-servers.ts @@ -0,0 +1,170 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from '@multiformats/multiaddr' +import { pipe } from 'it-pipe' +import type { WebRTCStar } from '../../src/index.js' + +const ma1 = new Multiaddr('/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star') +const ma2 = new Multiaddr('/ip4/127.0.0.1/tcp/15556/ws/p2p-webrtc-star') + +export default (create: () => Promise) => { + describe('multiple signal servers', () => { + let ws1: WebRTCStar + let ws2: WebRTCStar + + beforeEach(async () => { + ws1 = await create() + ws2 = await create() + }) + + it('can listen on multiple signal servers with the same transport', async () => { + const listener1 = ws1.createListener() + await listener1.listen(ma1) + + const listener2 = ws1.createListener() + await listener2.listen(ma2) + + expect(Array.from(ws1.sigServers.keys())).to.have.lengthOf(2) + + await Promise.all([ + listener1.close(), + listener2.close() + ]) + + expect(Array.from(ws1.sigServers.keys())).to.have.lengthOf(0) + }) + + it('can dial the first listener using multiple signal servers in one listener', async function () { + // Listen on two signalling servers in one instance + const listener1m1 = ws1.createListener({ + handler: (conn) => { + void conn.newStream(['echo']) + .then(({ stream }) => { + void pipe(stream, stream) + }) + } + }) + await listener1m1.listen(ma1) + + const listener1m2 = ws1.createListener({ + handler: (conn) => { + void conn.newStream(['echo']) + .then(({ stream }) => { + void pipe(stream, stream) + }) + } + }) + await listener1m2.listen(ma2) + + expect(Array.from(ws1.sigServers.keys())).to.have.lengthOf(2) + + // Create Listener 2 listening on the first signalling server + const listener2 = ws2.createListener({ + handler: (conn) => { + void conn.newStream(['echo']) + .then(({ stream }) => { + void pipe(stream, stream) + }) + } + }) + await listener2.listen(ma1) + + expect(Array.from(ws2.sigServers.keys())).to.have.lengthOf(1) + + // // Use first of the signal addresses + const [sigRefs1] = ws1.sigServers.values() + + await ws2.dial(sigRefs1.signallingAddr) + + await Promise.all([ + listener1m1.close(), + listener1m2.close(), + listener2.close() + ]) + }) + + it('can dial the last listener using multiple signal servers in one listener', async function () { + // Listen on two signalling servers in one instance + const listener1m1 = ws1.createListener({ + handler: (conn) => { + void conn.newStream(['echo']) + .then(({ stream }) => { + void pipe(stream, stream) + }) + } + }) + await listener1m1.listen(ma1) + + const listener1m2 = ws1.createListener({ + handler: (conn) => { + void conn.newStream(['echo']) + .then(({ stream }) => { + void pipe(stream, stream) + }) + } + }) + await listener1m2.listen(ma2) + + expect(Array.from(ws1.sigServers.keys())).to.have.lengthOf(2) + + // Create Listener 2 listening on the last signalling server + const listener2 = ws2.createListener({ + handler: (conn) => { + void conn.newStream(['echo']) + .then(({ stream }) => { + void pipe(stream, stream) + }) + } + }) + await listener2.listen(ma2) + + expect(Array.from(ws2.sigServers.keys())).to.have.lengthOf(1) + + // // Use last of the signal addresses + const [, sigRefs2] = ws1.sigServers.values() + + await ws2.dial(sigRefs2.signallingAddr) + + await Promise.all([ + listener1m1.close(), + listener1m2.close(), + listener2.close() + ]) + }) + + it('can close a single listener', async function () { + const listener1m1 = ws1.createListener() + await listener1m1.listen(ma1) + + const listener1m2 = ws1.createListener() + await listener1m2.listen(ma2) + + expect(Array.from(ws1.sigServers.keys())).to.have.lengthOf(2) + + await listener1m1.close() + expect(Array.from(ws1.sigServers.keys())).to.have.lengthOf(1) + + // Use the second multiaddr to dial + const listener2 = ws2.createListener({ + handler: (conn) => { + void conn.newStream(['echo']) + .then(({ stream }) => { + void pipe(stream, stream) + }) + } + }) + await listener2.listen(ma2) + + // The first was cleaned up on close + const [sigRefs] = ws1.sigServers.values() + + await ws2.dial(sigRefs.signallingAddr) + + await Promise.all([ + listener1m2.close(), + listener2.close() + ]) + }) + }) +} diff --git a/packages/webrtc-star-transport/test/transport/reconnect.node.ts b/packages/webrtc-star-transport/test/transport/reconnect.node.ts new file mode 100644 index 00000000..347efa32 --- /dev/null +++ b/packages/webrtc-star-transport/test/transport/reconnect.node.ts @@ -0,0 +1,93 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from '@multiformats/multiaddr' +import { sigServer } from '@libp2p/webrtc-star-signalling-server' +import { pEvent } from 'p-event' +import type { SigServer } from '@libp2p/webrtc-star-signalling-server' +import type { WebRTCStar } from '../../src/index.js' +import type { Listener } from '@libp2p/interfaces/transport' + +const SERVER_PORT = 13580 + +export default (create: () => Promise) => { + describe('reconnect to signaling server', () => { + let sigS: SigServer + let ws1: WebRTCStar + let ws2: WebRTCStar + let ws3: WebRTCStar + let listener1: Listener + let listener2: Listener + let listener3: Listener + const signallerAddr = new Multiaddr('/ip4/127.0.0.1/tcp/15555/ws/p2p-webrtc-star') + + before(async () => { + sigS = await sigServer({ port: SERVER_PORT }) + }) + + after(async () => { + await sigS.stop() + + if (listener1 != null) { + await listener1.close() + } + + if (listener2 != null) { + await listener2.close() + } + + if (listener3 != null) { + await listener3.close() + } + }) + + it('listen on the first', async () => { + ws1 = await create() + + listener1 = ws1.createListener() + await ws1.discovery.start() + + await listener1.listen(signallerAddr) + }) + + it('listen on the second, discover the first', async () => { + ws2 = await create() + + listener2 = ws2.createListener() + + await listener2.listen(signallerAddr) + const { detail: { multiaddrs } } = await pEvent<'peer', { detail: { multiaddrs: Multiaddr[] } }>(ws1.discovery, 'peer') + + // Check first of the signal addresses + const [sigRefs] = ws2.sigServers.values() + + expect(multiaddrs.map(m => m.toString())).to.include(sigRefs.signallingAddr.toString()) + }) + + it('stops the server', async () => { + await sigS.stop() + }) + + it('starts the server again', async () => { + sigS = await sigServer({ port: SERVER_PORT }) + }) + + it('wait a bit for clients to reconnect', (done) => { + setTimeout(done, 2000) + }) + + it('listen on the third, first discovers it', async () => { + ws3 = await create() + + listener3 = ws3.createListener() + await listener3.listen(signallerAddr) + + const { detail: { multiaddrs } } = await pEvent<'peer', { detail: { multiaddrs: Multiaddr[] } }>(ws1.discovery, 'peer') + + // Check first of the signal addresses + const [sigRefs] = ws3.sigServers.values() + + expect(multiaddrs.some((m) => m.equals(sigRefs.signallingAddr))).to.equal(true) + }) + }) +} diff --git a/packages/webrtc-star-transport/test/transport/track.ts b/packages/webrtc-star-transport/test/transport/track.ts new file mode 100644 index 00000000..c7f6663c --- /dev/null +++ b/packages/webrtc-star-transport/test/transport/track.ts @@ -0,0 +1,99 @@ +/* eslint-env mocha */ +/* eslint-disable no-console */ + +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from '@multiformats/multiaddr' +import { pipe } from 'it-pipe' +import pWaitFor from 'p-wait-for' +import { cleanUrlSIO } from '../../src/utils.js' +import type { WebRTCStar } from '../../src/index.js' +import type { Listener } from '@libp2p/interfaces/src/transport' + +export default (create: () => Promise) => { + describe('track connections', () => { + let ws1: WebRTCStar + let ws2: WebRTCStar + let ma: Multiaddr + let listener: Listener + let remoteListener: Listener + + const maHSDNS = new Multiaddr('/dns/star-signal.cloud.ipfs.team/wss/p2p-webrtc-star') + const maHSIP = new Multiaddr('/ip4/188.166.203.82/tcp/20000/wss/p2p-webrtc-star') + const maLS = new Multiaddr('/ip4/127.0.0.1/tcp/15555/wss/p2p-webrtc-star') + + if (process.env.WEBRTC_STAR_REMOTE_SIGNAL_DNS != null) { + // test with deployed signalling server using DNS + console.log('Using DNS:', maHSDNS) + ma = maHSDNS + } else if (process.env.WEBRTC_STAR_REMOTE_SIGNAL_IP != null) { + // test with deployed signalling server using IP + console.log('Using IP:', maHSIP) + ma = maHSIP + } else { + ma = maLS + } + + beforeEach(async () => { + // first + ws1 = await create() + listener = ws1.createListener({ + handler: (conn) => { + void conn.newStream(['echo']) + .then(({ stream }) => { + void pipe(stream, stream) + }) + } + }) + + // second + ws2 = await create() + remoteListener = ws2.createListener({ + handler: (conn) => { + void conn.newStream(['echo']) + .then(({ stream }) => { + void pipe(stream, stream) + }) + } + }) + + await Promise.all([listener.listen(ma), remoteListener.listen(ma)]) + }) + + afterEach(async () => { + await Promise.all([listener, remoteListener].map(async l => await l.close())) + }) + + it('should untrack conn after being closed', async function () { + const server = ws1.sigServers.get(cleanUrlSIO(ma)) + + if (server == null) { + throw new Error(`No sigserver found for ma ${ma.toString()}`) + } + + expect(server.connections).to.have.lengthOf(0) + + // Use one of the signal addresses + const [sigRef] = ws2.sigServers.values() + + const conn = await ws1.dial(sigRef.signallingAddr) + + const remoteServer = ws2.sigServers.get(cleanUrlSIO(ma)) + + if (remoteServer == null) { + throw new Error(`No sigserver found for ma ${ma.toString()}`) + } + + // Wait for the listener to begin tracking, this happens after signaling is complete + await pWaitFor(() => remoteServer.connections.length === 1) + expect(remoteServer.channels.size).to.equal(1) + expect(remoteServer.pendingSignals.size).to.equal(1) + + await conn.close() + + // Wait for tracking to clear + await pWaitFor(() => remoteServer.connections.length === 0) + expect(remoteServer.channels.size).to.equal(0) + expect(remoteServer.pendingSignals.size).to.equal(0) + }) + }) +} diff --git a/packages/transport/test/utils.spec.js b/packages/webrtc-star-transport/test/utils.spec.ts similarity index 94% rename from packages/transport/test/utils.spec.js rename to packages/webrtc-star-transport/test/utils.spec.ts index 2b1db8b7..040bfb9b 100644 --- a/packages/transport/test/utils.spec.js +++ b/packages/webrtc-star-transport/test/utils.spec.ts @@ -1,11 +1,8 @@ /* eslint-env mocha */ -'use strict' - -const { expect } = require('aegir/utils/chai') -const { Multiaddr } = require('multiaddr') -const { cleanMultiaddr } = require('../src/utils') -const { cleanUrlSIO } = require('../src/utils') +import { expect } from 'aegir/utils/chai.js' +import { Multiaddr } from '@multiformats/multiaddr' +import { cleanMultiaddr, cleanUrlSIO } from '../src/utils.js' describe('utils', () => { const legacyMultiaddrStringDNS = '/libp2p-webrtc-star/dns4/star-signal.cloud.ipfs.team/tcp/443/wss/p2p/QmWxLfixekyv6GAzvDEtXfXjj7gb1z3G8i5aQNHLhw1zA1' diff --git a/packages/webrtc-star-transport/tsconfig.json b/packages/webrtc-star-transport/tsconfig.json new file mode 100644 index 00000000..4585a435 --- /dev/null +++ b/packages/webrtc-star-transport/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "emitDeclarationOnly": false, + "module": "ES2020" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../webrtc-star-protocol" + }, + { + "path": "../webrtc-star-signalling-server" + } + ] +}