Exemple #1
0
    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)
Exemple #2
0
    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)
Exemple #3
0
    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)
Exemple #4
0
    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)
Exemple #5
0
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)
Exemple #6
0
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
Exemple #7
0
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
Exemple #8
0
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)
Exemple #9
0
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