From 5df7f444f3781258c2ee665edbf76653d61c8b37 Mon Sep 17 00:00:00 2001 From: "gregory.starck" Date: Fri, 27 Dec 2019 12:17:39 -0500 Subject: [PATCH] Allow false only_exposed and re-introduce Pickle serializer --- Pyro5/client.py | 8 ------- Pyro5/nameserver.py | 5 +++-- Pyro5/serializers.py | 44 +++++++++++++++++++++++++++++++++++++- Pyro5/server.py | 48 ++++++++++++++++++++++++------------------ tests/test_protocol.py | 4 ++-- 5 files changed, 76 insertions(+), 33 deletions(-) diff --git a/Pyro5/client.py b/Pyro5/client.py index 213d538..4bb7267 100644 --- a/Pyro5/client.py +++ b/Pyro5/client.py @@ -166,7 +166,6 @@ def __dir__(self): def _pyroRelease(self): """release the connection to the pyro daemon""" - self.__check_owner() if self._pyroConnection is not None: self._pyroConnection.close() self._pyroConnection = None @@ -194,7 +193,6 @@ def __pyroSetTimeout(self, timeout): def _pyroInvoke(self, methodname, vargs, kwargs, flags=0, objectId=None): """perform the remote method call communication""" - self.__check_owner() core.current_context.response_annotations = {} if self._pyroConnection is None: self.__pyroCreateConnection() @@ -326,7 +324,6 @@ def connect_and_handshake(conn): log.error(err) raise errors.ProtocolError(err) - self.__check_owner() if self._pyroConnection is not None: return False # already connected uri = core.resolve(self._pyroUri) @@ -451,11 +448,6 @@ def __serializeBlobArgs(self, vargs, kwargs, annotations, flags, objectId, metho # replaces SerializedBlob argument with the data to be serialized return serializer.dumpsCall(objectId, methodname, blob._data, kwargs), flags - def __check_owner(self): - if get_ident() != self.__pyroOwnerThread: - raise errors.PyroError("the calling thread is not the owner of this proxy, " - "create a new proxy in this thread or transfer ownership.") - class _RemoteMethod(object): """method call abstraction""" diff --git a/Pyro5/nameserver.py b/Pyro5/nameserver.py index d5aba05..f14bce6 100644 --- a/Pyro5/nameserver.py +++ b/Pyro5/nameserver.py @@ -444,7 +444,8 @@ def ping(self): class NameServerDaemon(server.Daemon): """Daemon that contains the Name Server.""" - def __init__(self, host=None, port=None, unixsocket=None, nathost=None, natport=None, storage=None): + def __init__(self, host=None, port=None, unixsocket=None, nathost=None, natport=None, storage=None, + only_exposed=True): if host is None: host = config.HOST elif not isinstance(host, str): @@ -471,7 +472,7 @@ def __init__(self, host=None, port=None, unixsocket=None, nathost=None, natport= if existing_count > 0: log.debug("number of existing entries in storage: %d", existing_count) super(NameServerDaemon, self).__init__(host, port, unixsocket, nathost=nathost, natport=natport) - self.register(self.nameserver, core.NAMESERVER_NAME) + self.register(self.nameserver, core.NAMESERVER_NAME, only_exposed=only_exposed) metadata = {"class:Pyro5.nameserver.NameServer"} self.nameserver.register(core.NAMESERVER_NAME, self.uriFor(self.nameserver), metadata=metadata) if config.NS_AUTOCLEAN > 0: diff --git a/Pyro5/serializers.py b/Pyro5/serializers.py index 7904001..31733e5 100644 --- a/Pyro5/serializers.py +++ b/Pyro5/serializers.py @@ -6,6 +6,8 @@ import array import builtins +import copyreg +import pickle import uuid import logging import struct @@ -20,6 +22,7 @@ from . import errors __all__ = ["SerializerBase", "SerpentSerializer", "JsonSerializer", "MarshalSerializer", "MsgpackSerializer", + "PickleSerializer", "serializers", "serializers_by_id"] log = logging.getLogger("Pyro5.serializers") @@ -492,12 +495,51 @@ def register_type_replacement(cls, object_type, replacement_function): cls.__type_replacements[object_type] = replacement_function +class PickleSerializer(SerializerBase): + """ + A (de)serializer that wraps the Pickle serialization protocol. + It can optionally compress the serialized data, and is thread safe. + """ + serializer_id = 5 # never change this + + # def _convertToBytes(self, data): + # ret = super()._convertToBytes(data) + # return self.loads(data) + + def dumpsCall(self, obj, method, vargs, kwargs): + return pickle.dumps((obj, method, vargs, kwargs)) + + def dumps(self, data): + return pickle.dumps(data) + + def loadsCall(self, data): + data = self._convertToBytes(data) + return pickle.loads(data) + + def loads(self, data): + data = self._convertToBytes(data) + return pickle.loads(data) + + @classmethod + def register_type_replacement(cls, object_type, replacement_function): + def copyreg_function(obj): + return replacement_function(obj).__reduce__() + + if object_type is type or not inspect.isclass(object_type): + raise ValueError("refusing to register replacement for a non-type or the type 'type' itself") + try: + copyreg.pickle(object_type, copyreg_function) + except TypeError: + pass + + """The various serializers that are supported""" serializers = { "serpent": SerpentSerializer(), "marshal": MarshalSerializer(), "json": JsonSerializer(), - "msgpack": MsgpackSerializer() + "msgpack": MsgpackSerializer(), + "pickle": PickleSerializer(), } """The available serializers by their internal id""" diff --git a/Pyro5/server.py b/Pyro5/server.py index 76acbec..3f69d21 100644 --- a/Pyro5/server.py +++ b/Pyro5/server.py @@ -14,6 +14,8 @@ import logging import inspect import warnings +from functools import partial + import serpent from . import config, core, errors, serializers, socketutil, protocol, client @@ -152,13 +154,13 @@ def info(self): core.DAEMON_NAME, self.daemon.locationStr, self.daemon.natLocationStr, len(self.daemon.objectsById), self.daemon.transportServer) - def get_metadata(self, objectId, as_lists=False): + def get_metadata(self, objectId, as_lists=False, only_exposed=True): """ Get metadata for the given object (exposed methods, oneways, attributes). """ obj = self.daemon.objectsById.get(objectId) if obj is not None: - metadata = get_exposed_members(obj, as_lists=as_lists) + metadata = get_exposed_members(obj, only_exposed=only_exposed, as_lists=as_lists) if not metadata["methods"] and not metadata["attrs"]: # Something seems wrong: nothing is remotely exposed. warnings.warn("Class %r doesn't expose any methods or attributes. Did you forget setting @expose on them?" % type(obj)) @@ -192,6 +194,8 @@ class Daemon(object): to the appropriate objects. """ + only_exposed = True + def __init__(self, host=None, port=0, unixsocket=None, nathost=None, natport=None, interface=DaemonObject, connected_socket=None): if connected_socket: nathost = natport = None @@ -265,7 +269,7 @@ def selector(self): return self.transportServer.selector @staticmethod - def serveSimple(objects, host=None, port=0, daemon=None, ns=True, verbose=True): + def serveSimple(objects, host=None, port=0, daemon=None, ns=True, verbose=True, only_exposed=True): """ Basic method to fire up a daemon (or supply one yourself). objects is a dict containing objects to register as keys, and @@ -284,7 +288,7 @@ def serveSimple(objects, host=None, port=0, daemon=None, ns=True, verbose=True): localname = None # name is used for the name server else: localname = name # no name server, use name in daemon - uri = daemon.register(obj, localname) + uri = daemon.register(obj, localname, only_exposed=only_exposed) if verbose: print("Object {0}:\n uri = {1}".format(repr(obj), uri)) if name and ns: @@ -359,7 +363,8 @@ def _handshake(self, conn, denied_reason=None): handshake_response = self.validateHandshake(conn, data["handshake"]) handshake_response = { "handshake": handshake_response, - "meta": self.objectsById[core.DAEMON_NAME].get_metadata(data["object"], as_lists=True) + "meta": self.objectsById[core.DAEMON_NAME].get_metadata(data["object"], as_lists=True, + only_exposed=self.only_exposed) } data = serializer.dumps(handshake_response) msgtype = protocol.MSG_CONNECTOK @@ -454,7 +459,7 @@ def handleRequest(self, conn): # batched method calls, loop over them all and collect all results data = [] for method, vargs, kwargs in vargs: - method = get_attribute(obj, method) + method = get_attribute(obj, method, only_exposed=self.only_exposed) try: result = method(*vargs, **kwargs) # this is the actual method call to the Pyro object except Exception as xv: @@ -469,12 +474,12 @@ def handleRequest(self, conn): # normal single method call if method == "__getattr__": # special case for direct attribute access (only exposed @properties are accessible) - data = get_exposed_property_value(obj, vargs[0]) + data = get_exposed_property_value(obj, vargs[0], only_exposed=self.only_exposed) elif method == "__setattr__": # special case for direct attribute access (only exposed @properties are accessible) - data = set_exposed_property_value(obj, vargs[0], vargs[1]) + data = set_exposed_property_value(obj, vargs[0], vargs[1], only_exposed=self.only_exposed) else: - method = get_attribute(obj, method) + method = get_attribute(obj, method, only_exposed=self.only_exposed) if request_flags & protocol.FLAGS_ONEWAY: # oneway call to be run inside its own thread, otherwise client blocking can still occur # on the next call on the same proxy @@ -636,7 +641,7 @@ def _sendExceptionResponse(self, connection, seq, serializer_id, exc_value, tbin protocol.log_wiredata(log, "daemon wiredata sending (error response)", msg) connection.send(msg.data) - def register(self, obj_or_class, objectId=None, force=False): + def register(self, obj_or_class, objectId=None, force=False, only_exposed=True): """ Register a Pyro object under the given id. Note that this object is now only known inside this daemon, it is not automatically available in a name server. @@ -668,9 +673,11 @@ def register(self, obj_or_class, objectId=None, force=False): # we need to do this for all known serializers for ser in serializers.serializers.values(): if inspect.isclass(obj_or_class): - ser.register_type_replacement(obj_or_class, pyro_obj_to_auto_proxy) + ser.register_type_replacement(obj_or_class, + partial(pyro_obj_to_auto_proxy, only_exposed=only_exposed)) else: - ser.register_type_replacement(type(obj_or_class), pyro_obj_to_auto_proxy) + ser.register_type_replacement(type(obj_or_class), + partial(pyro_obj_to_auto_proxy, only_exposed=only_exposed)) # register the object/class in the mapping self.objectsById[obj_or_class._pyroId] = obj_or_class return self.uriFor(objectId) @@ -730,7 +737,7 @@ def resetMetadataCache(self, objectOrId, nat=True): reset_exposed_members(registered_object, as_lists=True) reset_exposed_members(registered_object, as_lists=False) - def proxyFor(self, objectOrId, nat=True): + def proxyFor(self, objectOrId, nat=True, only_exposed=True): """ Get a fully initialized Pyro Proxy for the given object (or object id) for this daemon. If nat is False, the configured NAT address (if any) is ignored. @@ -743,7 +750,7 @@ def proxyFor(self, objectOrId, nat=True): registered_object = self.objectsById[uri.object] except KeyError: raise errors.DaemonError("object isn't registered in this daemon") - meta = get_exposed_members(registered_object) + meta = get_exposed_members(registered_object, only_exposed=only_exposed) proxy._pyroGetMetadata(known_metadata=meta) return proxy @@ -825,16 +832,16 @@ def __deserializeBlobArgs(self, protocolmsg): serializers.SerializerBase.register_class_to_dict(Daemon, serializers.serialize_pyro_object_to_dict, serpent_too=False) -def pyro_obj_to_auto_proxy(obj): +def pyro_obj_to_auto_proxy(obj, only_exposed=True): """reduce function that automatically replaces Pyro objects by a Proxy""" daemon = getattr(obj, "_pyroDaemon", None) if daemon: # only return a proxy if the object is a registered pyro object - return daemon.proxyFor(obj) + return daemon.proxyFor(obj, only_exposed=only_exposed) return obj -def get_attribute(obj, attr): +def get_attribute(obj, attr, only_exposed=True): """ Resolves an attribute name to an object. Raises an AttributeError if any attribute in the chain starts with a '``_``'. @@ -843,11 +850,12 @@ def get_attribute(obj, attr): """ if is_private_attribute(attr): raise AttributeError("attempt to access private attribute '%s'" % attr) - else: - obj = getattr(obj, attr) + obj = getattr(obj, attr) if getattr(obj, "_pyroExposed", False): return obj - raise AttributeError("attempt to access unexposed attribute '%s'" % attr) + if only_exposed: + raise AttributeError("attempt to access unexposed attribute '%s'" % attr) + return obj __exposed_member_cache = {} diff --git a/tests/test_protocol.py b/tests/test_protocol.py index f047d89..5312a7c 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -61,12 +61,12 @@ def test_validate(self): msg.data[38] = 0xff # kill the magic number with pytest.raises(Pyro5.errors.ProtocolError) as x: Pyro5.protocol.ReceivingMessage.validate(msg.data) - assert "magic number" in str(x) + assert "magic number" in str(x.value) msg.data[38] = orig_magic # repair the magic number msg.data[5] = 0xff # invalid protocol version with pytest.raises(Pyro5.errors.ProtocolError) as x: Pyro5.protocol.ReceivingMessage.validate(msg.data) - assert "protocol version" in str(x) + assert "protocol version" in str(x.value) def test_create_nopayload(self): send_msg = self.createmessage(compression=True)