tcp-proxy2.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  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. import pendulum
  10. from subprocess import check_output
  11. from colorama import Fore, Back, Style
  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. # Extract the version information from git.
  19. # The match gives us only tags starting with v[0-9]* Using anything else trips up on double digits.
  20. version = check_output(
  21. [
  22. "git",
  23. "describe",
  24. "--abbrev=8",
  25. "--long",
  26. "--tags",
  27. "--dirty",
  28. "--always",
  29. "--match",
  30. "v[0-9]*",
  31. ],
  32. universal_newlines=True,
  33. ).strip()
  34. def merge(color_string):
  35. """ Given a string of colorama ANSI, merge them if you can. """
  36. return color_string.replace("m\x1b[", ";")
  37. # Cleans all ANSI
  38. cleaner = re.compile(r"\x1b\[[0-9;]*[A-Zmh]")
  39. # Looks for ANSI (that should be considered to be a newline)
  40. # This needs to see what is send when something enters / leaves
  41. # the player's current sector. (That doesn't work/isn't
  42. # detected. NNY!)
  43. makeNL = re.compile(r"\x1b\[[0-9;]*[J]")
  44. def treatAsNL(line):
  45. """ Replace any ANSI codes that would be better understood as newlines. """
  46. global makeNL
  47. return makeNL.sub("\n", line)
  48. def cleanANSI(line):
  49. """ Remove all ANSI codes. """
  50. global cleaner
  51. return cleaner.sub("", line)
  52. # return re.sub(r'\x1b\[([0-9,A-Z]{1,2}(;[0-9]{1,2})?(;[0-9]{3})?)?[m|K]?', '', line)
  53. class Observer(object):
  54. def __init__(self):
  55. self.dispatch = {}
  56. def emit(self, signal, message):
  57. """ emit a signal, return True if sent somewhere. """
  58. if signal in self.dispatch:
  59. # something to do
  60. ret = False
  61. for listener in self.dispatch[signal]:
  62. ret = True
  63. reactor.callLater(0, listener, message)
  64. return ret
  65. return False
  66. def connect(self, signal, func):
  67. """ Connect a signal to a given function. """
  68. if not signal in self.dispatch:
  69. self.dispatch[signal] = []
  70. self.dispatch[signal].append(func)
  71. def disconnect(self, signal, func):
  72. """ Disconnect a signal with a certain function. """
  73. if signal in self.dispatch:
  74. self.dispatch[signal].remove(func)
  75. if len(self.dispatch[signal]) == 0:
  76. self.dispatch.pop(signal)
  77. def get_funcs(self, signal):
  78. """ Gives a copy of the dispatch for a given signal. """
  79. if signal in self.dispatch:
  80. return list(self.dispatch[signal])
  81. else:
  82. return []
  83. def set_funcs(self, signal, funcs):
  84. """ Replaces the dispatch for a given signal. """
  85. if signal in self.dispatch:
  86. if len(funcs) == 0:
  87. self.dispatch.pop(signal)
  88. else:
  89. self.dispatch = list(funcs)
  90. else:
  91. if len(funcs) != 0:
  92. self.dispatch = list(funcs)
  93. class Game(protocol.Protocol):
  94. def __init__(self):
  95. self.buffer = ""
  96. self.game = None
  97. self.to_player = True
  98. def connectionMade(self):
  99. log.msg("Connected to Game Server")
  100. self.queue_player = self.factory.queue_player
  101. self.queue_game = self.factory.queue_game
  102. self.observer = self.factory.observer
  103. self.setPlayerReceived()
  104. self.observer.connect("user", self.show_user)
  105. self.observer.connect("user-game", self.show_game)
  106. def show_user(self, user):
  107. """ This doesn't always show up. :P
  108. Because we're still connecting to the game server when
  109. the player object has already sent the 'user' signal.
  110. """
  111. log.msg("## User:", user)
  112. def show_game(self, game):
  113. log.msg("## User-Game:", game)
  114. def setPlayerReceived(self):
  115. """ Get deferred from client queue, callback clientDataReceived. """
  116. self.queue_player.get().addCallback(self.playerDataReceived)
  117. def playerDataReceived(self, chunk):
  118. if chunk is False:
  119. self.queue_player = None
  120. log.msg("Player: disconnected, close connection to game")
  121. # I don't believe I need this if I'm using protocol.Factory
  122. self.factory.continueTrying = False
  123. self.transport.loseConnection()
  124. else:
  125. # Pass received data to the server
  126. self.transport.write(chunk)
  127. self.setPlayerReceived()
  128. def lineReceived(self, line):
  129. """ line received from the game. """
  130. if LOG_LINES:
  131. log.msg(">> [{0}]".format(line))
  132. if "TWGS v2.20b" in line and "www.eisonline.com" in line:
  133. self.queue_game.put(
  134. "TWGS Proxy build "
  135. + version
  136. + " is active. \x1b[1;34m~\x1b[0m to activate.\n\r\n\r",
  137. )
  138. if "TradeWars Game Server" in line and "Copyright (C) EIS" in line:
  139. # We are not in a game
  140. if not self.game is None:
  141. # We were in a game.
  142. self.game = None
  143. self.observer.emit("user-game", (self.factory.player.user, self.game))
  144. if "Selection (? for menu): " in line:
  145. game = line[-1]
  146. if game >= "A" and game < "Q":
  147. self.game = game
  148. log.msg("Game: {0}".format(self.game))
  149. self.observer.emit("user-game", (self.factory.player.user, self.game))
  150. self.observer.emit("game-line", line)
  151. def dataReceived(self, chunk):
  152. """ Data received from the Game.
  153. Remove backspaces.
  154. Treat some ANSI codes as NewLine.
  155. Remove ANSI.
  156. Break into lines.
  157. Trim out carriage returns.
  158. Call lineReceived().
  159. "Optionally" pass data to player.
  160. FUTURE: trigger on prompt. [cleanANSI(buffer)]
  161. """
  162. # Sequence error:
  163. # If I don't put the chunk(I received) to the player.
  164. # anything I display -- lineReceive() put() ... would
  165. # be out of order. (I'd be responding -- before it
  166. # was displayed to the user.)
  167. if self.to_player:
  168. self.queue_game.put(chunk)
  169. self.buffer += chunk.decode("utf-8", "ignore")
  170. # Process any backspaces
  171. while "\x08" in self.buffer:
  172. part = self.buffer.partition("\x08")
  173. self.buffer = part[0][:-1] + part[2]
  174. # Treat some ANSI codes as a newline
  175. self.buffer = treatAsNL(self.buffer)
  176. # Break into lines
  177. while "\n" in self.buffer:
  178. part = self.buffer.partition("\n")
  179. line = part[0].replace("\r", "")
  180. # Clean ANSI codes from line
  181. line = cleanANSI(line)
  182. self.lineReceived(line)
  183. self.buffer = part[2]
  184. self.observer.emit("prompt", cleanANSI(self.buffer))
  185. def connectionLost(self, why):
  186. log.msg("Game connectionLost because: %s" % why)
  187. self.queue_game.put(False)
  188. self.transport.loseConnection()
  189. class GlueFactory(protocol.ClientFactory):
  190. # class GlueFactory(protocol.Factory):
  191. maxDelay = 10
  192. protocol = Game
  193. def __init__(self, player):
  194. self.player = player
  195. self.queue_player = player.queue_player
  196. self.queue_game = player.queue_game
  197. self.observer = player.observer
  198. def closeIt(self):
  199. log.msg("closeIt")
  200. self.queue_game.put(False)
  201. def getUser(self, user):
  202. log.msg("getUser( %s )" % user)
  203. self.twgs.logUser(user)
  204. # This was needed when I replaced ClientFactory with Factory.
  205. # def clientConnectionLost(self, connector, why):
  206. # log.msg("clientconnectionlost: %s" % why)
  207. # self.queue_client.put(False)
  208. def clientConnectionFailed(self, connector, why):
  209. log.msg("connection to game failed: %s" % why)
  210. self.queue_game.put(b"Sorry! I'm Unable to connect to the game server.\r\n")
  211. # syncterm gets cranky/locks up if we close this here.
  212. # (Because it is still sending rlogin information?)
  213. reactor.callLater(2, self.closeIt)
  214. class Player(protocol.Protocol):
  215. def __init__(self):
  216. self.buffer = ""
  217. self.user = None
  218. self.observer = Observer()
  219. def connectionMade(self):
  220. """ connected, setup queues.
  221. queue_player is data from player.
  222. queue_game is data to player. (possibly from game)
  223. """
  224. self.queue_player = defer.DeferredQueue()
  225. self.queue_game = defer.DeferredQueue()
  226. self.setGameReceived()
  227. # Connect GlueFactory to this Player object.
  228. factory = GlueFactory(self)
  229. # Make connection to the game server
  230. reactor.connectTCP(HOST, PORT, factory, 5)
  231. def setGameReceived(self):
  232. """ Get deferred from client queue, callback clientDataReceived. """
  233. self.queue_game.get().addCallback(self.gameDataReceived)
  234. def gameDataReceived(self, chunk):
  235. """ Data received from the game. """
  236. if chunk is False:
  237. self.transport.loseConnection()
  238. else:
  239. if type(chunk) == bytes:
  240. self.transport.write(chunk)
  241. elif type(chunk) == str:
  242. self.transport.write(chunk.encode())
  243. else:
  244. log.err("gameDataReceived: type (%s) given!", type(chunk))
  245. self.transport.write(chunk)
  246. self.setGameReceived()
  247. def dataReceived(self, chunk):
  248. if self.user is None:
  249. self.buffer += chunk.decode("utf-8", "ignore")
  250. parts = self.buffer.split("\x00")
  251. if len(parts) >= 5:
  252. # rlogin we have the username
  253. self.user = parts[1]
  254. log.msg("User: {0}".format(self.user))
  255. zpos = self.buffer.rindex("\x00")
  256. self.buffer = self.buffer[zpos + 1 :]
  257. # but I don't need the buffer anymore, so:
  258. self.buffer = ""
  259. # Pass user value on to whatever needs it.
  260. self.observer.emit("user", self.user)
  261. if not self.observer.emit("player", chunk):
  262. # Was not dispatched. Send to game.
  263. self.queue_player.put(chunk)
  264. def connectionLost(self, why):
  265. log.msg("lost connection %s" % why)
  266. self.queue_player.put(False)
  267. def connectionFailed(self, why):
  268. log.msg("connectionFailed: %s" % why)
  269. if __name__ == "__main__":
  270. if LOGFILE:
  271. log.startLogging(DailyLogFile("proxy.log", "."))
  272. else:
  273. log.startLogging(sys.stdout)
  274. log.msg("This is version: %s" % version)
  275. factory = protocol.Factory()
  276. factory.protocol = Player
  277. reactor.listenTCP(LISTEN_PORT, factory, interface=LISTEN_ON)
  278. reactor.run()