I implemented Pickle-RPC in reference to Python's xmlrpc module.
From client side, I can call methods of the instance which are registered to the RPC server.
s = ServerProxy(('127.0.0.1', 49152), DatagramRequestSender)
s.foo('bar')
I also want to access properties.
s = ServerProxy(('127.0.0.1', 49152), DatagramRequestSender)
s.foobar
How should I implement?
client.py
import pickle
import socket
from io import BytesIO
class StreamRequestSender:
max_packet_size = 8192
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
def send_request(self, address, request):
with socket.socket(self.address_family, self.socket_type) as client_socket:
with client_socket.makefile('wb') as wfile:
pickle.dump(request, wfile)
with client_socket.makefile('rb') as rfile:
response = pickle.load(rfile)
return response
class DatagramRequestSender(StreamRequestSender):
socket_type = socket.SOCK_DGRAM
def send_request(self, address, request):
with socket.socket(self.address_family, self.socket_type) as client_socket:
with BytesIO() as wfile:
pickle.dump(request, wfile)
client_socket.sendto(wfile.getvalue(), address)
data = client_socket.recv(self.max_packet_size)
with BytesIO(data) as rfile:
response = pickle.load(rfile)
return response
class ServerProxy:
def __init__(self, address, RequestSenderClass):
self.__address = address
self.__request_sender = RequestSenderClass()
def __send(self, method, args):
request = (method, args)
response = self.__request_sender.send_request(self.__address, request)
return response
def __getattr__(self, name):
return _Method(self.__send, name)
class _Method:
def __init__(self, send, name):
self.__send = send
self.__name = name
def __getattr__(self, name):
return _Method(self.__send, "{}.{}".format(self.__name, name))
def __call__(self, *args):
return self.__send(self.__name, args)
if __name__ == '__main__':
s = ServerProxy(('127.0.0.1', 49152), DatagramRequestSender)
print(s.pow(2, 160))
server.py
import pickle
import sys
from socketserver import StreamRequestHandler, DatagramRequestHandler, ThreadingTCPServer, ThreadingUDPServer
class RPCRequestHandler:
def handle(self):
method, args = pickle.load(self.rfile)
response = self.server.dispatch(method, args)
pickle.dump(response, self.wfile)
class StreamRPCRequestHandler(RPCRequestHandler, StreamRequestHandler):
pass
class DatagramRPCRequestHandler(RPCRequestHandler, DatagramRequestHandler):
pass
class RPCDispatcher:
def __init__(self, instance=None):
self.__instance = instance
def register_instance(self, instance):
self.__instance = instance
def dispatch(self, method, args):
_method = None
if self.__instance is not None:
try:
_method = self._resolve_dotted_attribute(self.__instance, method)
except AttributeError:
pass
if _method is not None:
return _method(*args)
else:
raise Exception('method "{}" is not supported'.format(method))
@staticmethod
def _resolve_dotted_attribute(obj, dotted_attribute):
attributes = dotted_attribute.split('.')
for attribute in attributes:
if attribute.startswith('_'):
raise AttributeError('attempt to access private attribute "{}"'.format(attribute))
else:
obj = getattr(obj, attribute)
return obj
class RPCServer(ThreadingUDPServer, RPCDispatcher):
def __init__(self, server_address, RPCRequestHandlerClass, instance=None):
ThreadingUDPServer.__init__(self, server_address, RPCRequestHandlerClass)
RPCDispatcher.__init__(self, instance)
if __name__ == '__main__':
class ExampleService:
def pow(self, base, exp):
return base ** exp
s = RPCServer(('127.0.0.1', 49152), DatagramRPCRequestHandler, ExampleService())
print('Serving Pickle-RPC on localhost port 49152')
print('It is advisable to run this example server within a secure, closed network.')
try:
s.serve_forever()
except KeyboardInterrupt:
print("\nKeyboard interrupt received, exiting.")
s.server_close()
sys.exit(0)
You'll have to intercept the attribute access on the object via overriding its __getattr__
method, figuring out that it is just an attribute access instead of a method call, and send a specific message to your server that doesn't call the method but instead returns the value of the referenced attribute directly. This is precisely what Pyro4 does. Code: https://github.com/irmen/Pyro4/blob/master/src/Pyro4/core.py#L255
Also: using pickle as a serialization format is a huge security problem. It allows arbitrary remote code execution. So for the love of all that is holy make sure you absolutely trust the clients and never accept random connections from over the internet.