def __init__(self, prefix, dataType, face, keyChain, database, repeatAttempts=None, keyRetrievalLink=None): self._face = face self._keyChain = keyChain self._database = database self._maxRepeatAttempts = 3 if repeatAttempts == None else repeatAttempts self._keyRetrievalLink = Producer.NO_LINK if keyRetrievalLink == None else Link(keyRetrievalLink) # The dictionary key is the key Name The value is a Producer._KeyInfo. self._eKeyInfo = {} # The dictionary key is the float time stamp. The value is a Producer._KeyRequest. self._keyRequests = {} fixedPrefix = Name(prefix) fixedDataType = Name(dataType) # Fill _ekeyInfo with all permutations of dataType, including the 'E-KEY' # component of the name. This will be used in createContentKey to send # interests without reconstructing names every time. fixedPrefix.append(Encryptor.NAME_COMPONENT_READ) while fixedDataType.size() > 0: nodeName = Name(fixedPrefix) nodeName.append(fixedDataType) nodeName.append(Encryptor.NAME_COMPONENT_E_KEY) self._eKeyInfo[nodeName] = Producer._KeyInfo() fixedDataType = fixedDataType.getPrefix(-1) fixedPrefix.append(dataType) self._namespace = Name(prefix) self._namespace.append(Encryptor.NAME_COMPONENT_SAMPLE) self._namespace.append(dataType)
def __init__(self, prefix, dataType, face, keyChain, database, repeatAttempts = None, keyRetrievalLink = None): self._face = face self._keyChain = keyChain self._database = database self._maxRepeatAttempts = (3 if repeatAttempts == None else repeatAttempts) self._keyRetrievalLink = (Producer.NO_LINK if keyRetrievalLink == None else Link(keyRetrievalLink)) # The dictionary key is the key Name The value is a Producer._KeyInfo. self._eKeyInfo = {} # The dictionary key is the float time stamp. The value is a Producer._KeyRequest. self._keyRequests = {} fixedPrefix = Name(prefix) fixedDataType = Name(dataType) # Fill _ekeyInfo with all permutations of dataType, including the 'E-KEY' # component of the name. This will be used in createContentKey to send # interests without reconstructing names every time. fixedPrefix.append(Encryptor.NAME_COMPONENT_READ) while fixedDataType.size() > 0: nodeName = Name(fixedPrefix) nodeName.append(fixedDataType) nodeName.append(Encryptor.NAME_COMPONENT_E_KEY) self._eKeyInfo[nodeName] = Producer._KeyInfo() fixedDataType = fixedDataType.getPrefix(-1) fixedPrefix.append(dataType) self._namespace = Name(prefix) self._namespace.append(Encryptor.NAME_COMPONENT_SAMPLE) self._namespace.append(dataType)
def signByIdentity(self, target, identityName=None, wireFormat=None): """ Sign the target. If it is a Data object, set its signature. If it is an array, return a signature object. :param target: If this is a Data object, wire encode for signing, update its signature and key locator field and wireEncoding. If it is an array, sign it and return a Signature object. :type target: Data or an array which implements the buffer protocol :param Name identityName: (optional) The identity name for the key to use for signing. If omitted, infer the signing identity from the data packet name. :param wireFormat: (optional) A WireFormat object used to encode the input. If omitted, use WireFormat.getDefaultWireFormat(). :type wireFormat: A subclass of WireFormat :return: The Signature object (only if the target is an array). :rtype: An object of a subclass of Signature """ if identityName == None: identityName = Name() if isinstance(target, Data): if identityName.size() == 0: inferredIdentity = self._policyManager.inferSigningIdentity( target.getName()) if inferredIdentity.size() == 0: signingCertificateName = self._identityManager.getDefaultCertificateName( ) else: signingCertificateName = \ self._identityManager.getDefaultCertificateNameForIdentity(inferredIdentity) else: signingCertificateName = \ self._identityManager.getDefaultCertificateNameForIdentity(identityName) if signingCertificateName.size() == 0: raise SecurityException("No qualified certificate name found!") if not self._policyManager.checkSigningPolicy( target.getName(), signingCertificateName): raise SecurityException( "Signing Cert name does not comply with signing policy") self._identityManager.signByCertificate(target, signingCertificateName, wireFormat) else: signingCertificateName = \ self._identityManager.getDefaultCertificateNameForIdentity(identityName) if signingCertificateName.size() == 0: raise SecurityException("No qualified certificate name found!") return self._identityManager.signByCertificate( target, signingCertificateName)
def signByIdentity(self, target, identityName = None, wireFormat = None): """ Sign the target. If it is a Data object, set its signature. If it is an array, return a signature object. :param target: If this is a Data object, wire encode for signing, update its signature and key locator field and wireEncoding. If it is an array, sign it and return a Signature object. :type target: Data or an array which implements the buffer protocol :param Name identityName: (optional) The identity name for the key to use for signing. If omitted, infer the signing identity from the data packet name. :param wireFormat: (optional) A WireFormat object used to encode the input. If omitted, use WireFormat.getDefaultWireFormat(). :type wireFormat: A subclass of WireFormat :return: The Signature object (only if the target is an array). :rtype: An object of a subclass of Signature """ if identityName == None: identityName = Name() if isinstance(target, Data): if identityName.size() == 0: inferredIdentity = self._policyManager.inferSigningIdentity( target.getName()) if inferredIdentity.size() == 0: signingCertificateName = self._identityManager.getDefaultCertificateName() else: signingCertificateName = \ self._identityManager.getDefaultCertificateNameForIdentity(inferredIdentity) else: signingCertificateName = \ self._identityManager.getDefaultCertificateNameForIdentity(identityName) if signingCertificateName.size() == 0: raise SecurityException("No qualified certificate name found!") if not self._policyManager.checkSigningPolicy( target.getName(), signingCertificateName): raise SecurityException( "Signing Cert name does not comply with signing policy") self._identityManager.signByCertificate( target, signingCertificateName, wireFormat) else: signingCertificateName = \ self._identityManager.getDefaultCertificateNameForIdentity(identityName) if signingCertificateName.size() == 0: raise SecurityException("No qualified certificate name found!") return self._identityManager.signByCertificate( target, signingCertificateName)
class Data(object): def __init__(self, value=None): if isinstance(value, Data): # Copy the values. self._name = ChangeCounter(Name(value.getName())) self._metaInfo = ChangeCounter(MetaInfo(value.getMetaInfo())) self._signature = ChangeCounter(value.getSignature().clone()) self._content = value._content self._defaultWireEncoding = value.getDefaultWireEncoding() self._defaultFullName = value._defaultFullName self._defaultWireEncodingFormat = value._defaultWireEncodingFormat else: self._name = ChangeCounter( Name(value) if type(value) is Name else Name()) self._metaInfo = ChangeCounter(MetaInfo()) self._signature = ChangeCounter(Sha256WithRsaSignature()) self._content = Blob() self._defaultWireEncoding = SignedBlob() self._defaultFullName = Name() self._defaultWireEncodingFormat = None self._getDefaultWireEncodingChangeCount = 0 self._changeCount = 0 self._lpPacket = None def wireEncode(self, wireFormat=None): """ Encode this Data for a particular wire format. If wireFormat is the default wire format, also set the defaultWireEncoding field to the encoded result. :param wireFormat: (optional) A WireFormat object used to encode this Data object. If omitted, use WireFormat.getDefaultWireFormat(). :type wireFormat: A subclass of WireFormat :return: The encoded buffer in a SignedBlob object. :rtype: SignedBlob """ if wireFormat == None: # Don't use a default argument since getDefaultWireFormat can change. wireFormat = WireFormat.getDefaultWireFormat() if (not self.getDefaultWireEncoding().isNull() and self.getDefaultWireEncodingFormat() == wireFormat): # We already have an encoding in the desired format. return self.getDefaultWireEncoding() (encoding, signedPortionBeginOffset, signedPortionEndOffset) = \ wireFormat.encodeData(self) wireEncoding = SignedBlob(encoding, signedPortionBeginOffset, signedPortionEndOffset) if wireFormat == WireFormat.getDefaultWireFormat(): # This is the default wire encoding. self._setDefaultWireEncoding(wireEncoding, WireFormat.getDefaultWireFormat()) return wireEncoding def wireDecode(self, input, wireFormat=None): """ Decode the input using a particular wire format and update this Data. If wireFormat is the default wire format, also set the defaultWireEncoding to another pointer to the input. :param input: The array with the bytes to decode. If input is not a Blob, then copy the bytes to save the defaultWireEncoding (otherwise take another pointer to the same Blob). :type input: A Blob or an array type with int elements :param wireFormat: (optional) A WireFormat object used to decode this Data object. If omitted, use WireFormat.getDefaultWireFormat(). :type wireFormat: A subclass of WireFormat """ if wireFormat == None: # Don't use a default argument since getDefaultWireFormat can change. wireFormat = WireFormat.getDefaultWireFormat() if isinstance(input, Blob): # Input is a blob, so get its buf() and set copy False. result = wireFormat.decodeData(self, input.buf(), False) else: result = wireFormat.decodeData(self, input, True) (signedPortionBeginOffset, signedPortionEndOffset) = result if wireFormat == WireFormat.getDefaultWireFormat(): # This is the default wire encoding. In the Blob constructor, set # copy true, but if input is already a Blob, it won't copy. self._setDefaultWireEncoding( SignedBlob(Blob(input, True), signedPortionBeginOffset, signedPortionEndOffset), WireFormat.getDefaultWireFormat()) else: self._setDefaultWireEncoding(SignedBlob(), None) def getName(self): """ Get the data packet's name. :return: The name. :rtype: Name """ return self._name.get() def getMetaInfo(self): """ Get the data packet's meta info. :return: The meta info. :rtype: MetaInfo """ return self._metaInfo.get() def getSignature(self): """ Get the data packet's signature object. :return: The signature object. :rtype: a subclass of Signature such as Sha256WithRsaSignature """ return self._signature.get() def getContent(self): """ Get the data packet's content. :return: The content as a Blob, which isNull() if unspecified. :rtype: Blob """ return self._content def getDefaultWireEncoding(self): """ Return the default wire encoding, which was encoded with getDefaultWireEncodingFormat(). :return: The default wire encoding, whose isNull() may be true if there is no default wire encoding. :rtype: SignedBlob """ if self._getDefaultWireEncodingChangeCount != self.getChangeCount(): # The values have changed, so the default wire encoding is # invalidated. self._defaultWireEncoding = SignedBlob() self._defaultWireEncodingFormat = None self._getDefaultWireEncodingChangeCount = self.getChangeCount() return self._defaultWireEncoding def getDefaultWireEncodingFormat(self): """ Get the WireFormat which is used by getDefaultWireEncoding(). :return: The WireFormat, which is only meaningful if the getDefaultWireEncoding() is not isNull(). :rtype: WireFormat """ return self._defaultWireEncodingFormat def getIncomingFaceId(self): """ Get the incoming face ID according to the incoming packet header. :return: The incoming face ID. If not specified, return None. :rtype: int """ field = (None if self._lpPacket == None else IncomingFaceId.getFirstHeader(self._lpPacket)) return None if field == None else field.getFaceId() def getFullName(self, wireFormat=None): """ Get the Data packet's full name, which includes the final ImplicitSha256Digest component based on the wire encoding for a particular wire format. :param wireFormat: (optional) A WireFormat object used to encode this object. If omitted, use WireFormat.getDefaultWireFormat(). :type wireFormat: A subclass of WireFormat :return: The full name. You must not change the Name object - if you need to change it then make a copy. :rtype: Name """ if wireFormat == None: # Don't use a default argument since getDefaultWireFormat can change. wireFormat = WireFormat.getDefaultWireFormat() # The default full name depends on the default wire encoding. if (not self.getDefaultWireEncoding().isNull() and self._defaultFullName.size() > 0 and self.getDefaultWireEncodingFormat() == wireFormat): # We already have a full name. A non-null default wire encoding # means that the Data packet fields have not changed. return self._defaultFullName fullName = Name(self.getName()) sha256 = hashes.Hash(hashes.SHA256(), backend=default_backend()) sha256.update(self.wireEncode(wireFormat).toBytes()) fullName.appendImplicitSha256Digest( Blob(bytearray(sha256.finalize()), False)) if wireFormat == WireFormat.getDefaultWireFormat(): # wireEncode has already set defaultWireEncodingFormat_. self._defaultFullName = fullName return fullName def setName(self, name): """ Set name to a copy of the given Name. :param Name name: The Name which is copied. :return: This Data so that you can chain calls to update values. :rtype: Data """ self._name.set(name if type(name) is Name else Name(name)) self._changeCount += 1 return self def setMetaInfo(self, metaInfo): """ Set metaInfo to a copy of the given MetaInfo. :param MetaInfo metaInfo: The MetaInfo which is copied. :return: This Data so that you can chain calls to update values. :rtype: Data """ self._metaInfo.set(MetaInfo() if metaInfo == None else MetaInfo(metaInfo)) self._changeCount += 1 return self def setSignature(self, signature): """ Set the signature to a copy of the given signature. :param signature: The signature object which is cloned. :type signature: a subclass of Signature such as Sha256WithRsaSignature :return: This Data so that you can chain calls to update values. :rtype: Data """ self._signature.set(Sha256WithRsaSignature() if signature == None else signature.clone()) self._changeCount += 1 return self def setContent(self, content): """ Set the content to the given value. :param content: The array with the content bytes. If content is not a Blob, then create a new Blob to copy the bytes (otherwise take another pointer to the same Blob). :type content: A Blob or an array type with int elements """ self._content = content if isinstance(content, Blob) else Blob(content) self._changeCount += 1 def setLpPacket(self, lpPacket): """ An internal library method to set the LpPacket for an incoming packet. The application should not call this. :param LpPacket lpPacket: The LpPacket. This does not make a copy. :return: This Data so that you can chain calls to update values. :rtype: Data :note: This is an experimental feature. This API may change in the future. """ self._lpPacket = lpPacket # Don't update _changeCount since this doesn't affect the wire encoding. return self def getChangeCount(self): """ Get the change count, which is incremented each time this object (or a child object) is changed. :return: The change count. :rtype: int """ # Make sure each of the checkChanged is called. changed = self._name.checkChanged() changed = self._metaInfo.checkChanged() or changed changed = self._signature.checkChanged() or changed if changed: # A child object has changed, so update the change count. self._changeCount += 1 return self._changeCount def _setDefaultWireEncoding(self, defaultWireEncoding, defaultWireEncodingFormat): self._defaultWireEncoding = defaultWireEncoding self._defaultWireEncodingFormat = defaultWireEncodingFormat # Set _getDefaultWireEncodingChangeCount so that the next call to # getDefaultWireEncoding() won't clear _defaultWireEncoding. self._getDefaultWireEncodingChangeCount = self.getChangeCount() # Create managed properties for read/write properties of the class for more pythonic syntax. name = property(getName, setName) metaInfo = property(getMetaInfo, setMetaInfo) signature = property(getSignature, setSignature) content = property(getContent, setContent)
class ChronoSync2013(object): """ Create a new ChronoSync2013 to communicate using the given face. Initialize the digest log with a digest of "00" and and empty content. Register the applicationBroadcastPrefix to receive interests for sync state messages and express an interest for the initial root digest "00". Note: Your application must call processEvents. Since processEvents modifies the internal ChronoSync data structures, your application should make sure that it calls processEvents in the same thread as this constructor (which also modifies the data structures). :param onReceivedSyncState: When ChronoSync receives a sync state message, this calls onReceivedSyncState(syncStates, isRecovery) where syncStates is the list of SyncState messages and isRecovery is true if this is the initial list of SyncState messages or from a recovery interest. (For example, if isRecovery is true, a chat application would not want to re-display all the associated chat messages.) The callback should send interests to fetch the application data for the sequence numbers in the sync state. NOTE: The library will log any exceptions raised by this callback, but for better error handling the callback should catch and properly handle any exceptions. :type onReceivedSyncState: function object :param onInitialized: This calls onInitialized() when the first sync data is received (or the interest times out because there are no other publishers yet). NOTE: The library will log any exceptions raised by this callback, but for better error handling the callback should catch and properly handle any exceptions. :type onInitialized: function object :param Name applicationDataPrefix: The prefix used by this application instance for application data. For example, "/my/local/prefix/ndnchat4/0K4wChff2v". This is used when sending a sync message for a new sequence number. In the sync message, this uses applicationDataPrefix.toUri(). :param Name applicationBroadcastPrefix: The broadcast name prefix including the application name. For example, "/ndn/broadcast/ChronoChat-0.3/ndnchat1". This makes a copy of the name. :param int sessionNo: The session number used with the applicationDataPrefix in sync state messages. :param Face face: The Face for calling registerPrefix and expressInterest. The Face object must remain valid for the life of this ChronoSync2013 object. :param KeyChain keyChain: To sign a data packet containing a sync state message, this calls keyChain.sign(data, certificateName). :param Name certificateName: The certificate name of the key to use for signing a data packet containing a sync state message. :param float syncLifetime: The interest lifetime in milliseconds for sending sync interests. :param onRegisterFailed: If failed to register the prefix to receive interests for the applicationBroadcastPrefix, this calls onRegisterFailed(applicationBroadcastPrefix). NOTE: The library will log any exceptions raised by this callback, but for better error handling the callback should catch and properly handle any exceptions. :type onRegisterFailed: function object """ def __init__(self, onReceivedSyncState, onInitialized, applicationDataPrefix, applicationBroadcastPrefix, sessionNo, face, keyChain, certificateName, syncLifetime, onRegisterFailed): self._onReceivedSyncState = onReceivedSyncState self._onInitialized = onInitialized self._applicationDataPrefixUri = applicationDataPrefix.toUri() self._applicationBroadcastPrefix = Name(applicationBroadcastPrefix) self._sessionNo = sessionNo self._face = face self._keyChain = keyChain self._certificateName = Name(certificateName) self._syncLifetime = syncLifetime self._contentCache = MemoryContentCache(face) self._digestLog = [] # of _DigestLogEntry self._digestTree = DigestTree() self._sequenceNo = -1 self._enabled = True emptyContent = SyncStateMsg() # Use getattr to avoid pylint errors. self._digestLog.append(self._DigestLogEntry("00", getattr(emptyContent, "ss"))) # Register the prefix with the contentCache_ and use our own onInterest # as the onDataNotFound fallback. self._contentCache.registerPrefix( self._applicationBroadcastPrefix, onRegisterFailed, self._onInterest) interest = Interest(self._applicationBroadcastPrefix) interest.getName().append("00") interest.setInterestLifetimeMilliseconds(1000) interest.setMustBeFresh(True) face.expressInterest(interest, self._onData, self._initialTimeOut) logging.getLogger(__name__).info("initial sync expressed") logging.getLogger(__name__).info("%s", interest.getName().toUri()) class SyncState(object): """ A SyncState holds the values of a sync state message which is passed to the onReceivedSyncState callback which was given to the ChronoSyn2013 constructor. Note: this has the same info as the Protobuf class sync_state_pb2.SyncState, but we make a separate class so that we don't need the Protobuf definition in the ChronoSync API. """ def __init__(self, dataPrefixUri, sessionNo, sequenceNo, applicationInfo): self._dataPrefixUri = dataPrefixUri self._sessionNo = sessionNo self._sequenceNo = sequenceNo self._applicationInfo = applicationInfo def getDataPrefix(self): """ Get the application data prefix for this sync state message. :return: The application data prefix as a Name URI string. :rtype: str """ return self._dataPrefixUri def getSessionNo(self): """ Get the session number associated with the application data prefix for this sync state message. :return: The session number. :rtype: int """ return self._sessionNo def getSequenceNo(self): """ Get the sequence number for this sync state message. :return: The sequence number. :rtype: int """ return self._sequenceNo def getApplicationInfo(self): """ Get the application info which was included when the sender published the next sequence number. :return: The applicationInfo Blob. If the sender did not provide any, return an isNull Blob. :rtype: Blob """ return self._applicationInfo class PrefixAndSessionNo(object): """ A PrefixAndSessionNo holds a user's data prefix and session number (used to return a list from getProducerPrefixes). """ def __init__(self, dataPrefixUri, sessionNo): self._dataPrefixUri = dataPrefixUri self._sessionNo = sessionNo def getDataPrefix(self): """ Get the application data prefix. :return: The application data prefix as a Name URI string. :rtype: str """ return self._dataPrefixUri def getSessionNo(self): """ Get the session number associated with the application data prefix. :return: The session number. :rtype: int """ return self._sessionNo def getProducerPrefixes(self): """ Get a copy of the current list of producer data prefixes, and the associated session number. You can use these in getProducerSequenceNo(). This includes the prefix for this user. :return: A copy of the list of each producer prefix and session number. :rtype: array of ChronoSync2013.PrefixAndSessionNo """ prefixes = [] for i in range(self._digestTree.size()): node = self._digestTree.get(i) prefixes.append(ChronoSync2013.PrefixAndSessionNo (node.getDataPrefix(), node.getSessionNo())) return prefixes def getProducerSequenceNo(self, dataPrefix, sessionNo): """ Get the current sequence number in the digest tree for the given producer dataPrefix and sessionNo. :param std dataPrefix: The producer data prefix as a Name URI string. :param int sessionNo: The producer session number. :return: The current producer sequence number, or -1 if the producer namePrefix and sessionNo are not in the digest tree. :rtype: int """ index = self._digestTree.find(dataPrefix, sessionNo) if index < 0: return -1 else: return self._digestTree.get(index).getSequenceNo() def publishNextSequenceNo(self, applicationInfo = None): """ Increment the sequence number, create a sync message with the new sequence number and publish a data packet where the name is the applicationBroadcastPrefix + the root digest of the current digest tree. Then add the sync message to the digest tree and digest log which creates a new root digest. Finally, express an interest for the next sync update with the name applicationBroadcastPrefix + the new root digest. After this, your application should publish the content for the new sequence number. You can get the new sequence number with getSequenceNo(). Note: Your application must call processEvents. Since processEvents modifies the internal ChronoSync data structures, your application should make sure that it calls processEvents in the same thread as publishNextSequenceNo() (which also modifies the data structures). :param Blob applicationInfo: (optional) This appends applicationInfo to the content of the sync messages. This same info is provided to the receiving application in the SyncState state object provided to the onReceivedSyncState callback. """ applicationInfo = (applicationInfo if isinstance(applicationInfo, Blob) else Blob(applicationInfo)) self._sequenceNo += 1 syncMessage = SyncStateMsg() content = getattr(syncMessage, "ss").add() content.name = self._applicationDataPrefixUri content.type = SyncState_UPDATE content.seqno.seq = self._sequenceNo content.seqno.session = self._sessionNo if not applicationInfo.isNull() and applicationInfo.size() > 0: content.application_info = applicationInfo.toBytes() self._broadcastSyncState(self._digestTree.getRoot(), syncMessage) if not self._update(getattr(syncMessage, "ss")): # Since we incremented the sequence number, we expect there to be a # new digest log entry. raise RuntimeError( "ChronoSync: update did not create a new digest log entry") # TODO: Should we have an option to not express an interest if this is the # final publish of the session? interest = Interest(self._applicationBroadcastPrefix) interest.getName().append(self._digestTree.getRoot()) interest.setInterestLifetimeMilliseconds(self._syncLifetime) self._face.expressInterest(interest, self._onData, self._syncTimeout) def getSequenceNo(self): """ Get the sequence number of the latest data published by this application instance. :return: The sequence number. :rtype: int """ return self._sequenceNo def shutdown(self): """ Unregister callbacks so that this does not respond to interests anymore. If you will discard this ChronoSync2013 object while your application is still running, you should call shutdown() first. After calling this, you should not call publishNextSequenceNo() again since the behavior will be undefined. Note: Because this modifies internal ChronoSync data structures, your application should make sure that it calls processEvents in the same thread as shutdown() (which also modifies the data structures). """ self._enabled = False self._contentCache.unregisterAll() class _DigestLogEntry(object): def __init__(self, digest, data): self._digest = digest # Copy. self._data = data[:] def getDigest(self): return self._digest def getData(self): """ Get the data. :return: The data as a list. :rtype: array of sync_state_pb2.SyncState. """ return self._data def _broadcastSyncState(self, digest, syncMessage): """ Make a data packet with the syncMessage and with name applicationBroadcastPrefix_ + digest. Sign and send. :param str digest: The root digest as a hex string for the data packet name. :param sync_state_pb2.SyncState syncMessage: """ data = Data(self._applicationBroadcastPrefix) data.getName().append(digest) # TODO: Check if this works in Python 3. data.setContent(Blob(syncMessage.SerializeToString())) self._keyChain.sign(data, self._certificateName) self._contentCache.add(data) def _update(self, content): """ Update the digest tree with the messages in content. If the digest tree root is not in the digest log, also add a log entry with the content. :param content: The list of SyncState. :type content: array of sync_state_pb2.SyncState :return: True if added a digest log entry (because the updated digest tree root was not in the log), False if didn't add a log entry. :rtype: bool """ for i in range(len(content)): syncState = content[i] if syncState.type == SyncState_UPDATE: if self._digestTree.update( syncState.name, syncState.seqno.session, syncState.seqno.seq): # The digest tree was updated. if self._applicationDataPrefixUri == syncState.name: self._sequenceNo = syncState.seqno.seq if self._logFind(self._digestTree.getRoot()) == -1: self._digestLog.append( self._DigestLogEntry(self._digestTree.getRoot(), content)) return True else: return False def _logFind(self, digest): """ Search the digest log by digest. """ for i in range(len(self._digestLog)): if digest == self._digestLog[i].getDigest(): return i return -1 def _onInterest(self, prefix, interest, face, interestFilterId, filter): """ Process the sync interest from the applicationBroadcastPrefix. If we can't satisfy the interest, add it to the pending interest table in the _contentCache so that a future call to contentCacheAdd may satisfy it. """ if not self._enabled: # Ignore callbacks after the application calls shutdown(). return # Search if the digest already exists in the digest log. logging.getLogger(__name__).info("Sync Interest received in callback.") logging.getLogger(__name__).info("%s", interest.getName().toUri()) syncDigest = interest.getName().get( self._applicationBroadcastPrefix.size()).toEscapedString() if interest.getName().size() == self._applicationBroadcastPrefix.size() + 2: # Assume this is a recovery interest. syncDigest = interest.getName().get( self._applicationBroadcastPrefix.size() + 1).toEscapedString() logging.getLogger(__name__).info("syncDigest: %s", syncDigest) if (interest.getName().size() == self._applicationBroadcastPrefix.size() + 2 or syncDigest == "00"): # Recovery interest or newcomer interest. self._processRecoveryInterest(interest, syncDigest, face) else: self._contentCache.storePendingInterest(interest, face) if syncDigest != self._digestTree.getRoot(): index = self._logFind(syncDigest) if index == -1: # To see whether there is any data packet coming back, wait # 2 seconds using the Interest timeout mechanism. # TODO: Are we sure using a "/local/timeout" interest is the # best future call approach? timeout = Interest(Name("/local/timeout")) timeout.setInterestLifetimeMilliseconds(2000) self._face.expressInterest( timeout, self._dummyOnData, self._makeJudgeRecovery(syncDigest, face)) logging.getLogger(__name__).info("set timer recover") else: # common interest processing self._processSyncInterest(index, syncDigest, face) def _onData(self, interest, data): """ Process Sync Data. """ if not self._enabled: # Ignore callbacks after the application calls shutdown(). return logging.getLogger(__name__).info( "Sync ContentObject received in callback") logging.getLogger(__name__).info( "name: %s", data.getName().toUri()) # TODO: Check if this works in Python 3. tempContent = SyncStateMsg() #pylint: disable=E1103 tempContent.ParseFromString(data.getContent().toBytes()) #pylint: enable=E1103 content = getattr(tempContent, "ss") if self._digestTree.getRoot() == "00": isRecovery = True #processing initial sync data self._initialOndata(content) else: self._update(content) if (interest.getName().size() == self._applicationBroadcastPrefix.size() + 2): # Assume this is a recovery interest. isRecovery = True else: isRecovery = False # Send the interests to fetch the application data. syncStates = [] for i in range(len(content)): syncState = content[i] # Only report UPDATE sync states. if syncState.type == SyncState_UPDATE: if len(syncState.application_info) > 0: applicationInfo = Blob(syncState.application_info, True) else: applicationInfo = Blob() syncStates.append(self.SyncState( syncState.name, syncState.seqno.session, syncState.seqno.seq, applicationInfo)) try: self._onReceivedSyncState(syncStates, isRecovery) except: logging.exception("Error in onReceivedSyncState") name = Name(self._applicationBroadcastPrefix) name.append(self._digestTree.getRoot()) syncInterest = Interest(name) syncInterest.setInterestLifetimeMilliseconds(self._syncLifetime) self._face.expressInterest(syncInterest, self._onData, self._syncTimeout) logging.getLogger(__name__).info("Syncinterest expressed:") logging.getLogger(__name__).info("%s", name.toUri()) def _initialTimeOut(self, interest): """ Initial sync interest timeout, which means there are no other publishers yet. """ if not self._enabled: # Ignore callbacks after the application calls shutdown(). return logging.getLogger(__name__).info("initial sync timeout") logging.getLogger(__name__).info("no other people") self._sequenceNo += 1 if self._sequenceNo != 0: # Since there were no other users, we expect sequence no 0. raise RuntimeError( "ChronoSync: sequenceNo_ is not the expected value of 0 for first use.") tempContent = SyncStateMsg() content = getattr(tempContent, "ss").add() content.name = self._applicationDataPrefixUri content.type = SyncState_UPDATE content.seqno.seq = self._sequenceNo content.seqno.session = self._sessionNo self._update(getattr(tempContent, "ss")) try: self._onInitialized() except: logging.exception("Error in onInitialized") name = Name(self._applicationBroadcastPrefix) name.append(self._digestTree.getRoot()) retryInterest = Interest(name) retryInterest.setInterestLifetimeMilliseconds(self._syncLifetime) self._face.expressInterest(retryInterest, self._onData, self._syncTimeout) logging.getLogger(__name__).info("Syncinterest expressed:") logging.getLogger(__name__).info("%s", name.toUri()) def _processRecoveryInterest(self, interest, syncDigest, face): logging.getLogger(__name__).info("processRecoveryInterest") if self._logFind(syncDigest) != -1: tempContent = SyncStateMsg() for i in range(self._digestTree.size()): content = getattr(tempContent, "ss").add() content.name = self._digestTree.get(i).getDataPrefix() content.type = SyncState_UPDATE content.seqno.seq = self._digestTree.get(i).getSequenceNo() content.seqno.session = self._digestTree.get(i).getSessionNo() if len(getattr(tempContent, "ss")) != 0: # TODO: Check if this works in Python 3. #pylint: disable=E1103 array = tempContent.SerializeToString() #pylint: enable=E1103 data = Data(interest.getName()) data.setContent(Blob(array)) if interest.getName().get(-1).toEscapedString() == "00": # Limit the lifetime of replies to interest for "00" since # they can be different. data.getMetaInfo().setFreshnessPeriod(1000) self._keyChain.sign(data, self._certificateName) try: face.putData(data) except Exception as ex: logging.getLogger(__name__).error( "Error in face.putData: %s", str(ex)) return logging.getLogger(__name__).info("send recovery data back") logging.getLogger(__name__).info("%s", interest.getName().toUri()) def _processSyncInterest(self, index, syncDigest, face): """ Common interest processing, using digest log to find the difference after syncDigest. :return: True if sent a data packet to satisfy the interest, otherwise False. :rtype: bool """ nameList = [] # of str sequenceNoList = [] # of int sessionNoList = [] # of int for j in range(index + 1, len(self._digestLog)): temp = self._digestLog[j].getData() # array of sync_state_pb2.SyncState. for i in range(len(temp)): syncState = temp[i] if syncState.type != SyncState_UPDATE: continue if self._digestTree.find( syncState.name, syncState.seqno.session) != -1: n = -1 for k in range(len(nameList)): if nameList[k] == syncState.name: n = k break if n == -1: nameList.append(syncState.name) sequenceNoList.append(syncState.seqno.seq) sessionNoList.append(syncState.seqno.session) else: sequenceNoList[n] = syncState.seqno.seq sessionNoList[n] = syncState.seqno.session tempContent = SyncStateMsg() for i in range(len(nameList)): content = getattr(tempContent, "ss").add() content.name = nameList[i] content.type = SyncState_UPDATE content.seqno.seq = sequenceNoList[i] content.seqno.session = sessionNoList[i] sent = False if len(getattr(tempContent, "ss")) != 0: name = Name(self._applicationBroadcastPrefix) name.append(syncDigest) # TODO: Check if this works in Python 3. #pylint: disable=E1103 array = tempContent.SerializeToString() #pylint: enable=E1103 data = Data(name) data.setContent(Blob(array)) self._keyChain.sign(data, self._certificateName) try: face.putData(data) except Exception as ex: logging.getLogger(__name__).error( "Error in face.putData: %s", str(ex)) return sent = True logging.getLogger(__name__).info("Sync Data send") logging.getLogger(__name__).info("%s", name.toUri()) return sent def _sendRecovery(self, syncDigest): """ Send Recovery Interest. """ logging.getLogger(__name__).info("unknown digest: ") name = Name(self._applicationBroadcastPrefix) name.append("recovery").append(syncDigest) interest = Interest(name) interest.setInterestLifetimeMilliseconds(self._syncLifetime) self._face.expressInterest(interest, self._onData, self._syncTimeout) logging.getLogger(__name__).info("Recovery Syncinterest expressed:") logging.getLogger(__name__).info("%s", name.toUri()) def _makeJudgeRecovery(self, syncDigest, face): """ Return a function for onTimeout which calls _judgeRecovery. """ def f(interest): self._judgeRecovery(interest, syncDigest, face) return f def _judgeRecovery(self, interest, syncDigest, face): """ This is called by _onInterest after a timeout to check if a recovery is needed. """ if not self._enabled: # Ignore callbacks after the application calls shutdown(). return index2 = self._logFind(syncDigest) if index2 != -1: if syncDigest != self._digestTree.getRoot(): self._processSyncInterest(index2, syncDigest, face) else: self._sendRecovery(syncDigest) def _syncTimeout(self, interest): """ Sync interest time out. If the interest is the static one send again. """ if not self._enabled: # Ignore callbacks after the application calls shutdown(). return logging.getLogger(__name__).info("Sync Interest time out.") logging.getLogger(__name__).info( "Sync Interest name: %s", interest.getName().toUri()) component = interest.getName().get( self._applicationBroadcastPrefix.size()).toEscapedString() if component == self._digestTree.getRoot(): name = Name(interest.getName()) retryInterest = Interest(interest.getName()) retryInterest.setInterestLifetimeMilliseconds(self._syncLifetime) self._face.expressInterest( retryInterest, self._onData, self._syncTimeout) logging.getLogger(__name__).info("Syncinterest expressed:") logging.getLogger(__name__).info("%s", name.toUri()) def _initialOndata(self, content): """ Process initial data which usually includes all other publisher's info, and send back the new comer's own info. """ # The user is a new comer and receive data of all other people in the group. self._update(content) digest = self._digestTree.getRoot() for i in range(len(content)): syncState = content[i] if (syncState.name == self._applicationDataPrefixUri and syncState.seqno.session == self._sessionNo): # If the user was an old comer, after add the static log he # needs to increase his sequence number by 1. tempContent = SyncStateMsg() # Use getattr to avoid pylint errors. content2 = getattr(tempContent, "ss").add() content2.name = self._applicationDataPrefixUri content2.type = SyncState_UPDATE content2.seqno.seq = syncState.seqno.seq + 1 content2.seqno.session = self._sessionNo if self._update(getattr(tempContent, "ss")): try: self._onInitialized() except: logging.exception("Error in onInitialized") tempContent2 = SyncStateMsg() if self._sequenceNo >= 0: # Send the data packet with the new sequence number back. content2 = getattr(tempContent2, "ss").add() content2.name = self._applicationDataPrefixUri content2.type = SyncState_UPDATE content2.seqno.seq = self._sequenceNo content2.seqno.session = self._sessionNo else: content2 = getattr(tempContent2, "ss").add() content2.name = self._applicationDataPrefixUri content2.type = SyncState_UPDATE content2.seqno.seq = 0 content2.seqno.session = self._sessionNo self._broadcastSyncState(digest, tempContent2) if (self._digestTree.find(self._applicationDataPrefixUri, self._sessionNo) == -1): # The user hasn't put himself in the digest tree. logging.getLogger(__name__).info("initial state") self._sequenceNo += 1 tempContent = SyncStateMsg() content2 = getattr(tempContent, "ss").add() content2.name = self._applicationDataPrefixUri content2.type = SyncState_UPDATE content2.seqno.seq = self._sequenceNo content2.seqno.session = self._sessionNo if self._update(getattr(tempContent, "ss")): try: self._onInitialized() except: logging.exception("Error in onInitialized") @staticmethod def _dummyOnData(interest, data): """ This is a do-nothing onData for using expressInterest for timeouts. This should never be called. """ pass
class ChronoSync2013(object): """ Create a new ChronoSync2013 to communicate using the given face. Initialize the digest log with a digest of "00" and and empty content. Register the applicationBroadcastPrefix to receive interests for sync state messages and express an interest for the initial root digest "00". Note: Your application must call processEvents. Since processEvents modifies the internal ChronoSync data structures, your application should make sure that it calls processEvents in the same thread as this constructor (which also modifies the data structures). :param onReceivedSyncState: When ChronoSync receives a sync state message, this calls onReceivedSyncState(syncStates, isRecovery) where syncStates is the list of SyncState messages and isRecovery is true if this is the initial list of SyncState messages or from a recovery interest. (For example, if isRecovery is true, a chat application would not want to re-display all the associated chat messages.) The callback should send interests to fetch the application data for the sequence numbers in the sync state. NOTE: The library will log any exceptions raised by this callback, but for better error handling the callback should catch and properly handle any exceptions. :type onReceivedSyncState: function object :param onInitialized: This calls onInitialized() when the first sync data is received (or the interest times out because there are no other publishers yet). NOTE: The library will log any exceptions raised by this callback, but for better error handling the callback should catch and properly handle any exceptions. :type onInitialized: function object :param Name applicationDataPrefix: The prefix used by this application instance for application data. For example, "/my/local/prefix/ndnchat4/0K4wChff2v". This is used when sending a sync message for a new sequence number. In the sync message, this uses applicationDataPrefix.toUri(). :param Name applicationBroadcastPrefix: The broadcast name prefix including the application name. For example, "/ndn/broadcast/ChronoChat-0.3/ndnchat1". This makes a copy of the name. :param int sessionNo: The session number used with the applicationDataPrefix in sync state messages. :param Face face: The Face for calling registerPrefix and expressInterest. The Face object must remain valid for the life of this ChronoSync2013 object. :param KeyChain keyChain: To sign a data packet containing a sync state message, this calls keyChain.sign(data, certificateName). :param Name certificateName: The certificate name of the key to use for signing a data packet containing a sync state message. :param float syncLifetime: The interest lifetime in milliseconds for sending sync interests. :param onRegisterFailed: If failed to register the prefix to receive interests for the applicationBroadcastPrefix, this calls onRegisterFailed(applicationBroadcastPrefix). NOTE: The library will log any exceptions raised by this callback, but for better error handling the callback should catch and properly handle any exceptions. :type onRegisterFailed: function object """ def __init__(self, onReceivedSyncState, onInitialized, applicationDataPrefix, applicationBroadcastPrefix, sessionNo, face, keyChain, certificateName, syncLifetime, onRegisterFailed): self._onReceivedSyncState = onReceivedSyncState self._onInitialized = onInitialized self._applicationDataPrefixUri = applicationDataPrefix.toUri() self._applicationBroadcastPrefix = Name(applicationBroadcastPrefix) self._sessionNo = sessionNo self._face = face self._keyChain = keyChain self._certificateName = Name(certificateName) self._syncLifetime = syncLifetime self._contentCache = MemoryContentCache(face) self._digestLog = [] # of _DigestLogEntry self._digestTree = DigestTree() self._sequenceNo = -1 self._enabled = True emptyContent = SyncStateMsg() # Use getattr to avoid pylint errors. self._digestLog.append( self._DigestLogEntry("00", getattr(emptyContent, "ss"))) # Register the prefix with the contentCache_ and use our own onInterest # as the onDataNotFound fallback. self._contentCache.registerPrefix(self._applicationBroadcastPrefix, onRegisterFailed, self._onInterest) interest = Interest(self._applicationBroadcastPrefix) interest.getName().append("00") interest.setInterestLifetimeMilliseconds(1000) interest.setMustBeFresh(True) face.expressInterest(interest, self._onData, self._initialTimeOut) logging.getLogger(__name__).info("initial sync expressed") logging.getLogger(__name__).info("%s", interest.getName().toUri()) class SyncState(object): """ A SyncState holds the values of a sync state message which is passed to the onReceivedSyncState callback which was given to the ChronoSyn2013 constructor. Note: this has the same info as the Protobuf class sync_state_pb2.SyncState, but we make a separate class so that we don't need the Protobuf definition in the ChronoSync API. """ def __init__(self, dataPrefixUri, sessionNo, sequenceNo, applicationInfo): self._dataPrefixUri = dataPrefixUri self._sessionNo = sessionNo self._sequenceNo = sequenceNo self._applicationInfo = applicationInfo def getDataPrefix(self): """ Get the application data prefix for this sync state message. :return: The application data prefix as a Name URI string. :rtype: str """ return self._dataPrefixUri def getSessionNo(self): """ Get the session number associated with the application data prefix for this sync state message. :return: The session number. :rtype: int """ return self._sessionNo def getSequenceNo(self): """ Get the sequence number for this sync state message. :return: The sequence number. :rtype: int """ return self._sequenceNo def getApplicationInfo(self): """ Get the application info which was included when the sender published the next sequence number. :return: The applicationInfo Blob. If the sender did not provide any, return an isNull Blob. :rtype: Blob """ return self._applicationInfo class PrefixAndSessionNo(object): """ A PrefixAndSessionNo holds a user's data prefix and session number (used to return a list from getProducerPrefixes). """ def __init__(self, dataPrefixUri, sessionNo): self._dataPrefixUri = dataPrefixUri self._sessionNo = sessionNo def getDataPrefix(self): """ Get the application data prefix. :return: The application data prefix as a Name URI string. :rtype: str """ return self._dataPrefixUri def getSessionNo(self): """ Get the session number associated with the application data prefix. :return: The session number. :rtype: int """ return self._sessionNo def getProducerPrefixes(self): """ Get a copy of the current list of producer data prefixes, and the associated session number. You can use these in getProducerSequenceNo(). This includes the prefix for this user. :return: A copy of the list of each producer prefix and session number. :rtype: array of ChronoSync2013.PrefixAndSessionNo """ prefixes = [] for i in range(self._digestTree.size()): node = self._digestTree.get(i) prefixes.append( ChronoSync2013.PrefixAndSessionNo(node.getDataPrefix(), node.getSessionNo())) return prefixes def getProducerSequenceNo(self, dataPrefix, sessionNo): """ Get the current sequence number in the digest tree for the given producer dataPrefix and sessionNo. :param std dataPrefix: The producer data prefix as a Name URI string. :param int sessionNo: The producer session number. :return: The current producer sequence number, or -1 if the producer namePrefix and sessionNo are not in the digest tree. :rtype: int """ index = self._digestTree.find(dataPrefix, sessionNo) if index < 0: return -1 else: return self._digestTree.get(index).getSequenceNo() def publishNextSequenceNo(self, applicationInfo=None): """ Increment the sequence number, create a sync message with the new sequence number and publish a data packet where the name is the applicationBroadcastPrefix + the root digest of the current digest tree. Then add the sync message to the digest tree and digest log which creates a new root digest. Finally, express an interest for the next sync update with the name applicationBroadcastPrefix + the new root digest. After this, your application should publish the content for the new sequence number. You can get the new sequence number with getSequenceNo(). Note: Your application must call processEvents. Since processEvents modifies the internal ChronoSync data structures, your application should make sure that it calls processEvents in the same thread as publishNextSequenceNo() (which also modifies the data structures). :param Blob applicationInfo: (optional) This appends applicationInfo to the content of the sync messages. This same info is provided to the receiving application in the SyncState state object provided to the onReceivedSyncState callback. """ applicationInfo = (applicationInfo if isinstance( applicationInfo, Blob) else Blob(applicationInfo)) self._sequenceNo += 1 syncMessage = SyncStateMsg() content = getattr(syncMessage, "ss").add() content.name = self._applicationDataPrefixUri content.type = SyncState_UPDATE content.seqno.seq = self._sequenceNo content.seqno.session = self._sessionNo if not applicationInfo.isNull() and applicationInfo.size() > 0: content.application_info = applicationInfo.toBytes() self._broadcastSyncState(self._digestTree.getRoot(), syncMessage) if not self._update(getattr(syncMessage, "ss")): # Since we incremented the sequence number, we expect there to be a # new digest log entry. raise RuntimeError( "ChronoSync: update did not create a new digest log entry") # TODO: Should we have an option to not express an interest if this is the # final publish of the session? interest = Interest(self._applicationBroadcastPrefix) interest.getName().append(self._digestTree.getRoot()) interest.setInterestLifetimeMilliseconds(self._syncLifetime) self._face.expressInterest(interest, self._onData, self._syncTimeout) def getSequenceNo(self): """ Get the sequence number of the latest data published by this application instance. :return: The sequence number. :rtype: int """ return self._sequenceNo def shutdown(self): """ Unregister callbacks so that this does not respond to interests anymore. If you will discard this ChronoSync2013 object while your application is still running, you should call shutdown() first. After calling this, you should not call publishNextSequenceNo() again since the behavior will be undefined. Note: Because this modifies internal ChronoSync data structures, your application should make sure that it calls processEvents in the same thread as shutdown() (which also modifies the data structures). """ self._enabled = False self._contentCache.unregisterAll() class _DigestLogEntry(object): def __init__(self, digest, data): self._digest = digest # Copy. self._data = data[:] def getDigest(self): return self._digest def getData(self): """ Get the data. :return: The data as a list. :rtype: array of sync_state_pb2.SyncState. """ return self._data def _broadcastSyncState(self, digest, syncMessage): """ Make a data packet with the syncMessage and with name applicationBroadcastPrefix_ + digest. Sign and send. :param str digest: The root digest as a hex string for the data packet name. :param sync_state_pb2.SyncState syncMessage: """ data = Data(self._applicationBroadcastPrefix) data.getName().append(digest) # TODO: Check if this works in Python 3. data.setContent(Blob(syncMessage.SerializeToString())) self._keyChain.sign(data, self._certificateName) self._contentCache.add(data) def _update(self, content): """ Update the digest tree with the messages in content. If the digest tree root is not in the digest log, also add a log entry with the content. :param content: The list of SyncState. :type content: array of sync_state_pb2.SyncState :return: True if added a digest log entry (because the updated digest tree root was not in the log), False if didn't add a log entry. :rtype: bool """ for i in range(len(content)): syncState = content[i] if syncState.type == SyncState_UPDATE: if self._digestTree.update(syncState.name, syncState.seqno.session, syncState.seqno.seq): # The digest tree was updated. if self._applicationDataPrefixUri == syncState.name: self._sequenceNo = syncState.seqno.seq if self._logFind(self._digestTree.getRoot()) == -1: self._digestLog.append( self._DigestLogEntry(self._digestTree.getRoot(), content)) return True else: return False def _logFind(self, digest): """ Search the digest log by digest. """ for i in range(len(self._digestLog)): if digest == self._digestLog[i].getDigest(): return i return -1 def _onInterest(self, prefix, interest, face, interestFilterId, filter): """ Process the sync interest from the applicationBroadcastPrefix. If we can't satisfy the interest, add it to the pending interest table in the _contentCache so that a future call to contentCacheAdd may satisfy it. """ if not self._enabled: # Ignore callbacks after the application calls shutdown(). return # Search if the digest already exists in the digest log. logging.getLogger(__name__).info("Sync Interest received in callback.") logging.getLogger(__name__).info("%s", interest.getName().toUri()) syncDigest = interest.getName().get( self._applicationBroadcastPrefix.size()).toEscapedString() if interest.getName().size( ) == self._applicationBroadcastPrefix.size() + 2: # Assume this is a recovery interest. syncDigest = interest.getName().get( self._applicationBroadcastPrefix.size() + 1).toEscapedString() logging.getLogger(__name__).info("syncDigest: %s", syncDigest) if (interest.getName().size() == self._applicationBroadcastPrefix.size() + 2 or syncDigest == "00"): # Recovery interest or newcomer interest. self._processRecoveryInterest(interest, syncDigest, face) else: self._contentCache.storePendingInterest(interest, face) if syncDigest != self._digestTree.getRoot(): index = self._logFind(syncDigest) if index == -1: # To see whether there is any data packet coming back, wait # 2 seconds using the Interest timeout mechanism. # TODO: Are we sure using a "/local/timeout" interest is the # best future call approach? timeout = Interest(Name("/local/timeout")) timeout.setInterestLifetimeMilliseconds(2000) self._face.expressInterest( timeout, self._dummyOnData, self._makeJudgeRecovery(syncDigest, face)) logging.getLogger(__name__).info("set timer recover") else: # common interest processing self._processSyncInterest(index, syncDigest, face) def _onData(self, interest, data): """ Process Sync Data. """ if not self._enabled: # Ignore callbacks after the application calls shutdown(). return logging.getLogger(__name__).info( "Sync ContentObject received in callback") logging.getLogger(__name__).info("name: %s", data.getName().toUri()) # TODO: Check if this works in Python 3. tempContent = SyncStateMsg() #pylint: disable=E1103 tempContent.ParseFromString(data.getContent().toBytes()) #pylint: enable=E1103 content = getattr(tempContent, "ss") if self._digestTree.getRoot() == "00": isRecovery = True #processing initial sync data self._initialOndata(content) else: self._update(content) if (interest.getName().size() == self._applicationBroadcastPrefix.size() + 2): # Assume this is a recovery interest. isRecovery = True else: isRecovery = False # Send the interests to fetch the application data. syncStates = [] for i in range(len(content)): syncState = content[i] # Only report UPDATE sync states. if syncState.type == SyncState_UPDATE: if len(syncState.application_info) > 0: applicationInfo = Blob(syncState.application_info, True) else: applicationInfo = Blob() syncStates.append( self.SyncState(syncState.name, syncState.seqno.session, syncState.seqno.seq, applicationInfo)) try: self._onReceivedSyncState(syncStates, isRecovery) except: logging.exception("Error in onReceivedSyncState") name = Name(self._applicationBroadcastPrefix) name.append(self._digestTree.getRoot()) syncInterest = Interest(name) syncInterest.setInterestLifetimeMilliseconds(self._syncLifetime) self._face.expressInterest(syncInterest, self._onData, self._syncTimeout) logging.getLogger(__name__).info("Syncinterest expressed:") logging.getLogger(__name__).info("%s", name.toUri()) def _initialTimeOut(self, interest): """ Initial sync interest timeout, which means there are no other publishers yet. """ if not self._enabled: # Ignore callbacks after the application calls shutdown(). return logging.getLogger(__name__).info("initial sync timeout") logging.getLogger(__name__).info("no other people") self._sequenceNo += 1 if self._sequenceNo != 0: # Since there were no other users, we expect sequence no 0. raise RuntimeError( "ChronoSync: sequenceNo_ is not the expected value of 0 for first use." ) tempContent = SyncStateMsg() content = getattr(tempContent, "ss").add() content.name = self._applicationDataPrefixUri content.type = SyncState_UPDATE content.seqno.seq = self._sequenceNo content.seqno.session = self._sessionNo self._update(getattr(tempContent, "ss")) try: self._onInitialized() except: logging.exception("Error in onInitialized") name = Name(self._applicationBroadcastPrefix) name.append(self._digestTree.getRoot()) retryInterest = Interest(name) retryInterest.setInterestLifetimeMilliseconds(self._syncLifetime) self._face.expressInterest(retryInterest, self._onData, self._syncTimeout) logging.getLogger(__name__).info("Syncinterest expressed:") logging.getLogger(__name__).info("%s", name.toUri()) def _processRecoveryInterest(self, interest, syncDigest, face): logging.getLogger(__name__).info("processRecoveryInterest") if self._logFind(syncDigest) != -1: tempContent = SyncStateMsg() for i in range(self._digestTree.size()): content = getattr(tempContent, "ss").add() content.name = self._digestTree.get(i).getDataPrefix() content.type = SyncState_UPDATE content.seqno.seq = self._digestTree.get(i).getSequenceNo() content.seqno.session = self._digestTree.get(i).getSessionNo() if len(getattr(tempContent, "ss")) != 0: # TODO: Check if this works in Python 3. #pylint: disable=E1103 array = tempContent.SerializeToString() #pylint: enable=E1103 data = Data(interest.getName()) data.setContent(Blob(array)) if interest.getName().get(-1).toEscapedString() == "00": # Limit the lifetime of replies to interest for "00" since # they can be different. data.getMetaInfo().setFreshnessPeriod(1000) self._keyChain.sign(data, self._certificateName) try: face.putData(data) except Exception as ex: logging.getLogger(__name__).error( "Error in face.putData: %s", str(ex)) return logging.getLogger(__name__).info("send recovery data back") logging.getLogger(__name__).info("%s", interest.getName().toUri()) def _processSyncInterest(self, index, syncDigest, face): """ Common interest processing, using digest log to find the difference after syncDigest. :return: True if sent a data packet to satisfy the interest, otherwise False. :rtype: bool """ nameList = [] # of str sequenceNoList = [] # of int sessionNoList = [] # of int for j in range(index + 1, len(self._digestLog)): temp = self._digestLog[j].getData( ) # array of sync_state_pb2.SyncState. for i in range(len(temp)): syncState = temp[i] if syncState.type != SyncState_UPDATE: continue if self._digestTree.find(syncState.name, syncState.seqno.session) != -1: n = -1 for k in range(len(nameList)): if nameList[k] == syncState.name: n = k break if n == -1: nameList.append(syncState.name) sequenceNoList.append(syncState.seqno.seq) sessionNoList.append(syncState.seqno.session) else: sequenceNoList[n] = syncState.seqno.seq sessionNoList[n] = syncState.seqno.session tempContent = SyncStateMsg() for i in range(len(nameList)): content = getattr(tempContent, "ss").add() content.name = nameList[i] content.type = SyncState_UPDATE content.seqno.seq = sequenceNoList[i] content.seqno.session = sessionNoList[i] sent = False if len(getattr(tempContent, "ss")) != 0: name = Name(self._applicationBroadcastPrefix) name.append(syncDigest) # TODO: Check if this works in Python 3. #pylint: disable=E1103 array = tempContent.SerializeToString() #pylint: enable=E1103 data = Data(name) data.setContent(Blob(array)) self._keyChain.sign(data, self._certificateName) try: face.putData(data) except Exception as ex: logging.getLogger(__name__).error("Error in face.putData: %s", str(ex)) return sent = True logging.getLogger(__name__).info("Sync Data send") logging.getLogger(__name__).info("%s", name.toUri()) return sent def _sendRecovery(self, syncDigest): """ Send Recovery Interest. """ logging.getLogger(__name__).info("unknown digest: ") name = Name(self._applicationBroadcastPrefix) name.append("recovery").append(syncDigest) interest = Interest(name) interest.setInterestLifetimeMilliseconds(self._syncLifetime) self._face.expressInterest(interest, self._onData, self._syncTimeout) logging.getLogger(__name__).info("Recovery Syncinterest expressed:") logging.getLogger(__name__).info("%s", name.toUri()) def _makeJudgeRecovery(self, syncDigest, face): """ Return a function for onTimeout which calls _judgeRecovery. """ def f(interest): self._judgeRecovery(interest, syncDigest, face) return f def _judgeRecovery(self, interest, syncDigest, face): """ This is called by _onInterest after a timeout to check if a recovery is needed. """ if not self._enabled: # Ignore callbacks after the application calls shutdown(). return index2 = self._logFind(syncDigest) if index2 != -1: if syncDigest != self._digestTree.getRoot(): self._processSyncInterest(index2, syncDigest, face) else: self._sendRecovery(syncDigest) def _syncTimeout(self, interest): """ Sync interest time out. If the interest is the static one send again. """ if not self._enabled: # Ignore callbacks after the application calls shutdown(). return logging.getLogger(__name__).info("Sync Interest time out.") logging.getLogger(__name__).info("Sync Interest name: %s", interest.getName().toUri()) component = interest.getName().get(4).toEscapedString() if component == self._digestTree.getRoot(): name = Name(interest.getName()) retryInterest = Interest(interest.getName()) retryInterest.setInterestLifetimeMilliseconds(self._syncLifetime) self._face.expressInterest(retryInterest, self._onData, self._syncTimeout) logging.getLogger(__name__).info("Syncinterest expressed:") logging.getLogger(__name__).info("%s", name.toUri()) def _initialOndata(self, content): """ Process initial data which usually includes all other publisher's info, and send back the new comer's own info. """ # The user is a new comer and receive data of all other people in the group. self._update(content) digest = self._digestTree.getRoot() for i in range(len(content)): syncState = content[i] if (syncState.name == self._applicationDataPrefixUri and syncState.seqno.session == self._sessionNo): # If the user was an old comer, after add the static log he # needs to increase his sequence number by 1. tempContent = SyncStateMsg() # Use getattr to avoid pylint errors. content2 = getattr(tempContent, "ss").add() content2.name = self._applicationDataPrefixUri content2.type = SyncState_UPDATE content2.seqno.seq = syncState.seqno.seq + 1 content2.seqno.session = self._sessionNo if self._update(getattr(tempContent, "ss")): try: self._onInitialized() except: logging.exception("Error in onInitialized") tempContent2 = SyncStateMsg() if self._sequenceNo >= 0: # Send the data packet with the new sequence number back. content2 = getattr(tempContent2, "ss").add() content2.name = self._applicationDataPrefixUri content2.type = SyncState_UPDATE content2.seqno.seq = self._sequenceNo content2.seqno.session = self._sessionNo else: content2 = getattr(tempContent2, "ss").add() content2.name = self._applicationDataPrefixUri content2.type = SyncState_UPDATE content2.seqno.seq = 0 content2.seqno.session = self._sessionNo self._broadcastSyncState(digest, tempContent2) if (self._digestTree.find(self._applicationDataPrefixUri, self._sessionNo) == -1): # The user hasn't put himself in the digest tree. logging.getLogger(__name__).info("initial state") self._sequenceNo += 1 tempContent = SyncStateMsg() content2 = getattr(tempContent, "ss").add() content2.name = self._applicationDataPrefixUri content2.type = SyncState_UPDATE content2.seqno.seq = self._sequenceNo content2.seqno.session = self._sessionNo if self._update(getattr(tempContent, "ss")): try: self._onInitialized() except: logging.exception("Error in onInitialized") @staticmethod def _dummyOnData(interest, data): """ This is a do-nothing onData for using expressInterest for timeouts. This should never be called. """ pass
class Data(object): def __init__(self, value = None): if isinstance(value, Data): # Copy the values. self._name = ChangeCounter(Name(value.getName())) self._metaInfo = ChangeCounter(MetaInfo(value.getMetaInfo())) self._signature = ChangeCounter(value.getSignature().clone()) self._content = value._content self._defaultWireEncoding = value.getDefaultWireEncoding() self._defaultFullName = value._defaultFullName self._defaultWireEncodingFormat = value._defaultWireEncodingFormat else: self._name = ChangeCounter(Name(value) if type(value) is Name else Name()) self._metaInfo = ChangeCounter(MetaInfo()) self._signature = ChangeCounter(Sha256WithRsaSignature()) self._content = Blob() self._defaultWireEncoding = SignedBlob() self._defaultFullName = Name() self._defaultWireEncodingFormat = None self._getDefaultWireEncodingChangeCount = 0 self._changeCount = 0 self._lpPacket = None def wireEncode(self, wireFormat = None): """ Encode this Data for a particular wire format. If wireFormat is the default wire format, also set the defaultWireEncoding field to the encoded result. :param wireFormat: (optional) A WireFormat object used to encode this Data object. If omitted, use WireFormat.getDefaultWireFormat(). :type wireFormat: A subclass of WireFormat :return: The encoded buffer in a SignedBlob object. :rtype: SignedBlob """ if wireFormat == None: # Don't use a default argument since getDefaultWireFormat can change. wireFormat = WireFormat.getDefaultWireFormat() if (not self.getDefaultWireEncoding().isNull() and self.getDefaultWireEncodingFormat() == wireFormat): # We already have an encoding in the desired format. return self.getDefaultWireEncoding() (encoding, signedPortionBeginOffset, signedPortionEndOffset) = \ wireFormat.encodeData(self) wireEncoding = SignedBlob( encoding, signedPortionBeginOffset, signedPortionEndOffset) if wireFormat == WireFormat.getDefaultWireFormat(): # This is the default wire encoding. self._setDefaultWireEncoding( wireEncoding, WireFormat.getDefaultWireFormat()) return wireEncoding def wireDecode(self, input, wireFormat = None): """ Decode the input using a particular wire format and update this Data. If wireFormat is the default wire format, also set the defaultWireEncoding to another pointer to the input. :param input: The array with the bytes to decode. If input is not a Blob, then copy the bytes to save the defaultWireEncoding (otherwise take another pointer to the same Blob). :type input: A Blob or an array type with int elements :param wireFormat: (optional) A WireFormat object used to decode this Data object. If omitted, use WireFormat.getDefaultWireFormat(). :type wireFormat: A subclass of WireFormat """ if wireFormat == None: # Don't use a default argument since getDefaultWireFormat can change. wireFormat = WireFormat.getDefaultWireFormat() if isinstance(input, Blob): # Input is a blob, so get its buf() and set copy False. result = wireFormat.decodeData(self, input.buf(), False) else: result = wireFormat.decodeData(self, input, True) (signedPortionBeginOffset, signedPortionEndOffset) = result if wireFormat == WireFormat.getDefaultWireFormat(): # This is the default wire encoding. In the Blob constructor, set # copy true, but if input is already a Blob, it won't copy. self._setDefaultWireEncoding(SignedBlob( Blob(input, True), signedPortionBeginOffset, signedPortionEndOffset), WireFormat.getDefaultWireFormat()) else: self._setDefaultWireEncoding(SignedBlob(), None) def getName(self): """ Get the data packet's name. :return: The name. :rtype: Name """ return self._name.get() def getMetaInfo(self): """ Get the data packet's meta info. :return: The meta info. :rtype: MetaInfo """ return self._metaInfo.get() def getSignature(self): """ Get the data packet's signature object. :return: The signature object. :rtype: a subclass of Signature such as Sha256WithRsaSignature """ return self._signature.get() def getContent(self): """ Get the data packet's content. :return: The content as a Blob, which isNull() if unspecified. :rtype: Blob """ return self._content def getDefaultWireEncoding(self): """ Return the default wire encoding, which was encoded with getDefaultWireEncodingFormat(). :return: The default wire encoding, whose isNull() may be true if there is no default wire encoding. :rtype: SignedBlob """ if self._getDefaultWireEncodingChangeCount != self.getChangeCount(): # The values have changed, so the default wire encoding is # invalidated. self._defaultWireEncoding = SignedBlob() self._defaultWireEncodingFormat = None self._getDefaultWireEncodingChangeCount = self.getChangeCount() return self._defaultWireEncoding def getDefaultWireEncodingFormat(self): """ Get the WireFormat which is used by getDefaultWireEncoding(). :return: The WireFormat, which is only meaningful if the getDefaultWireEncoding() is not isNull(). :rtype: WireFormat """ return self._defaultWireEncodingFormat def getIncomingFaceId(self): """ Get the incoming face ID according to the incoming packet header. :return: The incoming face ID. If not specified, return None. :rtype: int """ field = (None if self._lpPacket == None else IncomingFaceId.getFirstHeader(self._lpPacket)) return None if field == None else field.getFaceId() def getFullName(self, wireFormat = None): """ Get the Data packet's full name, which includes the final ImplicitSha256Digest component based on the wire encoding for a particular wire format. :param wireFormat: (optional) A WireFormat object used to encode this object. If omitted, use WireFormat.getDefaultWireFormat(). :type wireFormat: A subclass of WireFormat :return: The full name. You must not change the Name object - if you need to change it then make a copy. :rtype: Name """ if wireFormat == None: # Don't use a default argument since getDefaultWireFormat can change. wireFormat = WireFormat.getDefaultWireFormat() # The default full name depends on the default wire encoding. if (not self.getDefaultWireEncoding().isNull() and self._defaultFullName.size() > 0 and self.getDefaultWireEncodingFormat() == wireFormat): # We already have a full name. A non-null default wire encoding # means that the Data packet fields have not changed. return self._defaultFullName fullName = Name(self.getName()) sha256 = hashes.Hash(hashes.SHA256(), backend=default_backend()) sha256.update(self.wireEncode(wireFormat).toBytes()) fullName.appendImplicitSha256Digest( Blob(bytearray(sha256.finalize()), False)) if wireFormat == WireFormat.getDefaultWireFormat(): # wireEncode has already set defaultWireEncodingFormat_. self._defaultFullName = fullName return fullName def setName(self, name): """ Set name to a copy of the given Name. :param Name name: The Name which is copied. :return: This Data so that you can chain calls to update values. :rtype: Data """ self._name.set(name if type(name) is Name else Name(name)) self._changeCount += 1 return self def setMetaInfo(self, metaInfo): """ Set metaInfo to a copy of the given MetaInfo. :param MetaInfo metaInfo: The MetaInfo which is copied. :return: This Data so that you can chain calls to update values. :rtype: Data """ self._metaInfo.set(MetaInfo() if metaInfo == None else MetaInfo(metaInfo)) self._changeCount += 1 return self def setSignature(self, signature): """ Set the signature to a copy of the given signature. :param signature: The signature object which is cloned. :type signature: a subclass of Signature such as Sha256WithRsaSignature :return: This Data so that you can chain calls to update values. :rtype: Data """ self._signature.set(Sha256WithRsaSignature() if signature == None else signature.clone()) self._changeCount += 1 return self def setContent(self, content): """ Set the content to the given value. :param content: The array with the content bytes. If content is not a Blob, then create a new Blob to copy the bytes (otherwise take another pointer to the same Blob). :type content: A Blob or an array type with int elements """ self._content = content if isinstance(content, Blob) else Blob(content) self._changeCount += 1 def setLpPacket(self, lpPacket): """ An internal library method to set the LpPacket for an incoming packet. The application should not call this. :param LpPacket lpPacket: The LpPacket. This does not make a copy. :return: This Data so that you can chain calls to update values. :rtype: Data :note: This is an experimental feature. This API may change in the future. """ self._lpPacket = lpPacket # Don't update _changeCount since this doesn't affect the wire encoding. return self def getChangeCount(self): """ Get the change count, which is incremented each time this object (or a child object) is changed. :return: The change count. :rtype: int """ # Make sure each of the checkChanged is called. changed = self._name.checkChanged() changed = self._metaInfo.checkChanged() or changed changed = self._signature.checkChanged() or changed if changed: # A child object has changed, so update the change count. self._changeCount += 1 return self._changeCount def _setDefaultWireEncoding( self, defaultWireEncoding, defaultWireEncodingFormat): self._defaultWireEncoding = defaultWireEncoding self._defaultWireEncodingFormat = defaultWireEncodingFormat # Set _getDefaultWireEncodingChangeCount so that the next call to # getDefaultWireEncoding() won't clear _defaultWireEncoding. self._getDefaultWireEncodingChangeCount = self.getChangeCount() # Create managed properties for read/write properties of the class for more pythonic syntax. name = property(getName, setName) metaInfo = property(getMetaInfo, setMetaInfo) signature = property(getSignature, setSignature) content = property(getContent, setContent)
class StateVectorSync2018(object): """ Create a new StateVectorSync2018 to communicate using the given face. Register the applicationBroadcastPrefix to receive sync notification interests. Note: Your application must call processEvents. Since processEvents modifies the internal ChronoSync data structures, your application should make sure that it calls processEvents in the same thread as this constructor (which also modifies the data structures). :param onReceivedSyncState: When StateVectorSync2018 receives a state vector, this calls onReceivedSyncState(syncStates) where syncStates is the list of SyncState of only the members whose sequence number has increased from the previous onReceivedSyncState callback. The callback should send interests to fetch the application data for the sequence numbers in the sync state. NOTE: The library will log any exceptions raised by this callback, but for better error handling the callback should catch and properly handle any exceptions. :type onReceivedSyncState: function object :param onInitialized: This calls onInitialized() when this has registered the broadcast prefix and is initialized. NOTE: The library will log any exceptions raised by this callback, but for better error handling the callback should catch and properly handle any exceptions. :type onInitialized: function object :param Name applicationDataPrefix: The prefix used by this application instance for others to fetch application data and to identify this member. (If the application restarts with sequence 0 on startup, it needs to include a unique session number in the applicationDataPrefix.) For example, "/my/local/prefix/ndnchat4/0K4wChff2v/123". In the state vector, this member is identified by the string applicationDataPrefix.toUri(). :param Name applicationBroadcastPrefix: The broadcast name prefix including the application name. For example, "/ndn/broadcast/ChronoChat-0.3/ndnchat1". This makes a copy of the name. :param Face face: The Face for calling registerPrefix and expressInterest. The Face object must remain valid for the life of this StateVectorSync2018 object. :param KeyChain keyChain: The key chain to sign a data packet (not to sign a notification interest with HMAC). :param SigningInfo signingParams: The signing parameters to sign a data packet (not to sign a notification interest with HMAC). :param Blob hmacKey: The shared key for signing notification interests with HmacWithSha256. :param float notificationInterestLifetime: The interest lifetime in milliseconds for notification interests. :param onRegisterFailed: If failed to register the prefix to receive interests for the applicationBroadcastPrefix, this calls onRegisterFailed(applicationBroadcastPrefix). NOTE: The library will log any exceptions raised by this callback, but for better error handling the callback should catch and properly handle any exceptions. :type onRegisterFailed: function object :param int previousSequenceNumber (optional): The previously published sequence number for the same applicationDataPrefix. In case the applicationDataPrefix does not already include a unique session number, this can be used by the application to restore the state from a previous use. If omitted, this uses -1 so that the next published sequence number is 0. """ def __init__(self, onReceivedSyncState, onInitialized, applicationDataPrefix, applicationBroadcastPrefix, face, keyChain, signingParams, hmacKey, notificationInterestLifetime, onRegisterFailed, previousSequenceNumber=-1): self._onReceivedSyncState = onReceivedSyncState self._onInitialized = onInitialized self._applicationDataPrefixUri = applicationDataPrefix.toUri() self._applicationBroadcastPrefix = Name(applicationBroadcastPrefix) self._face = face self._keyChain = keyChain self._signingParams = signingParams self._hmacKey = hmacKey self._notificationInterestLifetime = notificationInterestLifetime # The dictionary key is member ID string. The value is the sequence number. self._stateVector = {} # The keys of _stateVector in sorted order, kept in sync with _stateVector. # (We don't use OrderedDict because it doesn't sort keys on insert.) self._sortedStateVectorKeys = [] self._sequenceNo = previousSequenceNumber self._enabled = True # Register to receive broadcast interests. self._face.registerPrefix(self._applicationBroadcastPrefix, self._onInterest, onRegisterFailed, self._onRegisterSuccess) class SyncState(object): """ A SyncState holds the values of one entry of a state vector which is passed to the onReceivedSyncState callback which was given to the StateVectorSync2018 constructor. """ def __init__(self, dataPrefixUri, sequenceNo): self._dataPrefixUri = dataPrefixUri self._sequenceNo = sequenceNo def getDataPrefix(self): """ Get the member data prefix. :return: The member data prefix as a Name URI string (which is also the member ID). :rtype: str """ return self._dataPrefixUri def getSequenceNo(self): """ Get the sequence number for this member. :return: The sequence number. :rtype: int """ return self._sequenceNo def __str__(self): return "SyncState(" + self._dataPrefixUri + ", " + str( self._sequenceNo) + ")" def __repr__(self): return self.__str__() def __eq__(self, other): return (isinstance(other, StateVectorSync2018.SyncState) and self._dataPrefixUri == other._dataPrefixUri and self._sequenceNo == other._sequenceNo) def __ne__(self, other): return not self == other def getProducerPrefixes(self): """ Get a copy of the current list of the Name URI for each producer data prefix (which is their member ID). You can use these in getProducerSequenceNo(). This includes the prefix for this user. :return: A copy of the list of each producer data prefix. :rtype: array of str """ # Just return a copy of the keys of the state vector dictionary. return self._sortedStateVectorKeys[:] def getProducerSequenceNo(self, producerDataPrefix): """ Get the current sequence number for the given producerDataPrefix in the current state vector. :param str producerDataPrefix: The producer's application data prefix as a Name URI string (also the member ID). :return: The current sequence sequence number for the member, or -1 if the producerDataPrefix is not in the state vector. :rtype: int """ if producerDataPrefix in self._stateVector: return self._stateVector[producerDataPrefix] else: return -1 def publishNextSequenceNo(self): """ Increment the sequence number and send a new notification interest where the name is the applicationBroadcastPrefix + the encoding of the new state vector. Use the hmacKey given to the constructor to sign with HmacWithSha256. After this, your application should publish the content for the new sequence number. You can get the new sequence number with getSequenceNo(). Note: Your application must call processEvents. Since processEvents modifies the internal ChronoSync data structures, your application should make sure that it calls processEvents in the same thread as publishNextSequenceNo() (which also modifies the data structures). """ self._sequenceNo += 1 self._setSequenceNumber(self._applicationDataPrefixUri, self._sequenceNo) logging.getLogger(__name__).info( "Broadcast new seq # %s. State vector %s", str(self._sequenceNo), str(self._stateVector)) self._broadcastStateVector() def getSequenceNo(self): """ Get the sequence number of the latest data published by this application instance. :return: The sequence number. :rtype: int """ return self._sequenceNo def shutdown(self): """ Unregister callbacks so that this does not respond to interests anymore. If you will discard this StateVectorSync2018 object while your application is still running, you should call shutdown() first. After calling this, you should not call publishNextSequenceNo() again since the behavior will be undefined. Note: Because this modifies internal data structures, your application should make sure that it calls processEvents in the same thread as shutdown() (which also modifies the data structures). """ self._enabled = False self._contentCache.unregisterAll() @staticmethod def encodeStateVector(stateVector, stateVectorKeys): """ Encode the stateVector as TLV. :param dict<str,int> stateVector: The state vector dictionary where the key is the member ID string and the value is the sequence number. :param list<str> stateVectorKeys: The key strings of stateVector, sorted in the order to be encoded. :return: A Blob containing the encoding. :rtype: Blob """ encoder = TlvEncoder(256) saveLength = len(encoder) # Encode backwards. for i in range(len(stateVectorKeys) - 1, -1, -1): saveLengthForEntry = len(encoder) encoder.writeNonNegativeIntegerTlv( StateVectorSync2018.TLV_StateVector_SequenceNumber, stateVector[stateVectorKeys[i]]) encoder.writeBlobTlv(StateVectorSync2018.TLV_StateVector_MemberId, Blob(stateVectorKeys[i]).buf()) encoder.writeTypeAndLength( StateVectorSync2018.TLV_StateVectorEntry, len(encoder) - saveLengthForEntry) encoder.writeTypeAndLength(StateVectorSync2018.TLV_StateVector, len(encoder) - saveLength) return Blob(encoder.getOutput(), False) @staticmethod def decodeStateVector(input): """ Decode the input as a TLV state vector. :param input: The array with the bytes to decode. :type input: An array type with int elements :return: A new dictionary where the key is the member ID string and the value is the sequence number. If the input encoding has repeated entries with the same member ID, this uses only the last entry. :rtype: dict<str,int> :raises ValueError: For invalid encoding. """ stateVector = {} # If input is a blob, get its buf(). decodeBuffer = input.buf() if isinstance(input, Blob) else input decoder = TlvDecoder(decodeBuffer) endOffset = decoder.readNestedTlvsStart( StateVectorSync2018.TLV_StateVector) while decoder.getOffset() < endOffset: entryEndOffset = decoder.readNestedTlvsStart( StateVectorSync2018.TLV_StateVectorEntry) memberIdBlob = Blob( decoder.readBlobTlv( StateVectorSync2018.TLV_StateVector_MemberId), False) stateVector[str(memberIdBlob)] = decoder.readNonNegativeIntegerTlv( StateVectorSync2018.TLV_StateVector_SequenceNumber) decoder.finishNestedTlvs(entryEndOffset) decoder.finishNestedTlvs(endOffset) return stateVector def _makeNotificationInterest(self): """ Make and return a new Interest where the name is _applicationBroadcastPrefix plus the encoding of _stateVector. Also use _hmacKey to sign it with HmacWithSha256. :return: The new signed notification interest. :rtype: Interest """ interest = Interest(self._applicationBroadcastPrefix) interest.setInterestLifetimeMilliseconds( self._notificationInterestLifetime) interest.getName().append( StateVectorSync2018.encodeStateVector(self._stateVector, self._sortedStateVectorKeys)) # TODO: Should we just use key name /A ? KeyChain.signWithHmacWithSha256(interest, self._hmacKey, Name("/A")) return interest def _broadcastStateVector(self): """ Call _makeNotificationInterest() and then expressInterest to broadcast the notification interest. """ interest = self._makeNotificationInterest() # A response is not required, so ignore the timeout and Data packet. self._face.expressInterest(interest, StateVectorSync2018._dummyOnData) def _setSequenceNumber(self, memberId, sequenceNumber): """ An internal method to update the _stateVector by setting memberId to sequenceNumber. This is needed because we also have to update _sortedStateVectorKeys. :param str memberId: The member ID string. :param int sequenceNumber: The sequence number for the member. """ if not memberId in self._sortedStateVectorKeys: # We need to keep _sortedStateVectorKeys synced with _stateVector. bisect.insort(self._sortedStateVectorKeys, memberId) self._stateVector[memberId] = sequenceNumber def _onInterest(self, prefix, interest, face, interestFilterId, filter): """ Process a received broadcast interest. """ # Verify the HMAC signature. verified = False try: verified = KeyChain.verifyInterestWithHmacWithSha256( interest, self._hmacKey) except: # Treat a decoding failure as verification failure. pass if not verified: # Signature verification failure. logging.getLogger(__name__).info( "Dropping Interest with failed signature: %s", interest.getName().toUri()) return encoding = interest.getName().get( self._applicationBroadcastPrefix.size()).getValue() receivedStateVector = StateVectorSync2018.decodeStateVector(encoding) logging.getLogger(__name__).info("Received broadcast state vector %s", str(receivedStateVector)) (syncStates, needToReply) = self._mergeStateVector(receivedStateVector) if len(syncStates) > 0: # Inform the application up new sync states. try: self._onReceivedSyncState(syncStates) except: logging.exception("Error in onReceivedSyncState") if needToReply: # Inform other members who may need to be updated. logging.getLogger(__name__).info( "Received state vector was outdated. Broadcast state vector %s", str(self._stateVector)) self._broadcastStateVector() def _mergeStateVector(self, receivedStateVector): """ Merge receivedStateVector into self._stateVector and return the updated entries. :param dict<str,int> receivedStateVector: The received state vector dictionary where the key is the member ID string and the value is the sequence number. :return: A tuple of (syncStates, needToReply) where syncStates is the list of new StateVectorSync2018.SyncState giving the entries in self._stateVector that were updated, and needToReply is True if receivedStateVector is lacking more current information which was in self._stateVector. :rtype: (list<StateVectorSync2018.SyncState>, bool) """ needToReply = False result = [] if self._stateVector == receivedStateVector: return (result, needToReply) for k, v in receivedStateVector.items(): if self._stateVector.get( k) == None or self._stateVector.get(k) < v: result.append(StateVectorSync2018.SyncState(k, v)) self._setSequenceNumber(k, v) for k, v in self._stateVector.items(): if receivedStateVector.get( k) == None or receivedStateVector.get(k) < v: needToReply = True break return (result, needToReply) def _onRegisterSuccess(self, prefix, registeredPrefixId): try: self._onInitialized() except: logging.exception("Error in onInitialized") @staticmethod def _dummyOnData(interest, data): """ This is a do-nothing onData for using expressInterest when we don't expect a response Data packet. """ pass # Assign TLV types as crtitical values for application use. TLV_StateVector = 129 TLV_StateVectorEntry = 131 TLV_StateVector_MemberId = 133 TLV_StateVector_SequenceNumber = 135