/responder3/core/gwss.py

https://github.com/skelsec/Responder3 · Python · 190 lines · 157 code · 3 blank · 30 comment · 0 complexity · ebf9436b332441af1ada509b123332d2 MD5 · raw file

  1. # Work with Python 3.6
  2. """
  3. Generic websocket based client-server framework
  4. For logging it uses the Logger object and the r3exception decorators
  5. Idea was to provide a common interface for remote communications where devs
  6. do not need to deal with teh actual server-client comms.
  7. Client reads a queue for !strings! and dispatches it to the server
  8. Server waits for incoming clients, and dispatches all incoming messages to a queue as GWSSPacket
  9. When cert-auth based SSL is set up via the SSL-CTX parameters,
  10. the server will yield the client's cerificate to the output queue
  11. Current: One-way comms
  12. The server is only capable of retrieving messages
  13. The client is only capable of sending messages
  14. TODO: Make it two-way comms
  15. """
  16. import asyncio
  17. import websockets
  18. import uuid
  19. from responder3.core.logging.logger import *
  20. from responder3.core.logging.log_objects import *
  21. from responder3.core.commons import *
  22. class GWSSPacket:
  23. def __init__(self, client_ip, client_port, data, client_cert = None):
  24. self.client_ip = client_ip
  25. self.client_port = client_port
  26. self.client_cert = client_cert
  27. self.data = data
  28. def get_addr_s(self):
  29. return '%s:%d' % (self.client_ip, self.client_port)
  30. class GenericWSPacket:
  31. def __init__(self, data):
  32. self.data = data #must be string!!!
  33. def to_dict(self):
  34. t = {}
  35. t['data'] = self.data
  36. return t
  37. def to_json(self):
  38. return json.dumps(self.to_dict())
  39. @staticmethod
  40. def from_dict(d):
  41. data = d['data']
  42. return GenericWSPacket(data)
  43. @staticmethod
  44. def from_json(raw_data):
  45. return GenericWSPacket.from_dict(json.loads(raw_data))
  46. class GenericWSClient:
  47. def __init__(self, logQ, server_url, out_queue, ssl_ctx = None):
  48. self.logger = Logger('GenericWSClient', logQ = logQ)
  49. self.server_url = server_url
  50. self.ssl_ctx = ssl_ctx
  51. self.out_queue = out_queue
  52. self.shutdown_evt = asyncio.Event() #to completely shutdown the client
  53. self.shutdown_session_evt = asyncio.Event() #to disconnect from server, and try to connect back
  54. self.ws_ping_interval = 5
  55. @r3exception
  56. async def keepalive(self, ws):
  57. await self.logger.debug('Keepalive running!')
  58. while not self.shutdown_session_evt.is_set():
  59. try:
  60. pong_waiter = await ws.ping()
  61. await asyncio.sleep(self.ws_ping_interval)
  62. except websockets.exceptions.ConnectionClosed:
  63. await self.logger.debug('Server disconnected!')
  64. self.shutdown_session_evt.set()
  65. continue
  66. except Exception as e:
  67. await self.logger.exception('Unexpected error!')
  68. self.shutdown_session_evt.set()
  69. continue
  70. @r3exception
  71. async def run(self):
  72. while not self.shutdown_evt.is_set():
  73. try:
  74. self.shutdown_session_evt.clear()
  75. await self.logger.debug('Connecting to server...')
  76. async with websockets.connect(self.server_url, ssl=self.ssl_ctx) as ws:
  77. await self.logger.debug('Connected to server!')
  78. asyncio.ensure_future(self.keepalive(ws))
  79. while not self.shutdown_session_evt.is_set():
  80. try:
  81. #waiting to get a log from the log queue, timeout is introducted so we can check
  82. #in the while loop above is the ws still exists
  83. str_data = await asyncio.wait_for(self.out_queue.get(), 1)
  84. except asyncio.TimeoutError:
  85. continue
  86. except:
  87. await self.logger.exception()
  88. try:
  89. packet = GenericWSPacket(str_data)
  90. await ws.send(packet.to_json())
  91. except Exception as e:
  92. self.shutdown_session_evt.set()
  93. raise e
  94. await self.logger.debug('Disconnecte from remote ws logger!')
  95. except Exception as e:
  96. await self.logger.exception()
  97. pass
  98. await asyncio.sleep(5)
  99. class GenericWSServer:
  100. def __init__(self, logQ, listen_ip, listen_port, queue_in, ssl_ctx = None):
  101. self.logger = Logger('GenericWSServer', logQ = logQ)
  102. self.listen_ip = listen_ip
  103. self.listen_port = listen_port
  104. self.ssl_ctx = ssl_ctx
  105. self.queue_in = queue_in
  106. self.shutdown_evt = asyncio.Event()
  107. self.shutdown_session_evt = asyncio.Event()
  108. self.classloader = R3ClientCommsClassLoader()
  109. self.clients = {} #client_id -> Responder3ClientSession
  110. self.ws_ping_interval = 5
  111. @r3exception
  112. async def keepalive(self, ws):
  113. await self.logger.debug('Keepalive running!')
  114. while not self.shutdown_session_evt.is_set():
  115. try:
  116. pong_waiter = await ws.ping()
  117. await asyncio.sleep(self.ws_ping_interval)
  118. except websockets.exceptions.ConnectionClosed:
  119. await self.logger.debug('Client disconnected!')
  120. self.shutdown_session_evt.set()
  121. continue
  122. except Exception as e:
  123. await self.logger.exception('Unexpected error!')
  124. self.shutdown_session_evt.set()
  125. continue
  126. @r3exception
  127. async def client_handler(self, ws, path):
  128. client_ip, client_port = ws.remote_address
  129. ################################################################################
  130. #!!!! If you see an error here, websockets library might have changed
  131. #By default the library doesnt offer high-lvel api to grab the client certificate
  132. #Check the new documentation of websockets if error comes in here!
  133. client_cert = ws.writer.get_extra_info('peercert')
  134. ################################################################################
  135. asyncio.ensure_future(self.keepalive(ws))
  136. client_id = str(uuid.uuid4()) #change this to ssl CN of the client!
  137. await self.logger.info('[%s] connected from %s' % (client_id, '%s:%d' % ws.remote_address))
  138. self.clients[client_id] = ws
  139. while not self.shutdown_session_evt.is_set():
  140. try:
  141. packet_raw = await ws.recv()
  142. except Exception as e:
  143. await self.logger.exception()
  144. self.shutdown_session_evt.set()
  145. continue
  146. packet = GenericWSPacket.from_json(packet_raw)
  147. gp = GWSSPacket(client_ip, client_port, packet.data, client_cert = client_cert)
  148. await self.queue_in.put(gp)
  149. del self.clients[client_id]
  150. await self.logger.info('[%s] disconnected' % client_id)
  151. async def run(self):
  152. server = await websockets.serve(self.client_handler, self.listen_ip, self.listen_port, ssl=self.ssl_ctx)
  153. await server.wait_closed()