Fix SSL certificate verification on macOS by adding certifi support and proper SSL context

This commit is contained in:
justin committed 2026-01-26 20:42:40 -07:00
1 parent b2dd4deb44
commit 945ece7fb9
1 file changed
+65 -48
+65 -48
View File
@@ -19,6 +19,7 @@ import re
import threading import threading
import tempfile import tempfile
import shutil import shutil
import ssl
from pathlib import Path from pathlib import Path
from typing import Dict, Optional, Tuple, List from typing import Dict, Optional, Tuple, List
from datetime import datetime from datetime import datetime
@@ -33,6 +34,7 @@ class MoneroMiner:
self.stats_thread = None self.stats_thread = None
self.stop_stats = False self.stop_stats = False
self.has_nvidia_gpu = False self.has_nvidia_gpu = False
self.ssl_context = self.setup_ssl_context()
# Known mining pools (with minimum payout amounts) # Known mining pools (with minimum payout amounts)
self.known_pools = [ self.known_pools = [
@@ -44,8 +46,23 @@ class MoneroMiner:
{"name": "C3Pool", "address": "mine.c3pool.com:13333", "tls": False, "coin": None, "min_payout": "0.001 XMR", "no_api": True}, {"name": "C3Pool", "address": "mine.c3pool.com:13333", "tls": False, "coin": None, "min_payout": "0.001 XMR", "no_api": True},
] ]
def setup_ssl_context(self):
# Setup SSL context with proper certificates for macOS
try:
import certifi
# Create SSL context with certifi certificates
context = ssl.create_default_context(cafile=certifi.where())
return context
except ImportError:
# Fallback: use default SSL context
try:
return ssl.create_default_context()
except:
# Last resort: unverified context (not recommended but works)
return ssl._create_unverified_context()
def detect_os(self) -> str: def detect_os(self) -> str:
"""Detect the operating system""" # Detect the operating system
system = platform.system() system = platform.system()
if system == 'Darwin': if system == 'Darwin':
return 'macos' return 'macos'
@@ -57,7 +74,7 @@ class MoneroMiner:
return 'unknown' return 'unknown'
def detect_cpu_arch(self) -> str: def detect_cpu_arch(self) -> str:
"""Detect CPU architecture""" # Detect CPU architecture
machine = platform.machine().lower() machine = platform.machine().lower()
if machine in ['arm64', 'aarch64']: if machine in ['arm64', 'aarch64']:
return 'arm64' return 'arm64'
@@ -69,7 +86,7 @@ class MoneroMiner:
return machine return machine
def detect_cpu_model(self) -> str: def detect_cpu_model(self) -> str:
"""Detect the specific CPU model for worker naming""" # Detect the specific CPU model for worker naming
try: try:
system = platform.system() system = platform.system()
@@ -146,7 +163,7 @@ class MoneroMiner:
return "arm64" if arch == 'arm64' else "x64" return "arm64" if arch == 'arm64' else "x64"
def detect_nvidia_gpu(self) -> bool: def detect_nvidia_gpu(self) -> bool:
"""Detect if an NVIDIA GPU is present""" # Detect if an NVIDIA GPU is present
os_type = self.detect_os() os_type = self.detect_os()
try: try:
@@ -185,13 +202,13 @@ class MoneroMiner:
return False return False
def get_latest_xmrig_release(self) -> Optional[Tuple[str, list]]: def get_latest_xmrig_release(self) -> Optional[Tuple[str, list]]:
"""Fetch the latest XMRig version and available files from GitHub""" # Fetch the latest XMRig version and available files from GitHub
try: try:
url = "https://api.github.com/repos/xmrig/xmrig/releases/latest" url = "https://api.github.com/repos/xmrig/xmrig/releases/latest"
req = urllib.request.Request(url) req = urllib.request.Request(url)
req.add_header('User-Agent', 'Mozilla/5.0') req.add_header('User-Agent', 'Mozilla/5.0')
with urllib.request.urlopen(req, timeout=10) as response: with urllib.request.urlopen(req, timeout=10, context=self.ssl_context) as response:
data = json.loads(response.read().decode()) data = json.loads(response.read().decode())
version = data['tag_name'].lstrip('v') version = data['tag_name'].lstrip('v')
@@ -204,13 +221,13 @@ class MoneroMiner:
return None return None
def get_latest_gminer_release(self) -> Optional[Tuple[str, List[dict]]]: def get_latest_gminer_release(self) -> Optional[Tuple[str, List[dict]]]:
"""Fetch the latest GMiner version and available files from GitHub""" # Fetch the latest GMiner version and available files from GitHub
try: try:
url = "https://api.github.com/repos/develsoftware/GMinerRelease/releases/latest" url = "https://api.github.com/repos/develsoftware/GMinerRelease/releases/latest"
req = urllib.request.Request(url) req = urllib.request.Request(url)
req.add_header('User-Agent', 'Mozilla/5.0') req.add_header('User-Agent', 'Mozilla/5.0')
with urllib.request.urlopen(req, timeout=10) as response: with urllib.request.urlopen(req, timeout=10, context=self.ssl_context) as response:
data = json.loads(response.read().decode()) data = json.loads(response.read().decode())
version = data['tag_name'].lstrip('v') version = data['tag_name'].lstrip('v')
@@ -228,7 +245,7 @@ class MoneroMiner:
return None return None
def find_matching_file(self, assets: list, os_type: str, arch: str) -> Optional[str]: def find_matching_file(self, assets: list, os_type: str, arch: str) -> Optional[str]:
"""Find the matching file for the given OS and architecture""" # Find the matching file for the given OS and architecture
# Build search patterns based on OS and architecture # Build search patterns based on OS and architecture
if os_type == 'macos': if os_type == 'macos':
if arch == 'arm64': if arch == 'arm64':
@@ -263,7 +280,7 @@ class MoneroMiner:
return None return None
def find_matching_gminer_file(self, assets: List[dict], os_type: str) -> Optional[dict]: def find_matching_gminer_file(self, assets: List[dict], os_type: str) -> Optional[dict]:
"""Find the matching GMiner file for the given OS""" # Find the matching GMiner file for the given OS
for asset in assets: for asset in assets:
name = asset['name'].lower() name = asset['name'].lower()
if os_type == 'windows' and 'windows' in name and name.endswith('.zip'): if os_type == 'windows' and 'windows' in name and name.endswith('.zip'):
@@ -273,7 +290,7 @@ class MoneroMiner:
return None return None
def check_xmrig_installed(self) -> bool: def check_xmrig_installed(self) -> bool:
"""Check if xmrig is installed""" # Check if xmrig is installed
# Check in script directory first # Check in script directory first
local_paths = [ local_paths = [
os.path.join(self.script_dir, 'xmrig'), os.path.join(self.script_dir, 'xmrig'),
@@ -310,7 +327,7 @@ class MoneroMiner:
return False return False
def check_gminer_installed(self) -> bool: def check_gminer_installed(self) -> bool:
"""Check if GMiner is installed in script directory""" # Check if GMiner is installed in script directory
os_type = self.detect_os() os_type = self.detect_os()
if os_type == 'windows': if os_type == 'windows':
@@ -321,14 +338,14 @@ class MoneroMiner:
return os.path.exists(gminer_path) and os.access(gminer_path, os.X_OK if os.name != 'nt' else os.F_OK) return os.path.exists(gminer_path) and os.access(gminer_path, os.X_OK if os.name != 'nt' else os.F_OK)
def download_file_with_progress(self, url: str, local_filename: str, max_retries: int = 3) -> bool: def download_file_with_progress(self, url: str, local_filename: str, max_retries: int = 3) -> bool:
"""Download a file with progress indication and retry logic""" # Download a file with progress indication and retry logic
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
req = urllib.request.Request(url) req = urllib.request.Request(url)
req.add_header('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36') req.add_header('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36')
req.add_header('Accept', '*/*') req.add_header('Accept', '*/*')
with urllib.request.urlopen(req, timeout=30) as response: with urllib.request.urlopen(req, timeout=30, context=self.ssl_context) as response:
total_size = int(response.headers.get('Content-Length', 0)) total_size = int(response.headers.get('Content-Length', 0))
downloaded = 0 downloaded = 0
@@ -365,7 +382,7 @@ class MoneroMiner:
return False return False
def download_and_install_xmrig(self) -> bool: def download_and_install_xmrig(self) -> bool:
"""Download and install XMRig based on OS and architecture""" # Download and install XMRig based on OS and architecture
os_type = self.detect_os() os_type = self.detect_os()
arch = self.detect_cpu_arch() arch = self.detect_cpu_arch()
@@ -551,7 +568,7 @@ class MoneroMiner:
return False return False
def download_and_install_gminer(self) -> bool: def download_and_install_gminer(self) -> bool:
"""Download and install GMiner for GPU mining""" # Download and install GMiner for GPU mining
os_type = self.detect_os() os_type = self.detect_os()
# GMiner only supports Windows and Linux # GMiner only supports Windows and Linux
@@ -671,7 +688,7 @@ class MoneroMiner:
return False return False
def prompt_wallet_address(self) -> str: def prompt_wallet_address(self) -> str:
"""Prompt user for wallet address""" # Prompt user for wallet address
default_wallet = "85NGrygcwq36kjLaNKZevT53H4EeHRQYeje5Y3gq3GYkKsEpwKpGjqd1tGxAVEjDfxEha6SYtyNYPbmPXduYqy47Q6JwbuW" default_wallet = "85NGrygcwq36kjLaNKZevT53H4EeHRQYeje5Y3gq3GYkKsEpwKpGjqd1tGxAVEjDfxEha6SYtyNYPbmPXduYqy47Q6JwbuW"
print("\n" + "="*60) print("\n" + "="*60)
@@ -692,7 +709,7 @@ class MoneroMiner:
return wallet return wallet
def prompt_pool_selection(self) -> Tuple[str, bool, Optional[str]]: def prompt_pool_selection(self) -> Tuple[str, bool, Optional[str]]:
"""Prompt user to select a mining pool""" # Prompt user to select a mining pool
print("\n" + "="*60) print("\n" + "="*60)
print("Mining Pool Selection") print("Mining Pool Selection")
print("="*60) print("="*60)
@@ -748,7 +765,7 @@ class MoneroMiner:
print("Please enter a valid number or press ENTER for default") print("Please enter a valid number or press ENTER for default")
def check_windows_admin(self) -> bool: def check_windows_admin(self) -> bool:
"""Check if script is running with Administrator privileges on Windows""" # Check if script is running with Administrator privileges on Windows
try: try:
import ctypes import ctypes
return ctypes.windll.shell32.IsUserAnAdmin() != 0 return ctypes.windll.shell32.IsUserAnAdmin() != 0
@@ -756,7 +773,7 @@ class MoneroMiner:
return False return False
def prompt_msr_mod(self, os_type: str) -> bool: def prompt_msr_mod(self, os_type: str) -> bool:
"""Prompt user to enable MSR MOD for performance optimization""" # Prompt user to enable MSR MOD for performance optimization
# MSR MOD is only supported on Windows and Linux # MSR MOD is only supported on Windows and Linux
if os_type not in ['windows', 'linux']: if os_type not in ['windows', 'linux']:
return False return False
@@ -806,7 +823,7 @@ class MoneroMiner:
return False return False
def setup_linux_msr(self): def setup_linux_msr(self):
"""Setup MSR kernel module on Linux""" # Setup MSR kernel module on Linux
try: try:
# Check if msr module is loaded # Check if msr module is loaded
result = subprocess.run(['lsmod'], capture_output=True, text=True) result = subprocess.run(['lsmod'], capture_output=True, text=True)
@@ -841,7 +858,7 @@ class MoneroMiner:
print(" 2. Start miner with: sudo python3 miner-monero.py") print(" 2. Start miner with: sudo python3 miner-monero.py")
def prompt_gpu_mining(self) -> bool: def prompt_gpu_mining(self) -> bool:
"""Prompt user to enable GPU mining if NVIDIA GPU is detected""" # Prompt user to enable GPU mining if NVIDIA GPU is detected
print("\n" + "="*60) print("\n" + "="*60)
print("GPU Mining Configuration") print("GPU Mining Configuration")
print("="*60) print("="*60)
@@ -866,7 +883,7 @@ class MoneroMiner:
return False return False
def setup_initial_config(self): def setup_initial_config(self):
"""Setup initial configuration with user input""" # Setup initial configuration with user input
os_type = self.detect_os() os_type = self.detect_os()
arch = self.detect_cpu_arch() arch = self.detect_cpu_arch()
is_mac = os_type == 'macos' is_mac = os_type == 'macos'
@@ -930,7 +947,7 @@ class MoneroMiner:
return config return config
def load_config(self) -> Dict: def load_config(self) -> Dict:
"""Load configuration from JSON file""" # Load configuration from JSON file
if not os.path.exists(self.config_file): if not os.path.exists(self.config_file):
print(f"Configuration file '{self.config_file}' not found.") print(f"Configuration file '{self.config_file}' not found.")
print("Setting up initial configuration...") print("Setting up initial configuration...")
@@ -949,7 +966,7 @@ class MoneroMiner:
sys.exit(1) sys.exit(1)
def validate_config(self, config: Dict): def validate_config(self, config: Dict):
"""Validate required configuration fields""" # Validate required configuration fields
required_fields = ['wallet_address', 'mining_mode'] required_fields = ['wallet_address', 'mining_mode']
for field in required_fields: for field in required_fields:
@@ -963,7 +980,7 @@ class MoneroMiner:
raise ValueError("pool_address is required for pool mining") raise ValueError("pool_address is required for pool mining")
def is_supportxmr_pool(self) -> bool: def is_supportxmr_pool(self) -> bool:
"""Check if the configured pool is SupportXMR""" # Check if the configured pool is SupportXMR
if self.config['mining_mode'] != 'pool': if self.config['mining_mode'] != 'pool':
return False return False
@@ -971,7 +988,7 @@ class MoneroMiner:
return 'supportxmr.com' in pool_addr return 'supportxmr.com' in pool_addr
def is_moneroocean_pool(self) -> bool: def is_moneroocean_pool(self) -> bool:
"""Check if the configured pool is MoneroOcean""" # Check if the configured pool is MoneroOcean
if self.config['mining_mode'] != 'pool': if self.config['mining_mode'] != 'pool':
return False return False
@@ -979,7 +996,7 @@ class MoneroMiner:
return 'moneroocean' in pool_addr return 'moneroocean' in pool_addr
def is_nanopool_pool(self) -> bool: def is_nanopool_pool(self) -> bool:
"""Check if the configured pool is NanoPool""" # Check if the configured pool is NanoPool
if self.config['mining_mode'] != 'pool': if self.config['mining_mode'] != 'pool':
return False return False
@@ -987,7 +1004,7 @@ class MoneroMiner:
return 'nanopool' in pool_addr return 'nanopool' in pool_addr
def is_hashvault_pool(self) -> bool: def is_hashvault_pool(self) -> bool:
"""Check if the configured pool is HashVault""" # Check if the configured pool is HashVault
if self.config['mining_mode'] != 'pool': if self.config['mining_mode'] != 'pool':
return False return False
@@ -995,7 +1012,7 @@ class MoneroMiner:
return 'hashvault' in pool_addr return 'hashvault' in pool_addr
def is_herominers_pool(self) -> bool: def is_herominers_pool(self) -> bool:
"""Check if the configured pool is HeroMiners""" # Check if the configured pool is HeroMiners
if self.config['mining_mode'] != 'pool': if self.config['mining_mode'] != 'pool':
return False return False
@@ -1003,7 +1020,7 @@ class MoneroMiner:
return 'herominers' in pool_addr return 'herominers' in pool_addr
def fetch_pool_stats(self) -> Optional[Dict]: def fetch_pool_stats(self) -> Optional[Dict]:
"""Fetch mining statistics from pool API""" # Fetch mining statistics from pool API
try: try:
wallet = self.config['wallet_address'] wallet = self.config['wallet_address']
@@ -1023,7 +1040,7 @@ class MoneroMiner:
req = urllib.request.Request(url) req = urllib.request.Request(url)
req.add_header('User-Agent', 'Mozilla/5.0') req.add_header('User-Agent', 'Mozilla/5.0')
with urllib.request.urlopen(req, timeout=10) as response: with urllib.request.urlopen(req, timeout=10, context=self.ssl_context) as response:
data = json.loads(response.read().decode()) data = json.loads(response.read().decode())
# NanoPool wraps data differently # NanoPool wraps data differently
@@ -1064,7 +1081,7 @@ class MoneroMiner:
return None return None
def format_hashrate(self, hash_rate: float) -> str: def format_hashrate(self, hash_rate: float) -> str:
"""Format hash rate in human-readable format""" # Format hash rate in human-readable format
if hash_rate >= 1000000: if hash_rate >= 1000000:
return f"{hash_rate / 1000000:.2f} MH/s" return f"{hash_rate / 1000000:.2f} MH/s"
elif hash_rate >= 1000: elif hash_rate >= 1000:
@@ -1073,12 +1090,12 @@ class MoneroMiner:
return f"{hash_rate:.2f} H/s" return f"{hash_rate:.2f} H/s"
def format_xmr(self, piconero: int) -> str: def format_xmr(self, piconero: int) -> str:
"""Convert piconero to XMR""" # Convert piconero to XMR
xmr = piconero / 1000000000000 xmr = piconero / 1000000000000
return f"{xmr:.12f} XMR" return f"{xmr:.12f} XMR"
def format_time_ago(self, timestamp: int) -> str: def format_time_ago(self, timestamp: int) -> str:
"""Format timestamp as time ago""" # Format timestamp as time ago
now = int(time.time()) now = int(time.time())
diff = now - timestamp diff = now - timestamp
@@ -1092,7 +1109,7 @@ class MoneroMiner:
return f"{diff // 86400} days ago" return f"{diff // 86400} days ago"
def display_pool_stats(self, stats: Dict): def display_pool_stats(self, stats: Dict):
"""Display pool statistics in a readable format""" # Display pool statistics in a readable format
print("\n" + "="*60) print("\n" + "="*60)
print(f"Pool Statistics - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") print(f"Pool Statistics - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("="*60) print("="*60)
@@ -1246,7 +1263,7 @@ class MoneroMiner:
print("="*60 + "\n") print("="*60 + "\n")
def stats_monitor_thread(self): def stats_monitor_thread(self):
"""Background thread to periodically fetch and display pool stats""" # Background thread to periodically fetch and display pool stats
interval = self.config.get('stats_update_interval', 300) interval = self.config.get('stats_update_interval', 300)
# Display stats immediately # Display stats immediately
@@ -1274,7 +1291,7 @@ class MoneroMiner:
self.display_pool_stats(stats) self.display_pool_stats(stats)
def start_stats_monitor(self): def start_stats_monitor(self):
"""Start the stats monitoring thread""" # Start the stats monitoring thread
if not self.is_supportxmr_pool() and not self.is_moneroocean_pool() and not self.is_nanopool_pool() and not self.is_hashvault_pool() and not self.is_herominers_pool(): if not self.is_supportxmr_pool() and not self.is_moneroocean_pool() and not self.is_nanopool_pool() and not self.is_hashvault_pool() and not self.is_herominers_pool():
return return
@@ -1298,13 +1315,13 @@ class MoneroMiner:
self.stats_thread.start() self.stats_thread.start()
def stop_stats_monitor(self): def stop_stats_monitor(self):
"""Stop the stats monitoring thread""" # Stop the stats monitoring thread
if self.stats_thread: if self.stats_thread:
self.stop_stats = True self.stop_stats = True
self.stats_thread.join(timeout=2) self.stats_thread.join(timeout=2)
def initialize(self): def initialize(self):
"""Initialize the miner - check for xmrig and setup config""" # Initialize the miner - check for xmrig and setup config
# Check for NVIDIA GPU first # Check for NVIDIA GPU first
print("\nChecking for NVIDIA GPU...") print("\nChecking for NVIDIA GPU...")
self.has_nvidia_gpu = self.detect_nvidia_gpu() self.has_nvidia_gpu = self.detect_nvidia_gpu()
@@ -1382,7 +1399,7 @@ class MoneroMiner:
self.config['gpu_mining'] = False self.config['gpu_mining'] = False
def build_command(self) -> list: def build_command(self) -> list:
"""Build the XMRig mining command based on configuration""" # Build the XMRig mining command based on configuration
cmd = [self.config.get('miner_executable', 'xmrig')] cmd = [self.config.get('miner_executable', 'xmrig')]
# Add wallet address # Add wallet address
@@ -1468,7 +1485,7 @@ class MoneroMiner:
return cmd return cmd
def build_gminer_command(self) -> list: def build_gminer_command(self) -> list:
"""Build the GMiner command for GPU mining""" # Build the GMiner command for GPU mining
os_type = self.detect_os() os_type = self.detect_os()
if os_type == 'windows': if os_type == 'windows':
@@ -1493,7 +1510,7 @@ class MoneroMiner:
return cmd return cmd
def start_gpu_mining(self): def start_gpu_mining(self):
"""Start the GPU mining process with GMiner""" # Start the GPU mining process with GMiner
if not self.config.get('gpu_mining', False): if not self.config.get('gpu_mining', False):
return return
@@ -1538,7 +1555,7 @@ class MoneroMiner:
print(f"Error starting GPU miner: {e}") print(f"Error starting GPU miner: {e}")
def stop_gpu_mining(self): def stop_gpu_mining(self):
"""Stop the GPU mining process""" # Stop the GPU mining process
if self.gpu_process: if self.gpu_process:
print("Stopping GPU miner...") print("Stopping GPU miner...")
self.gpu_process.terminate() self.gpu_process.terminate()
@@ -1550,7 +1567,7 @@ class MoneroMiner:
print("GPU miner stopped.") print("GPU miner stopped.")
def start_mining(self): def start_mining(self):
"""Start the mining process""" # Start the mining process
cmd = self.build_command() cmd = self.build_command()
# Check if MSR is enabled but not running with proper privileges # Check if MSR is enabled but not running with proper privileges
@@ -1646,7 +1663,7 @@ class MoneroMiner:
self.stop_gpu_mining() self.stop_gpu_mining()
def stop_mining(self): def stop_mining(self):
"""Stop the mining process""" # Stop the mining process
self.stop_stats_monitor() self.stop_stats_monitor()
self.stop_gpu_mining() self.stop_gpu_mining()
@@ -1661,14 +1678,14 @@ class MoneroMiner:
print("CPU miner stopped.") print("CPU miner stopped.")
def show_stats(self): def show_stats(self):
"""Display mining statistics if API is enabled""" # Display mining statistics if API is enabled
api_port = self.config.get('api_port', 0) api_port = self.config.get('api_port', 0)
if api_port > 0: if api_port > 0:
print(f"\nMining stats available at: http://127.0.0.1:{api_port}") print(f"\nMining stats available at: http://127.0.0.1:{api_port}")
print("You can view real-time statistics in your browser") print("You can view real-time statistics in your browser")
def signal_handler(signum, frame): def signal_handler(signum, frame):
"""Handle interrupt signals""" # Handle interrupt signals
print("\n\nShutdown signal received...") print("\n\nShutdown signal received...")
sys.exit(0) sys.exit(0)