blob: 5c8df13382faac92e32a6fb1279fa5ecf7c9f511 [file] [log] [blame]
Wentao Shangc0311e52012-12-03 10:38:23 -08001/**
Jeff Thompson5b265a72012-11-12 01:13:08 -08002 * @author: Meki Cherkaoui, Jeff Thompson, Wentao Shang
Jeff Thompson745026e2012-10-13 12:49:20 -07003 * See COPYING for copyright and distribution information.
4 * This class represents the top-level object for communicating with an NDN host.
5 */
Meki Cherkaoui8f173612012-06-06 01:05:40 -07006
Jeff Thompson3c263812012-12-01 17:20:28 -08007var LOG = 0;
Jeff Thompson5b265a72012-11-12 01:13:08 -08008
Jeff Thompsone06b31e2012-09-30 17:19:19 -07009/**
Jeff Thompson5b265a72012-11-12 01:13:08 -080010 * settings is an associative array with the following defaults:
11 * {
Jeff Thompson5b265a72012-11-12 01:13:08 -080012 * getTransport: function() { return new WebSocketTransport(); }
Jeff Thompsond3a80dc2012-12-16 17:52:43 -080013 * getHostAndPort: transport.defaultGetHostAndPort,
14 * host: 'localhost', // If null, use getHostAndPort when connecting.
15 * port: 9696,
Wentao Shangc0311e52012-12-03 10:38:23 -080016 * onopen: function() { if (LOG > 3) console.log("NDN connection established."); }
17 * onclose: function() { if (LOG > 3) console.log("NDN connection closed."); }
Jeff Thompson5b265a72012-11-12 01:13:08 -080018 * }
Jeff Thompsond3a80dc2012-12-16 17:52:43 -080019 *
20 * getHostAndPort is a function, on each call it returns a new { host: host, port: port } or
21 * null if there are no more hosts.
Jeff Thompsone06b31e2012-09-30 17:19:19 -070022 */
Jeff Thompson5b265a72012-11-12 01:13:08 -080023var NDN = function NDN(settings) {
24 settings = (settings || {});
Jeff Thompson5b265a72012-11-12 01:13:08 -080025 var getTransport = (settings.getTransport || function() { return new WebSocketTransport(); });
Wentao Shang0e291c82012-12-02 23:36:29 -080026 this.transport = getTransport();
Jeff Thompsond3a80dc2012-12-16 17:52:43 -080027 this.getHostAndPort = (settings.getHostAndPort || this.transport.defaultGetHostAndPort);
28 this.host = (settings.host !== undefined ? settings.host : 'localhost');
29 this.port = (settings.port || 9696);
Wentao Shang0e291c82012-12-02 23:36:29 -080030 this.readyStatus = NDN.UNOPEN;
31 // Event handler
Wentao Shangc0311e52012-12-03 10:38:23 -080032 this.onopen = (settings.onopen || function() { if (LOG > 3) console.log("NDN connection established."); });
33 this.onclose = (settings.onclose || function() { if (LOG > 3) console.log("NDN connection closed."); });
Jeff Thompson75771cb2013-01-20 23:27:38 -080034 this.ccndid = null;
Meki Cherkaoui8f173612012-06-06 01:05:40 -070035};
36
Wentao Shang0e291c82012-12-02 23:36:29 -080037NDN.UNOPEN = 0; // created but not opened yet
38NDN.OPENED = 1; // connection to ccnd opened
39NDN.CLOSED = 2; // connection to ccnd closed
Jeff Thompson5b265a72012-11-12 01:13:08 -080040
Wentao Shangb42483a2013-01-03 15:32:32 -080041NDN.ccndIdFetcher = new Name('/%C1.M.S.localhost/%C1.M.SRV/ccnd/KEY');
Jeff Thompson5b265a72012-11-12 01:13:08 -080042
Wentao Shangb42483a2013-01-03 15:32:32 -080043NDN.prototype.createRoute = function(host, port) {
Meki Cherkaoui8f173612012-06-06 01:05:40 -070044 this.host=host;
45 this.port=port;
Wentao Shangb42483a2013-01-03 15:32:32 -080046};
Meki Cherkaoui8f173612012-06-06 01:05:40 -070047
Wentao Shang82854bd2012-12-27 14:14:41 -080048
49NDN.KeyStore = new Array();
50
Wentao Shangb5d0c3e2012-12-30 11:12:03 -080051var KeyStoreEntry = function KeyStoreEntry(name, rsa, time) {
Wentao Shang82854bd2012-12-27 14:14:41 -080052 this.keyName = name; // KeyName
Wentao Shang82854bd2012-12-27 14:14:41 -080053 this.rsaKey = rsa; // RSA key
Wentao Shangb5d0c3e2012-12-30 11:12:03 -080054 this.timeStamp = time; // Time Stamp
55};
56
57NDN.addKeyEntry = function(/* KeyStoreEntry */ keyEntry) {
58 var result = NDN.getKeyByName(keyEntry.keyName);
59 if (result == null)
60 NDN.KeyStore.push(keyEntry);
61 else
62 result = keyEntry;
Wentao Shang82854bd2012-12-27 14:14:41 -080063};
64
65NDN.getKeyByName = function(/* KeyName */ name) {
66 var result = null;
67
68 for (var i = 0; i < NDN.KeyStore.length; i++) {
Wentao Shangb42483a2013-01-03 15:32:32 -080069 if (NDN.KeyStore[i].keyName.contentName.match(name.contentName)) {
Wentao Shang82854bd2012-12-27 14:14:41 -080070 if (result == null ||
71 NDN.KeyStore[i].keyName.contentName.components.length > result.keyName.contentName.components.length)
72 result = NDN.KeyStore[i];
73 }
74 }
75
76 return result;
77};
78
Jeff Thompsonbe85be62012-12-13 22:32:01 -080079// For fetching data
80NDN.PITTable = new Array();
81
82var PITEntry = function PITEntry(interest, closure) {
83 this.interest = interest; // Interest
84 this.closure = closure; // Closure
Wentao Shangfcb16262013-01-20 14:42:46 -080085 this.timerID = -1; // Timer ID
Jeff Thompsonbe85be62012-12-13 22:32:01 -080086};
87
88// Return the longest entry from NDN.PITTable that matches name.
89NDN.getEntryForExpressedInterest = function(/*Name*/ name) {
90 // TODO: handle multiple matches? Maybe not from registerPrefix because multiple ContentObject
91 // could be sent for one Interest?
92 var result = null;
93
94 for (var i = 0; i < NDN.PITTable.length; i++) {
95 if (NDN.PITTable[i].interest.matches_name(name)) {
96 if (result == null ||
97 NDN.PITTable[i].interest.name.components.length > result.interest.name.components.length)
98 result = NDN.PITTable[i];
99 }
100 }
101
102 return result;
103};
104
Jeff Thompson75771cb2013-01-20 23:27:38 -0800105// For publishing data
106NDN.CSTable = new Array();
107
108var CSEntry = function CSEntry(name, closure) {
109 this.name = name; // String
110 this.closure = closure; // Closure
111};
112
113function getEntryForRegisteredPrefix(name) {
114 for (var i = 0; i < NDN.CSTable.length; i++) {
115 if (NDN.CSTable[i].name.match(name) != null)
116 return NDN.CSTable[i];
117 }
118 return null;
119}
120
Jeff Thompsond3a80dc2012-12-16 17:52:43 -0800121/*
122 * Return a function that selects a host at random from hostList and returns { host: host, port: port }.
123 * If no more hosts remain, return null.
124 */
125NDN.makeShuffledGetHostAndPort = function(hostList, port) {
126 // Make a copy.
127 hostList = hostList.slice(0, hostList.length);
128 DataUtils.shuffle(hostList);
129
130 return function() {
131 if (hostList.length == 0)
132 return null;
133
134 return { host: hostList.splice(0, 1)[0], port: port };
135 };
136};
137
Jeff Thompson34419762012-10-15 22:24:12 -0700138/** Encode name as an Interest. If template is not null, use its attributes.
139 * Send the interest to host:port, read the entire response and call
140 * closure.upcall(Closure.UPCALL_CONTENT (or Closure.UPCALL_CONTENT_UNVERIFIED),
Jeff Thompson97f27432012-10-16 00:28:03 -0700141 * new UpcallInfo(this, interest, 0, contentObject)).
Jeff Thompson34419762012-10-15 22:24:12 -0700142 */
143NDN.prototype.expressInterest = function(
144 // Name
145 name,
146 // Closure
147 closure,
148 // Interest
149 template) {
Jeff Thompson5b265a72012-11-12 01:13:08 -0800150 var interest = new Interest(name);
Jeff Thompson34419762012-10-15 22:24:12 -0700151 if (template != null) {
Jeff Thompson4404ab52012-10-21 10:29:48 -0700152 interest.minSuffixComponents = template.minSuffixComponents;
153 interest.maxSuffixComponents = template.maxSuffixComponents;
154 interest.publisherPublicKeyDigest = template.publisherPublicKeyDigest;
155 interest.exclude = template.exclude;
156 interest.childSelector = template.childSelector;
157 interest.answerOriginKind = template.answerOriginKind;
158 interest.scope = template.scope;
159 interest.interestLifetime = template.interestLifetime;
Jeff Thompson34419762012-10-15 22:24:12 -0700160 }
161 else
Jeff Thompson42806a12012-12-29 18:19:39 -0800162 interest.interestLifetime = 4000; // default interest timeout value in milliseconds.
Jeff Thompson34419762012-10-15 22:24:12 -0700163
Jeff Thompsond3a80dc2012-12-16 17:52:43 -0800164 if (this.host == null || this.port == null) {
165 if (this.getHostAndPort == null)
166 console.log('ERROR: host OR port NOT SET');
167 else
168 this.connectAndExpressInterest(interest, closure);
169 }
170 else
171 this.transport.expressInterest(this, interest, closure);
172};
Jeff Thompson5b265a72012-11-12 01:13:08 -0800173
174NDN.prototype.registerPrefix = function(name, closure, flag) {
Jeff Thompson75771cb2013-01-20 23:27:38 -0800175 if (this.readyStatus != NDN.OPENED) {
176 console.log('Connection is not established.');
177 return -1;
178 }
179
180 if (this.ccndid == null) {
181 console.log('ccnd node ID unkonwn. Cannot register prefix.');
182 return -1;
183 }
184
185 var fe = new ForwardingEntry('selfreg', name, null, null, 3, 2147483647);
186 var bytes = encodeForwardingEntry(fe);
187
188 var si = new SignedInfo();
189 si.setFields();
190
191 var co = new ContentObject(new Name(), si, bytes, new Signature());
192 co.sign();
193 var coBinary = encodeToBinaryContentObject(co);
194
195 //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');
196 var nodename = this.ccndid;
197 var interestName = new Name(['ccnx', nodename, 'selfreg', coBinary]);
198
199 var interest = new Interest(interestName);
200 interest.scope = 1;
201 if (LOG > 3) console.log('Send Interest registration packet.');
202
203 var csEntry = new CSEntry(name.getName(), closure);
204 NDN.CSTable.push(csEntry);
205
206 this.transport.send(encodeToBinaryInterest(interest));
207
208 return 0;
209};
210
211/*
212 * This is called when an entire binary XML element is received, such as a ContentObject or Interest.
213 * Look up in the PITTable and call the closure callback.
214 */
215NDN.prototype.onReceivedElement = function(element) {
Jeff Thompsona46083c2013-01-20 23:55:21 -0800216 if (LOG>3) console.log('Complete element received. Length ' + element.length + '. Start decoding.');
Jeff Thompson75771cb2013-01-20 23:27:38 -0800217 var decoder = new BinaryXMLDecoder(element);
218 // Dispatch according to packet type
219 if (decoder.peekStartElement(CCNProtocolDTags.Interest)) { // Interest packet
220 if (LOG > 3) console.log('Interest packet received.');
221
222 var interest = new Interest();
223 interest.from_ccnb(decoder);
224 if (LOG > 3) console.log(interest);
225 var nameStr = escape(interest.name.getName());
226 if (LOG > 3) console.log(nameStr);
227
228 var entry = getEntryForRegisteredPrefix(nameStr);
229 if (entry != null) {
230 //console.log(entry);
231 var info = new UpcallInfo(this, interest, 0, null);
232 var ret = entry.closure.upcall(Closure.UPCALL_INTEREST, info);
233 if (ret == Closure.RESULT_INTEREST_CONSUMED && info.contentObject != null)
234 this.transport.send(encodeToBinaryContentObject(info.contentObject));
235 }
236 } else if (decoder.peekStartElement(CCNProtocolDTags.ContentObject)) { // Content packet
237 if (LOG > 3) console.log('ContentObject packet received.');
238
239 var co = new ContentObject();
240 co.from_ccnb(decoder);
241
242 if (this.ccndid == null && NDN.ccndIdFetcher.match(co.name)) {
243 // We are in starting phase, record publisherPublicKeyDigest in ccndid
244 if(!co.signedInfo || !co.signedInfo.publisher
245 || !co.signedInfo.publisher.publisherPublicKeyDigest) {
246 console.log("Cannot contact router, close NDN now.");
247
248 // Close NDN if we fail to connect to a ccn router
249 this.readyStatus = NDN.CLOSED;
250 this.onclose();
251 //console.log("NDN.onclose event fired.");
252 } else {
253 //console.log('Connected to ccnd.');
254 this.ccndid = co.signedInfo.publisher.publisherPublicKeyDigest;
255 if (LOG>3) console.log(ndn.ccndid);
256
257 // Call NDN.onopen after success
258 this.readyStatus = NDN.OPENED;
259 this.onopen();
260 //console.log("NDN.onopen event fired.");
261 }
262 } else {
263 var pitEntry = NDN.getEntryForExpressedInterest(co.name);
264 if (pitEntry != null) {
265 //console.log(pitEntry);
266 // Remove PIT entry from NDN.PITTable
267 var index = NDN.PITTable.indexOf(pitEntry);
268 if (index >= 0)
269 NDN.PITTable.splice(index, 1);
270
271 var currentClosure = pitEntry.closure;
272
273 // Cancel interest timer
274 clearTimeout(pitEntry.timerID);
275 //console.log("Clear interest timer");
276 //console.log(currentClosure.timerID);
277
278 // Key verification
279
280 // Recursive key fetching & verification closure
281 var KeyFetchClosure = function KeyFetchClosure(content, closure, key, sig, wit) {
282 this.contentObject = content; // unverified content object
283 this.closure = closure; // closure corresponding to the contentObject
284 this.keyName = key; // name of current key to be fetched
285 this.sigHex = sig; // hex signature string to be verified
286 this.witness = wit;
287
288 Closure.call(this);
289 };
290
291 var thisNdn = this;
292 KeyFetchClosure.prototype.upcall = function(kind, upcallInfo) {
293 if (kind == Closure.UPCALL_INTEREST_TIMED_OUT) {
294 console.log("In KeyFetchClosure.upcall: interest time out.");
295 console.log(this.keyName.contentName.getName());
296 } else if (kind == Closure.UPCALL_CONTENT) {
297 //console.log("In KeyFetchClosure.upcall: signature verification passed");
298
299 var rsakey = decodeSubjectPublicKeyInfo(upcallInfo.contentObject.content);
300 var verified = rsakey.verifyByteArray(this.contentObject.rawSignatureData, this.witness, this.sigHex);
301
302 var flag = (verified == true) ? Closure.UPCALL_CONTENT : Closure.UPCALL_CONTENT_BAD;
303 //console.log("raise encapsulated closure");
304 this.closure.upcall(flag, new UpcallInfo(thisNdn, null, 0, this.contentObject));
305
306 // Store key in cache
307 var keyEntry = new KeyStoreEntry(keylocator.keyName, rsakey, new Date().getTime());
308 NDN.addKeyEntry(keyEntry);
309 //console.log(NDN.KeyStore);
310 } else if (kind == Closure.UPCALL_CONTENT_BAD) {
311 console.log("In KeyFetchClosure.upcall: signature verification failed");
312 }
313 };
314
315 if (co.signedInfo && co.signedInfo.locator && co.signature) {
316 if (LOG > 3) console.log("Key verification...");
317 var sigHex = DataUtils.toHex(co.signature.signature).toLowerCase();
318
319 var wit = null;
320 if (co.signature.Witness != null) {
321 wit = new Witness();
322 wit.decode(co.signature.Witness);
323 }
324
325 var keylocator = co.signedInfo.locator;
326 if (keylocator.type == KeyLocatorType.KEYNAME) {
327 if (LOG > 3) console.log("KeyLocator contains KEYNAME");
328 //var keyname = keylocator.keyName.contentName.getName();
329 //console.log(nameStr);
330 //console.log(keyname);
331
332 if (keylocator.keyName.contentName.match(co.name)) {
333 if (LOG > 3) console.log("Content is key itself");
334
335 var rsakey = decodeSubjectPublicKeyInfo(co.content);
336 var verified = rsakey.verifyByteArray(co.rawSignatureData, wit, sigHex);
337 var flag = (verified == true) ? Closure.UPCALL_CONTENT : Closure.UPCALL_CONTENT_BAD;
338
339 currentClosure.upcall(flag, new UpcallInfo(this, null, 0, co));
340
341 // SWT: We don't need to store key here since the same key will be
342 // stored again in the closure.
343 //var keyEntry = new KeyStoreEntry(keylocator.keyName, rsakey, new Date().getTime());
344 //NDN.addKeyEntry(keyEntry);
345 //console.log(NDN.KeyStore);
346 } else {
347 // Check local key store
348 var keyEntry = NDN.getKeyByName(keylocator.keyName);
349 if (keyEntry) {
350 // Key found, verify now
351 if (LOG > 3) console.log("Local key cache hit");
352 var rsakey = keyEntry.rsaKey;
353 var verified = rsakey.verifyByteArray(co.rawSignatureData, wit, sigHex);
354 var flag = (verified == true) ? Closure.UPCALL_CONTENT : Closure.UPCALL_CONTENT_BAD;
355
356 // Raise callback
357 currentClosure.upcall(flag, new UpcallInfo(this, null, 0, co));
358 } else {
359 // Not found, fetch now
360 if (LOG > 3) console.log("Fetch key according to keylocator");
361 var nextClosure = new KeyFetchClosure(co, currentClosure, keylocator.keyName, sigHex, wit);
362 this.expressInterest(keylocator.keyName.contentName.getPrefix(4), nextClosure);
363 }
364 }
365 } else if (keylocator.type == KeyLocatorType.KEY) {
366 if (LOG > 3) console.log("Keylocator contains KEY");
367
368 var rsakey = decodeSubjectPublicKeyInfo(co.signedInfo.locator.publicKey);
369 var verified = rsakey.verifyByteArray(co.rawSignatureData, wit, sigHex);
370
371 var flag = (verified == true) ? Closure.UPCALL_CONTENT : Closure.UPCALL_CONTENT_BAD;
372 // Raise callback
373 currentClosure.upcall(Closure.UPCALL_CONTENT, new UpcallInfo(this, null, 0, co));
374
375 // Since KeyLocator does not contain key name for this key,
376 // we have no way to store it as a key entry in KeyStore.
377 } else {
378 var cert = keylocator.certificate;
379 console.log("KeyLocator contains CERT");
380 console.log(cert);
381
382 // TODO: verify certificate
383 }
384 }
385 }
386 }
387 } else
388 console.log('Incoming packet is not Interest or ContentObject. Discard now.');
Wentao Shangb42483a2013-01-03 15:32:32 -0800389};
Jeff Thompsond3a80dc2012-12-16 17:52:43 -0800390
391/*
392 * Assume this.getHostAndPort is not null. This is called when this.host is null or its host
393 * is not alive. Get a host and port, connect, then express callerInterest with callerClosure.
394 */
395NDN.prototype.connectAndExpressInterest = function(callerInterest, callerClosure) {
396 var hostAndPort = this.getHostAndPort();
397 if (hostAndPort == null) {
398 console.log('ERROR: No more hosts from getHostAndPort');
399 this.host = null;
400 return;
401 }
402
403 if (hostAndPort.host == this.host && hostAndPort.port == this.port) {
404 console.log('ERROR: The host returned by getHostAndPort is not alive: ' +
405 this.host + ":" + this.port);
406 return;
407 }
408
409 this.host = hostAndPort.host;
410 this.port = hostAndPort.port;
411 console.log("Trying host from getHostAndPort: " + this.host);
412
Jeff Thompson75771cb2013-01-20 23:27:38 -0800413 // Fetch any content.
414 var interest = new Interest(new Name("/"));
Jeff Thompson42806a12012-12-29 18:19:39 -0800415 interest.interestLifetime = 4000; // milliseconds
Jeff Thompsond3a80dc2012-12-16 17:52:43 -0800416
417 var thisNDN = this;
418 var timerID = setTimeout(function() {
419 console.log("Timeout waiting for host " + thisNDN.host);
420 // Try again.
421 thisNDN.connectAndExpressInterest(callerInterest, callerClosure);
422 }, 3000);
423
424 this.transport.expressInterest
425 (this, interest, new NDN.ConnectClosure(this, callerInterest, callerClosure, timerID));
Wentao Shangb42483a2013-01-03 15:32:32 -0800426};
Jeff Thompsond3a80dc2012-12-16 17:52:43 -0800427
428NDN.ConnectClosure = function ConnectClosure(ndn, callerInterest, callerClosure, timerID) {
429 // Inherit from Closure.
430 Closure.call(this);
431
432 this.ndn = ndn;
433 this.callerInterest = callerInterest;
434 this.callerClosure = callerClosure;
435 this.timerID = timerID;
436};
437
438NDN.ConnectClosure.prototype.upcall = function(kind, upcallInfo) {
439 if (!(kind == Closure.UPCALL_CONTENT ||
Jeff Thompson75771cb2013-01-20 23:27:38 -0800440 kind == Closure.UPCALL_CONTENT_UNVERIFIED))
Jeff Thompsond3a80dc2012-12-16 17:52:43 -0800441 // The upcall is not for us.
442 return Closure.RESULT_ERR;
443
444 // The host is alive, so cancel the timeout and issue the caller's interest.
445 clearTimeout(this.timerID);
446 console.log(this.ndn.host + ": Host is alive. Fetching callerInterest.");
447 this.ndn.transport.expressInterest(this.ndn, this.callerInterest, this.callerClosure);
448
449 return Closure.RESULT_OK;
450};
451
Jeff Thompson75771cb2013-01-20 23:27:38 -0800452/*
453 * A BinaryXmlElementReader lets you call onReceivedData multiple times which uses a
454 * BinaryXMLStructureDecoder to detect the end of a binary XML element and calls
455 * elementListener.onReceivedElement(element) with the element.
456 * This handles the case where a single call to onReceivedData may contain multiple elements.
457 */
458var BinaryXmlElementReader = function BinaryXmlElementReader(elementListener) {
459 this.elementListener = elementListener;
460 this.dataParts = [];
461 this.structureDecoder = new BinaryXMLStructureDecoder();
462};
463
464BinaryXmlElementReader.prototype.onReceivedData = function(/* Uint8Array */ rawData) {
Jeff Thompson75771cb2013-01-20 23:27:38 -0800465 // Process multiple objects in the data.
466 while(true) {
467 // Scan the input to check if a whole ccnb object has been read.
468 this.structureDecoder.seek(0);
469 if (this.structureDecoder.findElementEnd(rawData)) {
470 // Got the remainder of an object. Report to the caller.
471 this.dataParts.push(rawData.subarray(0, this.structureDecoder.offset));
Jeff Thompson75771cb2013-01-20 23:27:38 -0800472 this.elementListener.onReceivedElement(DataUtils.concatArrays(this.dataParts));
473
474 // Need to read a new object.
475 rawData = rawData.subarray(this.structureDecoder.offset, rawData.length);
476 this.dataParts = [];
477 this.structureDecoder = new BinaryXMLStructureDecoder();
478 if (rawData.length == 0)
479 // No more data in the packet.
480 return;
481
482 // else loop back to decode.
483 }
484 else {
485 // Save for a later call to concatArrays so that we only copy data once.
486 this.dataParts.push(rawData);
Jeff Thompsona46083c2013-01-20 23:55:21 -0800487 if (LOG>3) console.log('Incomplete packet received. Length ' + rawData.length + '. Wait for more input.');
Jeff Thompson75771cb2013-01-20 23:27:38 -0800488 return;
489 }
490 }
491}