Outils pour utilisateurs

Outils du site


pb_copyable_passing_complex_types

PB Copyable Passing Complex Types

Niveau Pro et intergalactique !
Cette API:

  • a entre 15 et 20 ans
  • n'est que vaguement maintenu
  • un exemple au moins ne marche pas
  • le tranfert de datas simples marche
  • le transfert d'objets complexes ne marche pas
  • les exemples ne respectent pas le PEP 8, c'est imbriqué au possible

Traduction Google de PB Copyable: Passing Complex Types améliorée par un humain ayant fait intergalactique en 1ère langue !

Aperçu

Ce chapitre se concentre sur l'utilisation de PB pour transmettre des types complexes (en particulier des instances de classe) vers et depuis un processus distant. La première section concerne simplement la copie du contenu d'un objet vers un processus distant (Copyable). La seconde explique comment copier ces contenus une fois, puis les mettre à jour ultérieurement lorsqu'ils changent (Cacheable).

Motivation

Dans le chapitre précédent , vous avez vu comment passer des types de base à un processus distant, en les utilisant dans les arguments ou les valeurs de retour d'une fonction callRemote. Cependant, si vous l'avez expérimenté, vous avez peut-être découvert des problèmes en essayant de passer quelque chose de plus compliqué qu'un type primitif int/list/dict/string ou un autre objet pb.Referenceable. À un moment donné, vous souhaitez transmettre des objets entiers entre des processus, au lieu de devoir les réduire à des dictionnaires d'un côté, puis de les réinstancier de l'autre.

Passer des objets

Le moyen le plus évident et le plus simple d'envoyer un objet à un processus distant consiste à utiliser quelque chose comme le code suivant. Il arrive aussi que ce code ne fonctionne pas, comme cela sera expliqué plus bas.

class LilyPond:
  def __init__(self, frogs):
    self.frogs = frogs
 
pond = LilyPond(12)
ref.callRemote("sendPond", pond)

Si vous essayez de l'exécuter, vous pouvez espérer qu'une extrémité distante appropriée qui implémente la méthode remote_sendPond verrait cette méthode être invoquée avec une instance de la class LilyPond. Mais à la place, vous rencontrerez l'exception InsecureJelly redoutée. C'est la manière de Twisted de vous dire que vous avez violé une restriction de sécurité et que le destinataire refuse d'accepter votre objet.

Options de sécurité

Quel est le problème ? Quel est le problème avec la simple copie d'une classe dans l'espace de noms d'un autre processus ?

Inverser la question pourrait permettre de voir plus facilement le problème : quel est le problème d'accepter la demande d'un étranger pour créer un objet arbitraire dans votre espace de noms local ? La vraie question est de savoir quel pouvoir vous leur accordez : quelles actions peuvent-ils vous convaincre de prendre sur la base des octets qu'ils vous envoient via cette connexion à distance.

Les objets représentent généralement plus de puissance que les types de base tels que les chaînes et les dictionnaires, car ils contiennent également (ou référencent) du code, qui peut modifier d'autres structures de données lors de leur exécution. Une fois que les données précédemment approuvées sont subverties, le reste du programme est compromis.

Les classes Python « piles incluses » intégrées sont relativement simples, mais vous ne voudriez toujours pas laisser un programme étranger les utiliser pour créer des objets arbitraires dans votre espace de noms ou sur votre ordinateur. Imaginez un protocole impliquant l'envoi d'un objet semblable à un fichier avec une méthode read() censée être utilisée plus tard pour récupérer un document. Imaginez ensuite ce qui se passerait si cet objet était créé avec os.fdopen(“~/.gnupg/secring.gpg”). Ou une instance de .telnetlib.Telnet(“localhost”, “chargen”)

Les cours que vous avez écrits pour votre propre programme sont susceptibles d'avoir beaucoup plus de pouvoir. Ils peuvent exécuter du code pendant init, ou même avoir une signification particulière simplement en raison de leur existence. Un programme peut avoir des objets User pour représenter des comptes d'utilisateurs et avoir une règle qui dit que tous les objets User du système sont référencés lors de l'autorisation d'une session de connexion. (Dans ce système, User.initajouterait probablement l'objet à une liste globale d'utilisateurs connus). Le simple fait de créer un objet donnerait accès à quelqu'un. Si vous pouviez être amené à créer un mauvais objet, un utilisateur non autorisé obtiendrait l'accès.

