-
Notifications
You must be signed in to change notification settings - Fork 16
feat: Browser to Browser #90
Changes from 16 commits
07b5011
cf7c2bd
f2fc6e3
eefde36
3abb661
e50dd76
84c5b3c
5e3d29e
43246b4
6472150
83cd254
f786bdb
5a08ed7
0ba969c
e1bf7de
eafad87
40d72fb
c24fb7e
daed656
df5ce02
20e7c28
8ea980b
69c4208
d622b27
ddad046
602adc9
c8c2888
41c8e2d
a031dca
40fd90c
d4a6858
0b6a80f
6e8b3f6
32aa877
d167645
55c4f7b
e7a2623
45f47f2
05c783d
eb64c85
9d831a8
08575d4
f2df558
a8efbfe
0a9f5d3
a86eb4b
497e7e2
57ad032
c30664b
257101e
489876b
256fc95
7b9e98f
57779d4
2184275
2f77031
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# js-libp2p-webrtc Browser to Browser | ||
|
||
This example leverages the [vite bundler](https://vitejs.dev/) to compile and serve the libp2p code in the browser. You can use other bundlers such as Webpack, but we will not be covering them here. | ||
|
||
## Running the Relay Server | ||
|
||
For browsers to communicate, we first need to run the Go LibP2P relay server: | ||
|
||
```shell | ||
npm run go-relay | ||
``` | ||
|
||
Alternatively, a NodeJS relay server is available: | ||
|
||
```shell | ||
npm run node-relay | ||
ckousik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
Copy one of the multiaddresses in the output. | ||
|
||
## Running the Example | ||
|
||
ckousik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
In a separate console tab, install dependencies and start the Vite server: | ||
|
||
```shell | ||
npm i && npm run start | ||
``` | ||
|
||
The browser window will automatically open. Let's call this `Browser A`. | ||
Using the copied multiaddress from the Go or NodeJS relay server, paste it into the `Remote MultiAddress` input and click the `Connect` button. | ||
`Browser A` is now connected to the relay server. | ||
Copy the multiaddress located after the `Listening on` message. | ||
ckousik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Now open a second browser with the url `http://localhost:5173/`. Let's call this `Browser B`. | ||
Using the copied multiaddress from `Listening on` section in `Browser A`, paste it into the `Remote MultiAddress` input and click the `Connect` button. | ||
`Browser B` is now connected to `Browser A`. | ||
Copy the multiaddress located after the `Listening on` message. | ||
|
||
Using the copied multiaddress from `Listening on` section in `Browser B`, paste it into the `Remote MultiAddress` input in `Browser A` and click the `Connect` button. | ||
`Browser A` is now connected to `Browser B`. | ||
|
||
The peers are now connected to each other. Enter a message and click the `Send` button in either/both browsers and see the echo'd messages. | ||
|
||
The output should look like: | ||
|
||
`Browser A` | ||
```text | ||
Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk' | ||
Listening on /ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC/p2p-webrtc-direct/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC | ||
Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9/p2p-webrtc-direct/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9' | ||
Sending message 'helloa' | ||
Received message 'helloa' | ||
Received message 'hellob' | ||
``` | ||
|
||
`Browser B` | ||
```text | ||
Dialing '/ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC/p2p-webrtc-direct/p2p/12D3KooW9wFiWFELqGJTbzEwtByXsPiHJdHB8n7Kin71VMYyERmC' | ||
Listening on /ip4/127.0.0.1/tcp/57708/ws/p2p/12D3KooWRqAUEzPwKMoGstpfJVqr3aoinwKVPu4DLo9nQncbnuLk/p2p-circuit/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9/p2p-webrtc-direct/p2p/12D3KooWBZyVLJfQkofqLK4op9TPkHuUumCZt1ybQrPvNm7TVQV9 | ||
Received message 'helloa' | ||
Sending message 'hellob' | ||
Received message 'hellob' | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>js-libp2p WebRTC</title> | ||
<style> | ||
label, | ||
button { | ||
display: block; | ||
font-weight: bold; | ||
margin: 5px 0; | ||
} | ||
div { | ||
margin-bottom: 20px; | ||
} | ||
#send-section { | ||
display: none; | ||
} | ||
input[type="text"] { | ||
width: 800px; | ||
} | ||
</style> | ||
</head> | ||
<body> | ||
<div id="app"> | ||
<div> | ||
<label for="peer">Remote MultiAddress:</label> | ||
<input type="text" id="peer" /> | ||
<button id="connect">Connect</button> | ||
</div> | ||
<div id="send-section"> | ||
<label for="message">Message:</label> | ||
<input type="text" id="message" value="hello" /> | ||
<button id="send">Send</button> | ||
</div> | ||
<div id="output"></div> | ||
</div> | ||
<script type="module" src="./index.js"></script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { multiaddr } from "@multiformats/multiaddr" | ||
import { pipe } from "it-pipe" | ||
import { fromString, toString } from "uint8arrays" | ||
import { webRTCDirect } from "js-libp2p-webrtc" | ||
import { webSockets } from "@libp2p/websockets" | ||
import * as filters from "@libp2p/websockets/filters" | ||
import { pushable } from "it-pushable" | ||
import { mplex } from "@libp2p/mplex" | ||
import { createLibp2p } from "libp2p" | ||
import { noise } from "@chainsafe/libp2p-noise" | ||
|
||
// singletons | ||
let outgoing_stream | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this a singleton? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are only sending on one stream in the example, so it is simpler to declare the variable early. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I should have been more clear. This shouldn't be a singleton because |
||
let webrtcDirectAddress | ||
|
||
const output = document.getElementById("output") | ||
const sendSection = document.getElementById("send-section") | ||
const appendOutput = (line) => { | ||
const div = document.createElement("div") | ||
div.appendChild(document.createTextNode(line)) | ||
output.append(div) | ||
} | ||
const clean = (line) => line.replaceAll("\n", "") | ||
const sender = pushable() | ||
|
||
const node = await createLibp2p({ | ||
transports: [ | ||
webSockets({ | ||
filter: filters.all, | ||
}), | ||
webRTCDirect({}), | ||
], | ||
connectionEncryption: [noise()], | ||
streamMuxers: [mplex()], | ||
relay: { | ||
enabled: true, | ||
autoRelay: { | ||
enabled: true, | ||
}, | ||
}, | ||
}) | ||
|
||
await node.start() | ||
|
||
// handle the echo protocol | ||
await node.handle("/echo/1.0.0", ({ stream }) => { | ||
pipe( | ||
stream, | ||
async function* (source) { | ||
for await (const buf of source) { | ||
const incoming = toString(buf.subarray()) | ||
appendOutput(`Received message '${clean(incoming)}'`) | ||
yield buf | ||
} | ||
}, | ||
stream | ||
) | ||
}) | ||
|
||
node.peerStore.addEventListener("change:multiaddrs", (event) => { | ||
const { peerId } = event.detail | ||
|
||
if (node.getMultiaddrs().length === 0 || !node.peerId.equals(peerId)) { | ||
return | ||
} | ||
|
||
node.getMultiaddrs().forEach((ma) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this be done by the library? Do we expect every user to do this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The webrtc library does not add any new multiaddresses to the peerStore. Given the discussion regarding multiaddresses, I feel it should add a new mulitaddress. However, the peerstore does not seem to have a way to atomically remove an address for a peer. https://github.com/libp2p/js-libp2p-interfaces/blob/master/packages/interface-peer-store/src/index.ts#L126 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you need to remove an address? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
if (ma.protoCodes().includes(290)) { | ||
const newWebrtcDirectAddress = ma.encapsulate( | ||
multiaddr(`/p2p-webrtc-direct/p2p/${node.peerId}`) | ||
) | ||
|
||
// only update if the address is new | ||
if (newWebrtcDirectAddress?.toString() !== webrtcDirectAddress?.toString()) { | ||
appendOutput(`Listening on ${newWebrtcDirectAddress}`) | ||
sendSection.style.display = "block" | ||
webrtcDirectAddress = newWebrtcDirectAddress | ||
} | ||
} | ||
}) | ||
}) | ||
|
||
window.connect.onclick = async () => { | ||
const ma = multiaddr(window.peer.value) | ||
appendOutput(`Dialing '${ma}'`) | ||
const connection = await node.dial(ma) | ||
|
||
if (!ma.protoCodes().includes(276)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 276? Let's not forget to change this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 276 is the protoCode for circuit-relay. This is just used to differentiate between dialing a relay peer to start listening vs dialing a peer over relay to create an echo stream. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a library where we can ensure the protocode is more self documenting? (similar to codes in dag-json, dag-cbor, etc.. from ipld) we shouldn't be throwing magic numbers around |
||
return | ||
} | ||
|
||
outgoing_stream = await connection.newStream(["/echo/1.0.0"]) | ||
|
||
pipe(sender, outgoing_stream, async (src) => { | ||
for await (const buf of src) { | ||
const response = toString(buf.subarray()) | ||
appendOutput(`Received message '${clean(response)}'`) | ||
} | ||
}) | ||
} | ||
|
||
window.send.onclick = async () => { | ||
const message = `${window.message.value}\n` | ||
appendOutput(`Sending message '${clean(message)}'`) | ||
sender.push(fromString(message)) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
{ | ||
"name": "js-libp2p-webrtc-browser-to-server", | ||
"version": "1.0.0", | ||
"description": "Connect a browser to a server", | ||
ckousik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"type": "module", | ||
"scripts": { | ||
"start": "vite", | ||
"build": "vite build", | ||
"go-relay": "cd ../go-libp2p-server && go run ./relay", | ||
"node-relay": "node relay.js", | ||
"test": "npm run build && playwright test tests" | ||
}, | ||
"dependencies": { | ||
"@chainsafe/libp2p-noise": "^11.0.0", | ||
"@libp2p/websockets": "^5.0.3", | ||
"@multiformats/multiaddr": "^11.0.5", | ||
"it-pushable": "^3.1.0", | ||
"js-libp2p-webrtc": "file:../../", | ||
"libp2p": "^0.42.0", | ||
ckousik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"vite": "^3.1.0" | ||
}, | ||
"devDependencies": { | ||
"@playwright/test": "^1.30.0", | ||
"test-util-ipfs-example": "^1.0.2" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { createLibp2p } from 'libp2p' | ||
import { webSockets } from '@libp2p/websockets' | ||
import { noise } from '@chainsafe/libp2p-noise' | ||
import { mplex } from '@libp2p/mplex' | ||
|
||
async function main () { | ||
const node = await createLibp2p({ | ||
addresses: { | ||
listen: ['/ip4/0.0.0.0/tcp/0/ws'] | ||
// TODO check "What is next?" section | ||
// announce: ['/dns4/auto-relay.libp2p.io/tcp/443/wss/p2p/QmWDn2LY8nannvSWJzruUYoLZ4vV83vfCBwd8DipvdgQc3'] | ||
ckousik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
transports: [ | ||
webSockets() | ||
], | ||
connectionEncryption: [ | ||
noise() | ||
], | ||
streamMuxers: [ | ||
mplex() | ||
], | ||
relay: { | ||
enabled: true, | ||
hop: { | ||
enabled: true | ||
}, | ||
advertise: { | ||
enabled: true, | ||
} | ||
} | ||
}) | ||
|
||
console.log(`Node started with id ${node.peerId.toString()}`) | ||
console.log('Listening on:') | ||
node.getMultiaddrs().forEach((ma) => console.log(ma.toString())) | ||
} | ||
|
||
main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/* eslint-disable no-console */ | ||
import { test, expect } from '@playwright/test' | ||
import { playwright } from 'test-util-ipfs-example' | ||
import { spawn, exec } from 'child_process' | ||
import { existsSync } from 'fs' | ||
|
||
// Setup | ||
const play = test.extend({ | ||
...playwright.servers() | ||
}) | ||
|
||
async function spawnGoLibp2p() { | ||
if (!existsSync('../../examples/go-libp2p-server/go-libp2p-server')) { | ||
await new Promise((resolve, reject) => { | ||
exec('go build', | ||
{ cwd: '../../examples/go-libp2p-server' }, | ||
(error, stdout, stderr) => { | ||
if (error) { | ||
throw (`exec error: ${error}`) | ||
} | ||
resolve() | ||
}) | ||
}) | ||
} | ||
|
||
const server = spawn('./go-libp2p-server', [], { cwd: '../../examples/go-libp2p-server', killSignal: 'SIGINT' }) | ||
server.stderr.on('data', (data) => { | ||
console.log(`stderr: ${data}`, typeof data) | ||
}) | ||
const serverAddr = await (new Promise(resolve => { | ||
server.stdout.on('data', (data) => { | ||
console.log(`stdout: ${data}`, typeof data) | ||
const addr = String(data).match(/p2p addr: ([^\s]*)/) | ||
if (addr !== null && addr.length > 0) { | ||
resolve(addr[1]) | ||
} | ||
}) | ||
})) | ||
return { server, serverAddr } | ||
} | ||
|
||
play.describe('bundle ipfs with parceljs:', () => { | ||
// DOM | ||
const connectBtn = '#connect' | ||
const connectAddr = '#peer' | ||
const messageInput = '#message' | ||
const sendBtn = '#send' | ||
const output = '#output' | ||
|
||
let server | ||
let serverAddr | ||
|
||
// eslint-disable-next-line no-empty-pattern | ||
play.beforeAll(async ({ }, testInfo) => { | ||
testInfo.setTimeout(5 * 60_000) | ||
const s = await spawnGoLibp2p() | ||
server = s.server | ||
serverAddr = s.serverAddr | ||
console.log('Server addr:', serverAddr) | ||
}, {}) | ||
|
||
play.afterAll(() => { | ||
server.kill('SIGINT') | ||
}) | ||
|
||
play.beforeEach(async ({ servers, page }) => { | ||
const url = `http://localhost:${servers[0].port}/` | ||
console.log(url) | ||
await page.goto(url) | ||
}) | ||
|
||
play('should connect to a go-libp2p node over webtransport', async ({ page }) => { | ||
const message = 'hello' | ||
|
||
// add the go libp2p multiaddress to the input field and submit | ||
await page.fill(connectAddr, serverAddr) | ||
await page.click(connectBtn) | ||
|
||
// send the relay message to the go libp2p server | ||
ckousik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
await page.fill(messageInput, message) | ||
await page.click(sendBtn) | ||
|
||
await page.waitForSelector('#output:has(div)') | ||
|
||
// Expected output: | ||
// | ||
// Dialing '${serverAddr}' | ||
// Peer connected '${serverAddr}' | ||
// Sending message '${message}' | ||
// Received message '${message}' | ||
const connections = await page.textContent(output) | ||
expect(connections).toContain(`Dialing '${serverAddr}'`) | ||
expect(connections).toContain(`Peer connected '${serverAddr}'`) | ||
expect(connections).toContain(`Sending message '${message}'`) | ||
expect(connections).toContain(`Received message '${message}'`) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
export default { | ||
build: { | ||
target: 'es2022' | ||
}, | ||
optimizeDeps: { | ||
esbuildOptions: { target: 'es2022', supported: { bigint: true } } | ||
}, | ||
server: { | ||
open: true | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should rename this to private-to-private or private browser-to-private browser to be better in line with current rename. Let's discuss in standup tomorrow
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could see both sides of this. If we're looking for this example's name to have parity with the spec, then renaming makes sense. On the other hand, the example is a browser connecting to a browser, so the naming is aligned with the example. I don't have a strong opinion either way.