+8
-4
| { | ||
| "name": "ssb-lan", | ||
| "description": "SSB plugin for discovery of other peers in the same LAN", | ||
| "version": "1.0.0", | ||
| "version": "1.0.1", | ||
| "homepage": "https://github.com/staltz/ssb-lan", | ||
| "main": "lib/index.js", | ||
| "types": "lib/index.d.ts", | ||
| "files": [ | ||
| "lib/*", | ||
| "*.js" | ||
| ], | ||
| "repository": { | ||
@@ -30,2 +34,4 @@ "type": "git", | ||
| }, | ||
| "author": "Andre Staltz <contact@staltz.com> (http://staltz.com)", | ||
| "license": "MIT", | ||
| "scripts": { | ||
@@ -35,5 +41,3 @@ "typescript": "tsc", | ||
| "test": "npm run typescript && npm run tape" | ||
| }, | ||
| "author": "Andre Staltz <contact@staltz.com> (http://staltz.com)", | ||
| "license": "MIT" | ||
| } | ||
| } |
-158
| import {plugin, muxrpc} from 'secret-stack-decorators'; | ||
| import {Discovery, SSBConfig} from './types'; | ||
| const broadcast = require('broadcast-stream'); | ||
| const Ref = require('ssb-ref'); | ||
| const Keys = require('ssb-keys'); | ||
| const Notify = require('pull-notify'); | ||
| const debug = require('debug')('ssb:lan'); | ||
| const NORMAL_PORT = require('../port'); | ||
| const LEGACY_PORT = 8008; | ||
| @plugin('1.0.0') | ||
| class LAN { | ||
| private readonly ssb: Record<string, any>; | ||
| private readonly notifyDiscovery: CallableFunction & Record<string, any>; | ||
| private readonly caps: Buffer; | ||
| private readonly legacyEnabled: boolean; | ||
| private legacyBroadcast?: Record<string, any>; | ||
| private normalBroadcast?: Record<string, any>; | ||
| private int?: any; | ||
| constructor(ssb: Record<string, any>, config: SSBConfig) { | ||
| this.ssb = ssb; | ||
| this.notifyDiscovery = Notify(); | ||
| this.caps = Buffer.from(config.caps.shs, 'base64'); | ||
| this.legacyEnabled = config.lan?.legacy !== false; | ||
| } | ||
| private readLegacy = (buf: any) => { | ||
| if (buf.loopback) return; | ||
| const address = buf.toString(); | ||
| const peerKey = Ref.getKeyFromAddress(address); | ||
| if (peerKey && peerKey !== this.ssb.id) { | ||
| this.notifyDiscovery({address, verified: false} as Discovery); | ||
| } | ||
| }; | ||
| private writeLegacy() { | ||
| if (!this.legacyBroadcast) return; | ||
| const address = | ||
| this.ssb.getAddress('private') ?? this.ssb.getAddress('local'); | ||
| if (address) this.legacyBroadcast.write(address); | ||
| } | ||
| private readNormal = (buf: any) => { | ||
| // split buf into [ciphertext,sig] | ||
| const ciphertext = buf.slice(0, buf.length - 64); | ||
| const sig = buf.slice(buf.length - 64, buf.length); | ||
| // decrypt address | ||
| let address: string; | ||
| try { | ||
| const obj = Keys.secretUnbox(ciphertext, this.caps); | ||
| address = obj.address; | ||
| } catch (err) { | ||
| debug('failed to interpret broadcasted message: %s', buf.toString('hex')); | ||
| return; | ||
| } | ||
| // validate address | ||
| const peerKey = Ref.getKeyFromAddress(address); | ||
| if (!peerKey) { | ||
| debug( | ||
| 'failed to parse address from broadcasted message: %s', | ||
| buf.toString('hex'), | ||
| ); | ||
| return; | ||
| } | ||
| // avoid discovering ourselves | ||
| if (peerKey === this.ssb.id) { | ||
| return; | ||
| } | ||
| // verify signature of address | ||
| const b64sig = sig.toString('base64') + '.sig.ed25519'; | ||
| const obj = {address, signature: b64sig}; | ||
| const verified = Keys.verifyObj({public: peerKey}, obj); | ||
| // notify | ||
| this.notifyDiscovery({address, verified} as Discovery); | ||
| }; | ||
| private writeNormal() { | ||
| if (!this.normalBroadcast) return; | ||
| const address = | ||
| this.ssb.getAddress('private') ?? this.ssb.getAddress('local'); | ||
| if (address) { | ||
| // encrypt address | ||
| const ciphertext = Keys.secretBox({address}, this.caps); | ||
| // sign address | ||
| const b64sig = Keys.signObj(this.ssb.keys, {address}).signature; | ||
| const sig = Buffer.from(b64sig.replace(/\.sig\.ed25519$/, ''), 'base64'); | ||
| // concatenate [ciphertext,sig] | ||
| const payload = Buffer.concat([ciphertext, sig]); | ||
| // broadcast | ||
| this.normalBroadcast.write(payload); | ||
| } | ||
| } | ||
| private writeBoth = () => { | ||
| this.writeLegacy(); | ||
| this.writeNormal(); | ||
| }; | ||
| @muxrpc('sync') | ||
| public start = () => { | ||
| try { | ||
| this.normalBroadcast = broadcast(NORMAL_PORT); | ||
| } catch (err) { | ||
| debug('LAN broadcast turned off because: %s', err); | ||
| this.normalBroadcast = void 0; | ||
| } | ||
| try { | ||
| this.legacyBroadcast = this.legacyEnabled | ||
| ? broadcast(LEGACY_PORT) | ||
| : void 0; | ||
| } catch (err) { | ||
| debug('legacy broadcast turned off because: %s', err); | ||
| this.legacyBroadcast = void 0; | ||
| } | ||
| // Read | ||
| if (this.normalBroadcast) this.normalBroadcast.on('data', this.readNormal); | ||
| if (this.legacyBroadcast) this.legacyBroadcast.on('data', this.readLegacy); | ||
| // Write now, then periodically | ||
| this.writeBoth(); | ||
| this.int = setInterval(this.writeBoth, 2e3); | ||
| if (this.int.unref) this.int.unref(); | ||
| }; | ||
| @muxrpc('sync') | ||
| public stop = () => { | ||
| clearInterval(this.int); | ||
| if (this.normalBroadcast) { | ||
| this.normalBroadcast.close(); | ||
| this.normalBroadcast = void 0; | ||
| } | ||
| if (this.legacyBroadcast) { | ||
| this.legacyBroadcast.close(); | ||
| this.legacyBroadcast = void 0; | ||
| } | ||
| }; | ||
| @muxrpc('source') | ||
| public discoveredPeers = () => { | ||
| return this.notifyDiscovery.listen(); | ||
| }; | ||
| } | ||
| export = LAN; |
-13
| export type Discovery = { | ||
| verified: boolean; | ||
| address: string; | ||
| }; | ||
| export type SSBConfig = { | ||
| caps: { | ||
| shs: string; | ||
| }; | ||
| lan?: { | ||
| legacy?: boolean; | ||
| }; | ||
| }; |
-159
| const pull = require('pull-stream'); | ||
| const tape = require('tape'); | ||
| const Keys = require('ssb-keys'); | ||
| const broadcast = require('broadcast-stream'); | ||
| const port = require('../port'); | ||
| const Ref = require('ssb-ref'); | ||
| const createSsbServer = require('ssb-server').use(require('../lib/index')); | ||
| tape('Legacy broadcasting can be turned off', t => { | ||
| t.plan(0); | ||
| const keys = Keys.generate(); | ||
| const alice = createSsbServer({ | ||
| temp: 'test-lan-alice', | ||
| timeout: 1000, | ||
| port: 8008, | ||
| keys, | ||
| lan: { | ||
| legacy: false, | ||
| }, | ||
| }); | ||
| alice.lan.start(); | ||
| const b = broadcast(8008); | ||
| b.on('data', buf => { | ||
| t.fail('No UDP packet should have been received, but we did receive'); | ||
| }); | ||
| setTimeout(() => { | ||
| b.close(); | ||
| alice.lan.stop(); | ||
| alice.close(); | ||
| t.end(); | ||
| }, 3000); | ||
| }); | ||
| tape('Legacy (when enabled) broadcasting looks correct', t => { | ||
| t.plan(1); | ||
| const keys = Keys.generate(); | ||
| const alice = createSsbServer({ | ||
| temp: 'test-lan-alice', | ||
| timeout: 1000, | ||
| port: 8008, | ||
| keys, | ||
| }); | ||
| alice.lan.start(); | ||
| const b = broadcast(8008); | ||
| b.on('data', buf => { | ||
| const msg = buf.toString(); | ||
| t.true(Ref.isAddress(msg), 'broadcasted content is a multiserver address'); | ||
| b.close(); | ||
| alice.lan.stop(); | ||
| alice.close(); | ||
| setTimeout(() => { | ||
| t.end(); | ||
| }, 1000); | ||
| }); | ||
| }); | ||
| tape('broadcast write is correct', t => { | ||
| t.plan(6); | ||
| const keys = Keys.generate(); | ||
| const alice = createSsbServer({ | ||
| temp: 'test-lan-alice1', | ||
| timeout: 1000, | ||
| port: 8008, | ||
| keys, | ||
| }); | ||
| alice.lan.start(); | ||
| const b = broadcast(port); | ||
| b.on('data', buf => { | ||
| try { | ||
| JSON.parse(buf.toString()); | ||
| t.fail('JSON parsing must fail'); | ||
| } catch (err) { | ||
| t.ok(err, 'JSON parsing should fail, because it is ciphertext'); | ||
| } | ||
| t.true(buf.length > 64, 'UDP payload has many bytes'); | ||
| const ciphertext = buf.slice(0, buf.length - 64); | ||
| t.true(ciphertext.length > 64, 'ciphertext has many bytes'); | ||
| const sig = buf.slice(buf.length - 64, buf.length); | ||
| const signature = sig.toString('base64') + '.sig.ed25519'; | ||
| t.equals(sig.length, 64, 'signature has 64 bytes'); | ||
| const {address} = Keys.secretUnbox(ciphertext, alice.config.caps.shs); | ||
| t.true(Ref.isAddress(address), 'plaintext is a multiserver address'); | ||
| const key = Ref.getKeyFromAddress(address); | ||
| const verification = Keys.verifyObj({public: key}, {address, signature}); | ||
| t.true(verification, 'signature is verified'); | ||
| b.close(); | ||
| alice.lan.stop(); | ||
| alice.close(); | ||
| setTimeout(() => { | ||
| t.end(); | ||
| }, 1000); | ||
| }); | ||
| }); | ||
| tape('broadcast read is correct', t => { | ||
| t.plan(4); | ||
| const aliceKeys = Keys.generate(); | ||
| const alice = createSsbServer({ | ||
| temp: 'test-lan-alice2', | ||
| timeout: 1000, | ||
| port: 8009, | ||
| keys: aliceKeys, | ||
| }); | ||
| alice.lan.start(); | ||
| pull( | ||
| alice.lan.discoveredPeers(), | ||
| pull.drain(discovery => { | ||
| t.equals(typeof discovery.address, 'string', 'address is a string'); | ||
| t.ok(discovery.address, 'address is okay'); | ||
| t.equals( | ||
| discovery.address.slice(0, 4), | ||
| 'net:', | ||
| 'address begins with net:', | ||
| ); | ||
| t.true(discovery.verified, 'discovery is verified'); | ||
| b.close(); | ||
| alice.lan.stop(); | ||
| alice.close(); | ||
| setTimeout(() => { | ||
| t.end(); | ||
| }, 1000); | ||
| }), | ||
| ); | ||
| const bobKeys = Keys.generate(); | ||
| const address = | ||
| 'net:192.168.1.11:26830~shs:' + bobKeys.public.replace(/\.ed25519$/, ''); | ||
| const ciphertext = Keys.secretBox({address}, alice.config.caps.shs); | ||
| const b64signature = Keys.signObj(bobKeys, {address}).signature; | ||
| const signature = Buffer.from( | ||
| b64signature.replace(/\.sig\.ed25519$/, ''), | ||
| 'base64', | ||
| ); | ||
| const payload = Buffer.concat([ciphertext, signature]); | ||
| const b = broadcast(port, true); | ||
| b.write(payload); | ||
| }); |
| { | ||
| "compilerOptions": { | ||
| "target": "es2018", | ||
| "module": "commonjs", | ||
| "lib": ["es5", "es2015", "es2016", "es2017"], | ||
| "skipLibCheck": true, | ||
| "declaration": true, | ||
| "outDir": "./lib", | ||
| "rootDir": "./src", | ||
| "removeComments": true, | ||
| "strict": true, | ||
| "alwaysStrict": true, | ||
| "noUnusedLocals": true, | ||
| "noUnusedParameters": true, | ||
| "downlevelIteration": true, | ||
| "experimentalDecorators": true, | ||
| "noImplicitReturns": true, | ||
| "moduleResolution": "node", | ||
| "esModuleInterop": false | ||
| }, | ||
| "exclude": ["node_modules", "test", "lib"] | ||
| } |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
0
-100%10937
-44.96%8
-33.33%171
-63.62%