blob: f88f178a3b51d1d2c50362ee0a81fb86f461ec41 [file] [log] [blame]
/**
* @author: Meki Cherkaoui, Jeff Thompson, Wentao Shang
* See COPYING for copyright and distribution information.
* This class represents the top-level object for communicating with an NDN host.
*/
/**
* Set this to a higher number to dump more debugging log messages.
* @type Number
*/
var LOG = 0;
/**
* Create a new NDN with the given settings.
* This throws an exception if NDN.supported is false.
* @constructor
* @param {Object} settings if not null, an associative array with the following defaults:
* {
* getTransport: function() { return new WebSocketTransport(); },
* getHostAndPort: transport.defaultGetHostAndPort, // a function, on each call it returns a new { host: host, port: port } or null if there are no more hosts.
* host: null, // If null, use getHostAndPort when connecting.
* port: 9696,
* onopen: function() { if (LOG > 3) console.log("NDN connection established."); },
* onclose: function() { if (LOG > 3) console.log("NDN connection closed."); },
* verify: true // If false, don't verify and call upcall with Closure.UPCALL_CONTENT_UNVERIFIED.
* }
*/
var NDN = function NDN(settings) {
if (!NDN.supported)
throw new Error("The necessary JavaScript support is not available on this platform.");
settings = (settings || {});
var getTransport = (settings.getTransport || function() { return new WebSocketTransport(); });
this.transport = getTransport();
this.getHostAndPort = (settings.getHostAndPort || this.transport.defaultGetHostAndPort);
this.host = (settings.host !== undefined ? settings.host : null);
this.port = (settings.port || 9696);
this.readyStatus = NDN.UNOPEN;
this.verify = (settings.verify !== undefined ? settings.verify : true);
// Event handler
this.onopen = (settings.onopen || function() { if (LOG > 3) console.log("NDN connection established."); });
this.onclose = (settings.onclose || function() { if (LOG > 3) console.log("NDN connection closed."); });
this.ccndid = null;
};
NDN.UNOPEN = 0; // created but not opened yet
NDN.OPENED = 1; // connection to ccnd opened
NDN.CLOSED = 2; // connection to ccnd closed
/**
* Return true if necessary JavaScript support is available, else log an error and return false.
*/
NDN.getSupported = function() {
try {
var dummy = new Uint8Array(1).subarray(0, 1);
} catch (ex) {
console.log("NDN not available: Uint8Array not supported. " + ex);
return false;
}
return true;
};
NDN.supported = NDN.getSupported();
NDN.ccndIdFetcher = new Name('/%C1.M.S.localhost/%C1.M.SRV/ccnd/KEY');
NDN.prototype.createRoute = function(host, port) {
this.host=host;
this.port=port;
};
NDN.KeyStore = new Array();
var KeyStoreEntry = function KeyStoreEntry(name, rsa, time) {
this.keyName = name; // KeyName
this.rsaKey = rsa; // RSA key
this.timeStamp = time; // Time Stamp
};
NDN.addKeyEntry = function(/* KeyStoreEntry */ keyEntry) {
var result = NDN.getKeyByName(keyEntry.keyName);
if (result == null)
NDN.KeyStore.push(keyEntry);
else
result = keyEntry;
};
NDN.getKeyByName = function(/* KeyName */ name) {
var result = null;
for (var i = 0; i < NDN.KeyStore.length; i++) {
if (NDN.KeyStore[i].keyName.contentName.match(name.contentName)) {
if (result == null ||
NDN.KeyStore[i].keyName.contentName.components.length > result.keyName.contentName.components.length)
result = NDN.KeyStore[i];
}
}
return result;
};
// For fetching data
NDN.PITTable = new Array();
/**
* @constructor
*/
var PITEntry = function PITEntry(interest, closure) {
this.interest = interest; // Interest
this.closure = closure; // Closure
this.timerID = -1; // Timer ID
};
/**
* Return the entry from NDN.PITTable where the name conforms to the interest selectors, and
* the interest name is the longest that matches name.
*/
NDN.getEntryForExpressedInterest = function(/*Name*/ name) {
var result = null;
for (var i = 0; i < NDN.PITTable.length; i++) {
if (NDN.PITTable[i].interest.matches_name(name)) {
if (result == null ||
NDN.PITTable[i].interest.name.components.length > result.interest.name.components.length)
result = NDN.PITTable[i];
}
}
return result;
};
// For publishing data
NDN.CSTable = new Array();
/**
* @constructor
*/
var CSEntry = function CSEntry(name, closure) {
this.name = name; // String
this.closure = closure; // Closure
};
function getEntryForRegisteredPrefix(name) {
for (var i = 0; i < NDN.CSTable.length; i++) {
if (NDN.CSTable[i].name.match(name))
return NDN.CSTable[i];
}
return null;
}
/**
* Return a function that selects a host at random from hostList and returns { host: host, port: port }.
* If no more hosts remain, return null.
*/
NDN.makeShuffledGetHostAndPort = function(hostList, port) {
// Make a copy.
hostList = hostList.slice(0, hostList.length);
DataUtils.shuffle(hostList);
return function() {
if (hostList.length == 0)
return null;
return { host: hostList.splice(0, 1)[0], port: port };
};
};
/**
* Encode name as an Interest and send the it to host:port, read the entire response and call
* closure.upcall(Closure.UPCALL_CONTENT (or Closure.UPCALL_CONTENT_UNVERIFIED),
* new UpcallInfo(this, interest, 0, contentObject)).
* @param {Name} name
* @param {Closure} closure
* @param {Interest} template if not null, use its attributes
*/
NDN.prototype.expressInterest = function (name, closure, template) {
var interest = new Interest(name);
if (template != null) {
interest.minSuffixComponents = template.minSuffixComponents;
interest.maxSuffixComponents = template.maxSuffixComponents;
interest.publisherPublicKeyDigest = template.publisherPublicKeyDigest;
interest.exclude = template.exclude;
interest.childSelector = template.childSelector;
interest.answerOriginKind = template.answerOriginKind;
interest.scope = template.scope;
interest.interestLifetime = template.interestLifetime;
}
else
interest.interestLifetime = 4000; // default interest timeout value in milliseconds.
if (this.host == null || this.port == null) {
if (this.getHostAndPort == null)
console.log('ERROR: host OR port NOT SET');
else {
var thisNDN = this;
this.connectAndExecute
(function() { thisNDN.reconnectAndExpressInterest(interest, closure); });
}
}
else
this.reconnectAndExpressInterest(interest, closure);
};
/**
* If the host and port are different than the ones in this.transport, then call
* this.transport.connect to change the connection (or connect for the first time).
* Then call expressInterestHelper.
*/
NDN.prototype.reconnectAndExpressInterest = function(interest, closure) {
if (this.transport.connectedHost != this.host || this.transport.connectedPort != this.port) {
var thisNDN = this;
this.transport.connect(thisNDN, function() { thisNDN.expressInterestHelper(interest, closure); });
}
else
this.expressInterestHelper(interest, closure);
};
/**
* Do the work of reconnectAndExpressInterest once we know we are connected. Set the PITTable and call
* this.transport.send to send the interest.
*/
NDN.prototype.expressInterestHelper = function(interest, closure) {
var binaryInterest = encodeToBinaryInterest(interest);
var thisNDN = this;
//TODO: check local content store first
if (closure != null) {
var pitEntry = new PITEntry(interest, closure);
// TODO: This needs to be a single thread-safe transaction on a global object.
NDN.PITTable.push(pitEntry);
closure.pitEntry = pitEntry;
// Set interest timer.
var timeoutMilliseconds = (interest.interestLifetime || 4000);
var timeoutCallback = function() {
if (LOG > 1) console.log("Interest time out: " + interest.name.to_uri());
// Remove PIT entry from NDN.PITTable, even if we add it again later to re-express
// the interest because we don't want to match it in the mean time.
// TODO: Make this a thread-safe operation on the global PITTable.
var index = NDN.PITTable.indexOf(pitEntry);
if (index >= 0)
NDN.PITTable.splice(index, 1);
// Raise closure callback
if (closure.upcall(Closure.UPCALL_INTEREST_TIMED_OUT,
new UpcallInfo(thisNDN, interest, 0, null)) == Closure.RESULT_REEXPRESS) {
if (LOG > 1) console.log("Re-express interest: " + interest.name.to_uri());
pitEntry.timerID = setTimeout(timeoutCallback, timeoutMilliseconds);
NDN.PITTable.push(pitEntry);
thisNDN.transport.send(binaryInterest);
}
};
pitEntry.timerID = setTimeout(timeoutCallback, timeoutMilliseconds);
}
this.transport.send(binaryInterest);
};
/**
* Register name with the connected NDN hub and receive interests with closure.upcall.
* @param {Name} name
* @param {Closure} closure
* @param {number} flag
*/
NDN.prototype.registerPrefix = function(name, closure, flag) {
var thisNDN = this;
var onConnected = function() {
if (thisNDN.ccndid == null) {
// Fetch ccndid first, then register.
var interest = new Interest(NDN.ccndIdFetcher);
interest.interestLifetime = 4000; // milliseconds
if (LOG>3) console.log('Expressing interest for ccndid from ccnd.');
thisNDN.reconnectAndExpressInterest
(interest, new NDN.FetchCcndidClosure(thisNDN, name, closure, flag));
}
else
thisNDN.registerPrefixHelper(name, closure, flag);
};
if (this.host == null || this.port == null) {
if (this.getHostAndPort == null)
console.log('ERROR: host OR port NOT SET');
else
this.connectAndExecute(onConnected);
}
else
onConnected();
};
/**
* This is a closure to receive the ContentObject for NDN.ccndIdFetcher and call
* registerPrefixHelper(name, callerClosure, flag).
*/
NDN.FetchCcndidClosure = function FetchCcndidClosure(ndn, name, callerClosure, flag) {
// Inherit from Closure.
Closure.call(this);
this.ndn = ndn;
this.name = name;
this.callerClosure = callerClosure;
this.flag = flag;
};
NDN.FetchCcndidClosure.prototype.upcall = function(kind, upcallInfo) {
if (kind == Closure.UPCALL_INTEREST_TIMED_OUT) {
console.log("Timeout while requesting the ccndid. Cannot registerPrefix for " +
this.name.to_uri() + " .");
return Closure.RESULT_OK;
}
if (!(kind == Closure.UPCALL_CONTENT ||
kind == Closure.UPCALL_CONTENT_UNVERIFIED))
// The upcall is not for us.
return Closure.RESULT_ERR;
var co = upcallInfo.contentObject;
if (!co.signedInfo || !co.signedInfo.publisher
|| !co.signedInfo.publisher.publisherPublicKeyDigest)
console.log
("ContentObject doesn't have a publisherPublicKeyDigest. Cannot set ccndid and registerPrefix for "
+ this.name.to_uri() + " .");
else {
if (LOG>3) console.log('Got ccndid from ccnd.');
this.ndn.ccndid = co.signedInfo.publisher.publisherPublicKeyDigest;
if (LOG>3) console.log(this.ndn.ccndid);
this.ndn.registerPrefixHelper(this.name, this.callerClosure, this.flag);
}
return Closure.RESULT_OK;
};
/**
* Do the work of registerPrefix once we know we are connected with a ccndid.
*/
NDN.prototype.registerPrefixHelper = function(name, closure, flag) {
var fe = new ForwardingEntry('selfreg', name, null, null, 3, 2147483647);
var bytes = encodeForwardingEntry(fe);
var si = new SignedInfo();
si.setFields();
var co = new ContentObject(new Name(), si, bytes);
co.sign();
var coBinary = encodeToBinaryContentObject(co);
//var nodename = unescape('%00%88%E2%F4%9C%91%16%16%D6%21%8E%A0c%95%A5%A6r%11%E0%A0%82%89%A6%A9%85%AB%D6%E2%065%DB%AF');
var nodename = this.ccndid;
var interestName = new Name(['ccnx', nodename, 'selfreg', coBinary]);
var interest = new Interest(interestName);
interest.scope = 1;
if (LOG > 3) console.log('Send Interest registration packet.');
var csEntry = new CSEntry(name.getName(), closure);
NDN.CSTable.push(csEntry);
this.transport.send(encodeToBinaryInterest(interest));
};
/**
* This is called when an entire binary XML element is received, such as a ContentObject or Interest.
* Look up in the PITTable and call the closure callback.
*/
NDN.prototype.onReceivedElement = function(element) {
if (LOG>3) console.log('Complete element received. Length ' + element.length + '. Start decoding.');
var decoder = new BinaryXMLDecoder(element);
// Dispatch according to packet type
if (decoder.peekStartElement(CCNProtocolDTags.Interest)) { // Interest packet
if (LOG > 3) console.log('Interest packet received.');
var interest = new Interest();
interest.from_ccnb(decoder);
if (LOG > 3) console.log(interest);
var nameStr = escape(interest.name.getName());
if (LOG > 3) console.log(nameStr);
var entry = getEntryForRegisteredPrefix(nameStr);
if (entry != null) {
//console.log(entry);
var info = new UpcallInfo(this, interest, 0, null);
var ret = entry.closure.upcall(Closure.UPCALL_INTEREST, info);
if (ret == Closure.RESULT_INTEREST_CONSUMED && info.contentObject != null)
this.transport.send(encodeToBinaryContentObject(info.contentObject));
}
} else if (decoder.peekStartElement(CCNProtocolDTags.ContentObject)) { // Content packet
if (LOG > 3) console.log('ContentObject packet received.');
var co = new ContentObject();
co.from_ccnb(decoder);
var pitEntry = NDN.getEntryForExpressedInterest(co.name);
if (pitEntry != null) {
// Cancel interest timer
clearTimeout(pitEntry.timerID);
// Remove PIT entry from NDN.PITTable
var index = NDN.PITTable.indexOf(pitEntry);
if (index >= 0)
NDN.PITTable.splice(index, 1);
var currentClosure = pitEntry.closure;
if (this.verify == false) {
// Pass content up without verifying the signature
currentClosure.upcall(Closure.UPCALL_CONTENT_UNVERIFIED, new UpcallInfo(this, pitEntry.interest, 0, co));
return;
}
// Key verification
// Recursive key fetching & verification closure
var KeyFetchClosure = function KeyFetchClosure(content, closure, key, sig, wit) {
this.contentObject = content; // unverified content object
this.closure = closure; // closure corresponding to the contentObject
this.keyName = key; // name of current key to be fetched
this.sigHex = sig; // hex signature string to be verified
this.witness = wit;
Closure.call(this);
};
var thisNDN = this;
KeyFetchClosure.prototype.upcall = function(kind, upcallInfo) {
if (kind == Closure.UPCALL_INTEREST_TIMED_OUT) {
console.log("In KeyFetchClosure.upcall: interest time out.");
console.log(this.keyName.contentName.getName());
} else if (kind == Closure.UPCALL_CONTENT) {
//console.log("In KeyFetchClosure.upcall: signature verification passed");
var rsakey = decodeSubjectPublicKeyInfo(upcallInfo.contentObject.content);
var verified = rsakey.verifyByteArray(this.contentObject.rawSignatureData, this.witness, this.sigHex);
var flag = (verified == true) ? Closure.UPCALL_CONTENT : Closure.UPCALL_CONTENT_BAD;
//console.log("raise encapsulated closure");
this.closure.upcall(flag, new UpcallInfo(thisNDN, null, 0, this.contentObject));
// Store key in cache
var keyEntry = new KeyStoreEntry(keylocator.keyName, rsakey, new Date().getTime());
NDN.addKeyEntry(keyEntry);
//console.log(NDN.KeyStore);
} else if (kind == Closure.UPCALL_CONTENT_BAD) {
console.log("In KeyFetchClosure.upcall: signature verification failed");
}
};
if (co.signedInfo && co.signedInfo.locator && co.signature) {
if (LOG > 3) console.log("Key verification...");
var sigHex = DataUtils.toHex(co.signature.signature).toLowerCase();
var wit = null;
if (co.signature.Witness != null) {
wit = new Witness();
wit.decode(co.signature.Witness);
}
var keylocator = co.signedInfo.locator;
if (keylocator.type == KeyLocatorType.KEYNAME) {
if (LOG > 3) console.log("KeyLocator contains KEYNAME");
//var keyname = keylocator.keyName.contentName.getName();
//console.log(nameStr);
//console.log(keyname);
if (keylocator.keyName.contentName.match(co.name)) {
if (LOG > 3) console.log("Content is key itself");
var rsakey = decodeSubjectPublicKeyInfo(co.content);
var verified = rsakey.verifyByteArray(co.rawSignatureData, wit, sigHex);
var flag = (verified == true) ? Closure.UPCALL_CONTENT : Closure.UPCALL_CONTENT_BAD;
currentClosure.upcall(flag, new UpcallInfo(this, pitEntry.interest, 0, co));
// SWT: We don't need to store key here since the same key will be
// stored again in the closure.
//var keyEntry = new KeyStoreEntry(keylocator.keyName, rsakey, new Date().getTime());
//NDN.addKeyEntry(keyEntry);
//console.log(NDN.KeyStore);
} else {
// Check local key store
var keyEntry = NDN.getKeyByName(keylocator.keyName);
if (keyEntry) {
// Key found, verify now
if (LOG > 3) console.log("Local key cache hit");
var rsakey = keyEntry.rsaKey;
var verified = rsakey.verifyByteArray(co.rawSignatureData, wit, sigHex);
var flag = (verified == true) ? Closure.UPCALL_CONTENT : Closure.UPCALL_CONTENT_BAD;
// Raise callback
currentClosure.upcall(flag, new UpcallInfo(this, pitEntry.interest, 0, co));
} else {
// Not found, fetch now
if (LOG > 3) console.log("Fetch key according to keylocator");
var nextClosure = new KeyFetchClosure(co, currentClosure, keylocator.keyName, sigHex, wit);
this.expressInterest(keylocator.keyName.contentName.getPrefix(4), nextClosure);
}
}
} else if (keylocator.type == KeyLocatorType.KEY) {
if (LOG > 3) console.log("Keylocator contains KEY");
var rsakey = decodeSubjectPublicKeyInfo(co.signedInfo.locator.publicKey);
var verified = rsakey.verifyByteArray(co.rawSignatureData, wit, sigHex);
var flag = (verified == true) ? Closure.UPCALL_CONTENT : Closure.UPCALL_CONTENT_BAD;
// Raise callback
currentClosure.upcall(Closure.UPCALL_CONTENT, new UpcallInfo(this, pitEntry.interest, 0, co));
// Since KeyLocator does not contain key name for this key,
// we have no way to store it as a key entry in KeyStore.
} else {
var cert = keylocator.certificate;
console.log("KeyLocator contains CERT");
console.log(cert);
// TODO: verify certificate
}
}
}
} else
console.log('Incoming packet is not Interest or ContentObject. Discard now.');
};
/**
* Assume this.getHostAndPort is not null. This is called when this.host is null or its host
* is not alive. Get a host and port, connect, then execute onConnected().
*/
NDN.prototype.connectAndExecute = function(onConnected) {
var hostAndPort = this.getHostAndPort();
if (hostAndPort == null) {
console.log('ERROR: No more hosts from getHostAndPort');
this.host = null;
return;
}
if (hostAndPort.host == this.host && hostAndPort.port == this.port) {
console.log('ERROR: The host returned by getHostAndPort is not alive: ' +
this.host + ":" + this.port);
return;
}
this.host = hostAndPort.host;
this.port = hostAndPort.port;
if (LOG>0) console.log("connectAndExecute: trying host from getHostAndPort: " + this.host);
// Fetch any content.
var interest = new Interest(new Name("/"));
interest.interestLifetime = 4000; // milliseconds
var thisNDN = this;
var timerID = setTimeout(function() {
if (LOG>0) console.log("connectAndExecute: timeout waiting for host " + thisNDN.host);
// Try again.
thisNDN.connectAndExecute(onConnected);
}, 3000);
this.reconnectAndExpressInterest
(interest, new NDN.ConnectClosure(this, onConnected, timerID));
};
NDN.ConnectClosure = function ConnectClosure(ndn, onConnected, timerID) {
// Inherit from Closure.
Closure.call(this);
this.ndn = ndn;
this.onConnected = onConnected;
this.timerID = timerID;
};
NDN.ConnectClosure.prototype.upcall = function(kind, upcallInfo) {
if (!(kind == Closure.UPCALL_CONTENT ||
kind == Closure.UPCALL_CONTENT_UNVERIFIED))
// The upcall is not for us.
return Closure.RESULT_ERR;
// The host is alive, so cancel the timeout and continue with onConnected().
clearTimeout(this.timerID);
// Call NDN.onopen after success
this.ndn.readyStatus = NDN.OPENED;
this.ndn.onopen();
if (LOG>0) console.log("connectAndExecute: connected to host " + this.ndn.host);
this.onConnected();
return Closure.RESULT_OK;
};
/**
* A BinaryXmlElementReader lets you call onReceivedData multiple times which uses a
* BinaryXMLStructureDecoder to detect the end of a binary XML element and calls
* elementListener.onReceivedElement(element) with the element.
* This handles the case where a single call to onReceivedData may contain multiple elements.
* @constructor
* @param {{onReceivedElement:function}} elementListener
*/
var BinaryXmlElementReader = function BinaryXmlElementReader(elementListener) {
this.elementListener = elementListener;
this.dataParts = [];
this.structureDecoder = new BinaryXMLStructureDecoder();
};
BinaryXmlElementReader.prototype.onReceivedData = function(/* Uint8Array */ data) {
// Process multiple objects in the data.
while(true) {
// Scan the input to check if a whole ccnb object has been read.
this.structureDecoder.seek(0);
if (this.structureDecoder.findElementEnd(data)) {
// Got the remainder of an object. Report to the caller.
this.dataParts.push(data.subarray(0, this.structureDecoder.offset));
var element = DataUtils.concatArrays(this.dataParts);
this.dataParts = [];
try {
this.elementListener.onReceivedElement(element);
} catch (ex) {
console.log("BinaryXmlElementReader: ignoring exception from onReceivedElement: " + ex);
}
// Need to read a new object.
data = data.subarray(this.structureDecoder.offset, data.length);
this.structureDecoder = new BinaryXMLStructureDecoder();
if (data.length == 0)
// No more data in the packet.
return;
// else loop back to decode.
}
else {
// Save for a later call to concatArrays so that we only copy data once.
this.dataParts.push(data);
if (LOG>3) console.log('Incomplete packet received. Length ' + data.length + '. Wait for more input.');
return;
}
}
};