tcp-proxy3.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  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.enterprise import adbapi
  9. import pendulum
  10. from subprocess import check_output
  11. RAW = True
  12. try:
  13. from config_dev import *
  14. except ModuleNotFoundError:
  15. from config import *
  16. # from config import *
  17. # Connect to:
  18. # HOST = "twgs"
  19. # PORT = 2002
  20. # Listen on:
  21. # LISTEN_PORT = 2002
  22. # LISTEN_ON = "0.0.0.0"
  23. version = check_output(
  24. [
  25. "git",
  26. "describe",
  27. "--long",
  28. "--tags",
  29. # "--dirty",
  30. "--always",
  31. "--match",
  32. "v[0-9]\.[0-9]\.[0-9]",
  33. ],
  34. universal_newlines=True,
  35. ).strip()
  36. cleaner = re.compile(r"\x1b\[[0-9;]*[A-Zmh]")
  37. makeNL = re.compile(r"\x1b\[[0-9;]*[J]")
  38. def treatAsNL(line):
  39. global makeNL
  40. return makeNL.sub("\n", line)
  41. def cleanANSI(line):
  42. global cleaner
  43. return cleaner.sub("", line)
  44. # return re.sub(r'\x1b\[([0-9,A-Z]{1,2}(;[0-9]{1,2})?(;[0-9]{3})?)?[m|K]?', '', line)
  45. class PlayerProtocol(protocol.Protocol):
  46. def __init__(self):
  47. self.user = None
  48. self.dbinit = False
  49. self.db = None
  50. self.buffer = ""
  51. def connectionMade(self):
  52. log.msg("Client: connected to peer")
  53. self.queue_twgs = self.factory.queue_twgs
  54. self.queue_twgs.get().addCallback(self.serverDataReceived)
  55. def serverDataReceived(self, chunk):
  56. # rlogin looks like this: \x00 password \x00 username \x00 terminal/speed \x00
  57. # b'\x00up2lat3\x00bugz\x00ansi-bbs/115200\x00'
  58. # We're looking for 4 \x00!
  59. # TODO: Line processing, and line cleaning (remove ANSI color codes)
  60. if chunk is False:
  61. self.queue_twgs = None
  62. log.msg("Client: disconnecting from peer")
  63. self.factory.continueTrying = False
  64. self.transport.loseConnection()
  65. else:
  66. if self.user is None:
  67. # Decode the rlogin data
  68. self.buffer += chunk.decode("utf-8", "ignore")
  69. # Ok, process this
  70. # self.buffer += chunk.decode('utf-8')
  71. # We don't have the username yet
  72. parts = self.buffer.split("\x00")
  73. if len(parts) >= 5:
  74. # Got it!
  75. self.user = parts[1]
  76. log.msg("User: {0}".format(self.user))
  77. # Reset buffer -- remove everything before last \x00
  78. zpos = self.buffer.rindex("\x00")
  79. self.buffer = self.buffer[zpos + 1 :]
  80. self.buffer = ""
  81. # init sqlite db using the username
  82. self.factory.getUser(self.user)
  83. # Pass received data to the server
  84. self.transport.write(chunk)
  85. self.queue_twgs.get().addCallback(self.serverDataReceived)
  86. def dataReceived(self, chunk):
  87. # log.msg("Client: %d bytes received from peer" % len(chunk))
  88. # clean, strip ANSI, etc.
  89. # log.msg("<<", chunk.decode("utf-8", "ignore"))
  90. # log.msg("<<", repr(chunk))
  91. self.factory.queue_client.put(chunk)
  92. def connectionLost(self, why):
  93. log.msg("connectionLost because: %s" % why)
  94. # if self.cli_queue:
  95. # self.cli_queue = None
  96. # log.msg("Client: peer disconnect unexpectedly")
  97. self.factory.queue_client.put(False)
  98. self.queue_twgs = None
  99. self.transport.loseConnection()
  100. class GlueFactory(protocol.ClientFactory):
  101. # class GlueFactory(protocol.Factory):
  102. maxDelay = 10
  103. protocol = PlayerProtocol
  104. def __init__(self, queue_client, queue_twgs, twgs):
  105. self.queue_client = queue_client
  106. self.queue_twgs = queue_twgs
  107. self.twgs = twgs
  108. def closeIt(self):
  109. log.msg("closeIt")
  110. self.queue_client.put(False)
  111. def getUser(self, user):
  112. log.msg("getUser( %s )" % user)
  113. self.twgs.logUser(user)
  114. # This was needed when I replaced ClientFactory with Factory.
  115. # def clientConnectionLost(self, connector, why):
  116. # log.msg("clientconnectionlost: %s" % why)
  117. # self.queue_client.put(False)
  118. def clientConnectionFailed(self, connector, why):
  119. log.msg("connectionFailed: %s" % why)
  120. self.queue_client.put(b"Sorry! I'm Unable to connect to the game server.\r\n")
  121. # syncterm gets cranky/locks up if we close this here.
  122. # (Because it is still sending rlogin information?)
  123. reactor.callLater(2, self.closeIt)
  124. # ProxyServer is created for each connection
  125. class ProxyAction:
  126. def __init__(self, twgs):
  127. self.active = False
  128. self.twgs = twgs
  129. self.queue_client = twgs.queue_client
  130. self.queue_twgs = twgs.queue_twgs
  131. self.buffer = ""
  132. self.prompt = ""
  133. self.rptstate = 0
  134. def isActive(self):
  135. return self.active
  136. def keepAlive(self):
  137. if self.active:
  138. self.queue_twgs.put(b" ")
  139. reactor.callLater(30, self.keepAlive)
  140. def menu(self):
  141. self.send("\r\n**********\r\nTWGS Proxy ready...\r\n")
  142. self.send("(T) Display Time\r\n")
  143. self.send("(P) CIM Port Report\r\n")
  144. self.send("(Q) Quit\r\n")
  145. self.send(" --==> ")
  146. def activate(self, prompt):
  147. cleaned = cleanANSI(prompt)
  148. if cleaned.startswith("Command [TL=0"):
  149. self.active = True
  150. self.prompt = prompt
  151. self.menu()
  152. reactor.callLater(30, self.keepAlive)
  153. else:
  154. self.send("\a")
  155. def sendtwgs(self, text):
  156. self.queue_twgs.put(text.encode())
  157. def server(self, line):
  158. # 2019-11-20 18:37:28-0500 [PlayerProtocol,client] >>> [: ]
  159. # 2019-11-20 18:37:28-0500 [PlayerProtocol,client] >>> [ 436 2870 100% - 1520 100% - 2820 100% ]
  160. # 2019-11-20 18:37:28-0500 [PlayerProtocol,client] >>> []
  161. # 2019-11-20 18:37:42-0500 [PlayerProtocol,client] >>> [: ENDINTERROG]
  162. # 2019-11-20 18:37:42-0500 [PlayerProtocol,client] >>> []
  163. if self.rptstate == 1:
  164. if line.startswith(":"):
  165. self.sendtwgs("R")
  166. self.rptstate == 2
  167. if self.rptstate == 2:
  168. if line.startswith(":"):
  169. self.sendtwgs("Q")
  170. self.rptstate == 3
  171. else:
  172. log.msg("[[{0}]]".format(line))
  173. if self.rptstate == 3:
  174. if line == ": ENDINTERROG":
  175. self.rptstate == 3
  176. self.twgs.passon = True
  177. self.menu()
  178. def send(self, text):
  179. self.queue_client.put((text.encode(),))
  180. def received(self, chunk):
  181. # self.buffer += chunk.encode('UTF-8', 'ignore')
  182. text = chunk.decode("utf-8", "ignore").upper()
  183. if text == "T":
  184. now = pendulum.now("America/New_York")
  185. self.send("\r\nThe time is: {0}.\r\n".format(now.to_rss_string()))
  186. if text == "P":
  187. # Port Report
  188. self.sendtwgs("^")
  189. self.twgs.passon = False
  190. self.rptstate = 1
  191. if text == "Q":
  192. self.send("\r\nReturning to TWGS.\r\n{0}".format(self.prompt))
  193. self.active = False
  194. self.twgs.passon = True
  195. class TWGSServer(protocol.Protocol):
  196. def __init__(self):
  197. self.buffer = ""
  198. self.fpRaw = None
  199. self.fpLines = None
  200. self.action = None
  201. self.user = ""
  202. self.game = ""
  203. self.passon = True
  204. def connectionMade(self):
  205. self.queue_twgs = defer.DeferredQueue()
  206. self.queue_client = defer.DeferredQueue()
  207. self.queue_client.get().addCallback(self.clientDataReceived)
  208. self.action = ProxyAction(self)
  209. factory = GlueFactory(self.queue_client, self.queue_twgs, self)
  210. reactor.connectTCP(HOST, PORT, factory, 5)
  211. def logUser(self, user):
  212. now = pendulum.now()
  213. self.user = user
  214. filename = now.format("YYYY-MM-DD_HHmm") + "-" + user.lower()
  215. if RAW:
  216. self.fpRaw = open(filename + ".raw", "ab")
  217. self.fpLines = open(filename + ".lines", "a")
  218. # print("Log created:", now.to_rss_string(), "\n", file=self.fpRaw)
  219. print("Log created:", now.to_rss_string(), "\n", file=self.fpLines)
  220. def setGame(self, game):
  221. if self.game != game:
  222. log.msg("USER {0} ENTERED {1}".format(self.user, self.game))
  223. self.data = {}
  224. self.game = game
  225. def gotLine(self, line):
  226. # log.msg(">>> [{0}]".format(line.decode("utf-8", "ignore")))
  227. log.msg(">>> [{0}]".format(line))
  228. if self.fpLines is not None:
  229. print(line, file=self.fpLines)
  230. if "TWGS v2.20b" in line:
  231. if "www.eisonline.com" in line:
  232. # Must not be unicode
  233. # Is there a way to NOT have this logged?
  234. self.queue_client.put(
  235. (
  236. b"TWGS Proxy build "
  237. + version.encode()
  238. + b" is active. \x1b[1;34m~\x1b[0m to activate.\n\r\n\r",
  239. )
  240. )
  241. if "TradeWars Game Server" in line and "Copyright (C) EIS" in line:
  242. # We are not in a game
  243. self.game = ""
  244. if "Selection (? for menu): " in line:
  245. game = line[-1]
  246. if game >= "A" and game < "Q":
  247. self.setGame(game)
  248. # If we're not passing it on to the user, we better be looking at it.
  249. if self.action and not self.passon:
  250. self.action.server(line)
  251. def clientDataReceived(self, chunk):
  252. if chunk is False:
  253. self.transport.loseConnection()
  254. else:
  255. if type(chunk) is tuple:
  256. self.transport.write(chunk[0])
  257. self.queue_client.get().addCallback(self.clientDataReceived)
  258. else:
  259. if self.fpRaw is not None:
  260. self.fpRaw.write(chunk)
  261. self.buffer += chunk.decode("utf-8", "ignore")
  262. # Process any backspaces in the buffer
  263. while "\x08" in self.buffer:
  264. part = self.buffer.partition("\x08")
  265. self.buffer = part[0][:-1] + part[2]
  266. # Treat some ANSI codes as a newline (for purposes of Lines)
  267. self.buffer = treatAsNL(self.buffer)
  268. # I think I need something else in here. When something enters or leaves the sector
  269. # The message isn't shown on it's own line. I think they are using ANSI codes to
  270. # clear the line. (Which certainly would be faster!)
  271. # Break the buffer into lines
  272. while "\n" in self.buffer:
  273. part = self.buffer.partition("\n")
  274. line = part[0].replace("\r", "")
  275. # Clean ANSI codes from the line
  276. line = cleanANSI(line)
  277. self.gotLine(line)
  278. self.buffer = part[2]
  279. # log.msg("Server: writing %d bytes to original client" % len(chunk))
  280. if self.passon:
  281. self.transport.write(chunk)
  282. self.queue_client.get().addCallback(self.clientDataReceived)
  283. def dataReceived(self, chunk):
  284. # log.msg("Server: %d bytes received" % len(chunk))
  285. if self.action and self.action.isActive():
  286. # Do something completely different here
  287. self.action.received(chunk)
  288. else:
  289. if chunk == b"~":
  290. self.action.activate(self.buffer)
  291. else:
  292. self.queue_twgs.put(chunk)
  293. def connectionLost(self, why):
  294. log.msg("lost connection %s" % why)
  295. self.queue_twgs.put(False)
  296. if self.fpRaw is not None:
  297. self.fpRaw.close()
  298. self.fpRaw = None
  299. if self.fpLines is not None:
  300. if self.buffer != "":
  301. print(self.buffer, file=self.fpLines)
  302. self.fpLines.close()
  303. self.fpLines = None
  304. def connectionFailed(self, why):
  305. log.msg("connectionFailed: %s" % why)
  306. if __name__ == "__main__":
  307. log.startLogging(sys.stdout)
  308. log.msg("This is version: %s" % version)
  309. factory = protocol.Factory()
  310. factory.protocol = TWGSServer
  311. reactor.listenTCP(LISTEN_PORT, factory, interface=LISTEN_ON)
  312. reactor.run()