From 9c67364caa1880782f01718ebb002e282e26bd3c Mon Sep 17 00:00:00 2001 From: Aleksei Date: Thu, 25 Feb 2021 16:34:02 +0100 Subject: [PATCH] Add an example of webrtc-direct (#868) Co-authored-by: Vasco Santos --- .github/workflows/main.yml | 9 ++- examples/webrtc-direct/README.md | 33 ++++++++++ examples/webrtc-direct/dialer.js | 57 ++++++++++++++++++ examples/webrtc-direct/index.html | 17 ++++++ examples/webrtc-direct/listener.js | 44 ++++++++++++++ examples/webrtc-direct/package.json | 31 ++++++++++ examples/webrtc-direct/test.js | 93 +++++++++++++++++++++++++++++ 7 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 examples/webrtc-direct/README.md create mode 100644 examples/webrtc-direct/dialer.js create mode 100644 examples/webrtc-direct/index.html create mode 100644 examples/webrtc-direct/listener.js create mode 100644 examples/webrtc-direct/package.json create mode 100644 examples/webrtc-direct/test.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 375e35e1..7d593491 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -92,7 +92,7 @@ jobs: steps: - uses: actions/checkout@v2 - run: yarn - - run: cd examples && yarn && npm run test -- echo + - run: cd examples && yarn && npm run test -- echo test-libp2p-in-the-browser-example: needs: check runs-on: macos-latest @@ -135,3 +135,10 @@ jobs: - uses: actions/checkout@v2 - run: yarn - run: cd examples && yarn && npm run test -- transports + test-webrtc-direct-example: + needs: check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: yarn + - run: cd examples && yarn && npm run test -- webrtc-direct diff --git a/examples/webrtc-direct/README.md b/examples/webrtc-direct/README.md new file mode 100644 index 00000000..3eb406a6 --- /dev/null +++ b/examples/webrtc-direct/README.md @@ -0,0 +1,33 @@ +### Webrtc-direct example + +An example that uses [js-libp2p-webrtc-direct](https://github.com/libp2p/js-libp2p-webrtc-direct) for connecting +nodejs libp2p and browser libp2p clients. To run the example: + +## 0. Run a nodejs libp2p listener + +When in the root folder of this example, type `node listener.js` in terminal. You should see an address that listens for +incoming connections. Below is just an example of such address. In your case the suffix hash (`peerId`) will be different. + +```bash +$ node listener.js +Listening on: +/ip4/127.0.0.1/tcp/9090/http/p2p-webrtc-direct/p2p/QmUKQCzEUhhhobcNSrXU5uzxTqbvF1BjMCGNGZzZU14Kgd +``` + +## 1. Prepare a browser libp2p dialer +Confirm that the above address is the same as the field `list` in `public/dialer.js`: +```js + peerDiscovery: { + [Bootstrap.tag]: { + enabled: true, + // paste the address into `list` + list: ['/ip4/127.0.0.1/tcp/9090/http/p2p-webrtc-direct/p2p/QmUKQCzEUhhhobcNSrXU5uzxTqbvF1BjMCGNGZzZU14Kgd'] + } + } +``` + +## 2. Run a browser libp2p dialer +When in the root folder of this example, type `npm run dev` in terminal. You should see an address where you can browse +the running client. Open this address in your browser. In console +logs you should see logs about successful connection with the node client. In the output of node client you should see +a log message about successful connection as well. diff --git a/examples/webrtc-direct/dialer.js b/examples/webrtc-direct/dialer.js new file mode 100644 index 00000000..a7ba8558 --- /dev/null +++ b/examples/webrtc-direct/dialer.js @@ -0,0 +1,57 @@ +import 'babel-polyfill' +const Libp2p = require('libp2p') +const WebRTCDirect = require('libp2p-webrtc-direct') +const Mplex = require('libp2p-mplex') +const {NOISE} = require('libp2p-noise') +const Bootstrap = require('libp2p-bootstrap') + +document.addEventListener('DOMContentLoaded', async () => { + // use the same peer id as in `listener.js` to avoid copy-pasting of listener's peer id into `peerDiscovery` + const hardcodedPeerId = '12D3KooWCuo3MdXfMgaqpLC5Houi1TRoFqgK9aoxok4NK5udMu8m' + const libp2p = await Libp2p.create({ + modules: { + transport: [WebRTCDirect], + streamMuxer: [Mplex], + connEncryption: [NOISE], + peerDiscovery: [Bootstrap] + }, + config: { + peerDiscovery: { + [Bootstrap.tag]: { + enabled: true, + list: [`/ip4/127.0.0.1/tcp/9090/http/p2p-webrtc-direct/p2p/${hardcodedPeerId}`] + } + } + } + }) + + const status = document.getElementById('status') + const output = document.getElementById('output') + + output.textContent = '' + + function log (txt) { + console.info(txt) + output.textContent += `${txt.trim()}\n` + } + + // Listen for new peers + libp2p.on('peer:discovery', (peerId) => { + log(`Found peer ${peerId.toB58String()}`) + }) + + // Listen for new connections to peers + libp2p.connectionManager.on('peer:connect', (connection) => { + log(`Connected to ${connection.remotePeer.toB58String()}`) + }) + + // Listen for peers disconnecting + libp2p.connectionManager.on('peer:disconnect', (connection) => { + log(`Disconnected from ${connection.remotePeer.toB58String()}`) + }) + + await libp2p.start() + status.innerText = 'libp2p started!' + log(`libp2p id is ${libp2p.peerId.toB58String()}`) + +}) diff --git a/examples/webrtc-direct/index.html b/examples/webrtc-direct/index.html new file mode 100644 index 00000000..a29b43bf --- /dev/null +++ b/examples/webrtc-direct/index.html @@ -0,0 +1,17 @@ + + + + + js-libp2p parcel.js browser example + + + +
+

Starting libp2p...

+
+
+

+  
+ + + diff --git a/examples/webrtc-direct/listener.js b/examples/webrtc-direct/listener.js new file mode 100644 index 00000000..47f3a971 --- /dev/null +++ b/examples/webrtc-direct/listener.js @@ -0,0 +1,44 @@ +const Libp2p = require('libp2p') +const Bootstrap = require('libp2p-bootstrap') +const WebRTCDirect = require('libp2p-webrtc-direct') +const Mplex = require('libp2p-mplex') +const {NOISE} = require('libp2p-noise') +const PeerId = require('peer-id') + +;(async () => { + // hardcoded peer id to avoid copy-pasting of listener's peer id into the dialer's bootstrap list + // generated with cmd `peer-id --type=ed25519` + const hardcodedPeerId = await PeerId.createFromJSON({ + "id": "12D3KooWCuo3MdXfMgaqpLC5Houi1TRoFqgK9aoxok4NK5udMu8m", + "privKey": "CAESQAG6Ld7ev6nnD0FKPs033/j0eQpjWilhxnzJ2CCTqT0+LfcWoI2Vr+zdc1vwk7XAVdyoCa2nwUR3RJebPWsF1/I=", + "pubKey": "CAESIC33FqCNla/s3XNb8JO1wFXcqAmtp8FEd0SXmz1rBdfy" + }) + const node = await Libp2p.create({ + peerId: hardcodedPeerId, + addresses: { + listen: ['/ip4/127.0.0.1/tcp/9090/http/p2p-webrtc-direct'] + }, + modules: { + transport: [WebRTCDirect], + streamMuxer: [Mplex], + connEncryption: [NOISE] + }, + config: { + peerDiscovery: { + [Bootstrap.tag]: { + enabled: false, + } + } + } + }) + + node.connectionManager.on('peer:connect', (connection) => { + console.info(`Connected to ${connection.remotePeer.toB58String()}!`) + }) + + await node.start() + + console.log('Listening on:') + node.multiaddrs.forEach((ma) => console.log(`${ma.toString()}/p2p/${node.peerId.toB58String()}`)) + +})() diff --git a/examples/webrtc-direct/package.json b/examples/webrtc-direct/package.json new file mode 100644 index 00000000..33f2250d --- /dev/null +++ b/examples/webrtc-direct/package.json @@ -0,0 +1,31 @@ +{ + "name": "webrtc-direct", + "version": "0.0.1", + "private": true, + "description": "", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "parcel build index.html", + "start": "parcel index.html" + }, + "license": "ISC", + "devDependencies": { + "@babel/cli": "^7.8.3", + "@babel/core": "^7.8.3", + "babel-plugin-syntax-async-functions": "^6.13.0", + "babel-plugin-transform-regenerator": "^6.26.0", + "babel-polyfill": "^6.26.0", + "parcel-bundler": "^1.12.4" + }, + "dependencies": { + "libp2p": "../../", + "libp2p-bootstrap": "^0.12.1", + "libp2p-mplex": "^0.10.1", + "libp2p-noise": "^2.0.1", + "libp2p-webrtc-direct": "^0.5.0", + "peer-id": "^0.14.3" + }, + "browser": { + "ipfs": "ipfs/dist/index.min.js" + } +} diff --git a/examples/webrtc-direct/test.js b/examples/webrtc-direct/test.js new file mode 100644 index 00000000..1768e1a0 --- /dev/null +++ b/examples/webrtc-direct/test.js @@ -0,0 +1,93 @@ +'use strict' + +const path = require('path') +const execa = require('execa') +const pDefer = require('p-defer') +const uint8ArrayToString = require('uint8arrays/to-string') +const { chromium } = require('playwright'); + +function startNode (name, args = []) { + return execa('node', [path.join(__dirname, name), ...args], { + cwd: path.resolve(__dirname), + all: true + }) +} + +function startBrowser (name, args = []) { + return execa('parcel', [path.join(__dirname, name), ...args], { + preferLocal: true, + localDir: __dirname, + cwd: __dirname, + all: true + }) +} + +async function test () { + // Step 1, listener process + const listenerProcReady = pDefer() + let listenerOutput = '' + process.stdout.write('listener.js\n') + const listenerProc = startNode('listener.js') + + listenerProc.all.on('data', async (data) => { + process.stdout.write(data) + listenerOutput += uint8ArrayToString(data) + if (listenerOutput.includes('Listening on:') && listenerOutput.includes('12D3KooWCuo3MdXfMgaqpLC5Houi1TRoFqgK9aoxok4NK5udMu8m')) { + listenerProcReady.resolve() + } + }) + + await listenerProcReady.promise + process.stdout.write('==================================================================\n') + + // Step 2, dialer process + process.stdout.write('dialer.js\n') + let dialerUrl = '' + const dialerProc = startBrowser('index.html') + + dialerProc.all.on('data', async (chunk) => { + /**@type {string} */ + const out = chunk.toString() + + if (out.includes('Server running at')) { + dialerUrl = out.replace('Server running at ', '') + } + + if (out.includes('✨ Built in ')) { + try { + const browser = await chromium.launch(); + const page = await browser.newPage(); + await page.goto(dialerUrl); + await page.waitForFunction(selector => document.querySelector(selector).innerText === 'libp2p started!', '#status') + await page.waitForFunction( + selector => { + const text = document.querySelector(selector).innerText + return text.includes('libp2p id is') && + text.includes('Found peer') && + text.includes('Connected to') + }, + '#output', + { timeout: 10000 } + ) + await browser.close(); + } catch (err) { + console.error(err) + process.exit(1) + } finally { + dialerProc.cancel() + listenerProc.kill() + } + } + }) + + await Promise.all([ + listenerProc, + dialerProc, + ]).catch((err) => { + if (err.signal !== 'SIGTERM') { + throw err + } + }) +} + +module.exports = test