'''This application acts as a proxy between clients and an rpc server. Both the server and the clients connect as a TCP client. ''' from twisted.internet import protocol, task from twisted.python import log from packetformat import PacketFormat import paatti_pb2 class LoginProtocol(PacketFormat): '''Wait for first packet and then hand off to either ClientSide or ServerSide handler. ''' def connectionMade(self): self.newproto = None log.msg("New connection") def messageReceived(self, msgtype, payload): if self.newproto is not None: self.newproto.messageReceived(msgtype, payload) return if msgtype != paatti_pb2.Login: log.msg("Not logged in, command " + str(msgtype)) error = paatti_pb2.ErrorResponse(scope = paatti_pb2.ErrorResponse.PROTOCOL, message = "Not logged in") self.sendMessage(paatti_pb2.Error, error) return req = paatti_pb2.LoginRequest.FromString(payload) if req.im_rpc_server: log.msg("Logged in as server") self.newproto = ServerSide() else: log.msg("Logged in as client") self.newproto = ClientSide() self.sendMessage(msgtype, paatti_pb2.LoginResponse()) self.newproto.factory = self.factory self.newproto.makeConnection(self.transport) self.transport.protocol = self.newproto class ServerSide(PacketFormat): '''Server-facing end of the proxy. A single one will exist.''' def connectionMade(self): if self.factory.server is not None: log.msg("Cleaning up old server") self.factory.server.transport.loseConnection() self.factory.server = self # Now is a great time to clean up requests from any lost clients self.factory.queue = [x for x in self.factory.queue if x[0] and x[0].connected] # Send remaining of the queued commands for client, msgtype, payload in self.factory.queue: self.sendMessage(msgtype, payload) def connectionLost(self, reason): log.msg("Server connection lost: " + reason.getErrorMessage()) self.factory.server = None def messageReceived(self, msgtype, payload): if not self.connected or self.transport.disconnecting: return # The request that this reply should match req = self.factory.queue[0] if msgtype != req[1] and msgtype != paatti_pb2.Error: self.protocolError("Response type doesn't match request. " + "\nRequest: " + repr(req) + "\nResponse: " + repr((msgtype, payload))) return # Completed, remove from queue self.factory.queue.pop(0) if req[0] and req[0].connected: req[0].sendMessage(msgtype, payload) class ClientSide(PacketFormat): '''Client-facing end of the proxy. Multiple of these will exist.''' def messageReceived(self, msgtype, payload): self.factory.queueMessage(self, msgtype, payload) def connectionLost(self, reason): log.msg("Client connection lost: " + reason.getErrorMessage()) self.connected = False class ProxyFactory(protocol.ServerFactory): '''Command queue is stored in the factory. Commands will persist over lost connections and reconnects, until eventually they complete. ''' protocol = LoginProtocol def __init__(self): # Messages are stored as (client, msgtype, payload) # List contains commands waiting for response, first item # is the next to complete. self.queue = [] self.server = None self.keepalivetask = task.LoopingCall(self.keepAlive) self.keepalivetask.start(60) def keepAlive(self): if not self.queue and self.server is not None: self.queueMessage(None, paatti_pb2.Ping, paatti_pb2.PingRequest()) elif self.queue: log.msg("Not sending keepalive, queue: " + repr(self.queue)) def queueMessage(self, client, msgtype, payload): self.queue.append((client, msgtype, payload)) if self.server is not None: self.server.sendMessage(msgtype, payload) from twisted.application import service, internet application = service.Application("rpcproxy") internet.TCPServer(50140, ProxyFactory()).setServiceParent(application)