diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 79a16af..0000000 --- a/.flake8 +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length = 120 \ No newline at end of file diff --git a/config_server.py b/config_server.py index cb38a68..70e088b 100644 --- a/config_server.py +++ b/config_server.py @@ -13,16 +13,17 @@ class DreamPiConfigurationService(BaseHTTPRequestHandler): - def _get_post_data(self) -> Dict[str, List[str]]: - ctype, pdict = cgi.parse_header(self.headers['content-type']) + ctype, pdict = cgi.parse_header(self.headers["content-type"]) - if ctype == 'multipart/form-data': - pdict = { k: v.encode() for k, v in pdict.items() } + if ctype == "multipart/form-data": + pdict = {k: v.encode() for k, v in pdict.items()} postvars = cgi.parse_multipart(self.rfile, pdict) - elif ctype == 'application/x-www-form-urlencoded': - length = int(self.headers['content-length']) - postvars = cgi.parse_qs(self.rfile.read(length).decode(), keep_blank_values=True) + elif ctype == "application/x-www-form-urlencoded": + length = int(self.headers["content-length"]) + postvars = cgi.parse_qs( + self.rfile.read(length).decode(), keep_blank_values=True + ) else: postvars = {} @@ -39,11 +40,11 @@ def do_GET(self): with open(CONFIGURATION_FILE, "r") as f: enabled_state = json.loads(f.read())["enabled"] - self.wfile.write(json.dumps({ - "mac_address": hash_mac_address(), - "is_enabled": enabled_state - }).encode()) - + self.wfile.write( + json.dumps( + {"mac_address": hash_mac_address(), "is_enabled": enabled_state} + ).encode() + ) def do_POST(self): enabled_state = True @@ -54,7 +55,7 @@ def do_POST(self): self.end_headers() post_data = self._get_post_data() - if 'disable' in post_data: + if "disable" in post_data: enabled_state = False else: enabled_state = True @@ -62,22 +63,25 @@ def do_POST(self): with open(CONFIGURATION_FILE, "w") as f: f.write(json.dumps({"enabled": enabled_state})) - self.wfile.write(json.dumps({ - "mac_address": hash_mac_address(), - "is_enabled": enabled_state - }).encode()) + self.wfile.write( + json.dumps( + {"mac_address": hash_mac_address(), "is_enabled": enabled_state} + ).encode() + ) server = None thread = None + def start(): global server global thread - server = HTTPServer(('0.0.0.0', 1998), DreamPiConfigurationService) + server = HTTPServer(("0.0.0.0", 1998), DreamPiConfigurationService) thread = threading.Thread(target=server.serve_forever) thread.start() + def stop(): global server global thread diff --git a/dcnow.py b/dcnow.py index 020ff72..8238672 100644 --- a/dcnow.py +++ b/dcnow.py @@ -10,13 +10,13 @@ import logging.handlers import urllib.request import urllib.parse -import sh # type: ignore - sh module is dynamic +import sh # type: ignore - sh module is dynamic from typing import List, Optional from hashlib import sha256 from uuid import getnode as get_mac -logger = logging.getLogger('dcnow') +logger = logging.getLogger("dcnow") API_ROOT = "https://dcnow-2016.appspot.com" UPDATE_END_POINT = "/api/update/{mac_address}/" @@ -28,7 +28,9 @@ def hash_mac_address(): mac = get_mac() - return sha256(':'.join(("%012X" % mac)[i:i+2] for i in range(0, 12, 2)).encode()).hexdigest() + return sha256( + ":".join(("%012X" % mac)[i : i + 2] for i in range(0, 12, 2)).encode() + ).hexdigest() class DreamcastNowThread(threading.Thread): @@ -42,7 +44,9 @@ def post_update(): if not self._service.enabled: return - lines: List[str] = list(sh.tail("/var/log/syslog", "-n", "10", _iter=True)) # type: ignore - sh has dynamic members + lines: List[str] = list( + sh.tail("/var/log/syslog", "-n", "10", _iter=True) + ) # type: ignore - sh has dynamic members dns_query = None for line in lines[::-1]: line: str = line @@ -52,21 +56,25 @@ def post_update(): if "query[A]" in line: # We did a DNS lookup, what was it? - remainder = line[line.find("query[A]") + len("query[A]"):].strip() + remainder = line[line.find("query[A]") + len("query[A]") :].strip() domain = remainder.split(" ", 1)[0].strip() dns_query = sha256(domain.encode()).hexdigest() break - user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT), Dreamcast Now' - header = { 'User-Agent' : user_agent } + user_agent = "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT), Dreamcast Now" + header = {"User-Agent": user_agent} mac_address = self._service.mac_address_hash data = {} if dns_query: data["dns_query"] = dns_query data = urllib.parse.urlencode(data).encode() - req = urllib.request.Request(API_ROOT + UPDATE_END_POINT.format(mac_address=mac_address), data, header) - urllib.request.urlopen(req) # Send POST update + req = urllib.request.Request( + API_ROOT + UPDATE_END_POINT.format(mac_address=mac_address), + data, + header, + ) + urllib.request.urlopen(req) # Send POST update while self._running: try: @@ -88,8 +96,10 @@ def __init__(self): self.reload_settings() logger.setLevel(logging.INFO) - syslog_handler = logging.handlers.SysLogHandler(address='/dev/log') - syslog_handler.setFormatter(logging.Formatter('%(name)s[%(process)d]: %(levelname)s %(message)s')) + syslog_handler = logging.handlers.SysLogHandler(address="/dev/log") + syslog_handler.setFormatter( + logging.Formatter("%(name)s[%(process)d]: %(levelname)s %(message)s") + ) logger.addHandler(syslog_handler) def update_mac_address(self): @@ -116,11 +126,11 @@ def go_offline(self): if self._thread is not None: self._thread.stop() self._thread = None - + @property def enabled(self): return self._enabled - + @property def mac_address_hash(self): return self._mac_address_hash diff --git a/dreampi.py b/dreampi.py index 70e58a4..0661045 100755 --- a/dreampi.py +++ b/dreampi.py @@ -30,7 +30,7 @@ DNS_FILE = "https://dreamcast.online/dreampi/dreampi_dns.conf" -logger = logging.getLogger('dreampi') +logger = logging.getLogger("dreampi") def check_internet_connection(): @@ -42,7 +42,7 @@ def check_internet_connection(): "8.8.8.8", # Google DNS "8.8.4.4", "208.67.222.222", # Open DNS - "208.67.220.220" + "208.67.220.220", ] port = 53 @@ -73,12 +73,14 @@ def update_dns_file(): try: response = urllib.request.urlopen(DNS_FILE) except urllib.error.HTTPError as e: - logging.info(f"Did not find remote DNS config (HTTP code {e.code}); will use upstream") + logging.info( + f"Did not find remote DNS config (HTTP code {e.code}); will use upstream" + ) return except urllib.error.URLError as e: logging.exception("Failed to check for remote DNS config") return - + # Stop the server subprocess.check_call("sudo service dnsmasq stop".split()) @@ -88,7 +90,7 @@ def update_dns_file(): f.write(response.read()) except IOError: logging.exception("Found remote DNS config but failed to apply it locally") - + # Start the server again subprocess.check_call("sudo service dnsmasq start".split()) @@ -135,7 +137,7 @@ def stop_afo_patching(afo_patcher_rule: iptc.Rule): def start_service(name): try: logger.info("Starting {} process - Thanks Jonas Karlsson!".format(name)) - with open(os.devnull, 'wb') as devnull: + with open(os.devnull, "wb") as devnull: subprocess.check_call(["sudo", "service", name, "start"], stdout=devnull) except (subprocess.CalledProcessError, IOError): logging.warning("Unable to start the {} process".format(name)) @@ -144,7 +146,7 @@ def start_service(name): def stop_service(name): try: logger.info("Stopping {} process".format(name)) - with open(os.devnull, 'wb') as devnull: + with open(os.devnull, "wb") as devnull: subprocess.check_call(["sudo", "service", name, "stop"], stdout=devnull) except (subprocess.CalledProcessError, IOError): logging.warning("Unable to stop the {} process".format(name)) @@ -155,8 +157,8 @@ def get_default_iface_name_linux(): with open(route) as f: for line in f.readlines(): try: - iface, dest, _, flags, _, _, _, _, _, _, _, = line.strip().split() - if dest != '00000000' or not int(flags, 16) & 2: + iface, dest, _, flags, _, _, _, _, _, _, _, = line.strip().split() + if dest != "00000000" or not int(flags, 16) & 2: continue return iface except: @@ -198,30 +200,23 @@ def autoconfigure_ppp(device, speed) -> str: Returns the IP allocated to the Dreamcast """ - gateway_ip = subprocess.check_output("route -n | grep 'UG[ \t]' | awk '{print $2}'", shell=True).decode() + gateway_ip = subprocess.check_output( + "route -n | grep 'UG[ \t]' | awk '{print $2}'", shell=True + ).decode() subnet = gateway_ip.split(".")[:3] - PEERS_TEMPLATE = ( - "{device}\n" - "{device_speed}\n" - "{this_ip}:{dc_ip}\n" - "noauth\n" - ) + PEERS_TEMPLATE = "{device}\n" "{device_speed}\n" "{this_ip}:{dc_ip}\n" "noauth\n" - OPTIONS_TEMPLATE = ( - "debug\n" - "ms-dns {this_ip}\n" - "proxyarp\n" - "ktune\n" - "noccp\n" - ) + OPTIONS_TEMPLATE = "debug\n" "ms-dns {this_ip}\n" "proxyarp\n" "ktune\n" "noccp\n" this_ip = find_next_unused_ip(".".join(subnet) + ".100") dreamcast_ip = find_next_unused_ip(this_ip) logger.info("Dreamcast IP: {}".format(dreamcast_ip)) - peers_content = PEERS_TEMPLATE.format(device=device, device_speed=speed, this_ip=this_ip, dc_ip=dreamcast_ip) + peers_content = PEERS_TEMPLATE.format( + device=device, device_speed=speed, this_ip=this_ip, dc_ip=dreamcast_ip + ) with open("/etc/ppp/peers/dreamcast", "w") as f: f.write(peers_content) @@ -234,7 +229,9 @@ def autoconfigure_ppp(device, speed) -> str: return dreamcast_ip -ENABLE_SPEED_DETECTION = False # Set this to true if you want to use wvdialconf for device detection +ENABLE_SPEED_DETECTION = ( + False +) # Set this to true if you want to use wvdialconf for device detection def detect_device_and_speed() -> Optional[Tuple[str, int]]: @@ -299,7 +296,7 @@ def daemonize(self): atexit.register(self.delete_pid) pid = str(os.getpid()) - with open(self.pidfile, 'w+') as f: + with open(self.pidfile, "w+") as f: f.write("%s\n" % pid) def delete_pid(self): @@ -307,7 +304,7 @@ def delete_pid(self): def _read_pid_from_pidfile(self): try: - with open(self.pidfile, 'r') as pf: + with open(self.pidfile, "r") as pf: pid = int(pf.read().strip()) except IOError: pid = None @@ -414,9 +411,7 @@ def start_dial_tone(self): self._sending_tone = True - self._time_since_last_dial_tone = ( - datetime.now() - timedelta(seconds=100) - ) + self._time_since_last_dial_tone = datetime.now() - timedelta(seconds=100) self._dial_tone_counter = 0 @@ -442,7 +437,9 @@ def answer(self): logger.info(subprocess.check_output(["pon", "dreamcast"]).decode()) logger.info("Connected") - def send_command(self, command: bytes, timeout=60, ignore_responses: Optional[List[bytes]]=None): + def send_command( + self, command: bytes, timeout=60, ignore_responses: Optional[List[bytes]] = None + ): if self._serial is None: raise Exception("Not connected") if ignore_responses is None: @@ -469,11 +466,13 @@ def send_command(self, command: bytes, timeout=60, ignore_responses: Optional[Li line = line + new_data for resp in VALID_RESPONSES: if resp in line: - logger.info(line[line.find(resp):].decode()) + logger.info(line[line.find(resp) :].decode()) return # We are done if (datetime.now() - start).total_seconds() > timeout: - raise IOError("There was a timeout while waiting for a response from the modem") + raise IOError( + "There was a timeout while waiting for a response from the modem" + ) def send_escape(self): if self._serial is None: @@ -494,8 +493,14 @@ def update(self): if self._serial is None: raise Exception("Not connected") - if not self._time_since_last_dial_tone or ((now - (self._time_since_last_dial_tone)).microseconds * 1000) >= TIME_BETWEEN_UPLOADS_MS: - byte = self._dial_tone_wav[self._dial_tone_counter:self._dial_tone_counter+BUFFER_LENGTH] + if ( + not self._time_since_last_dial_tone + or ((now - (self._time_since_last_dial_tone)).microseconds * 1000) + >= TIME_BETWEEN_UPLOADS_MS + ): + byte = self._dial_tone_wav[ + self._dial_tone_counter : self._dial_tone_counter + BUFFER_LENGTH + ] self._dial_tone_counter += BUFFER_LENGTH if self._dial_tone_counter >= len(self._dial_tone_wav): self._dial_tone_counter = 0 @@ -520,7 +525,7 @@ def process(): dial_tone_enabled = "--disable-dial-tone" not in sys.argv # Make sure pppd isn't running - with open(os.devnull, 'wb') as devnull: + with open(os.devnull, "wb") as devnull: subprocess.call(["sudo", "killall", "pppd"], stderr=devnull) device_and_speed, internet_connected = None, False @@ -602,7 +607,9 @@ def process(): dcnow.go_online() # We start watching /var/log/messages for the hang up message - for line in sh.tail("-f", "/var/log/messages", "-n", "1", _iter=True): # type: ignore - sh module is dynamic + for line in sh.tail( + "-f", "/var/log/messages", "-n", "1", _iter=True + ): # type: ignore - sh module is dynamic line: str = line if "Modem hangup" in line: logger.info("Detected modem hang up, going back to listening") @@ -677,10 +684,12 @@ def main(): logger.info("Dreampi quit successfully") -if __name__ == '__main__': +if __name__ == "__main__": logger.setLevel(logging.INFO) - syslog_handler = logging.handlers.SysLogHandler(address='/dev/log') - syslog_handler.setFormatter(logging.Formatter('%(name)s[%(process)d]: %(levelname)s %(message)s')) + syslog_handler = logging.handlers.SysLogHandler(address="/dev/log") + syslog_handler.setFormatter( + logging.Formatter("%(name)s[%(process)d]: %(levelname)s %(message)s") + ) logger.addHandler(syslog_handler) if len(sys.argv) > 1 and "--no-daemon" in sys.argv: diff --git a/port_forwarding.py b/port_forwarding.py index a7c8af5..ab4ae7f 100644 --- a/port_forwarding.py +++ b/port_forwarding.py @@ -6,6 +6,7 @@ from typing import Any import miniupnpc + class PortForwarding: """ This class is used to forward the ports of all supported Dreamcast games @@ -16,28 +17,28 @@ class PortForwarding: # List of ports and the game they're for PORTS = [ - (1028, 'UDP', 'Planet Ring'), - (1285, 'UDP', 'Planet Ring'), - (3512, 'TCP', 'The Next Tetris: Online Edition'), - (3512, 'UDP', 'The Next Tetris: Online Edition'), - (6001, 'UDP', 'Ooga Booga'), - (6500, 'UDP', 'PBA Tour Bowling 2001 / Starlancer'), - (7648, 'UDP', 'Planet Ring'), - (7980, 'UDP', 'Alien Front Online'), - (9789, 'UDP', 'ChuChu Rocket!'), - (13139, 'UDP', 'PBA Tour Bowling 2001'), - (13713, 'UDP', 'World Series Baseball 2K2'), - (17219, 'TCP', 'Worms World Party'), - (37171, 'UDP', 'World Series Baseball 2K2'), - (47624, 'TCP', 'PBA Tour Bowling 2001 / Starlancer'), - (list(range(2300, 2401)), 'TCP', 'PBA Tour Bowling 2001 / Starlancer'), - (list(range(2300, 2401)), 'UDP', 'PBA Tour Bowling 2001 / Starlancer') + (1028, "UDP", "Planet Ring"), + (1285, "UDP", "Planet Ring"), + (3512, "TCP", "The Next Tetris: Online Edition"), + (3512, "UDP", "The Next Tetris: Online Edition"), + (6001, "UDP", "Ooga Booga"), + (6500, "UDP", "PBA Tour Bowling 2001 / Starlancer"), + (7648, "UDP", "Planet Ring"), + (7980, "UDP", "Alien Front Online"), + (9789, "UDP", "ChuChu Rocket!"), + (13139, "UDP", "PBA Tour Bowling 2001"), + (13713, "UDP", "World Series Baseball 2K2"), + (17219, "TCP", "Worms World Party"), + (37171, "UDP", "World Series Baseball 2K2"), + (47624, "TCP", "PBA Tour Bowling 2001 / Starlancer"), + (list(range(2300, 2401)), "TCP", "PBA Tour Bowling 2001 / Starlancer"), + (list(range(2300, 2401)), "UDP", "PBA Tour Bowling 2001 / Starlancer"), ] def __init__(self, dc_ip: str, logger: Logger): self._dreamcast_ip = dc_ip self._logger = logger - self._upnp: Any = miniupnpc.UPnP() # type: ignore - this module has no types + self._upnp: Any = miniupnpc.UPnP() # type: ignore - this module has no types def forward_all(self): """ @@ -49,20 +50,50 @@ def forward_all(self): port, proto, game = portinfo if isinstance(port, list): - self._logger.info("Trying to create UPnP port mapping for {} ({}-{}/{})".format(game, port[0], port[-1], proto)) + self._logger.info( + "Trying to create UPnP port mapping for {} ({}-{}/{})".format( + game, port[0], port[-1], proto + ) + ) for p in port: try: - self._upnp.addportmapping(p, proto, self._dreamcast_ip, p, "DreamPi: {}".format(game), '') + self._upnp.addportmapping( + p, + proto, + self._dreamcast_ip, + p, + "DreamPi: {}".format(game), + "", + ) except Exception as e: - self._logger.warn("Could not create UPnP port mapping for {} ({}/{}): {}".format(game, p, proto, e)) + self._logger.warn( + "Could not create UPnP port mapping for {} ({}/{}): {}".format( + game, p, proto, e + ) + ) else: - self._logger.info("Trying to create UPnP port mapping for {} ({}/{})".format(game, port, proto)) + self._logger.info( + "Trying to create UPnP port mapping for {} ({}/{})".format( + game, port, proto + ) + ) try: - self._upnp.addportmapping(port, proto, self._dreamcast_ip, port, "DreamPi: {}".format(game), '') + self._upnp.addportmapping( + port, + proto, + self._dreamcast_ip, + port, + "DreamPi: {}".format(game), + "", + ) except Exception as e: - self._logger.warn("Could not create UPnP port mapping for {} ({}/{}): {}".format(game, port, proto, e)) + self._logger.warn( + "Could not create UPnP port mapping for {} ({}/{}): {}".format( + game, port, proto, e + ) + ) def delete_all(self) -> bool: """ @@ -74,26 +105,44 @@ def delete_all(self) -> bool: self._upnp.discover() self._upnp.selectigd() except Exception as e: - self._logger.info("Could not find a UPnP internet gateway device on your network. Not automatically forwarding ports.") + self._logger.info( + "Could not find a UPnP internet gateway device on your network. Not automatically forwarding ports." + ) return False for portinfo in self.PORTS: port, proto, game = portinfo if isinstance(port, list): - self._logger.info("Trying to delete UPnP port mapping for {} ({}-{}/{})".format(game, port[0], port[-1], proto)) + self._logger.info( + "Trying to delete UPnP port mapping for {} ({}-{}/{})".format( + game, port[0], port[-1], proto + ) + ) for p in port: try: self._upnp.deleteportmapping(p, proto) except Exception as e: - self._logger.debug("Could not delete UPnP port mapping for {} ({}/{}): {}".format(game, p, proto, e)) + self._logger.debug( + "Could not delete UPnP port mapping for {} ({}/{}): {}".format( + game, p, proto, e + ) + ) else: - self._logger.info("Trying to delete UPnP port mapping for {} ({}/{})".format(game, port, proto)) + self._logger.info( + "Trying to delete UPnP port mapping for {} ({}/{})".format( + game, port, proto + ) + ) try: self._upnp.deleteportmapping(port, proto) except Exception as e: - self._logger.debug("Could not delete UPnP port mapping for {} ({}/{}): {}".format(game, port, proto, e)) + self._logger.debug( + "Could not delete UPnP port mapping for {} ({}/{}): {}".format( + game, port, proto, e + ) + ) return True