UNPKG

ndn-js

Version:

A JavaScript client library for Named Data Networking

493 lines (443 loc) 16.4 kB
/** * Copyright (C) 2016-2019 Regents of the University of California. * @author: Jeff Thompson <jefft0@remap.ucla.edu> * @author: Wentao Shang * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * A copy of the GNU Lesser General Public License is in the file COPYING. */ /** * A MicroForwarder holds a PIT, FIB and faces to function as a simple NDN * forwarder. * Create a new MicroForwarder, using chrome.runtime.onConnect.addListener to * get the port and add a new face to use a RuntimePortTransport to communiate * with the WebExtensions port. */ var MicroForwarder = function MicroForwarder() { this.PIT_ = []; // of PitEntry this.FIB_ = []; // of FibEntry this.faces_ = []; // of ForwarderFace this.CS_ = {}; // Key: The Data name URI. Value: The Data object. // Add a listener to wait for a connection request from a tab and add a face. var thisForwarder = this; chrome.runtime.onConnect.addListener(function(port) { thisForwarder.addFace ("internal://port", new RuntimePortTransport(), new RuntimePortTransport.ConnectionInfo(port)); }); // Use Native Messaging to connect to the ndn_multicast app to send and // receive to other computers on the LAN using the NDN multicast group. // Make a FIB entry to multicast all Interests. if (chrome.runtime.connectNative) { var port = chrome.runtime.connectNative("ndn_multicast"); var faceId = this.addFace ("nativePort://ndn_multicast", new RuntimePortTransport(), new RuntimePortTransport.ConnectionInfo(port)); this.registerRoute(new Name("/ndn"), faceId); } }; /** * Add a new face to communicate with the given transport. This immediately * connects using the connectionInfo. If the transport connection is closed, * disable and remove the face. * @param {string} uri The URI to use in the faces/query and faces/list * commands. * @param {Transport} transport An object of a subclass of Transport to use * for communication. If the transport object has a "setOnReceivedObject" * method, then use it to set the onReceivedObject callback. * @param {TransportConnectionInfo} connectionInfo This must be a * ConnectionInfo from the same subclass of Transport as transport. * @return {number} The new face ID. */ MicroForwarder.prototype.addFace = function(uri, transport, connectionInfo) { var face = null; var thisForwarder = this; if ("setOnReceivedObject" in transport) transport.setOnReceivedObject (function(obj) { thisForwarder.onReceivedObject(face, obj); }); face = new ForwarderFace(uri, transport); function onClosedCallback() { face.disable(); for (var i = 0; i < thisForwarder.faces_.length; ++i) { if (thisForwarder.faces_[i] === face) { // TODO: Mark this face as disconnected so the FIB doesn't use it. thisForwarder.faces_.splice(i, 1); break; } } } transport.connect (connectionInfo, { onReceivedElement: function(element) { thisForwarder.onReceivedElement(face, element); } }, function(){}, onClosedCallback); this.faces_.push(face); return face.faceId; }; /** * Find or create the FIB entry with the given name and add the ForwarderFace * with the given faceId. * @param {Name} name The name of the FIB entry. * @param {number} faceId The face ID of the face for the route. * @return {boolean} True for success, or false if can't find the ForwarderFace * with faceId. */ MicroForwarder.prototype.registerRoute = function(name, faceId) { // Find the face with the faceId. var nexthopFace = null; for (var i = 0; i < this.faces_.length; ++i) { if (this.faces_[i].faceId == faceId) { nexthopFace = this.faces_[i]; break; } } if (nexthopFace == null) return false; // Check for a FIB entry for the name and add the face. for (var i = 0; i < this.FIB_.length; ++i) { var fibEntry = this.FIB_[i]; if (fibEntry.name.equals(name)) { // Make sure the face is not already added. if (fibEntry.faces.indexOf(nexthopFace) < 0) fibEntry.faces.push(nexthopFace); return true; } } // Make a new FIB entry. var fibEntry = new FibEntry(name); fibEntry.faces.push(nexthopFace); this.FIB_.push(fibEntry); return true; } /** * This is called by the listener when an entire TLV element is received. * If it is an Interest, look in the FIB for forwarding. If it is a Data packet, * look in the PIT to match an Interest. * @param {ForwarderFace} face The ForwarderFace with the transport that * received the element. * @param {Buffer} element The received element. */ MicroForwarder.prototype.onReceivedElement = function(face, element) { if (LOG > 3) console.log("Complete element received. Length " + element.length + "\n"); // First, decode as Interest or Data. var interest = null; var data = null; if (element[0] == Tlv.Interest || element[0] == Tlv.Data) { var decoder = new TlvDecoder(element); if (decoder.peekType(Tlv.Interest, element.length)) { interest = new Interest(); interest.wireDecode(element, TlvWireFormat.get()); } else if (decoder.peekType(Tlv.Data, element.length)) { data = new Data(); data.wireDecode(element, TlvWireFormat.get()); } } // Now process as Interest or Data. if (interest !== null) { var interestUri = interest.getName().toUri(); if (LOG > 3) console.log("Interest packet received: " + interestUri + "\n"); if (MicroForwarder.localhostNamePrefix.match(interest.getName())) { this.onReceivedLocalhostInterest(face, interest); return; } // Check CS. // TODO: This uses exact name match. Should match on prefix and use selectors. if (interestUri in this.CS_) { if (LOG > 3) console.log("Data found in CS: " + interestUri + "\n"); face.sendBuffer(this.CS_[interestUri].wireEncode().buf()); return; } for (var i = 0; i < this.PIT_.length; ++i) { // TODO: Check interest equality of appropriate selectors. if (this.PIT_[i].face == face && this.PIT_[i].interest.getName().equals(interest.getName())) { // Duplicate PIT entry. // TODO: Update the interest timeout? if (LOG > 3) console.log("Duplicate Interest: " + interest.getName().toUri()); return; } } // Add to the PIT. var pitEntry = new PitEntry(interest, face); this.PIT_.push(pitEntry); // Set the interest timeout timer. var thisForwarder = this; var timeoutCallback = function() { if (LOG > 3) console.log("Interest time out: " + interest.getName().toUri() + "\n"); // Remove the face's entry from the PIT var index = thisForwarder.PIT_.indexOf(pitEntry); if (index >= 0) thisForwarder.PIT_.splice(index, 1); }; var timeoutMilliseconds = (interest.getInterestLifetimeMilliseconds() || 4000); pitEntry.timerId_ = setTimeout(timeoutCallback, timeoutMilliseconds); if (MicroForwarder.broadcastNamePrefix.match(interest.getName())) { // Special case: broadcast to all faces. for (var i = 0; i < this.faces_.length; ++i) { var outFace = this.faces_[i]; // Don't send the interest back to where it came from. if (outFace != face) outFace.sendBuffer(element); } } else { // Send the interest to the faces in matching FIB entries. for (var i = 0; i < this.FIB_.length; ++i) { var fibEntry = this.FIB_[i]; // TODO: Need to do longest prefix match? if (fibEntry.name.match(interest.getName())) { for (var j = 0; j < fibEntry.faces.length; ++j) { var outFace = fibEntry.faces[j]; // Don't send the interest back to where it came from. if (outFace != face) outFace.sendBuffer(element); } } } } } else if (data !== null) { if (LOG > 3) console.log("Data packet received: " + data.getName().toUri() + "\n"); //insert into CS if (LOG > 3) console.log("Insert Data in CS" + data.getName().toUri() + "\n"); this.CS_[data.getName().toUri()] = data; // Send the data packet to the face for each matching PIT entry. // Iterate backwards so we can remove the entry and keep iterating. for (var i = this.PIT_.length - 1; i >= 0; --i) { var entry = this.PIT_[i]; if (entry.face != face && entry.face != null && entry.interest.matchesData(data)) { // Clear the timeout. clearTimeout(entry.timerId_); entry.timerId_ = -1; // Remove the entry before sending. this.PIT_.splice(i, 1); if (LOG > 3) console.log("Sending Data to match interest " + entry.interest.getName().toUri() + "\n"); entry.face.sendBuffer(element); entry.face = null; } } } }; /** * Process a received interest if it begins with /localhost. * @param {ForwarderFace} face The ForwarderFace with the transport that * received the interest. * @param {Interest} interest The received interest. */ MicroForwarder.prototype.onReceivedLocalhostInterest = function(face, interest) { if (MicroForwarder.registerNamePrefix.match(interest.getName())) { // Decode the ControlParameters. var controlParameters = new ControlParameters(); try { controlParameters.wireDecode(interest.getName().get(4).getValue()); } catch (ex) { if (LOG > 3) console.log("Error decoding register interest ControlParameters " + ex + "\n"); return; } // TODO: Verify the signature? if (LOG > 3) console.log("Received register request " + controlParameters.getName().toUri() + "\n"); if (!this.registerRoute(controlParameters.getName(), face.faceId)) // TODO: Send error reply? return; // Send the ControlResponse. var controlResponse = new ControlResponse(); controlResponse.setStatusText("Success"); controlResponse.setStatusCode(200); controlResponse.setBodyAsControlParameters(controlParameters); var responseData = new Data(interest.getName()); responseData.setContent(controlResponse.wireEncode()); // TODO: Sign the responseData. face.sendBuffer(responseData.wireEncode().buf()); } else { if (LOG > 3) console.log("Unrecognized localhost prefix " + interest.getName() + "\n"); } }; /** * This is called when a JavaScript object is received on a local face. * @param {ForwarderFace} face The ForwarderFace with the transport that * received the object. * @param {object} obj The JavaScript object. */ MicroForwarder.prototype.onReceivedObject = function(face, obj) { if (obj.type == "fib/list") { obj.fib = []; for (var i = 0; i < this.FIB_.length; ++i) { var fibEntry = this.FIB_[i]; var entry = { name: fibEntry.name.toUri(), nextHops: [] }; for (var j = 0; j < fibEntry.faces.length; ++j) { // Don't show disabled faces, e.g. for a closed browser tab. if (fibEntry.faces[j].isEnabled()) entry.nextHops.push({ faceId: fibEntry.faces[j].faceId }); } if (entry.nextHops.length > 0) obj.fib.push(entry); } face.sendObject(obj); } else if (obj.type == "faces/list") { obj.faces = []; for (var i = 0; i < this.faces_.length; ++i) { obj.faces.push({ faceId: this.faces_[i].faceId, uri: this.faces_[i].uri }); } face.sendObject(obj); } else if (obj.type == "faces/query") { for (var i = 0; i < this.faces_.length; ++i) { if (this.faces_[i].uri == obj.uri) { // We found the desired face. obj.faceId = this.faces_[i].faceId; break; } } face.sendObject(obj); } else if (obj.type == "faces/create") { // TODO: Re-check that the face doesn't exist. var sentReply = false; var newFace = null; var thisForwarder = this; // Some transports can't report a connection failure, so use a timeout. var timerId = setTimeout(function() { // A problem opening the WebSocket. // Only reply once. if (sentReply) return; sentReply = true; obj.statusCode = 503; face.sendObject(obj); }, 3000); function onConnected() { if (sentReply) // Only reply once. return; sentReply = true; // Cancel the timeout timer. clearTimeout(timerId); thisForwarder.faces_.push(newFace); obj.faceId = newFace.faceId; obj.statusCode = 200; face.sendObject(obj); } var transport = new WebSocketTransport(); newFace = new ForwarderFace(obj.uri, transport); transport.connect (new WebSocketTransport.ConnectionInfo(obj.uri), { onReceivedElement: function(element) { thisForwarder.onReceivedElement(newFace, element); } }, onConnected); } else if (obj.type == "rib/register") { var faceId; if (obj.faceId != null) faceId = obj.faceId; else // Use the requesting face. faceId = face.faceId; if (!this.registerRoute(new Name(obj.nameUri), faceId)) // TODO: Send error reply? return; obj.statusCode = 200; face.sendObject(obj); } }; MicroForwarder.localhostNamePrefix = new Name("/localhost"); MicroForwarder.registerNamePrefix = new Name("/localhost/nfd/rib/register"); MicroForwarder.broadcastNamePrefix = new Name("/ndn/broadcast"); /** * A PitEntry is used in the PIT to record the face on which an Interest came in. * @param {Interest} interest * @param {ForwarderFace} face * @constructor */ var PitEntry = function PitEntry(interest, face) { this.interest = interest; this.face = face; }; /** * A FibEntry is used in the FIB to match a registered name with related faces. * @param {Name} name The registered name for this FIB entry. * @constructor */ var FibEntry = function FibEntry(name) { this.name = name; this.faces = []; // of ForwarderFace }; /** * A ForwarderFace is used by the faces list to represent a connection using the * given Transport. * Create a new ForwarderFace and set the faceId to a unique value. * @param {string} uri The URI to use in the faces/query and faces/list * commands. * @param {Transport} transport Communicate using the Transport object. You must * call transport.connect with an elementListener object whose * onReceivedElement(element) calls * microForwarder.onReceivedElement(face, element), with this face. If available * the transport's onReceivedObject(obj) should call * microForwarder.onReceivedObject(face, obj), with this face. * @constructor */ var ForwarderFace = function ForwarderFace(uri, transport) { this.uri = uri; this.transport = transport; this.faceId = ++ForwarderFace.lastFaceId; }; ForwarderFace.lastFaceId = 0; /** * Check if this face is still enabled. * @return {boolean} True if this face is still enabled. */ ForwarderFace.prototype.isEnabled = function() { return this.transport != null; }; /** * Disable this face so that isEnabled() returns false. */ ForwarderFace.prototype.disable = function() { this.transport = null; }; /** * Send the object to the transport, if this face is still enabled. * @param {object} obj The object to send. */ ForwarderFace.prototype.sendObject = function(obj) { if (this.transport != null && this.transport.sendObject != null) this.transport.sendObject(obj); }; /** * Send the buffer to the transport, if this face is still enabled. * @param {Buffer} buffer The bytes to send. */ ForwarderFace.prototype.sendBuffer = function(buffer) { if (this.transport != null) this.transport.send(buffer); }; // Create the only instance and start listening on the WebExtensions port. var microForwarder = new MicroForwarder();