proxy.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. #!/usr/bin/env python3
  2. #
  3. # miniirc bouncer thing
  4. #
  5. # © 2022 by luk3yx
  6. #
  7. import miniirc, socket, threading, miniirc_idc
  8. from miniirc_extras.utils import (
  9. ircv2_message_unparser,
  10. ircv3_message_parser,
  11. )
  12. from concurrent.futures import ThreadPoolExecutor
  13. miniirc.version = None
  14. # A single network
  15. class Network:
  16. _buffer = b""
  17. IRC = miniirc_idc.IDC
  18. encoding = "utf-8"
  19. _001 = False
  20. _main_lock = False
  21. block_incoming = frozenset(("PING", "CAP", "AUTHENTICATE"))
  22. block_outgoing = frozenset(("CAP",))
  23. # :( what socket is this even
  24. # Send messages
  25. def send(self, cmd, hostmask, tags, args):
  26. raw = ircv2_message_unparser(
  27. cmd,
  28. hostmask or (cmd, cmd, cmd),
  29. tags,
  30. args,
  31. colon=False,
  32. encoding=self.encoding,
  33. )
  34. print("->", raw)
  35. self.sock.sendall(raw[:510] + b"\r\n")
  36. # Receive messages
  37. def recv(self):
  38. while True:
  39. while b"\n" not in self._buffer:
  40. buf = self.sock.recv(4096)
  41. assert buf, "The socket has been closed!"
  42. self._buffer += buf.replace(b"\r", b"\n")
  43. msg, self._buffer = self._buffer.split(b"\n", 1)
  44. msg = msg.decode(self.encoding, "replace")
  45. if msg:
  46. cmd, _, tags, args = ircv3_message_parser(
  47. msg, colon=False
  48. )
  49. return tags, cmd.upper(), args
  50. # Handle everything
  51. def _miniirc_handler(self, irc, cmd, hostmask, tags, args):
  52. if cmd.startswith("IRCV3 ") or cmd in self.block_incoming:
  53. return
  54. elif cmd == "001":
  55. if self._001:
  56. return
  57. self._001 = True
  58. # Clear the SendQ
  59. if self._sendq:
  60. while len(self._sendq) > 0:
  61. self._sendcmd(*self._sendq.pop(0))
  62. # Start the main loop
  63. self._main()
  64. elif cmd == "ERROR":
  65. self.send("PING", None, {}, [":ERROR"])
  66. elif cmd == "PONG" and args and args[-1] == "miniirc-ping":
  67. return
  68. # Send the command to the client
  69. try:
  70. self.send(cmd, hostmask, tags, args)
  71. except Exception as e:
  72. print(repr(e))
  73. self.irc.disconnect(
  74. "Connection closed.", auto_reconnect=False
  75. )
  76. # The initial main loop
  77. def _init_thread(self):
  78. self._sendq = []
  79. nick = None
  80. user = None
  81. # Wait for NICK and USER to be sent
  82. while not nick or not user:
  83. tags, cmd, args = self.recv()
  84. if cmd == "NICK" and len(args) == 1:
  85. nick = args[0]
  86. elif cmd == "USER" and len(args) > 1:
  87. user = args
  88. else:
  89. self._sendq.append((tags, cmd, args))
  90. # Set values
  91. self.irc.nick = nick
  92. self.irc.ident = user[0]
  93. self.irc.realname = user[-1]
  94. # Connect
  95. self.irc.connect()
  96. # Send a command
  97. def _sendcmd(self, tags, cmd, args):
  98. if cmd not in self.block_outgoing:
  99. raw = ircv2_message_unparser(
  100. cmd,
  101. (cmd, cmd, cmd),
  102. {},
  103. args,
  104. colon=False,
  105. encoding=None,
  106. )
  107. self.irc.quote(raw, tags=tags)
  108. # The more permanent main loop
  109. def _main(self, single_thread=False):
  110. if not single_thread:
  111. if self._main_lock and self._main_lock.is_alive():
  112. return self._main_lock
  113. t = threading.Thread(target=self._main, args=(True,))
  114. t.start()
  115. return t
  116. # Clear the RecvQ
  117. if self._recvq:
  118. while len(self._recvq) > 0:
  119. self.send(*self._recvq.pop(0))
  120. self._recvq = None
  121. # Send everything to IRC
  122. while True:
  123. try:
  124. tags, cmd, args = self.recv()
  125. except Exception as e:
  126. print(repr(e))
  127. return self.irc.disconnect()
  128. self._sendcmd(tags, cmd, args)
  129. # Generic init function
  130. def _init(self, conn, irc):
  131. # self.sock is the socket returned by sock.accept()
  132. # It's the socket it uses to talk to WeeChat
  133. # This bouncer was made for IRC-to-IRC so there isn't
  134. # anything IDC-specific here (except for changing it to use IDC()
  135. # instead of IRC())
  136. self.sock, self.ip = conn
  137. self.irc = irc
  138. # Add the IRC handler
  139. self._recvq = []
  140. self.irc.CmdHandler(ircv3=True, colon=False)(
  141. self._miniirc_handler
  142. )
  143. # Start the main loop
  144. self.thread = threading.Thread(target=self._init_thread)
  145. self.thread.start()
  146. # Create the IRC object
  147. def __init__(self, conn, *args, bad_cmds=None, **kwargs):
  148. if bad_cmds is not None:
  149. self.bad_cmds = bad_cmds
  150. self._init(
  151. conn,
  152. self.IRC(
  153. *args,
  154. auto_connect=False,
  155. executor=ThreadPoolExecutor(1),
  156. **kwargs
  157. ),
  158. )
  159. # The bouncer class
  160. class Bouncer:
  161. addr = ("", 1025)
  162. Network = Network
  163. # Main loop
  164. def main(self):
  165. # Create a socket object
  166. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
  167. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  168. sock.bind(self.addr)
  169. sock.listen(1)
  170. net = self.Network(sock.accept(), *self.args, **self.kwargs)
  171. net.thread.join()
  172. net.irc.wait_until_disconnected()
  173. # The main init
  174. def __init__(self, *args, **kwargs):
  175. self.args, self.kwargs = args, kwargs
  176. # Debugging
  177. def main():
  178. Bouncer(
  179. "127.0.0.1",
  180. 6835,
  181. "luk3yx",
  182. debug=True,
  183. ns_identity="luk3yx billy",
  184. ).main()
  185. if __name__ == "__main__":
  186. main()