diff --git a/packages/internet_detector.vm/internet_detector.vm.nuspec b/packages/internet_detector.vm/internet_detector.vm.nuspec new file mode 100644 index 000000000..3dd20ba9f --- /dev/null +++ b/packages/internet_detector.vm/internet_detector.vm.nuspec @@ -0,0 +1,14 @@ + + + + internet_detector.vm + 1.0.0 + Elliot Chernofsky and Ana Martinez Gomez + Tool that changes the background and a taskbar icon if it detects internet connectivity + + + + + + + diff --git a/packages/internet_detector.vm/tools/chocolateyinstall.ps1 b/packages/internet_detector.vm/tools/chocolateyinstall.ps1 new file mode 100644 index 000000000..afa945f5b --- /dev/null +++ b/packages/internet_detector.vm/tools/chocolateyinstall.ps1 @@ -0,0 +1,37 @@ +$ErrorActionPreference = 'Stop' +Import-Module vm.common -Force -DisableNameChecking + +$toolName = 'internet_detector' +$category = 'Utilities' + +# Install dependency for windows api +VM-Pip-Install "pywin32" + +# Create tool directory +$toolDir = Join-Path ${Env:RAW_TOOLS_DIR} $toolName +New-Item -Path $toolDir -ItemType Directory -Force -ea 0 +VM-Assert-Path $toolDir + +$packageToolDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" + +# Move images to %VM_COMMON_DIR% directory +$imagesPath = Join-Path $packageToolDir "Images" +Move-Item "$imagesPath\*" ${Env:VM_COMMON_DIR} -Force + +# Get path to pythonw for executing Python without terminal popup +$executablePath = (Get-Command pythonw).Source +$filePath = Join-Path $packageToolDir "$toolName.pyw" + +VM-Install-Shortcut $toolName $category $executablePath -arguments $filePath + +# Create scheduled task for tool to run every 2 minutes. +$action = New-ScheduledTaskAction -Execute $executablePath -Argument "$filePath" +$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 2) +Register-ScheduledTask -Action $action -Trigger $trigger -TaskName 'Internet Detector' -Force + +# Move updated `default.ini` with tool added to ProcessBlackList to FakeNet Config Directory +$fakenetConfigDir = Join-Path ${Env:UserProfile} "Desktop\fakenet_logs.lnk" +if (Test-Path $fakenetConfigDir) { # Check if the shortcut exists + $targetPath = (New-Object -ComObject WScript.Shell).CreateShortcut($fakenetConfigDir).TargetPath + Move-Item "$packageToolDir\default.ini" -Destination $targetPath +} diff --git a/packages/internet_detector.vm/tools/chocolateyuninstall.ps1 b/packages/internet_detector.vm/tools/chocolateyuninstall.ps1 new file mode 100644 index 000000000..94da3c136 --- /dev/null +++ b/packages/internet_detector.vm/tools/chocolateyuninstall.ps1 @@ -0,0 +1,8 @@ +$ErrorActionPreference = 'Continue' +Import-Module vm.common -Force -DisableNameChecking + +$toolName = 'internet_detector' +$category = 'Utilities' + +VM-Uninstall $toolName $category +Unregister-ScheduledTask -TaskName 'Internet Detector' -Confirm:$false diff --git a/packages/internet_detector.vm/tools/default.ini b/packages/internet_detector.vm/tools/default.ini new file mode 100644 index 000000000..f743fbd1d --- /dev/null +++ b/packages/internet_detector.vm/tools/default.ini @@ -0,0 +1,352 @@ +############################################################################### +# Fakenet Configuration + +[FakeNet] + +# Specify whether or not FakeNet should divert traffic. Disable if you want to +# just start listeners and direct traffic manually (e.g. modify DNS server) +DivertTraffic: Yes + +############################################################################### +# Diverter Configuration + +[Diverter] + +# Specify what mode of operation to use. Options: +# SingleHost - manipulate local traffic +# MultiHost - manipulate traffic from foreign hosts +# Auto - Use SingleMode on Windows or use MultiHost on Linux +# +# The current support for these modes on each supported platform is as follows: +# | Windows | Linux | +# -----------+------------+--------------+ +# SingleHost | Functional | Experimental | +# MultiHost | - | Functional | +# NetworkMode: SingleHost +# NetworkMode: MultiHost +NetworkMode: Auto + +# DebugLevel: specify fine-grained debug print flags to enable. Enabling all +# logging when verbose mode is selected results in overwhelming output, hence +# this setting. Valid values (comma-separated) are: +# +# GENPKT Generic packet information +# GENPKTV Packet analysis, displays IP, TCP, UDP fields, very wide output +# CB Diverter packet handler callback start/finish logging +# NONLOC Nonlocal packet verbose logging +# DPF Dynamic port forwarding decisions +# DPFV Dynamic port forwarding table activity +# IPNAT NAT decisions +# MANGLE Packet mangling (modification) activity +# PCAP PCAP writes of original and mangled packets +# IGN Cases where packets are forwarded as is +# FTP FTP-specific logic +# IGN-FTP Cases where packets are forwarded as is due to FTP Active Mode +# MISC Miscellaneous +# NFQUEUE NetfilterQueue activity (Linux only) +# PROCFS Procfs read/write activity (Linux only) +# IPTABLES iptables firewall rule activity (Linux only) +DebugLevel: Off + +# Restrict which interface on which Fakenet-NG will intercept and handle +# packets. Specify (only) one interface and Fakenet-NG will ignore all other +# interfaces. This feature only applies to interfaces on different subnets. +# Specify interface by name only (ex: eth0). To disable, set to "Off". In +# order to run multiple instance of Fakenet-NG on different interfaces within +# the same guest, LinuxFlushIptables must be turned off to avoid the latest +# instance flushing the rules associated with other instances or restoring +# rules to an incorrect state upon exit. +LinuxRestrictInterface: Off + +# Set LinuxFlushIptables to Yes to have the Linux Diverter flush all iptables +# rules before adding its FakeNet-NG-specific rules to iptables. This setting +# also restores rules via `iptables-restore` when it exits, unless its +# termination is interrupted. +LinuxFlushIptables: Yes + +# Incorporated so that users of the binary release may make this work for +# various Linux distros. On Ubuntu, this is `service dns-clean restart`. For +# other distributions, it may be `nscd -I hosts`. Check your manual for +# details. +LinuxFlushDNSCommand: service dns-clean restart + +# Specify whether or not to save captured traffic. You can also change +# the file prefix for the generated PCAPs. +DumpPackets: Yes +DumpPacketsFilePrefix: packets + +# DHCP server running under VMWare Host-Only networking does not configure +# interface gateway and DNS server. Gateway must be configured to allow +# Windows to attempt to route external traffic so that FakeNet could +# could intercept it. This option will automatically generate and set +# appropriate gateway and DNS addresses to allow normal operation. +FixGateway: Yes +FixDNS: Yes + +# Enable 'ModifyLocalDNS' to statically set DNS server to the local machine. +# Linux: Modifies (and restores) /etc/resolv.conf on Linux to make this an +# ephemeral change. +ModifyLocalDNS: Yes + +# Enable 'StopDNSService' to stop Windows DNS client to see the actual +# processes resolving domains. This is a no-op on Linux, until such time as DNS +# caching is observed to interfere with finding the pid associated with a DNS +# request. +StopDNSService: Yes + +# Enable 'RedirectAllTraffic' to optionally divert traffic going to ports not +# specifically listed in one of the listeners below. 'DefaultTCPListener' and +# 'DefaultUDPListener' will handle TCP and UDP traffic going to unspecified ports. +# +# NOTE: Setting default UDP listener will intercept all DNS traffic unless you +# enable a dedicated UDP port 53 DNS listener or add UDP port 53 to the +# 'BlackListPortsUDP' below so that system's default DNS server is used instead. + +RedirectAllTraffic: Yes +DefaultTCPListener: ProxyTCPListener +DefaultUDPListener: ProxyUDPListener + +# Specify TCP and UDP ports to ignore when diverting packets. +# For example, you may want to avoid diverting UDP port 53 (DNS) traffic +# when trying to intercept a specific process while allowing the rest to +# function normally +# +# NOTE: This setting is only honored when 'RedirectAllTraffic' is enabled. + +BlackListPortsTCP: 139 +BlackListPortsUDP: 67, 68, 137, 138, 443, 1900, 5355 + +# Specify processes to ignore when diverting traffic. Windows example used +# here. +# ProcessBlackList: java.exe + +# Specify processes to consider when diverting traffic (others will be +# ignored). Linux examples used here. +# ProcessWhiteList: wget, nc + +# Specify hosts to ignore when diverting traffic. +# HostBlackList: 6.6.6.6 + +############################################################################### +# Listener Configuration +# +# Listener configuration consists of generic settings used by the diverter which +# are the same for all listeners and listener specific settings. +# +# NOTE: Listener section names will be used for logging. +# +# NOTE: Settings labels are not case-sensitive. +# +# The following settings are available for all listeners: +# * Enabled - specify whether or not the listener is enabled. +# * Port - TCP or UDP port to listen on. +# * Protocol - TCP or UDP +# * Listener - Listener name to handle traffic. +# * ProcessWhiteList - Only traffic from these processes will be modified +# and the rest will simply be forwarded. +# * ProcessBlackList - Traffic from all but these processes will be simply forwarded +# and the rest will be modified as needed. +# * HostWhiteList - Only traffic to these hosts will be modified and +# the rest will be simply forwarded. +# * HostBlackList - Traffic to these hosts will be simply forwarded +# and the rest will be modified as needed. +# * ExecuteCmd - Execute command on the first connection packet. This is feature is useful +# for extending FakeNet-NG's functionality (e.g. launch a debugger on the +# connecting pid to help with unpacking and decoding.) +# +# The following format string variables are made available: +# * {pid} - process id +# * {procname} - process executable name +# * {src_addr} - source address +# * {src_port} - source port +# * {dst_addr} - destination address +# * {dst_port} - destination port +# +# Listener entry which does not specify a specific listener service +# will still redirect all packets to the local machine on the specified port and +# subject to all the filters (processes, hosts, etc.). However, you must set-up a +# third party service (e.g. proxy servers) to accept these connections. This feature can be +# used to provide FakeNet-NG's passive traffic diverting and filtering capabilities to other +# applications. +# +# Listener specific settings: +# +# * Timeout - Set connection timeout for any listeners that support +# TCP connections (e.g. RawListener, DNSListener, HTTPListener +# SMTPListener). +# * UseSSL - Enable SSL support on the listener (RawListener, HTTPListener) +# * Webroot - Set webroot path for HTTPListener. +# * DumpHTTPPosts - Store HTTP Post requests for the HTTPListener. +# * DumpHTTPPostsFilePrefix - File prefix for the stored HTTP Post requests used by the HTTPListener. +# * TFTPFilePrefix - File prefix for the stored tftp uploads used by the TFTPListener. +# * DNSResponse - IP address to respond with for A record DNS queries. (DNSListener) +# * NXDomains - A number of DNS requests to ignore to let the malware cycle through +# all of the backup C2 servers. (DNSListener) +# * Banner - FTPListener, IRCListener: FTP or IRC banner to display. +# Valid settings are any banner string, or ! where +# is a valid key in the BANNERS dictionary within +# FTPListener.py or IRCListener.py, or !random to +# randomize among the banners in the BANNERS dictionary. +# The default value if none is specified is !generic, +# which selects the banner in the BANNERS dictionary going +# by that key. Banner string may specify the following +# escapes/insertions: +# {servername} - ServerName setting value +# {tz} - Time zone, currently hard-coded to 'UTC' +# * ServerName - FTPListener, IRCListener: FTP or IRC server name for +# insertion into selected default banners or into a +# user-specified banner string. Valid settings are any +# hostname string, !hostname to insert the actual hostname +# of the system, or !random to generate a random hostname +# between 1 and 15 characters (inclusive). + +[ProxyTCPListener] +Enabled: True +Protocol: TCP +Listener: ProxyListener +Port: 38926 +Listeners: HTTPListener, RawListener, FTPListener, DNSListener, POPListener, SMTPListener, TFTPListener, IRCListener +Hidden: False + +[ProxyUDPListener] +Enabled: True +Protocol: UDP +Listener: ProxyListener +Port: 38926 +Listeners: RawListener, DNSListener, TFTPListener, FTPListener +Hidden: False + +[Forwarder] +Enabled: False +Port: 8080 +Protocol: TCP +ProcessWhiteList: chrome.exe +Hidden: False + +[RawTCPListener] +Enabled: True +Port: 1337 +Protocol: TCP +Listener: RawListener +UseSSL: No +Timeout: 10 +Hidden: False +# To read about customizing responses, see docs/CustomResponse.md +# Custom: sample_custom_response.ini + +[RawUDPListener] +Enabled: True +Port: 1337 +Protocol: UDP +Listener: RawListener +UseSSL: No +Timeout: 10 +Hidden: False +# To read about customizing responses, see docs/CustomResponse.md +# Custom: sample_custom_response.ini + +[FilteredListener] +Enabled: False +Port: 31337 +Protocol: TCP +Listener: RawListener +UseSSL: No +Timeout: 10 +ProcessWhiteList: ncat.exe, nc.exe +HostBlackList: 5.5.5.5 +Hidden: False + +[DNS Server] +Enabled: True +Port: 53 +Protocol: UDP +Listener: DNSListener +ProcessBlackList: pythonw.exe +ResponseA: 192.0.2.123 +ResponseMX: mail.evil2.com +ResponseTXT: FAKENET +NXDomains: 0 +Hidden: False + +[HTTPListener80] +Enabled: True +Port: 80 +Protocol: TCP +Listener: HTTPListener +UseSSL: No +Webroot: defaultFiles/ +Timeout: 10 +ProcessBlackList: pythonw.exe +DumpHTTPPosts: Yes +DumpHTTPPostsFilePrefix: http +Hidden: False +# To read about customizing responses, see docs/CustomResponse.md +# Custom: sample_custom_response.ini + +[HTTPListener443] +Enabled: True +Port: 443 +Protocol: TCP +Listener: HTTPListener +UseSSL: Yes +Webroot: defaultFiles/ +Timeout: 10 +ProcessBlackList: pythonw.exe +DumpHTTPPosts: Yes +DumpHTTPPostsFilePrefix: http +Hidden: False + +[SMTPListener] +Enabled: True +Port: 25 +Protocol: TCP +Listener: SMTPListener +UseSSL: No +Hidden: False + +[FTPListener21] +Enabled: True +Port: 21 +Protocol: TCP +Listener: FTPListener +UseSSL: No +FTProot: defaultFiles/ +PasvPorts: 60000-60010 +Hidden: False +Banner: !generic +ServerName: !gethostname + +[FTPListenerPASV] +Enabled: True +Port: 60000-60010 +Protocol: TCP +Hidden: False + +[IRCServer] +Enabled: True +Port: 6667 +Protocol: TCP +Listener: IRCListener +UseSSL: No +Banner: !generic +ServerName: !gethostname +Timeout: 30 +Hidden: False + +[TFTPListener] +Enabled: True +Port: 69 +Protocol: UDP +Listener: TFTPListener +TFTPRoot: defaultFiles/ +Hidden: False +TFTPFilePrefix: tftp + +[POPServer] +Enabled: True +Port: 110 +Protocol: TCP +Listener: POPListener +UseSSL: No +Hidden: False + diff --git a/packages/internet_detector.vm/tools/images/background-internet.png b/packages/internet_detector.vm/tools/images/background-internet.png new file mode 100644 index 000000000..b380cb483 Binary files /dev/null and b/packages/internet_detector.vm/tools/images/background-internet.png differ diff --git a/packages/internet_detector.vm/tools/images/indicator_off.ico b/packages/internet_detector.vm/tools/images/indicator_off.ico new file mode 100644 index 000000000..c58bc6871 Binary files /dev/null and b/packages/internet_detector.vm/tools/images/indicator_off.ico differ diff --git a/packages/internet_detector.vm/tools/images/indicator_on.ico b/packages/internet_detector.vm/tools/images/indicator_on.ico new file mode 100644 index 000000000..e65a6e2f1 Binary files /dev/null and b/packages/internet_detector.vm/tools/images/indicator_on.ico differ diff --git a/packages/internet_detector.vm/tools/internet_detector.pyw b/packages/internet_detector.vm/tools/internet_detector.pyw new file mode 100644 index 000000000..3b22c25ed --- /dev/null +++ b/packages/internet_detector.vm/tools/internet_detector.pyw @@ -0,0 +1,425 @@ +# This tool checks if internet connectivity exists by reaching out to specific websites and checking if they return expected values and +# display the current state via changes to the background, theme, and icon in the taskbar. +# * It works even with a tool like FakeNet running (provided it uses the default configuration) +# If internet is detected, the tool: +# - Changes the background to the image "%VM_COMMON_DIR%\background-dynamic.png". +# - Change the the taskbar to be transparent and if the theme is dark mode also the color to "Rose". +# - Shows the icon "%VM_COMMON_DIR%\indicator_on.ico" in the taskbar. +# If internet is NOT detected, the tool: +# - Changes the background to the image "%VM_COMMON_DIR%\background.png" (default background image). +# - Changes the taskbar to be transparent keeping the current theme/color (dark or light). +# - Shows the icon "%VM_COMMON_DIR%\indicator_off.ico" in the taskbar. +VERSION = "1.0.0" +TOOL_NAME = "internet_detector" + +import threading +import requests +import win32api +import win32gui +import win32con +import urllib3 +import winreg +import signal +import ctypes +import time +import os +import re + + +# Define constants +CHECK_INTERVAL = 2 # Seconds +CONNECT_TEST_URL_AND_RESPONSES = { + "https://www.msftconnecttest.com/connecttest.txt": "Microsoft Connect Test", # HTTPS Test #1 + "http://www.google.com": "Google", # HTTP Test + "https://www.wikipedia.com": "Wikipedia", # HTTPS Test #2 + "https://www.youtube.com": "YouTube", # HTTPS Test #3 +} +SPI_SETDESKWALLPAPER = 20 +SPIF_UPDATEINIFILE = 0x01 +SPIF_SENDWININICHANGE = 0x02 +WM_DWMCOLORIZATIONCOLORCHANGED = 0x0320 +ACCENT_PREVALENCE = 1 +ROSE_COLOR = "#C30052" # Rose Color = #ff5200c3 which correlates to #C30052 before parsing +ROSE_ACCENT_PALETTE = "FFABCE00FF7FB400F74A9200C30052008C003A0069002C004D002000567C7300" +ICON_INDICATOR_ON = os.path.join(os.environ.get("VM_COMMON_DIR"), "indicator_on.ico") +ICON_INDICATOR_OFF = os.path.join(os.environ.get("VM_COMMON_DIR"), "indicator_off.ico") +DEFAULT_BACKGROUND = os.path.join(os.environ.get("VM_COMMON_DIR"), "background.png") +INTERNET_BACKGROUND = os.path.join(os.environ.get("VM_COMMON_DIR"), "background-internet.png") + +# Global variables +tray_icon = None +stop_event = threading.Event() # To signal the background thread to stop +hwnd = None # We'll assign the window handle here later +check_thread = None +tray_icon_thread = None +default_transparency = None +default_color_prevalence = None +default_color = None +default_palette = None +# Win32 API icon handles +hicon_indicator_off = None +hicon_indicator_on = None + + +def signal_handler(sig, frame): + global check_thread, tray_icon_thread, tray_icon + print("Ctrl+C detected. Exiting...") + stop_event.set() # Signal the background thread to stop + if check_thread: + check_thread.join() + if tray_icon_thread: + tray_icon_thread.join() + if tray_icon: + del tray_icon + exit(0) + + +def load_icon(icon_path): + try: + return win32gui.LoadImage(None, icon_path, win32con.IMAGE_ICON, 0, 0, win32con.LR_LOADFROMFILE) + except Exception as e: + print(f"Error loading indicator icon: {e}") + return None + + +class SysTrayIcon: + def __init__(self, hwnd, icon, tooltip): + self.hwnd = hwnd + self.icon = icon + self.tooltip = tooltip + # System tray icon data structure + self.nid = ( + self.hwnd, + 0, + win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP, + win32con.WM_USER + 20, + self.icon, + self.tooltip, + ) + # Add the icon to the system tray + win32gui.Shell_NotifyIcon(win32gui.NIM_ADD, self.nid) + + def set_tooltip(self, new_tooltip): + self.tooltip = new_tooltip + self.nid = ( + self.hwnd, + 0, + win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP, + win32con.WM_USER + 20, + self.icon, + self.tooltip, + ) + win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, self.nid) + + def set_icon(self, icon): + self.icon = icon + self.nid = ( + self.hwnd, + 0, + win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP, + win32con.WM_USER + 20, + self.icon, + self.tooltip, + ) + win32gui.Shell_NotifyIcon(win32gui.NIM_MODIFY, self.nid) + + def __del__(self): + # Remove the icon from the system tray when the object is destroyed + win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, self.nid) + + +def get_current_color_prevalence(): + try: + # Open the registry key for accent palette settings + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", + 0, + winreg.KEY_READ + ) + value, _ = winreg.QueryValueEx(key, "ColorPrevalence") + winreg.CloseKey(key) + return value + + except WindowsError: + print("Error accessing registry.") + return None + + +# Reads color palette binary data from a registry key and returns it as a hex string. +def get_current_color_palette(): + try: + # Open the registry key for accent palette settings + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Explorer\Accent", + 0, + winreg.KEY_READ + ) + value, _ = winreg.QueryValueEx(key, "AccentPalette") + winreg.CloseKey(key) + # return the data as hex string + return value.hex() + + except WindowsError: + print("Error accessing registry.") + return None + + +# Attempts to get the current taskbar color based on user personalization settings and returns it in hex format. +def get_current_taskbar_color(): + try: + # Open the registry key for personalization settings + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Explorer\Accent", + 0, + winreg.KEY_READ + ) + accent_color, _ = winreg.QueryValueEx(key, "AccentColorMenu") + winreg.CloseKey(key) + + # Convert the accent color value (DWORD) to RGB + r = accent_color & 0xFF + g = (accent_color >> 8) & 0xFF + b = (accent_color >> 16) & 0xFF + # Convert RGB to hex + return f"#{r:02X}{g:02X}{b:02X}" + + except WindowsError: + print("Error accessing registry.") + return None + + +def set_taskbar_accent_color(hex_color, hex_color_palette, color_prevalence): + """ + Sets the accent color location in the Windows registry. + + Args: + hex_color = the hex value of RGB color with starting "#" (e.g., '#RRGGBB') + hex_color_palette = the hex string of the corresponding color paletter for the provided hex_color + color_prevalence = 0 - don't use accent color; 1 - use accent color + """ + try: + print("Setting taskbar color to:", hex_color) + # Convert hex color to RGB + r = int(hex_color[1:3], 16) + g = int(hex_color[3:5], 16) + b = int(hex_color[5:7], 16) + + # Convert RGB to DWORD + color_value = (b << 16) | (g << 8) | r + + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", + 0, + winreg.KEY_ALL_ACCESS + ) + winreg.SetValueEx(key, "AccentColorPolicy", 0, winreg.REG_DWORD, 1) # Set color for taskbar + winreg.SetValueEx(key, "ColorPrevalence", 0, winreg.REG_DWORD, color_prevalence) # Enable accent color + winreg.SetValueEx(key, "AccentColorInactive", 0, winreg.REG_DWORD, 0) # Disable inactive color + winreg.CloseKey(key) + + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Explorer\Accent", + 0, + winreg.KEY_ALL_ACCESS, + ) + binary_data = bytes.fromhex(hex_color_palette) + winreg.SetValueEx(key, "AccentColorMenu", 0, winreg.REG_DWORD, color_value) + winreg.SetValueEx(key, "AccentPalette", 0, winreg.REG_BINARY, binary_data) + winreg.CloseKey(key) + + except WindowsError as e: + print(f"Error accessing or modifying registry: {e}") + + +def get_transparency_effects(): + try: + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", + 0, + winreg.KEY_ALL_ACCESS, + ) + value, _ = winreg.QueryValueEx(key, "EnableTransparency") + winreg.CloseKey(key) + return value + + except WindowsError as e: + print(f"Error accessing or modifying registry: {e}") + + +def set_transparency_effects(value): + try: + key = winreg.OpenKey( + winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", + 0, + winreg.KEY_ALL_ACCESS, + ) + + winreg.SetValueEx(key, "EnableTransparency", 0, winreg.REG_DWORD, value) + winreg.CloseKey(key) + except WindowsError as e: + print(f"Error accessing or modifying registry: {e}") + + +# Attempt to extract a known good value in response. +def extract_title(data): + match = re.search(r"(.*?)", data) + if match: + return match.group(1) + else: + return None + + +def check_internet(): + for url, expected_response in CONNECT_TEST_URL_AND_RESPONSES.items(): + try: + # Perform internet connectivity tests + response = requests.get(url, timeout=5, verify=False) + if expected_response in (extract_title(response.text) or response.text): + print(f"Internet connectivity detected via URL: {url}") + return True + except: + pass + return False + + +def check_internet_and_update_tray_icon(): + global tray_icon, hicon_indicator_off, hicon_indicator_on, default_color + if check_internet(): + tray_icon.set_icon(hicon_indicator_on) + tray_icon.set_tooltip("Internet Connection: Detected") + set_transparency_effects(1) + set_taskbar_accent_color(ROSE_COLOR, ROSE_ACCENT_PALETTE, ACCENT_PREVALENCE) + # Set the background to internet connection background + if get_wallpaper_path() != INTERNET_BACKGROUND: # Checked so program isn't continuously setting the wallpaper + set_wallpaper(INTERNET_BACKGROUND) + else: + tray_icon.set_icon(hicon_indicator_off) + tray_icon.set_tooltip("Internet Connection: Not Detected") + set_transparency_effects(default_transparency) + set_taskbar_accent_color(default_color, default_palette, default_color_prevalence) # change color back to what user had originally + # Reset background when internet is not detected + if get_wallpaper_path() != DEFAULT_BACKGROUND: # Checked so program isn't continuously setting the wallpaper + set_wallpaper(DEFAULT_BACKGROUND) + + +def check_internet_loop(): + while not stop_event.is_set(): + check_internet_and_update_tray_icon() + time.sleep(CHECK_INTERVAL) + + +def tray_icon_loop(): + global hwnd, tray_icon, hicon_indicator_off, hicon_indicator_on, stop_event + # Load icons + hicon_indicator_off = load_icon(ICON_INDICATOR_OFF) + hicon_indicator_on = load_icon(ICON_INDICATOR_ON) + + # Wait for hwnd to be initialized + while hwnd is None: + time.sleep(0.1) + + if hicon_indicator_off is None or hicon_indicator_on is None: + print("Error: Failed to load icons. Exiting TrayIconThread.") + return + + tray_icon = SysTrayIcon(hwnd, hicon_indicator_off, "Internet Detector") + + while not stop_event.is_set(): + msg = win32gui.PeekMessage(hwnd, 0, 0, 0) + if msg and len(msg) == 6: + win32gui.TranslateMessage(msg) + win32gui.DispatchMessage(msg) + time.sleep(0.1) + + +def get_wallpaper_path(): + """Attempts to retrieve the path to the current wallpaper image.""" + # Try to get the path from the registry (for wallpapers set through Windows settings) + try: + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Control Panel\Desktop", 0, winreg.KEY_READ) + value, _ = winreg.QueryValueEx(key, "Wallpaper") + winreg.CloseKey(key) + if value: + return value + except WindowsError: + pass + + # Check for cached wallpaper files (if the above fails) + cached_files_dir = os.path.join(os.getenv("APPDATA"), r"Microsoft\Windows\Themes\CachedFiles") + transcoded_wallpaper_path = os.path.join(os.getenv("APPDATA"), r"Microsoft\Windows\Themes\TranscodedWallpaper") + + for file in os.listdir(cached_files_dir): + if file.endswith((".jpg", ".jpeg", ".bmp", ".png")): + return os.path.join(cached_files_dir, file) + + if os.path.exists(transcoded_wallpaper_path): + return transcoded_wallpaper_path + + # If all else fails, return None + return None + + +def set_wallpaper(image_path): + """Sets the desktop wallpaper to the image at the specified path.""" + print(f"Setting wallpaper to: {image_path}") + result = ctypes.windll.user32.SystemParametersInfoW( + SPI_SETDESKWALLPAPER, 0, image_path, SPIF_UPDATEINIFILE | SPIF_SENDWININICHANGE + ) + if not result: + print("Error setting wallpaper. Make sure the image path is correct.") + + +def main_loop(): + global stop_event, check_thread, tray_icon_thread, tray_icon + # Create and start the threads + tray_icon_thread = threading.Thread(target=tray_icon_loop) + check_thread = threading.Thread(target=check_internet_loop) + + tray_icon_thread.start() + # Wait for the tray icon to finish initializing + while tray_icon is None: + time.sleep(0.1) + + check_thread.start() + + while not stop_event.is_set(): + time.sleep(1) + + +if __name__ == "__main__": + signal.signal(signal.SIGINT, signal_handler) + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + default_transparency = get_transparency_effects() + default_color_prevalence = get_current_color_prevalence() + default_color = get_current_taskbar_color() + default_palette = get_current_color_palette() + + # Create a hidden window to receive messages (required for system tray icons) + def wndProc(hwnd, msg, wparam, lparam): + if lparam == win32con.WM_LBUTTONDBLCLK: + print("Left button double clicked") + elif msg == win32con.WM_COMMAND: + if wparam == 1023: # Example menu item ID + print("Exit selected") + win32gui.DestroyWindow(hwnd) + return win32gui.DefWindowProc(hwnd, msg, wparam, lparam) + + wc = win32gui.WNDCLASS() + hinst = wc.hInstance = win32api.GetModuleHandle(None) + wc.lpszClassName = "Internet Detector" + wc.lpfnWndProc = wndProc + classAtom = win32gui.RegisterClass(wc) + hwnd = win32gui.CreateWindow(classAtom, "Internet Detector", 0, 0, 0, 0, 0, 0, 0, hinst, None) + + print(f"{TOOL_NAME} Version: {VERSION}") + print(f"Current wallpaper: {get_wallpaper_path()}") + print(f"Current color: {default_color}") + + main_loop()