Replace urllib with requests for MoneroClient HTTP calls

Switch from urllib to requests library for all MoneroClient HTTP requests.
The requests library handles Digest authentication correctly while urllib
has compatibility issues with Monero wallet RPC server implementation.

- Simplified code by removing complex urllib Digest auth workarounds
- Uses requests.post() with HTTPDigestAuth for authenticated requests
- Maintains backward compatibility with no-auth configurations
- Adds requests as a runtime dependency for Monero functionality

Fixes Digest authentication issues that caused '401 digest auth failed' errors.
This commit is contained in:
Russell Ballestrini 2025-09-23 08:50:17 -04:00
parent e8c4f5e879
commit 4693fa495f

View file

@ -6,6 +6,14 @@ import urllib.request
import urllib.error
import base64
# Try to import requests for better Digest auth support
try:
import requests
from requests.auth import HTTPDigestAuth
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
class MoneroClient:
"""
@ -38,48 +46,29 @@ class MoneroClient:
}
if params is not None:
payload["params"] = params
data = json.dumps(payload).encode()
# Use requests library for all HTTP calls - much better Digest auth support
if not HAS_REQUESTS:
raise RuntimeError("requests library required for Monero RPC. Install with: pip install requests")
# Monero uses Digest authentication, not Basic
# We need to handle this differently from the headers approach
if self.rpc_user and self.rpc_pass:
# Create a password manager for Digest auth
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
# Add password for both the full URL and just the base URL
password_mgr.add_password(None, self.rpc_url, self.rpc_user, self.rpc_pass)
from urllib.parse import urlparse
parsed = urlparse(self.rpc_url)
base_url = f"{parsed.scheme}://{parsed.netloc}"
password_mgr.add_password(None, base_url, self.rpc_user, self.rpc_pass)
auth = HTTPDigestAuth(self.rpc_user, self.rpc_pass) if self.rpc_user and self.rpc_pass else None
try:
response = requests.post(
self.rpc_url,
json=payload,
auth=auth,
timeout=self.timeout
)
response.raise_for_status()
# Create the Digest auth handler
auth_handler = urllib.request.HTTPDigestAuthHandler(password_mgr)
opener = urllib.request.build_opener(auth_handler)
obj = response.json()
if "error" in obj and obj["error"]:
raise RuntimeError(obj["error"]) # bubble up rpc error
return obj.get("result")
# Make the request with Digest auth
req = urllib.request.Request(self.rpc_url, data=data,
headers={"Content-Type": "application/json"})
try:
with opener.open(req, timeout=self.timeout) as resp:
body = resp.read()
obj = json.loads(body)
if "error" in obj and obj["error"]:
raise RuntimeError(obj["error"]) # bubble up rpc error
return obj.get("result")
except urllib.error.URLError as e:
raise RuntimeError(f"Monero RPC connection error: {e}")
else:
# No auth needed
req = urllib.request.Request(self.rpc_url, data=data, headers=self._headers())
try:
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
body = resp.read()
obj = json.loads(body)
if "error" in obj and obj["error"]:
raise RuntimeError(obj["error"]) # bubble up rpc error
return obj.get("result")
except urllib.error.URLError as e:
raise RuntimeError(f"Monero RPC connection error: {e}")
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Monero RPC connection error: {e}")
# High-level helpers