La création d'objets doit donc faire partie de la conception de la sécurité d'un système. La ligne pointillée entre « confiance à l'intérieur » et « non confiance à l'extérieur » doit décrire ce qui peut être fait en réponse à des événements extérieurs. L'un de ces événements est la réception d'un objet via un appel de procédure à distance PB, qui est une demande de création d'un objet dans votre espace de noms “intérieur”. La question est de savoir quoi faire en réponse à cela. Pour cette raison, vous devez spécifier explicitement quelles classes distantes seront acceptées et comment leurs représentants locaux doivent être créés.

Quelle classe utiliser ?

Une autre question fondamentale à laquelle il faut répondre avant de pouvoir faire quoi que ce soit d'utile avec un objet sérialisé entrant est : quelle classe devons-nous créer ? La réponse simpliste est de créer le « même type » qui a été sérialisé du côté de l'expéditeur du câble, mais ce n'est pas aussi facile ou aussi simple que vous pourriez le penser. N'oubliez pas que la demande provient d'un programme différent, utilisant un ensemble potentiellement différent de bibliothèques de classes. En fait, puisque PB a également été implémenté en Java, Emacs-Lisp et d'autres langages, il n'y a aucune garantie que l'expéditeur exécute même Python ! Tout ce que nous savons du côté récepteur est une liste de deux choses qui décrivent l'instance qu'ils essaient de nous envoyer : le nom de la classe et une représentation du contenu de l'objet.

