tcp-proxy2-rev0.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. #!/usr/bin/env python3
  2. import sys
  3. import re
  4. from twisted.internet import defer
  5. from twisted.internet import protocol
  6. from twisted.internet import reactor
  7. from twisted.python import log
  8. from twisted.python.logfile import DailyLogFile
  9. # from twisted.enterprise import adbapi
  10. import pendulum
  11. from subprocess import check_output
  12. # This isn't the best configuration, but it's simple
  13. # and works. Mostly.
  14. try:
  15. from config_dev import *
  16. except ModuleNotFoundError:
  17. from config import *
  18. # from config import *
  19. # Connect to:
  20. # HOST = "twgs"
  21. # PORT = 2002
  22. # Listen on:
  23. # LISTEN_PORT = 2002
  24. # LISTEN_ON = "0.0.0.0"
  25. version = check_output(
  26. [
  27. "git",
  28. "describe",
  29. "--long",
  30. "--tags",
  31. # "--dirty",
  32. "--always",
  33. "--match",
  34. "v[0-9]\.[0-9]\.[0-9]",
  35. ],
  36. universal_newlines=True,
  37. ).strip()
  38. # Cleans all ANSI
  39. cleaner = re.compile(r"\x1b\[[0-9;]*[A-Zmh]")
  40. # Looks for ANSI (that should be considered to be a newline)
  41. makeNL = re.compile(r"\x1b\[[0-9;]*[J]")
  42. def treatAsNL(line):
  43. """ Replace any ANSI codes that would be better understood as newlines. """
  44. global makeNL
  45. return makeNL.sub("\n", line)
  46. def cleanANSI(line):
  47. """ Remove all ANSI codes. """
  48. global cleaner
  49. return cleaner.sub("", line)
  50. # return re.sub(r'\x1b\[([0-9,A-Z]{1,2}(;[0-9]{1,2})?(;[0-9]{3})?)?[m|K]?', '', line)
  51. class Game(protocol.Protocol):
  52. def __init__(self):
  53. # user is rlogin username
  54. self.user = None
  55. # buffer is used to capture the rlogin username
  56. self.buffer = ""
  57. def connectionMade(self):
  58. log.msg("Client: connected to peer")
  59. self.queue_player = self.factory.queue_player
  60. self.queue_game = self.factory.queue_game
  61. self.setPlayerReceived()
  62. # self.queue_twgs.get().addCallback(self.serverDataReceived)
  63. def setPlayerReceived(self):
  64. """ Get deferred from client queue, callback clientDataReceived. """
  65. self.queue_player.get().addCallback(self, playerDataReceived)
  66. def playerDataReceived(self, chunk):
  67. # rlogin looks like this: \x00 password \x00 username \x00 terminal/speed \x00
  68. # b'\x00up2lat3\x00bugz\x00ansi-bbs/115200\x00'
  69. # We're looking for 4 \x00!
  70. # TODO: Line processing, and line cleaning (remove ANSI color codes)
  71. if chunk is False:
  72. self.queue_twgs = None
  73. log.msg("Client: disconnecting from peer")
  74. self.factory.continueTrying = False
  75. self.transport.loseConnection()
  76. else:
  77. if self.user is None:
  78. # Decode the rlogin data
  79. self.buffer += chunk.decode("utf-8", "ignore")
  80. # Ok, process this
  81. # self.buffer += chunk.decode('utf-8')
  82. # We don't have the username yet
  83. parts = self.buffer.split("\x00")
  84. if len(parts) >= 5:
  85. # Got it!
  86. self.user = parts[1]
  87. log.msg("User: {0}".format(self.user))
  88. # Reset buffer -- remove everything before last \x00
  89. zpos = self.buffer.rindex("\x00")
  90. self.buffer = self.buffer[zpos + 1 :]
  91. self.buffer = ""
  92. # init sqlite db using the username
  93. self.factory.getUser(self.user)
  94. # Pass received data to the server
  95. self.transport.write(chunk)
  96. self.setPlayerReceived()
  97. # self.queue_twgs.get().addCallback(self.serverDataReceived)
  98. def dataReceived(self, chunk):
  99. # log.msg("Client: %d bytes received from peer" % len(chunk))
  100. # clean, strip ANSI, etc.
  101. # log.msg("<<", chunk.decode("utf-8", "ignore"))
  102. # log.msg("<<", repr(chunk))
  103. # self.factory.queue_client.put(chunk)
  104. # self.queue_game.put(chunk)
  105. self.queue_player.put(chunk)
  106. def connectionLost(self, why):
  107. log.msg("Game connectionLost because: %s" % why)
  108. self.queue_game.put(False)
  109. # self.factory.queue_client.put(False)
  110. # self.queue_twgs = None
  111. self.transport.loseConnection()
  112. class GlueFactory(protocol.ClientFactory):
  113. # class GlueFactory(protocol.Factory):
  114. maxDelay = 10
  115. protocol = Game
  116. def __init__(self, twgs):
  117. self.twgs = twgs
  118. self.queue_player = twgs.queue_player
  119. self.queue_game = twgs.queue_game
  120. def closeIt(self):
  121. log.msg("closeIt")
  122. self.queue_player.put(False)
  123. def getUser(self, user):
  124. log.msg("getUser( %s )" % user)
  125. self.twgs.logUser(user)
  126. # This was needed when I replaced ClientFactory with Factory.
  127. # def clientConnectionLost(self, connector, why):
  128. # log.msg("clientconnectionlost: %s" % why)
  129. # self.queue_client.put(False)
  130. def clientConnectionFailed(self, connector, why):
  131. log.msg("connection to game failed: %s" % why)
  132. self.queue_player.put(b"Sorry! I'm Unable to connect to the game server.\r\n")
  133. # syncterm gets cranky/locks up if we close this here.
  134. # (Because it is still sending rlogin information?)
  135. reactor.callLater(2, self.closeIt)
  136. class Player(protocol.Protocol):
  137. def __init__(self):
  138. self.buffer = ""
  139. self.fpRaw = None
  140. self.fpLines = None
  141. self.action = None
  142. self.username = ""
  143. self.game = ""
  144. self.passon = True
  145. def connectionMade(self):
  146. """ connected, setup queues. """
  147. self.queue_player = defer.DeferredQueue()
  148. self.queue_game = defer.DeferredQueue()
  149. self.setGameReceived()
  150. # self.action = ProxyAction(self)
  151. factory = GlueFactory(self)
  152. # Make connection to the game server
  153. reactor.connectTCP(HOST, PORT, factory, 5)
  154. def setGameReceived(self):
  155. """ Get deferred from client queue, callback clientDataReceived. """
  156. self.queue_game.get().addCallback(self.gameDataReceived)
  157. def logUser(self, user):
  158. """ We have the username. """
  159. now = pendulum.now()
  160. self.username = user
  161. filename = now.format("YYYY-MM-DD_HHmm") + "-" + user.lower()
  162. # Are we saving RAW output?
  163. if RAW:
  164. self.fpRaw = open(filename + ".raw", "ab")
  165. self.fpLines = open(filename + ".lines", "a")
  166. print("Log created:", now.to_rss_string(), "\n", file=self.fpLines)
  167. def setGame(self, game):
  168. """ We have the game (A-P) they are playing. """
  169. if self.game != game:
  170. log.msg("USER {0} ENTERED {1}".format(self.username, self.game))
  171. self.data = {}
  172. self.game = game
  173. def gotLine(self, line):
  174. """ We got a line from the server.
  175. This is ANSI filtered.
  176. Backspaces have removed the character from the line.
  177. The line is unicode. We don't need to decode it. ;)
  178. """
  179. log.msg(">>> [{0}]".format(line))
  180. if self.fpLines is not None:
  181. print(line, file=self.fpLines)
  182. if "TWGS v2.20b" in line:
  183. if "www.eisonline.com" in line:
  184. # Must not be unicode
  185. # Is there a way to NOT have this logged?
  186. self.queue_client.put(
  187. (
  188. b"TWGS Proxy build "
  189. + version.encode()
  190. + b" is active. \x1b[1;34m~\x1b[0m to activate.\n\r\n\r",
  191. )
  192. )
  193. if "TradeWars Game Server" in line and "Copyright (C) EIS" in line:
  194. # We are not in a game
  195. self.game = ""
  196. if "Selection (? for menu): " in line:
  197. game = line[-1]
  198. if game >= "A" and game < "Q":
  199. self.setGame(game)
  200. # If we're not passing it on to the user, we better be looking at it.
  201. if self.action and not self.passon:
  202. self.action.server(line)
  203. def gameDataReceived(self, chunk):
  204. """ Data received from the client/player. """
  205. if chunk is False:
  206. self.transport.loseConnection()
  207. else:
  208. if type(chunk) is tuple:
  209. # Special case where we want to send something to the user
  210. # but not have it logged.
  211. self.transport.write(chunk[0])
  212. self.setGameReceived()
  213. else:
  214. if self.fpRaw is not None:
  215. self.fpRaw.write(chunk)
  216. self.buffer += chunk.decode("utf-8", "ignore")
  217. # Process any backspaces in the buffer
  218. while "\x08" in self.buffer:
  219. part = self.buffer.partition("\x08")
  220. self.buffer = part[0][:-1] + part[2]
  221. # Treat some ANSI codes as a newline (for purposes of Lines)
  222. self.buffer = treatAsNL(self.buffer)
  223. # I think I need something else in here. When something enters or leaves the sector
  224. # The message isn't shown on it's own line. I think they are using ANSI codes to
  225. # clear the line. (Which certainly would be faster!)
  226. # Break the buffer into lines
  227. while "\n" in self.buffer:
  228. part = self.buffer.partition("\n")
  229. line = part[0].replace("\r", "")
  230. # Clean ANSI codes from the line
  231. line = cleanANSI(line)
  232. self.gotLine(line)
  233. self.buffer = part[2]
  234. # log.msg("Server: writing %d bytes to original client" % len(chunk))
  235. if self.passon:
  236. self.transport.write(chunk)
  237. self.setGameReceived()
  238. def dataReceived(self, chunk):
  239. if self.action and self.action.isActive():
  240. # Do something completely different here
  241. self.action.received(chunk)
  242. else:
  243. # Did player activate hotkey?
  244. if chunk == b"~":
  245. self.action.activate(self.buffer)
  246. else:
  247. self.queue_game.put(chunk)
  248. def connectionLost(self, why):
  249. log.msg("lost connection %s" % why)
  250. self.queue_game.put(False)
  251. # Close log files, if open
  252. if self.fpRaw is not None:
  253. self.fpRaw.close()
  254. self.fpRaw = None
  255. if self.fpLines is not None:
  256. if self.buffer != "":
  257. print(self.buffer, file=self.fpLines)
  258. self.fpLines.close()
  259. self.fpLines = None
  260. def connectionFailed(self, why):
  261. log.msg("connectionFailed: %s" % why)
  262. if __name__ == "__main__":
  263. log.startLogging(DailyLogFile("proxy.log", "."))
  264. log.msg("This is version: %s" % version)
  265. factory = protocol.Factory()
  266. factory.protocol = Player
  267. reactor.listenTCP(LISTEN_PORT, factory, interface=LISTEN_ON)
  268. reactor.run()