@browser-network/network
Advanced tools
Comparing version 0.0.7 to 0.0.8
@@ -5,11 +5,11 @@ /// <reference types="node" /> | ||
import { Repeater } from './Repeater'; | ||
import EventEmitter from './EventEmitter'; | ||
import TypedEventEmitter from './TypedEventEmitter'; | ||
import BehaviorCache from './BehaviorCache'; | ||
import { NetworkConfig } from './NetworkConfig.d'; | ||
import { Connection, PendingConnection } from './Connection.d'; | ||
import { Connection } from './Connection'; | ||
export declare type Message = Mes.Message; | ||
declare type NetworkProps = { | ||
secret: t.Secret; | ||
networkId: t.NetworkId; | ||
switchAddress: t.SwitchAddress; | ||
networkId: t.NetworkId; | ||
clientId: t.ClientId; | ||
config?: Partial<NetworkConfig>; | ||
@@ -19,5 +19,6 @@ }; | ||
'switchboard-response': t.SwitchboardBook; | ||
'add-connection': PendingConnection | Connection; | ||
'add-connection': Connection; | ||
'destroy-connection': Connection['id']; | ||
'broadcast-message': Mes.Message; | ||
'bad-message': Mes.Message; | ||
'message': { | ||
@@ -28,5 +29,5 @@ appId: string; | ||
}; | ||
export declare class Network extends EventEmitter<Events> { | ||
export declare class Network extends TypedEventEmitter<Events> { | ||
config: NetworkConfig; | ||
clientId: t.ClientId; | ||
address: t.Address; | ||
networkId: t.NetworkId; | ||
@@ -39,4 +40,5 @@ switchAddress: t.SwitchAddress; | ||
behaviorCache: BehaviorCache; | ||
private _secret; | ||
_connections: { | ||
[connectionId: t.GUID]: PendingConnection | Connection; | ||
[connectionId: t.GUID]: Connection; | ||
}; | ||
@@ -46,6 +48,7 @@ _seenMessageIds: { | ||
}; | ||
_switchboardVolunteerDelayTimeout: ReturnType<typeof setInterval>; | ||
_switchboardVolunteerDelayTimeout: ReturnType<typeof setTimeout>; | ||
_offerBroadcastInterval: ReturnType<typeof setInterval>; | ||
_garbageCollectInterval: ReturnType<typeof setInterval>; | ||
constructor({ switchAddress, networkId, clientId, config }: NetworkProps); | ||
constructor({ secret, switchAddress, networkId, config }: NetworkProps); | ||
teardown(): void; | ||
broadcast<M extends { | ||
@@ -55,9 +58,10 @@ type: string; | ||
appId: string; | ||
}>(message: M & Partial<Mes.Message>): void; | ||
connections(): (PendingConnection | Connection)[]; | ||
}>(message: M & Partial<Mes.Message>): Promise<void>; | ||
connections(): Connection[]; | ||
isRude(ip: t.IPAddress): boolean; | ||
addToRudeList(ip: t.IPAddress, clientId?: t.ClientId): void; | ||
addToRudeList(ip: t.IPAddress, address?: t.Address): void; | ||
private startOfferBroadcastInterval; | ||
private stopOfferBroadcastInterval; | ||
private startGarbageCollectionInterval; | ||
private rebroadcast; | ||
private stopGarbageCollectionInterval; | ||
private broadcastMessage; | ||
@@ -83,9 +87,6 @@ private beginSwitchboardRequestPeriod; | ||
private destroyConnection; | ||
private send; | ||
private signal; | ||
private broadcastOffer; | ||
private hasConnection; | ||
private getConnectionByClientId; | ||
private getConnectionByAddress; | ||
private getOrGenerateOpenConnection; | ||
} | ||
export {}; |
@@ -27,2 +27,25 @@ var __extends = (this && this.__extends) || (function () { | ||
}; | ||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
var desc = Object.getOwnPropertyDescriptor(m, k); | ||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | ||
desc = { enumerable: true, get: function() { return m[k]; } }; | ||
} | ||
Object.defineProperty(o, k2, desc); | ||
}) : (function(o, m, k, k2) { | ||
if (k2 === undefined) k2 = k; | ||
o[k2] = m[k]; | ||
})); | ||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | ||
Object.defineProperty(o, "default", { enumerable: true, value: v }); | ||
}) : function(o, v) { | ||
o["default"] = v; | ||
}); | ||
var __importStar = (this && this.__importStar) || function (mod) { | ||
if (mod && mod.__esModule) return mod; | ||
var result = {}; | ||
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | ||
__setModuleDefault(result, mod); | ||
return result; | ||
}; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
@@ -73,3 +96,3 @@ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
else if (typeof define === "function" && define.amd) { | ||
define(["require", "exports", "simple-peer", "axios", "uuid", "./util", "./Repeater", "./EventEmitter", "./BehaviorCache"], factory); | ||
define(["require", "exports", "axios", "uuid", "./util", "./Repeater", "./TypedEventEmitter", "./BehaviorCache", "./Connection", "@browser-network/crypto"], factory); | ||
} | ||
@@ -80,3 +103,2 @@ })(function (require, exports) { | ||
exports.Network = void 0; | ||
var simple_peer_1 = __importDefault(require("simple-peer")); | ||
var axios_1 = __importDefault(require("axios")); | ||
@@ -88,6 +110,7 @@ // Tried using crypto.randomUUID() but browserify like 10x's the build | ||
var Repeater_1 = require("./Repeater"); | ||
var EventEmitter_1 = __importDefault(require("./EventEmitter")); | ||
var TypedEventEmitter_1 = __importDefault(require("./TypedEventEmitter")); | ||
var BehaviorCache_1 = __importDefault(require("./BehaviorCache")); | ||
var Connection_1 = require("./Connection"); | ||
var bnc = __importStar(require("@browser-network/crypto")); | ||
var debug = (0, util_1.debugFactory)('Network'); | ||
var IS_NODE = typeof process !== 'undefined'; | ||
// Every app, even including this network, needs a unique id to identify | ||
@@ -109,7 +132,8 @@ // messages coming over the network. The only constraint is that you | ||
// Same | ||
var SWITCHBOARD_REQUEST_ITERATIONS = 15; | ||
var SWITCHBOARD_REQUEST_ITERATIONS = Infinity; | ||
// TODO Use TypedEventEmitter only as a type and not as the actual code. | ||
var Network = /** @class */ (function (_super) { | ||
__extends(Network, _super); | ||
function Network(_a) { | ||
var switchAddress = _a.switchAddress, networkId = _a.networkId, clientId = _a.clientId, _b = _a.config, config = _b === void 0 ? {} : _b; | ||
var secret = _a.secret, switchAddress = _a.switchAddress, networkId = _a.networkId, _b = _a.config, config = _b === void 0 ? {} : _b; | ||
var _this = _super.call(this) || this; | ||
@@ -119,3 +143,4 @@ _this.rudeIps = {}; | ||
_this._seenMessageIds = {}; | ||
_this.config = Object.assign(config, { | ||
_this._secret = secret; | ||
_this.config = Object.assign({ | ||
offerBroadcastInterval: 1000 * 5, | ||
@@ -127,6 +152,6 @@ switchboardRequestInterval: 1000 * 3, | ||
maxConnections: 10 | ||
}); | ||
}, config); | ||
_this.switchAddress = switchAddress; | ||
_this.networkId = networkId; | ||
_this.clientId = clientId; | ||
_this.address = bnc.derivePubKey(secret); | ||
_this.startOfferBroadcastInterval(); | ||
@@ -151,15 +176,51 @@ _this.startGarbageCollectionInterval(); | ||
} | ||
// Stop all listeners, intervals, and connections, so that a process running a network | ||
// can gracefully stop its own process. | ||
Network.prototype.teardown = function () { | ||
this.switchboardRequester.stop(); | ||
this.stopOfferBroadcastInterval(); | ||
this.stopGarbageCollectionInterval(); | ||
clearTimeout(this._switchboardVolunteerDelayTimeout); | ||
for (var _i = 0, _a = this.connections(); _i < _a.length; _i++) { | ||
var c = _a[_i]; | ||
this.destroyConnection(c); | ||
} | ||
for (var conId in this._connections) { | ||
delete this._connections[conId]; | ||
} | ||
this.off(); | ||
}; | ||
// The primary means of sending a message into the network for an application. | ||
// You can pass in a union of your different message types for added type safety. | ||
Network.prototype.broadcast = function (message) { | ||
// TODO require: data, appId, type | ||
// We forbid id and clientId from being passed in. | ||
message.id = (0, uuid_1.v4)(); | ||
message.clientId = this.clientId; | ||
// TODO validate shape here | ||
var toBroadcast = Object.assign(message, { | ||
ttl: 6, | ||
destination: '*' | ||
return __awaiter(this, void 0, void 0, function () { | ||
var toBroadcast, _a, _b; | ||
var _c; | ||
return __generator(this, function (_d) { | ||
switch (_d.label) { | ||
case 0: | ||
// required: data, appId, type | ||
if (!message.type || !message.data || !message.appId) { | ||
throw new TypeError('Must supply at least type, data and appId'); | ||
} | ||
toBroadcast = Object.assign({ | ||
id: (0, uuid_1.v4)(), | ||
address: this.address, | ||
ttl: 6, | ||
destination: '*', | ||
signatures: [] | ||
}, message); | ||
_b = (_a = toBroadcast.signatures).push; | ||
_c = { | ||
signer: this.address | ||
}; | ||
return [4 /*yield*/, bnc.sign(this._secret, toBroadcast)]; | ||
case 1: | ||
_b.apply(_a, [(_c.signature = _d.sent(), | ||
_c)]); | ||
this.broadcastMessage(toBroadcast); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
this.broadcastMessage(toBroadcast); | ||
}; | ||
@@ -176,10 +237,10 @@ // List of all our current connections | ||
// Add an ip to a rude list, which means we won't connect to then any more. | ||
// If an optional clientId is provided, and we're connected to that clientId, | ||
// If an optional address is provided, and we're connected to that address, | ||
// we'll drop them as well. | ||
Network.prototype.addToRudeList = function (ip, clientId) { | ||
Network.prototype.addToRudeList = function (ip, address) { | ||
this.rudeIps[ip] = Date.now(); | ||
debug(1, 'added to rude list:', ip, clientId); | ||
debug(1, 'added to rude list:', ip, address); | ||
// We can check and make sure we're aren't / don't stay connected to this person | ||
if (clientId) { | ||
var connection = this.getConnectionByClientId(clientId); | ||
if (address) { | ||
var connection = this.getConnectionByAddress(address); | ||
if (!connection) { | ||
@@ -192,3 +253,3 @@ return; | ||
data: 'rude', | ||
destination: clientId | ||
destination: address | ||
}); | ||
@@ -200,12 +261,16 @@ this.destroyConnection(connection); | ||
Network.prototype.startOfferBroadcastInterval = function () { | ||
var _this = this; | ||
if (this._offerBroadcastInterval) { | ||
return; | ||
} | ||
this._offerBroadcastInterval = setInterval(this.broadcastOffer.bind(this), this.config.offerBroadcastInterval); | ||
this._offerBroadcastInterval = setInterval(function () { | ||
var openCon = _this.getOrGenerateOpenConnection(); | ||
if (openCon.negotiation.sdp) | ||
_this.broadcastOffer(); | ||
}, this.config.offerBroadcastInterval); | ||
}; | ||
// // Temporarily removed but kept for safe keeping | ||
// private stopOfferBroadcastInterval() { | ||
// clearInterval(this._offerBroadcastInterval) | ||
// delete this._offerBroadcastInterval | ||
// } | ||
Network.prototype.stopOfferBroadcastInterval = function () { | ||
clearInterval(this._offerBroadcastInterval); | ||
delete this._offerBroadcastInterval; | ||
}; | ||
// Safely start it | ||
@@ -218,17 +283,9 @@ Network.prototype.startGarbageCollectionInterval = function () { | ||
}; | ||
// // Temporarily removed but kept for safe keeping | ||
// private stopGarbageCollectionInterval() { | ||
// clearInterval(this._garbageCollectInterval) | ||
// delete this._garbageCollectInterval | ||
// } | ||
Network.prototype.rebroadcast = function (message) { | ||
if (!message.ttl) { | ||
return; | ||
} | ||
message.ttl -= 1; | ||
this.broadcastMessage(message); | ||
Network.prototype.stopGarbageCollectionInterval = function () { | ||
clearInterval(this._garbageCollectInterval); | ||
delete this._garbageCollectInterval; | ||
}; | ||
// Send message to all our connections | ||
// TODO Fold this into broadcast | ||
Network.prototype.broadcastMessage = function (message) { | ||
// TODO validate message shape at runtime | ||
// TODO make helpers for this | ||
@@ -240,5 +297,25 @@ this._seenMessageIds[message.id] = Date.now(); | ||
// anything over that. | ||
if (!connection.negotiation.sdp) | ||
return; | ||
this.send(connection, message); | ||
if (!connection.negotiation.sdp) { | ||
continue; | ||
} | ||
try { | ||
// The difference between write and send is that write queues, send | ||
// throws if it's not writable yet. Previously there was a race | ||
// condition here leading to many initial connections when | ||
// using write. Once we removed the asynchronicity from connection | ||
// creation, that race condition went away and we're free to use .write | ||
// again. However, ephemerality is built into the network, so it's understood | ||
// that messages won't always make it. With our rudeness checking on, maybe | ||
// it's best not to queue up messages before sending, and just send when | ||
// we're connected. | ||
// connection.peer.write(JSON.stringify(message)) | ||
if (!connection.peer.connected) { | ||
continue; | ||
} | ||
connection.peer.send(JSON.stringify(message)); | ||
debug(5, 'sending', message, 'to', connection.address); | ||
} | ||
catch (e) { | ||
debug(3, 'got error trying to send to', connection.address, e); | ||
} | ||
} | ||
@@ -256,4 +333,3 @@ this.emit('broadcast-message', message); | ||
this.switchboardRequester.begin(); | ||
// TODO this is _alright_ but not great. | ||
this.broadcastMessage({ | ||
this.broadcast({ | ||
id: (0, uuid_1.v4)(), | ||
@@ -264,3 +340,3 @@ appId: APP_ID, | ||
ttl: 2, | ||
clientId: this.clientId, | ||
address: this.address, | ||
data: {} | ||
@@ -277,6 +353,6 @@ }); | ||
existingConnection = this.getOrGenerateOpenConnection(); | ||
// We don't want to send switchboard requests for PendingConnections | ||
// We don't want to send switchboard requests for pending connections | ||
if (!existingConnection.negotiation.sdp) | ||
return [2 /*return*/]; | ||
return [4 /*yield*/, this.sendNegotiationToSwitchingService(__assign({ clientId: this.clientId, networkId: this.networkId, connectionId: existingConnection.id }, existingConnection.negotiation))]; | ||
return [4 /*yield*/, this.sendNegotiationToSwitchingService(__assign({ address: this.address, networkId: this.networkId, connectionId: existingConnection.id }, existingConnection.negotiation))]; | ||
case 1: | ||
@@ -301,3 +377,3 @@ resp = _a.sent(); | ||
switch (negotiation.type) { | ||
case 'offer': { | ||
case 'offer': | ||
var connection_1 = this_1.handleOffer(negotiation); | ||
@@ -307,19 +383,9 @@ if (!connection_1) { | ||
} | ||
// TODO Factor this right on out of here. Connection should be a class | ||
// that emits an event when the sdp info is ready, among other things. | ||
var interval_1 = setInterval(function () { | ||
// We are just gonna keep trying until the sdp has come through, which comes a little | ||
// bit after the connection is initially formed. | ||
if (!connection_1.negotiation.sdp) | ||
return; | ||
connection_1.on('sdp', function () { | ||
_this.sendNegotiationToSwitchingService(__assign({ connectionId: connection_1.id, timestamp: Date.now(), networkId: _this.networkId }, connection_1.negotiation)); | ||
// we only want to do this once. | ||
clearInterval(interval_1); | ||
}, 300); | ||
}); | ||
break; | ||
} | ||
case 'answer': { | ||
case 'answer': | ||
this_1.handleAnswer(negotiation); | ||
break; | ||
} | ||
default: | ||
@@ -342,3 +408,4 @@ (0, util_1.exhaustive)(negotiation, 'We got something from the switchboard that has a weird type'); | ||
}; | ||
// TODO Pull this off the proto | ||
// TODO Pull this off the proto, along with the other switchboard stuff. These | ||
// should be able to live entirely on their own class. | ||
Network.prototype.sendNegotiationToSwitchingService = function (negotiation) { | ||
@@ -365,42 +432,75 @@ return __awaiter(this, void 0, void 0, function () { | ||
Network.prototype.handleMessage = function (message) { | ||
// If we've already seen this message, we do nothing | ||
// with it. | ||
if (this._seenMessageIds[message.id]) { | ||
return; | ||
} | ||
// Now we've seen this message. | ||
this._seenMessageIds[message.id] = Date.now(); | ||
debug(5, 'handleMessage:', message); | ||
// We are only interested in our own application here. | ||
// The network is actually an application on the network, lolz. | ||
// Note we're using 'massage' here only so typescript knows | ||
// about the correct typing. Try getting exhaustiveness without | ||
// it. | ||
var massage = message; | ||
if (message.appId === APP_ID) { | ||
switch (massage.type) { | ||
case 'offer': | ||
this.handleOfferMessage(massage); | ||
break; | ||
; | ||
case 'answer': | ||
this.handleAnswerMessage(massage); | ||
break; | ||
; | ||
case 'log': | ||
this.handleLogMessage(massage); | ||
break; | ||
; | ||
case 'switchboard-volunteer': | ||
this.handleSwitchboardVolunteerMessage(massage); | ||
break; | ||
; | ||
default: | ||
(0, util_1.exhaustive)(massage, 'Someone sent a message with our appId but of the wrong type!'); | ||
break; | ||
; | ||
} | ||
} | ||
this.rebroadcast(message); | ||
this.emit('message', { appId: message.appId, message: message }); | ||
return __awaiter(this, void 0, void 0, function () { | ||
var signatures, signature, isValidSignature, massage; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
// If we've already seen this message, we do nothing | ||
// with it. | ||
if (this._seenMessageIds[message.id]) { | ||
return [2 /*return*/]; | ||
} | ||
// Now we've seen this message. | ||
this._seenMessageIds[message.id] = Date.now(); | ||
debug(5, 'handleMessage:', message); | ||
// Ensure the message is cryptographically sound | ||
// Firstly, if there are no signatures, it is not sound. | ||
if (message.signatures.length === 0) { | ||
debug(3, 'received message with no signatures!', message); | ||
this.emit('bad-message', message); | ||
} | ||
signatures = []; | ||
_a.label = 1; | ||
case 1: | ||
if (!(message.signatures.length !== 0)) return [3 /*break*/, 3]; | ||
signature = message.signatures.pop(); | ||
signatures.unshift(signature); | ||
return [4 /*yield*/, bnc.verifySignature(message, signature.signature, signature.signer)]; | ||
case 2: | ||
isValidSignature = _a.sent(); | ||
if (!isValidSignature) { | ||
debug(3, 'received message with unverifiable signature!', message); | ||
this.emit('bad-message', message); | ||
return [2 /*return*/]; | ||
} | ||
return [3 /*break*/, 1]; | ||
case 3: | ||
// Now we repair the mutation from above | ||
message.signatures = signatures; | ||
massage = message; | ||
if (message.appId === APP_ID) { | ||
switch (massage.type) { | ||
case 'offer': | ||
this.handleOfferMessage(massage); | ||
break; | ||
; | ||
case 'answer': | ||
this.handleAnswerMessage(massage); | ||
break; | ||
; | ||
case 'log': | ||
this.handleLogMessage(massage); | ||
break; | ||
; | ||
case 'switchboard-volunteer': | ||
this.handleSwitchboardVolunteerMessage(massage); | ||
break; | ||
; | ||
default: | ||
(0, util_1.exhaustive)(massage, 'Someone sent a message with our appId but of the wrong type!'); | ||
break; | ||
; | ||
} | ||
} | ||
// Instead of decrementing the ttl value, since the signatures depend on it | ||
// staying the same, we count the signatures to see how many hops the message | ||
// has taken. | ||
if (message.signatures.length < message.ttl) { | ||
this.broadcast(message); | ||
} | ||
this.emit('message', { appId: message.appId, message: message }); | ||
return [2 /*return*/]; | ||
} | ||
}); | ||
}); | ||
}; | ||
@@ -413,10 +513,7 @@ Network.prototype.handleOfferMessage = function (message) { | ||
} | ||
var interval = setInterval(function () { | ||
if (!connection.negotiation.sdp) | ||
return; | ||
_this.broadcastMessage(__assign(__assign({}, connection.negotiation), { appId: APP_ID, id: (0, uuid_1.v4)(), ttl: 6, clientId: _this.clientId, destination: message.clientId, data: { | ||
connection.on('sdp', function () { | ||
_this.broadcast(__assign(__assign({}, connection.negotiation), { appId: APP_ID, id: (0, uuid_1.v4)(), ttl: 6, address: _this.address, destination: message.address, data: { | ||
connectionId: message.data.connectionId | ||
} })); | ||
clearInterval(interval); | ||
}, 300); | ||
}); | ||
}; | ||
@@ -428,13 +525,13 @@ Network.prototype.handleAnswerMessage = function (message) { | ||
// Only log messages sent to us | ||
if (!['*', this.clientId].includes(message.destination)) { | ||
if (!['*', this.address].includes(message.destination)) { | ||
return; | ||
} | ||
console.log(message.clientId + ':', message.data.contents); | ||
console.log(message.address + ':', message.data.contents); | ||
}; | ||
Network.prototype.handleSwitchboardVolunteerMessage = function (message) { | ||
if (!this.config.respectSwitchboardVolunteerMessages) { | ||
debug(5, 'Switchboard Volunteer Message heard but feature is disabled. Heard from:', message.clientId); | ||
debug(5, 'Switchboard Volunteer Message heard but feature is disabled. Heard from:', message.address); | ||
return; | ||
} | ||
debug(3, 'heard switchboard volunteer, backing off switchboard requests:', message.clientId); | ||
debug(3, 'heard switchboard volunteer, backing off switchboard requests:', message.address); | ||
this.switchboardRequester.stop(); | ||
@@ -456,3 +553,3 @@ if (this._switchboardVolunteerDelayTimeout) { | ||
// Somebody else already got to this open connection | ||
connection.clientId || | ||
connection.address || | ||
// The ip trying to connect is on our naughty list or not presenting an sdp string | ||
@@ -466,5 +563,5 @@ !answer.sdp || this.isRude((0, util_1.getIpFromRTCSDP)(answer.sdp)) || | ||
// Now we know who is at the other end of the open offer we'd previously created. | ||
connection.clientId = answer.clientId; | ||
connection.address = answer.address; | ||
// Punch through that nat | ||
this.signal(connection.peer, answer); | ||
connection.signal(answer); | ||
}; | ||
@@ -477,5 +574,5 @@ Network.prototype.handleOffer = function (offer) { | ||
// It's ourselves | ||
offer.clientId === this.clientId || | ||
offer.address === this.address || | ||
// We're are already connected to this client | ||
this.hasConnection(offer.clientId) || | ||
!!this.getConnectionByAddress(offer.address) || | ||
// They're on our rude list or not presenting an sdp string | ||
@@ -488,15 +585,14 @@ !offer.sdp || this.isRude((0, util_1.getIpFromRTCSDP)(offer.sdp)) || | ||
// There's an offer in the book for a client to whom we're not connected. | ||
debug(3, 'fielding an offer from', offer.clientId); | ||
debug(3, 'fielding an offer from', offer.address); | ||
// Generate the answer response to peer's answer (new peer object) | ||
// Always will be present b/c it's new | ||
var connection = this.generateAnswerConnection(offer); | ||
this.addConnection(connection, offer.clientId); | ||
this.addConnection(connection, offer.address); | ||
return connection; | ||
}; | ||
Network.prototype.generateOfferConnection = function () { | ||
var peer = new simple_peer_1["default"]({ initiator: true, trickle: false, wrtc: IS_NODE ? require('wrtc') : undefined }); | ||
var id = (0, uuid_1.v4)(); | ||
var negotiation = { | ||
type: 'offer', | ||
clientId: this.clientId, | ||
address: this.address, | ||
connectionId: id, | ||
@@ -507,16 +603,9 @@ sdp: null, | ||
}; | ||
var pendingConnection = { id: id, peer: peer, negotiation: negotiation }; | ||
peer.on('signal', function (data) { | ||
if (data.type === 'offer') { | ||
pendingConnection.negotiation.sdp = data.sdp; | ||
} | ||
}); | ||
return pendingConnection; | ||
return new Connection_1.Connection(id, true, negotiation); | ||
}; | ||
Network.prototype.generateAnswerConnection = function (offer) { | ||
debug(5, 'generateAnswerConnection called for offer:', offer.clientId, offer.connectionId); | ||
var peer = new simple_peer_1["default"]({ initiator: false, trickle: false, wrtc: IS_NODE ? require('wrtc') : undefined }); | ||
debug(5, 'generateAnswerConnection called for offer:', offer.address, offer.connectionId); | ||
var negotiation = { | ||
type: 'answer', | ||
clientId: this.clientId, | ||
address: this.address, | ||
connectionId: offer.connectionId, | ||
@@ -527,15 +616,11 @@ sdp: null, | ||
}; | ||
var pendingConnection = { id: (0, uuid_1.v4)(), peer: peer, negotiation: negotiation }; | ||
peer.on('signal', function (data) { | ||
if (data.type === 'answer') { | ||
pendingConnection.negotiation.sdp = data.sdp; | ||
} | ||
}); | ||
this.signal(peer, offer); | ||
return pendingConnection; | ||
var connection = new Connection_1.Connection((0, uuid_1.v4)(), false, negotiation); | ||
connection.signal(offer); | ||
return connection; | ||
}; | ||
Network.prototype.addConnection = function (connection, clientId) { | ||
Network.prototype.addConnection = function (connection, address) { | ||
// This always needs to happen when we add the connection to our pool, | ||
// lest we're adding an offer. | ||
connection.clientId = clientId; | ||
if (address) | ||
connection.registerAddress(address); | ||
this._connections[connection.id] = connection; | ||
@@ -549,13 +634,13 @@ this.registerRTCEventHandlers(connection); | ||
peer.on('connect', function () { | ||
debug(2, 'CONNECT', connection.clientId); | ||
debug(2, 'CONNECT', connection.address); | ||
// Send a welcome log message for the warm fuzzies | ||
_this.broadcastMessage({ | ||
_this.broadcast({ | ||
type: 'log', | ||
clientId: _this.clientId, | ||
address: _this.address, | ||
appId: APP_ID, | ||
id: (0, uuid_1.v4)(), | ||
ttl: 1, | ||
destination: connection.clientId, | ||
destination: connection.address, | ||
data: { | ||
contents: 'you are now proudly connected to ' + _this.clientId | ||
contents: 'Heyo!' | ||
} | ||
@@ -565,9 +650,9 @@ }); | ||
peer.on('data', function (data) { | ||
var clientId = connection.clientId, negotiation = connection.negotiation; | ||
var address = connection.address, negotiation = connection.negotiation; | ||
var peerAddress = (0, util_1.getIpFromRTCSDP)(negotiation.sdp); | ||
debug(5, 'got message from:', peerAddress, clientId); | ||
debug(5, 'got message from:', peerAddress, address); | ||
// Ensure the machine on the other end of this connection is behaving themselves | ||
if (!_this.behaviorCache.isOnGoodBehavior(peerAddress)) { | ||
debug(1, 'whoops, the machine belonging to', clientId, 'is exhibiting bad behavior!'); | ||
_this.addToRudeList(peerAddress, clientId); | ||
debug(1, 'whoops, the machine belonging to', address, 'is exhibiting bad behavior!'); | ||
_this.addToRudeList(peerAddress, address); | ||
return; | ||
@@ -581,3 +666,3 @@ } | ||
catch (e) { | ||
return debug(3, 'failed to parse message from', clientId + ':', str, e); | ||
return debug(3, 'failed to parse message from', address + ':', str, e); | ||
} | ||
@@ -587,5 +672,5 @@ _this.handleMessage(message); | ||
peer.on('close', function () { _this.destroyConnection(connection); }); | ||
peer.on('end', function () { debug(5, 'p.on("end") fired for client', connection.clientId); }); | ||
peer.on('writable', function () { debug(5, 'p.on("writable") fired for client', connection.clientId); }); | ||
peer.on('error', function (err) { debug(4, "p.on(error) handler for ".concat(connection.clientId, ":"), err); }); | ||
peer.on('end', function () { debug(5, 'p.on("end") fired for client', connection.address); }); | ||
peer.on('writable', function () { debug(5, 'p.on("writable") fired for client', connection.address); }); | ||
peer.on('error', function (err) { debug(4, "p.on(error) handler for ".concat(connection.address, ":"), err); }); | ||
}; | ||
@@ -609,3 +694,3 @@ Network.prototype.garbageCollect = function () { | ||
// But it'd be better if we weren't having duplicate clients at all. | ||
var seenClientIds = {}; | ||
var seenAddresses = {}; | ||
// The actual garbage collection action | ||
@@ -618,3 +703,3 @@ var collect = function (connection) { | ||
var connection = this._connections[connectionId]; | ||
var clientId = connection.clientId, destroyed = connection.peer.destroyed; | ||
var address = connection.address, destroyed = connection.peer.destroyed; | ||
if (destroyed) { | ||
@@ -634,10 +719,12 @@ return collect(connection); | ||
// but that did not seem to have any effect. | ||
// This reads "if we've seen this clientId already, assess if either of the connections | ||
// This reads "if we've seen this address already, assess if either of the connections | ||
// have no channelName and remove it if it doesn't." | ||
var seenConnectionId = seenClientIds[clientId]; | ||
var seenConnectionId = seenAddresses[address]; | ||
if (seenConnectionId) { | ||
// These two mean if either has no channelName, remove it. | ||
// @ts-ignore -- not in the types, but not underscore prefixed.. | ||
if (connection.peer.channelName === null) { | ||
return collect(connection); | ||
} | ||
// @ts-ignore | ||
if (this._connections[seenConnectionId].peer.channelName === null) { | ||
@@ -647,7 +734,7 @@ return collect(this._connections[seenConnectionId]); | ||
} | ||
seenClientIds[clientId] = connection.id; | ||
seenAddresses[address] = connection.id; | ||
} | ||
}; | ||
Network.prototype.destroyConnection = function (connection) { | ||
debug(4, 'destroying connection', connection.clientId); | ||
debug(4, 'destroying connection', connection.address); | ||
var peer = connection.peer; | ||
@@ -660,66 +747,23 @@ peer.removeAllListeners(); | ||
}; | ||
// Send message to a specific connection | ||
Network.prototype.send = function (connection, message) { | ||
try { | ||
// The difference between write and send is that write queues, send | ||
// throws if it's not writable yet. Previously there was a race | ||
// condition here leading to many initial connections when | ||
// using write. Once we removed the asynchronicity from connection | ||
// creation, that race condition went away and we're free to use .write | ||
// again. However, ephemerality is built into the network, so it's understood | ||
// that messages won't always make it. With our rudeness checking on, maybe | ||
// it's best not to queue up messages before sending, and just send when | ||
// we're connected. | ||
// connection.peer.write(JSON.stringify(message)) | ||
if (!connection.peer.connected) { | ||
return; | ||
} | ||
connection.peer.send(JSON.stringify(message)); | ||
debug(5, 'sending', message, 'to', connection.clientId); | ||
} | ||
catch (e) { | ||
debug(3, 'got error trying to send to', connection.clientId, e); | ||
} | ||
}; | ||
// Safely signal a peer | ||
Network.prototype.signal = function (peer, data) { | ||
debug(5, 'signaling peer:', peer, data); | ||
try { | ||
peer.signal(data); | ||
} | ||
catch (e) { | ||
debug(3, 'error signaling peer:', e); | ||
} | ||
}; | ||
Network.prototype.broadcastOffer = function () { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var openConnection, offer; | ||
return __generator(this, function (_a) { | ||
openConnection = this.getOrGenerateOpenConnection(); | ||
offer = { | ||
id: (0, uuid_1.v4)(), | ||
ttl: 6, | ||
type: 'offer', | ||
clientId: this.clientId, | ||
appId: APP_ID, | ||
destination: '*', | ||
data: __assign({ timestamp: Date.now(), connectionId: openConnection.id }, openConnection.negotiation) | ||
}; | ||
this.broadcastMessage(offer); | ||
return [2 /*return*/]; | ||
}); | ||
}); | ||
var openConnection = this.getOrGenerateOpenConnection(); | ||
// We don't want to send messages about pending connections | ||
if (!openConnection.negotiation.sdp) | ||
return; | ||
var offer = { | ||
ttl: 6, | ||
type: 'offer', | ||
appId: APP_ID, | ||
destination: '*', | ||
data: __assign({ timestamp: Date.now(), connectionId: openConnection.id }, openConnection.negotiation) | ||
}; | ||
this.broadcast(offer); | ||
}; | ||
Network.prototype.hasConnection = function (clientId) { | ||
return !!this.getConnectionByClientId(clientId); | ||
Network.prototype.getConnectionByAddress = function (address) { | ||
return this.connections().find(function (con) { return con.address === address; }); | ||
}; | ||
Network.prototype.getConnectionByClientId = function (clientId) { | ||
return this.connections().find(function (con) { return con.clientId === clientId; }); | ||
}; | ||
// If we have an open connection in the pool, return that. | ||
// Otherwise, generate an open connection. | ||
Network.prototype.getOrGenerateOpenConnection = function () { | ||
// return this.connections().find(con => con.negotiation.type === 'offer') // TODO | ||
// let oc = this.connections().find(con => !con.peer.connected) // TODO | ||
var oc = this.connections().find(function (con) { return !con.clientId; }); // TODO | ||
var oc = this.connections().find(function (con) { return !con.address; }); | ||
if (!oc) { | ||
@@ -732,4 +776,4 @@ oc = this.generateOfferConnection(); | ||
return Network; | ||
}(EventEmitter_1["default"])); | ||
}(TypedEventEmitter_1["default"])); | ||
exports.Network = Network; | ||
}); |
@@ -24,2 +24,3 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { | ||
return function (logLevel) { | ||
var _a; | ||
var args = []; | ||
@@ -29,3 +30,3 @@ for (var _i = 1; _i < arguments.length; _i++) { | ||
} | ||
if (globalThis.DEBUG >= logLevel) { | ||
if (globalThis.DEBUG >= logLevel || Number((_a = globalThis.process) === null || _a === void 0 ? void 0 : _a.env.DEBUG) >= logLevel) { | ||
var d = new Date(); | ||
@@ -32,0 +33,0 @@ console.log.apply(console, __spreadArray(["[".concat(logLevel, "] ").concat(appName, " ").concat(d.toLocaleTimeString(), ": ")], args, false)); |
{ | ||
"name": "@browser-network/network", | ||
"version": "0.0.7", | ||
"version": "0.0.8", | ||
"description": "A WebRTC based direct peer to peer network in the browser.", | ||
@@ -12,3 +12,3 @@ "main": "./dist/src/index.js", | ||
"scripts": { | ||
"test": "ts-node test/index.ts", | ||
"test": "tap --ts --no-timeout --no-coverage test/*.ts", | ||
"clean": "shx rm -rf dist build umd; shx mkdir umd", | ||
@@ -19,3 +19,3 @@ "compile:ts": "tsc", | ||
"build": "npm run clean; npm-run-all compile:**", | ||
"build:watch": "nodemon -e ts,json -i dist -x 'npm run compile:ts && npm run compile:pack'", | ||
"build:watch": "nodemon -e ts,json -i dist -i build -i umd -x 'npm run compile:ts && npm run compile:pack'", | ||
"start:dev": "node serve.js & npm run build:watch", | ||
@@ -46,3 +46,5 @@ "release": "npm run build && np --no-cleanup --no-tests --no-yarn --message=\"New release! Version: %s\"" | ||
"dependencies": { | ||
"@browser-network/crypto": "^0.0.1", | ||
"axios": "^0.26.1", | ||
"eccrypto": "^1.1.6", | ||
"simple-peer": "^9.11.1", | ||
@@ -54,5 +56,6 @@ "uuid": "^8.3.2", | ||
"@mapbox/node-pre-gyp": "^1.0.9", | ||
"@types/eccrypto": "^1.1.3", | ||
"@types/node": "^16", | ||
"@types/simple-peer": "^9.11.4", | ||
"@types/tape": "^4.13.2", | ||
"@types/tap": "^15.0.6", | ||
"@types/uuid": "^8.3.4", | ||
@@ -64,4 +67,5 @@ "browserify": "^17.0.0", | ||
"shx": "^0.3.4", | ||
"tap": "^16.0.1", | ||
"tap-spec": "^5.0.0", | ||
"tape": "^5.5.2", | ||
"ts-node": "^10.7.0", | ||
"typescript": "^4.4.4", | ||
@@ -68,0 +72,0 @@ "uglify-js": "^3.15.3" |
@@ -66,4 +66,8 @@ # Distributed Browser Network | ||
browser window :P Note that if you wish to do _slightly more programming_, you can | ||
also run a node.js node with the same `networkId`. | ||
also run a node.js node with the same `networkId`, and it will act as a headless | ||
browser window, fulfilling all the same functionality as a browser window would. | ||
* Cryptographic security - Network uses `eccrypto` to ensure veracity of messages. | ||
It's cryptographically difficult to spoof or modify a message that's not your own. | ||
### How it works | ||
@@ -148,3 +152,3 @@ | ||
switchAddress: 'http://localhost:5678', // default address of switchboard | ||
clientId: crypto.randomUUID(), // arbitrary string | ||
address: crypto.randomUUID(), // arbitrary string | ||
networkId: 'test-network' | ||
@@ -197,3 +201,3 @@ }) | ||
switchAddress: 'http://localhost:5678', // default address of switchboard | ||
clientId: globalThis.crypto.randomUUID(), // arbitrary string | ||
address: globalThis.crypto.randomUUID(), // arbitrary string | ||
networkId: '<something unique but the same b/t all your nodes>', | ||
@@ -287,14 +291,5 @@ config:{ | ||
* Assess how much of our inter peer data could be represented with buffers | ||
* if a broadcast is made with a specific clientId, and we're connected to that clientId, | ||
just go ahead and send directly to that clientId instead of broadcasting to everyone. | ||
* if a broadcast is made with a specific address, and we're connected to that address, | ||
just go ahead and send directly to that address instead of broadcasting to everyone. | ||
* Log message config param - toggle for whether to respect log messages. Might be | ||
a security vulnerability. | ||
* Bring cryptographic integrity into this project. Originally I was unsure of whether | ||
it would be helpful because it doesn't prevent spam. If you got blocked somehow, | ||
you could just change your priv key and boom you're a new user. Although now that | ||
I write it out, I could see a reputation system working just fine about that. | ||
Anyways there's another reason to bring it in - to prevent spoofing the clientId | ||
of a message. Right now I can broadcast a message and put any clientId I'd like | ||
on it. As far as network is currently concerned this is not an issue, you can't spoof | ||
someone's sdp information. The worst you could do is confuse someone's connection pool. | ||
Actually that's a low hanging DOS right there. So yeah, network needs cryptographic principles. |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1567200
17
29224
6
16
292
40
+ Addedeccrypto@^1.1.6
+ Added@browser-network/crypto@0.0.1(transitive)
+ Addedacorn@7.1.1(transitive)
+ Addedbindings@1.5.0(transitive)
+ Addedbip66@1.1.5(transitive)
+ Addedbn.js@4.12.1(transitive)
+ Addedbrorand@1.1.0(transitive)
+ Addedbrowserify-aes@1.2.0(transitive)
+ Addedbuffer-xor@1.0.3(transitive)
+ Addedcipher-base@1.0.6(transitive)
+ Addedcreate-hash@1.2.0(transitive)
+ Addedcreate-hmac@1.1.7(transitive)
+ Addeddrbg.js@1.0.1(transitive)
+ Addedeccrypto@1.1.6(transitive)
+ Addedelliptic@6.5.4(transitive)
+ Addedes6-promise@4.2.8(transitive)
+ Addedevp_bytestokey@1.0.3(transitive)
+ Addedfile-uri-to-path@1.0.0(transitive)
+ Addedhash-base@3.1.0(transitive)
+ Addedhash.js@1.1.7(transitive)
+ Addedhmac-drbg@1.0.1(transitive)
+ Addedmd5.js@1.3.5(transitive)
+ Addedminimalistic-assert@1.0.1(transitive)
+ Addedminimalistic-crypto-utils@1.0.1(transitive)
+ Addednan@2.14.0(transitive)
+ Addedripemd160@2.0.2(transitive)
+ Addedsecp256k1@3.7.1(transitive)
+ Addedsha.js@2.4.11(transitive)