/* | |
* @author: Jeff Thompson | |
* See COPYING for copyright and distribution information. | |
* This is the ndn protocol handler. | |
* Protocol handling code derived from http://mike.kaply.com/2011/01/18/writing-a-firefox-protocol-handler/ | |
*/ | |
const Cc = Components.classes; | |
const Ci = Components.interfaces; | |
const Cr = Components.results; | |
const nsIProtocolHandler = Ci.nsIProtocolHandler; | |
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); | |
Components.utils.import("chrome://modules/content/ndn-js.jsm"); | |
Components.utils.import("chrome://modules/content/ContentChannel.jsm"); | |
Components.utils.import("chrome://modules/content/NdnProtocolInfo.jsm"); | |
function NdnProtocol() { | |
// TODO: Remove host: null when null is the default. | |
this.ndn = new NDN({ getTransport: function() { return new XpcomTransport(); }, | |
host: null, verify: false }); | |
} | |
NdnProtocol.prototype = { | |
scheme: "ndn", | |
protocolFlags: nsIProtocolHandler.URI_NORELATIVE | | |
nsIProtocolHandler.URI_NOAUTH | | |
nsIProtocolHandler.URI_LOADABLE_BY_ANYONE, | |
newURI: function(aSpec, aOriginCharset, aBaseURI) | |
{ | |
var uri = Cc["@mozilla.org/network/simple-uri;1"].createInstance(Ci.nsIURI); | |
// We have to trim now because nsIURI converts spaces to %20 and we can't trim in newChannel. | |
var uriParts = NdnProtocolInfo.splitUri(aSpec); | |
if (aBaseURI == null || uriParts.name.length < 1 || uriParts.name[0] == '/') | |
// Just reconstruct the trimmed URI. | |
uri.spec = "ndn:" + uriParts.name + uriParts.search + uriParts.hash; | |
else { | |
// Make a URI relative to the base name up to the file name component. | |
var baseUriParts = NdnProtocolInfo.splitUri(aBaseURI.spec); | |
var baseName = new Name(baseUriParts.name); | |
var iFileName = baseName.indexOfFileName(); | |
var relativeName = uriParts.name; | |
// Handle ../ | |
while (true) { | |
if (relativeName.substr(0, 2) == "./") | |
relativeName = relativeName.substr(2); | |
else if (relativeName.substr(0, 3) == "../") { | |
relativeName = relativeName.substr(3); | |
if (iFileName > 0) | |
--iFileName; | |
} | |
else | |
break; | |
} | |
var prefixUri = "/"; | |
if (iFileName > 0) | |
prefixUri = new Name(baseName.components.slice(0, iFileName)).to_uri() + "/"; | |
uri.spec = "ndn:" + prefixUri + relativeName + uriParts.search + uriParts.hash; | |
} | |
return uri; | |
}, | |
newChannel: function(aURI) | |
{ | |
var thisNdnProtocol = this; | |
try { | |
var uriParts = NdnProtocolInfo.splitUri(aURI.spec); | |
var template = new Interest(new Name([])); | |
// Use the same default as NDN.expressInterest. | |
template.interestLifetime = 4000; // milliseconds | |
var searchWithoutNdn = extractNdnSearch(uriParts.search, template); | |
var segmentTemplate = new Interest(new Name([])); | |
// Only use the interest selectors which make sense for fetching further segments. | |
segmentTemplate.publisherPublicKeyDigest = template.publisherPublicKeyDigest; | |
segmentTemplate.scope = template.scope; | |
segmentTemplate.interestLifetime = template.interestLifetime; | |
var requestContent = function(contentListener) { | |
var name = new Name(uriParts.name); | |
// TODO: Strip off an ending implicit digest before checking the last component? | |
var uriEndsWithSegmentNumber = endsWithSegmentNumber(name); | |
// Use the same NDN object each time. | |
thisNdnProtocol.ndn.expressInterest(name, | |
new ContentClosure(thisNdnProtocol.ndn, contentListener, uriEndsWithSegmentNumber, | |
aURI.originCharset, searchWithoutNdn + uriParts.hash, segmentTemplate), | |
template); | |
}; | |
return new ContentChannel(aURI, requestContent); | |
} catch (ex) { | |
dump("NdnProtocol.newChannel exception: " + ex + "\n" + ex.stack); | |
} | |
}, | |
classDescription: "ndn Protocol Handler", | |
contractID: "@mozilla.org/network/protocol;1?name=" + "ndn", | |
classID: Components.ID('{8122e660-1012-11e2-892e-0800200c9a66}'), | |
QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler]) | |
}; | |
if (XPCOMUtils.generateNSGetFactory) | |
var NSGetFactory = XPCOMUtils.generateNSGetFactory([NdnProtocol]); | |
else | |
var NSGetModule = XPCOMUtils.generateNSGetModule([NdnProtocol]); | |
/* | |
* Create a closure for calling expressInterest. | |
* contentListener is from the call to requestContent. | |
* uriEndsWithSegmentNumber is true if the URI passed to newChannel has a segment number | |
* (used to determine whether to request only that segment number and for updating the URL bar). | |
* uriOriginCharset is the charset of the URI passed to newChannel (used for making a new URI) | |
* uriSearchAndHash is the search and hash part of the URI passed to newChannel, including the '?' | |
* and/or '#' but without the interest selector fields. | |
* segmentTemplate is the template used in expressInterest to fetch further segments. | |
*/ | |
var ContentClosure = function ContentClosure | |
(ndn, contentListener, uriEndsWithSegmentNumber, uriOriginCharset, uriSearchAndHash, | |
segmentTemplate) { | |
// Inherit from Closure. | |
Closure.call(this); | |
this.ndn = ndn; | |
this.contentListener = contentListener; | |
this.uriEndsWithSegmentNumber = uriEndsWithSegmentNumber; | |
this.uriOriginCharset = uriOriginCharset; | |
this.uriSearchAndHash = uriSearchAndHash; | |
this.segmentTemplate = segmentTemplate; | |
this.segmentStore = new SegmentStore(); | |
this.contentSha256 = new Sha256(); | |
this.didRequestFinalSegment = false; | |
this.finalSegmentNumber = null; | |
}; | |
ContentClosure.prototype.upcall = function(kind, upcallInfo) { | |
try { | |
if (!(kind == Closure.UPCALL_CONTENT || | |
kind == Closure.UPCALL_CONTENT_UNVERIFIED)) | |
// The upcall is not for us. | |
return Closure.RESULT_ERR; | |
var contentObject = upcallInfo.contentObject; | |
if (contentObject.content == null) { | |
dump("NdnProtocol.ContentClosure: contentObject.content is null\n"); | |
return Closure.RESULT_ERR; | |
} | |
// Now that we're connected, report the host and port. | |
NdnProtocolInfo.setConnectedNdnHub(this.ndn.host, this.ndn.port); | |
// If !this.uriEndsWithSegmentNumber, we use the segmentNumber to load multiple segments. | |
// If this.uriEndsWithSegmentNumber, then we leave segmentNumber null. | |
var segmentNumber = null; | |
if (!this.uriEndsWithSegmentNumber && endsWithSegmentNumber(contentObject.name)) { | |
segmentNumber = DataUtils.bigEndianToUnsignedInt | |
(contentObject.name.components[contentObject.name.components.length - 1]); | |
this.segmentStore.storeContent(segmentNumber, contentObject); | |
} | |
if (segmentNumber == null || segmentNumber == 0) { | |
// This is the first or only segment, so start. | |
// Get the URI from the ContentObject including the version. | |
var contentUriSpec; | |
if (!this.uriEndsWithSegmentNumber && endsWithSegmentNumber(contentObject.name)) { | |
var nameWithoutSegmentNumber = new Name | |
(contentObject.name.components.slice(0, contentObject.name.components.length - 1)); | |
contentUriSpec = "ndn:" + nameWithoutSegmentNumber.to_uri(); | |
} | |
else | |
contentUriSpec = "ndn:" + contentObject.name.to_uri(); | |
// Include the search and hash. | |
contentUriSpec += this.uriSearchAndHash; | |
var contentTypeEtc = getNameContentTypeAndCharset(contentObject.name); | |
var ioService = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); | |
this.contentListener.onStart(contentTypeEtc.contentType, contentTypeEtc.contentCharset, | |
ioService.newURI(contentUriSpec, this.uriOriginCharset, null)); | |
} | |
if (segmentNumber == null) { | |
// We are not doing segments, so just finish. | |
this.contentListener.onReceivedContent(DataUtils.toString(contentObject.content)); | |
this.contentSha256.update(contentObject.content); | |
this.contentListener.onStop(); | |
if (!this.uriEndsWithSegmentNumber) { | |
var nameContentDigest = contentObject.name.getContentDigestValue(); | |
if (nameContentDigest != null && | |
!DataUtils.arraysEqual(nameContentDigest, this.contentSha256.finalize())) | |
// TODO: How to show the user an error for invalid digest? | |
dump("Content does not match digest in name " + contentObject.name.to_uri()); | |
} | |
return Closure.RESULT_OK; | |
} | |
if (contentObject.signedInfo != null && contentObject.signedInfo.finalBlockID != null) | |
this.finalSegmentNumber = DataUtils.bigEndianToUnsignedInt(contentObject.signedInfo.finalBlockID); | |
// The content was already put in the store. Retrieve as much as possible. | |
var entry; | |
while ((entry = this.segmentStore.maybeRetrieveNextEntry()) != null) { | |
segmentNumber = entry.key; | |
contentObject = entry.value; | |
this.contentListener.onReceivedContent(DataUtils.toString(contentObject.content)); | |
this.contentSha256.update(contentObject.content); | |
if (this.finalSegmentNumber != null && segmentNumber == this.finalSegmentNumber) { | |
// Finished. | |
this.contentListener.onStop(); | |
var nameContentDigest = contentObject.name.getContentDigestValue(); | |
if (nameContentDigest != null && | |
!DataUtils.arraysEqual(nameContentDigest, this.contentSha256.finalize())) | |
// TODO: How to show the user an error for invalid digest? | |
dump("Content does not match digest in name " + contentObject.name.to_uri()); | |
return Closure.RESULT_OK; | |
} | |
} | |
if (this.finalSegmentNumber == null && !this.didRequestFinalSegment) { | |
this.didRequestFinalSegment = true; | |
// Try to determine the final segment now. | |
var components = contentObject.name.components.slice | |
(0, contentObject.name.components.length - 1); | |
// Temporarily set the childSelector in the segmentTemplate. | |
this.segmentTemplate.childSelector = 1; | |
this.ndn.expressInterest(new Name(components), this, this.segmentTemplate); | |
this.segmentTemplate.childSelector = null; | |
} | |
// Request new segments. | |
var toRequest = this.segmentStore.requestSegmentNumbers(2); | |
for (var i = 0; i < toRequest.length; ++i) { | |
if (this.finalSegmentNumber != null && toRequest[i] > this.finalSegmentNumber) | |
continue; | |
// Make a name for the segment and get it. | |
var segmentNumberBigEndian = DataUtils.nonNegativeIntToBigEndian(toRequest[i]); | |
// Put a 0 byte in front. | |
var segmentNumberComponent = new Uint8Array(segmentNumberBigEndian.length + 1); | |
segmentNumberComponent.set(segmentNumberBigEndian, 1); | |
var components = contentObject.name.components.slice | |
(0, contentObject.name.components.length - 1); | |
components.push(segmentNumberComponent); | |
this.ndn.expressInterest(new Name(components), this, this.segmentTemplate); | |
} | |
return Closure.RESULT_OK; | |
} catch (ex) { | |
dump("ContentClosure.upcall exception: " + ex + "\n" + ex.stack); | |
return Closure.RESULT_ERR; | |
} | |
}; | |
/* | |
* A SegmentStore stores segments until they are retrieved in order starting with segment 0. | |
*/ | |
var SegmentStore = function SegmentStore() { | |
// Each entry is an object where the key is the segment number and value is null if | |
// the segment number is requested or the contentObject if received. | |
this.store = new SortedArray(); | |
this.maxRetrievedSegmentNumber = -1; | |
}; | |
SegmentStore.prototype.storeContent = function(segmentNumber, contentObject) { | |
// We don't expect to try to store a segment that has already been retrieved, but check anyway. | |
if (segmentNumber > this.maxRetrievedSegmentNumber) | |
this.store.set(segmentNumber, contentObject); | |
}; | |
/* | |
* If the min segment number is this.maxRetrievedSegmentNumber + 1 and its value is not null, | |
* then delete from the store, return the entry with key and value, and update maxRetrievedSegmentNumber. | |
* Otherwise return null. | |
*/ | |
SegmentStore.prototype.maybeRetrieveNextEntry = function() { | |
if (this.store.entries.length > 0 && this.store.entries[0].value != null && | |
this.store.entries[0].key == this.maxRetrievedSegmentNumber + 1) { | |
var entry = this.store.entries[0]; | |
this.store.removeAt(0); | |
++this.maxRetrievedSegmentNumber; | |
return entry; | |
} | |
else | |
return null; | |
}; | |
/* | |
* Return an array of the next segment numbers that need to be requested so that the total | |
* requested segments is totalRequestedSegments. If a segment store entry value is null, it is | |
* already requested and is not returned. If a segment number is returned, create a | |
* entry in the segment store with a null value. | |
*/ | |
SegmentStore.prototype.requestSegmentNumbers = function(totalRequestedSegments) { | |
// First, count how many are already requested. | |
var nRequestedSegments = 0; | |
for (var i = 0; i < this.store.entries.length; ++i) { | |
if (this.store.entries[i].value == null) { | |
++nRequestedSegments; | |
if (nRequestedSegments >= totalRequestedSegments) | |
// Already maxed out on requests. | |
return []; | |
} | |
} | |
var toRequest = []; | |
var nextSegmentNumber = this.maxRetrievedSegmentNumber + 1; | |
for (var i = 0; i < this.store.entries.length; ++i) { | |
var entry = this.store.entries[i]; | |
// Fill in the gap before the segment number in the entry. | |
while (nextSegmentNumber < entry.key) { | |
toRequest.push(nextSegmentNumber); | |
++nextSegmentNumber; | |
++nRequestedSegments; | |
if (nRequestedSegments >= totalRequestedSegments) | |
break; | |
} | |
if (nRequestedSegments >= totalRequestedSegments) | |
break; | |
nextSegmentNumber = entry.key + 1; | |
} | |
// We already filled in the gaps for the segments in the store. Continue after the last. | |
while (nRequestedSegments < totalRequestedSegments) { | |
toRequest.push(nextSegmentNumber); | |
++nextSegmentNumber; | |
++nRequestedSegments; | |
} | |
// Mark the new segment numbers as requested. | |
for (var i = 0; i < toRequest.length; ++i) | |
this.store.set(toRequest[i], null); | |
return toRequest; | |
} | |
/* | |
* A SortedArray is an array of objects with key and value, where the key is an integer. | |
*/ | |
var SortedArray = function SortedArray() { | |
this.entries = []; | |
} | |
SortedArray.prototype.sortEntries = function() { | |
this.entries.sort(function(a, b) { return a.key - b.key; }); | |
}; | |
SortedArray.prototype.indexOfKey = function(key) { | |
for (var i = 0; i < this.entries.length; ++i) { | |
if (this.entries[i].key == key) | |
return i; | |
} | |
return -1; | |
} | |
SortedArray.prototype.set = function(key, value) { | |
var i = this.indexOfKey(key); | |
if (i >= 0) { | |
this.entries[i].value = value; | |
return; | |
} | |
this.entries.push({ key: key, value: value}); | |
this.sortEntries(); | |
} | |
SortedArray.prototype.removeAt = function(index) { | |
this.entries.splice(index, 1); | |
} | |
/* | |
* Scan the name from the last component to the first (skipping special name components) | |
* for a recognized file name extension, and return an object with properties contentType and charset. | |
*/ | |
function getNameContentTypeAndCharset(name) { | |
var iFileName = name.indexOfFileName(); | |
if (iFileName < 0) | |
// Get the default mime type. | |
return MimeTypes.getContentTypeAndCharset(""); | |
return MimeTypes.getContentTypeAndCharset | |
(DataUtils.toString(name.components[iFileName]).toLowerCase()); | |
} | |
/* | |
* Return true if the last component in the name is a segment number.. | |
*/ | |
function endsWithSegmentNumber(name) { | |
return name.components != null && name.components.length >= 1 && | |
name.components[name.components.length - 1].length >= 1 && | |
name.components[name.components.length - 1][0] == 0; | |
} | |
/* | |
* Find all search keys starting with "ndn." and set the attribute in template. | |
* Return the search string including the starting "?" but with the "ndn." keys removed, | |
* or return "" if there are no search terms left. | |
*/ | |
function extractNdnSearch(search, template) { | |
if (!(search.length >= 1 && search[0] == '?')) | |
return search; | |
var terms = search.substr(1).split('&'); | |
var i = 0; | |
while (i < terms.length) { | |
var keyValue = terms[i].split('='); | |
var key = keyValue[0].trim(); | |
if (key.substr(0, 4) == "ndn.") { | |
if (keyValue.length >= 1) { | |
var value = keyValue[1].trim(); | |
var nonNegativeInt = parseInt(value); | |
if (key == "ndn.MinSuffixComponents" && nonNegativeInt >= 0) | |
template.minSuffixComponents = nonNegativeInt; | |
if (key == "ndn.MaxSuffixComponents" && nonNegativeInt >= 0) | |
template.maxSuffixComponents = nonNegativeInt; | |
if (key == "ndn.ChildSelector" && nonNegativeInt >= 0) | |
template.childSelector = nonNegativeInt; | |
if (key == "ndn.AnswerOriginKind" && nonNegativeInt >= 0) | |
template.answerOriginKind = nonNegativeInt; | |
if (key == "ndn.Scope" && nonNegativeInt >= 0) | |
template.scope = nonNegativeInt; | |
if (key == "ndn.InterestLifetime" && nonNegativeInt >= 0) | |
template.interestLifetime = nonNegativeInt; | |
if (key == "ndn.PublisherPublicKeyDigest" && nonNegativeInt >= 0) | |
template.publisherPublicKeyDigest = DataUtils.toNumbersFromString(unescape(value)); | |
if (key == "ndn.Nonce" && nonNegativeInt >= 0) | |
template.nonce = DataUtils.toNumbersFromString(unescape(value)); | |
// TODO: handle Exclude. | |
} | |
// Remove the "ndn." term and don't advance i. | |
terms.splice(i, 1); | |
} | |
else | |
++i; | |
} | |
if (terms.length == 0) | |
return ""; | |
else | |
return "?" + terms.join('&'); | |
} |