PB vous permet de spécifier le mappage des noms de classe distants aux classes locales avec la fonction setUnjellyableForClass.

  • Notez que, dans ce contexte, “unjelly” est un verbe avec le sens opposé de “jelly”. Le verbe “to jelly” signifie sérialiser un objet ou une structure de données en une séquence d'octets (ou une autre représentation primitive transmissible/stockable), tandis que “to unjelly” signifie désérialiser le flux d'octets en un objet vivant dans l'espace mémoire du récepteur. “Unjellyable” est un nom, ( pas un adjectif), faisant référence à la classe qui sert de destination ou de destinataire du processus de dégel. “A est unjellyable dans B” signifie qu'une représentation sérialisée A (d'un objet distant) peut être désérialisée dans un objet local de type B. Ce sont ces objets “B” qui sont le deuxième argument “Unjellyable” de la fonction setUnjellyableForClass. En particulier, «non gélifiant» ne signifie pas «ne peut pas être gélifié». Unpersistable signifie “non persistant”, mais “unjelly”, “unserialize” et “unpickle” signifient inverser les opérations de “jellying”, “serializing” et “pickling”.

Cette fonction prend une référence de classe distante/expéditeur (soit le nom complet tel qu'utilisé par l'expéditeur, soit un objet de classe à partir duquel le nom peut être extrait), et une classe locale/destinataire (utilisée pour créer la représentation locale pour objets sérialisés entrants). Chaque fois que l'extrémité distante envoie un objet, le nom de classe qu'elle transmet est recherché dans la table contrôlée par cette fonction. Si une classe correspondante est trouvée, elle est utilisée pour créer l'objet local. Sinon, vous obtenez l'exception InsecureJelly.

En général, vous vous attendez à ce que les deux extrémités partagent la même base de code : soit vous contrôlez le programme qui s'exécute aux deux extrémités du câble, soit les deux programmes partagent une sorte de langage commun qui est implémenté dans le code qui existe aux deux extrémités. Vous ne vous attendriez pas à ce qu'ils vous envoient un objet de la classe MyFooziWhatZit à moins que vous n'ayez également une définition pour cette classe. Il est donc raisonnable que la couche Jelly rejette toutes les classes entrantes à l'exception de celles que vous avez explicitement marquées avec setUnjellyableForClass. Mais gardez à l'esprit que l'idée que se fait l'expéditeur d'un objet User peut différer de celui du destinataire, soit par des collisions d'espaces de noms entre des packages non liés, une différence de version entre des nœuds qui n'ont pas été mis à jour au même rythme, ou un intrus malveillant essayant de faire échouer votre code d'une manière intéressante ou potentiellement vulnérable.

pb.Copyable

Ok, assez de cette théorie. Comment envoyer un objet à part entière d'un côté à l'autre ?

copy_sender.py
from twisted.internet import reactor
from twisted.python import log
from twisted.spread import jelly, pb
class LilyPond:
    def setStuff(self, color, numFrogs):
        self.color = color
        self.numFrogs = numFrogs
    def countFrogs(self):
        print("%d frogs" % self.numFrogs)
class CopyPond(LilyPond, pb.Copyable):
    pass
class Sender:
    def __init__(self, pond):
        self.pond = pond
    def got_obj(self, remote):
        self.remote = remote
        d = remote.callRemote("takePond", self.pond)
        d.addCallback(self.ok).addErrback(self.notOk)
    def ok(self, response):
        print("pond arrived", response)
        reactor.stop()
    def notOk(self, failure):
        print("error during takePond:")
        if failure.type == jelly.InsecureJelly:
            print(" InsecureJelly")
        else:
            print(failure)
        reactor.stop()
        return None
def main():
    from copy_sender import CopyPond  # so it's not __main__.CopyPond
    pond = CopyPond()
    pond.setStuff("green", 7)
    pond.countFrogs()
    # class name:
    print(".".join([pond.__class__.__module__, pond.__class__.__name__]))
    sender = Sender(pond)
    factory = pb.PBClientFactory()
    reactor.connectTCP("localhost", 8800, factory)
    deferred = factory.getRootObject()
    deferred.addCallback(sender.got_obj)
    reactor.run()
if __name__ == "__main__":
    main()
copy_receiver.tac
"""
PB copy receiver example.
This is a Twisted Application Configuration (tac) file.  Run with e.g.
   twistd -ny copy_receiver.tac
See the twistd(1) man page or
http://twistedmatrix.com/documents/current/howto/application for details.
"""
import sys
if __name__ == "__main__":
    print(__doc__)
    sys.exit(1)
from copy_sender import CopyPond, LilyPond
from twisted.application import internet, service
from twisted.internet import reactor
from twisted.python import log
from twisted.spread import pb
class ReceiverPond(pb.RemoteCopy, LilyPond):
    pass
pb.setUnjellyableForClass(CopyPond, ReceiverPond)
class Receiver(pb.Root):
    def remote_takePond(self, pond):
        print(" got pond:", pond)
        pond.countFrogs()
        return "safe and sound"  # positive acknowledgement
    def remote_shutdown(self):
        reactor.stop()
application = service.Application("copy_receiver")
internet.TCPServer(8800, pb.PBServerFactory(Receiver())).setServiceParent(
    service.IServiceCollection(application))

Le côté envoi a une classe appelée LilyPond. Pour le rendre éligible au transport callRemote(soit comme argument, soit comme valeur de retour, soit comme quelque chose référencé par l'un ou l'autre [comme une valeur de dictionnaire]), il doit hériter de l'une des quatre classes Serializable. Dans cette section, nous nous concentrons sur Copyable. La sous-classe copiable de LilyPond appelle CopyPond. Nous en créons une instance et l'envoyons callRemote comme argument à la méthode remote_takePond du récepteur.
La couche Jelly sérialisera (“jelly”) cet objet en tant qu'instance avec un nom de classe copy_sender.CopyPond et un morceau de données qui représente l'état de l'objet pond.__class__.__module__ et pond.__class__.__name__ sont utilisés pour dériver la chaîne du nom de classe.
La méthode getStateToCopy de l'objet est utilisée pour obtenir l'état : celui-ci est fourni par pb.Copyable, et la valeur par défaut récupère simplement self.__dict__. Cela fonctionne exactement comme la méthode facultative __getstate__ utilisée par pickle. La paire de nom et d'état est envoyée sur le fil au récepteur.

L'extrémité réceptrice définit une classe locale nommée pour représenter les instances ReceiverPondentrantes . LilyPondCette classe dérive de la classe de l'expéditeur LilyPond(avec un nom complet de copy_sender.LilyPond), qui spécifie comment nous nous attendons à ce qu'elle se comporte. Nous espérons qu'il s'agit de la même LilyPondclasse que l'expéditeur utilisé. (À tout le moins, nous espérons que le nôtre pourra accepter un État créé par le leur). Il hérite également de pb.RemoteCopy, qui est une exigence pour toutes les classes qui agissent dans ce rôle de représentation locale (celles qui sont attribuées au second argument de setUnjellyableForClass). RemoteCopy fournit les méthodes qui indiquent à la couche Jelly comment créer l'objet local à partir de l'état sérialisé entrant.

Ensuite setUnjellyableForClass est utilisé pour enregistrer les deux classes. Cela a deux effets : les instances de la classe distante (le premier argument) seront autorisées à travers la couche de sécurité, et les instances de la classe locale (le deuxième argument) seront utilisées pour contenir l'état qui est transmis lorsque l'expéditeur sérialise l'objet distant.

Lorsque le récepteur désérialise (“unjellies”) l'objet, il crée une instance de la classe ReceiverPond locale et transmet l'état transmis (généralement sous la forme d'un dictionnaire) à la méthode setCopyableState de cet objet. Cela agit exactement comme la méthode __setstate__ utilisée par pickle lors de la désérialisation d'un objet. getStateToCopy/ setCopyableStatesont distincts de __getstate__/ __setstate__ pour permettre aux objets d'être persistants (dans le temps) différemment de leur transmission (dans l'espace [mémoire]). Sortie:

[-] twisted.spread.pb.PBServerFactory starting on 8800
[-] Starting factory <twisted.spread.pb.PBServerFactory instance at
0x406159cc>
[Broker,0,127.0.0.1]  got pond: <__builtin__.ReceiverPond instance at
0x406ec5ec>
[Broker,0,127.0.0.1] 7 frogs

$ ./copy_sender.py
7 frogs
copy_sender.CopyPond
pond arrived safe and sound
Main loop terminated.

Contrôle de l'état copié

En remplaçant getStateToCopy et setCopyableState, vous pouvez contrôler la façon dont l'objet est transmis sur le fil. Par exemple, vous souhaiterez peut-être effectuer une réduction des données : pré-calculer certains résultats au lieu d'envoyer toutes les données brutes sur le câble. Ou vous pouvez remplacer les références à un objet local du côté de l'expéditeur par des marqueurs avant l'envoi, puis à la réception remplacer ces marqueurs par des références à un proxy côté destinataire qui pourrait effectuer les mêmes opérations sur un cache local de données.

Une autre bonne utilisation de getStateToCopy est d'implémenter des attributs « locaux uniquement » : des données qui ne sont accessibles que par le processus local, et non par les utilisateurs distants. Par exemple, un attribut .password peut être retiré de l'état d'objet avant d'être envoyé à un système distant. Combiné avec le fait que les objets Copyable reviennent inchangés après un aller-retour, cela pourrait être utilisé pour construire un système de défi-réponse (en fait, PB le fait avec des objets pb.Referenceable pour implémenter l'autorisation comme décrit ici ).

Tout retour getStateToCopy de l'objet expéditeur sera sérialisé et envoyé sur le réseau ; setCopyableState reçoit tout ce qui passe par le fil et est responsable de la configuration de l'état de l'objet dans lequel il vit.

copy2_classes.py
from twisted.spread import pb
class FrogPond:
    def __init__(self, numFrogs, numToads):
        self.numFrogs = numFrogs
        self.numToads = numToads
    def count(self):
        return self.numFrogs + self.numToads
class SenderPond(FrogPond, pb.Copyable):
    def getStateToCopy(self):
        d = self.__dict__.copy()
        d["frogsAndToads"] = d["numFrogs"] + d["numToads"]
        del d["numFrogs"]
        del d["numToads"]
        return d
class ReceiverPond(pb.RemoteCopy):
    def setCopyableState(self, state):
        self.__dict__ = state
    def count(self):
        return self.frogsAndToads
pb.setUnjellyableForClass(SenderPond, ReceiverPond)
copy2_sender.py
from copy2_classes import SenderPond
from twisted.internet import reactor
from twisted.python import log
from twisted.spread import jelly, pb
class Sender:
    def __init__(self, pond):
        self.pond = pond
    def got_obj(self, obj):
        d = obj.callRemote("takePond", self.pond)
        d.addCallback(self.ok).addErrback(self.notOk)
    def ok(self, response):
        print("pond arrived", response)
        reactor.stop()
    def notOk(self, failure):
        print("error during takePond:")
        if failure.type == jelly.InsecureJelly:
            print(" InsecureJelly")
        else:
            print(failure)
        reactor.stop()
        return None
def main():
    pond = SenderPond(3, 4)
    print("count %d" % pond.count())
    sender = Sender(pond)
    factory = pb.PBClientFactory()
    reactor.connectTCP("localhost", 8800, factory)
    deferred = factory.getRootObject()
    deferred.addCallback(sender.got_obj)
    reactor.run()
if __name__ == "__main__":
    main()
copy2_receiver.py
import copy2_classes  # needed to get ReceiverPond registered with Jelly
from twisted.application import internet, service
from twisted.internet import reactor
from twisted.spread import pb
class Receiver(pb.Root):
    def remote_takePond(self, pond):
        print(" got pond:", pond)
        print(" count %d" % pond.count())
        return "safe and sound"  # positive acknowledgement
    def remote_shutdown(self):
        reactor.stop()
application = service.Application("copy_receiver")
internet.TCPServer(8800, pb.PBServerFactory(Receiver())).setServiceParent(
    service.IServiceCollection(application))

Dans cet exemple, les classes sont définies dans un fichier source séparé, qui établit également la liaison entre elles. Les SenderPond et ReceiverPond ne sont pas liés sauf pour cette liaison : ils implémentent les mêmes méthodes, mais utilisent des variables d'instance internes différentes pour les accomplir.
Le destinataire de l'objet n'a même pas besoin d'importer la définition de classe dans son espace de noms. Il suffit qu'ils importent la définition de la classe (et donc exécutent l'instruction setUnjellyableForClass). La couche Jelly se souvient de la définition de classe jusqu'à ce qu'un objet correspondant soit reçu. L'expéditeur de l'objet a besoin de la définition, bien sûr, pour créer l'objet en premier lieu. Sortie:

$ twistd -n -y copy2_receiver.py
[-] twisted.spread.pb.PBServerFactory starting on 8800
[-] Starting factory <twisted.spread.pb.PBServerFactory instance at
0x40604b4c>
[Broker,0,127.0.0.1]  got pond: <copy2_classes.ReceiverPond instance at
0x406eb2ac>
[Broker,0,127.0.0.1]  count 7

$ ./copy2_sender.py
count 7
pond arrived safe and sound
Main loop terminated.

Choses à surveiller

  • Le premier argument de setUnjellyableForClass doit faire référence à la classe connue de l'expéditeur . L'expéditeur n'a aucun moyen de savoir comment vos instructions locales d'import sont configurées, et la sémantique d'espace de noms flexible de Python vous permet d'accéder à la même classe via une variété de noms différents. Vous devez correspondre à tout ce que fait l'expéditeur. Le fait que les deux extrémités importent la classe à partir d'un fichier séparé, en utilisant un nom de module canonique (pas d'importations “frères et sœurs”), est un bon moyen de bien faire les choses, en particulier lorsque les classes d'envoi et de réception sont définies ensemble, avec le setUnjellyableForClass suivant immédiatement leur définition.
  • La classe envoyée doit hériter de pb.Copyable. La classe enregistrée pour le recevoir doit hériter de pb.RemoteCopy
  • pb.RemoteCopy est en fait défini dans twisted.spread.flavors, mais pb.RemoteCopy c'est le moyen préféré d'y accéder
  • La même classe peut être utilisée pour envoyer et recevoir. Faites-le simplement hériter à la fois de pb.Copyable et pb.RemoteCopy. Cela permettra également d'envoyer la même classe de manière symétrique dans les deux sens sur le fil. Mais ne vous méprenez pas sur le moment où il arrive (et utilise setCopyableState) par rapport au moment où il va (en utilisant getStateToCopy).
  • InsecureJelly Les exceptions sont levées par le destinataire. Ils seront livrés de manière asynchrone à u n gestionnaire errback. Si vous n'en ajoutez pas au Deferredretour par callRemote, vous ne recevrez jamais de notification du problème.
  • La classe dérivée de pb.RemoteCopy sera créée à l'aide d'une méthode __init__ qui ne prend aucun argument. Toutes les configurations doivent être effectuées dans la méthode setCopyableState. Comme le dit la docstring de RemoteCopy, n'implémentez pas un constructeur qui nécessite des arguments dans une sous-classe de RemoteCopy.

Plus d'information

  • pb.Copyable est principalement implémenté dans twisted.spread.flavors, et les docstrings y sont la meilleure source d'informations supplémentaires.
  • Copyable est également utilisé twisted.web.distrib pour fournir des requêtes HTTP à d'autres programmes pour le rendu, permettant de déléguer des sous-arborescences d'espace URL à plusieurs programmes (sur plusieurs machines).

pb.Cacheable

pb_copyable_passing_complex_types.txt · Dernière modification : 2022/12/15 10:17 de serge