debug du upnp mediaserver

This commit is contained in:
2025-11-26 21:40:24 +01:00
parent e21fa5948e
commit ccd2112305
18 changed files with 1170 additions and 80 deletions

166
tools/compare_upnp.py Executable file
View File

@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""
Compare UPnP MediaServers
"""
from urllib.request import urlopen, Request
import re
# Devices à comparer
DEVICES = {
"PMO Music 1": "http://192.168.0.138:8080/device/659878e3-9790-4ba0-a710-946e9470bd01/desc.xml",
"PMO Music 2": "http://192.168.0.138:8080/device/8b8e9b19-9c65-4d59-b127-b34717658085/desc.xml",
"Upmpdcli": "http://192.168.0.200:49152/uuid-c110358f-d885-b44a-d6d3-dca6329ead0d/description.xml",
"Freebox": "http://192.168.0.254:52424/device.xml",
}
def fetch_description(url):
"""Récupère la description XML"""
try:
req = Request(url, headers={'User-Agent': 'PMOMusic/1.0'})
response = urlopen(req, timeout=3)
return response.read().decode('utf-8')
except Exception as e:
return f"Error: {e}"
def extract_info(xml):
"""Extrait les infos clés"""
info = {}
patterns = {
'deviceType': r'<deviceType>([^<]+)</deviceType>',
'friendlyName': r'<friendlyName>([^<]+)</friendlyName>',
'manufacturer': r'<manufacturer>([^<]+)</manufacturer>',
'modelName': r'<modelName>([^<]+)</modelName>',
'UDN': r'<UDN>([^<]+)</UDN>',
'specVersion': r'<specVersion>.*?<major>(\d+)</major>.*?<minor>(\d+)</minor>',
}
for key, pattern in patterns.items():
match = re.search(pattern, xml, re.DOTALL)
if match:
if key == 'specVersion':
info[key] = f"{match.group(1)}.{match.group(2)}"
else:
info[key] = match.group(1)
# Extraire les services
services = re.findall(r'<serviceType>([^<]+)</serviceType>', xml)
info['services'] = services
# Vérifier les icônes
has_icons = bool(re.search(r'<iconList>', xml))
info['hasIcons'] = has_icons
return info
def main():
print("=" * 100)
print(" 🔍 UPnP MediaServer Comparison")
print("=" * 100)
print()
results = {}
for name, url in DEVICES.items():
print(f"📡 Fetching {name}...")
xml = fetch_description(url)
if not xml.startswith("Error"):
results[name] = {
'xml': xml,
'info': extract_info(xml)
}
print(f" ✅ Fetched ({len(xml)} bytes)")
else:
print(f"{xml}")
print()
# Comparer les résultats
print("=" * 100)
print(" 📊 COMPARISON")
print("=" * 100)
print()
# Tableau comparatif
print(f"{'Property':<20} | {'PMO Music 1':<30} | {'PMO Music 2':<30} | {'Upmpdcli':<30} | {'Freebox':<30}")
print("-" * 150)
properties = ['deviceType', 'specVersion', 'UDN', 'friendlyName', 'manufacturer', 'modelName', 'hasIcons']
for prop in properties:
row = f"{prop:<20} |"
for device in ["PMO Music 1", "PMO Music 2", "Upmpdcli", "Freebox"]:
if device in results:
value = str(results[device]['info'].get(prop, 'N/A'))[:28]
row += f" {value:<30} |"
else:
row += f" {'N/A':<30} |"
print(row)
print()
print("=" * 100)
print(" 🔌 SERVICES")
print("=" * 100)
print()
for name, data in results.items():
print(f"\n{name}:")
for service in data['info'].get('services', []):
print(f" - {service}")
# Afficher les XMLs complets pour PMO Music et un qui fonctionne
print("\n" + "=" * 100)
print(" 📄 FULL XML COMPARISON")
print("=" * 100)
if "PMO Music 1" in results:
print("\n" + "=" * 50)
print(" PMO Music MediaServer XML:")
print("=" * 50)
print(results["PMO Music 1"]['xml'])
if "Upmpdcli" in results:
print("\n" + "=" * 50)
print(" Upmpdcli (WORKING) XML:")
print("=" * 50)
print(results["Upmpdcli"]['xml'])
# Analyse des différences critiques
print("\n" + "=" * 100)
print(" ⚠️ CRITICAL DIFFERENCES")
print("=" * 100)
print()
if "PMO Music 1" in results and "Upmpdcli" in results:
pmo_udn = results["PMO Music 1"]['info'].get('UDN', '')
upmp_udn = results["Upmpdcli"]['info'].get('UDN', '')
print(f"UDN Format:")
print(f" PMO Music: {pmo_udn}")
print(f" Upmpdcli: {upmp_udn}")
if not pmo_udn.startswith('uuid:'):
print(f" ❌ PROBLÈME: PMO Music UDN ne commence pas par 'uuid:'")
else:
print(f" ✅ PMO Music UDN format correct")
if not upmp_udn.startswith('uuid:'):
print(f" ❌ PROBLÈME: Upmpdcli UDN ne commence pas par 'uuid:'")
else:
print(f" ✅ Upmpdcli UDN format correct")
print()
pmo_icons = results["PMO Music 1"]['info'].get('hasIcons', False)
upmp_icons = results["Upmpdcli"]['info'].get('hasIcons', False)
print(f"Icons:")
print(f" PMO Music: {pmo_icons}")
print(f" Upmpdcli: {upmp_icons}")
if not pmo_icons and upmp_icons:
print(f" ⚠️ PMO Music n'a pas d'iconList (mais peut ne pas être critique)")
if __name__ == "__main__":
main()

175
tools/discover_upnp.py Executable file
View File

@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""
UPnP Device Discovery Tool
Envoie une requête M-SEARCH SSDP et collecte les réponses des devices
"""
import socket
import struct
import time
import sys
from urllib.parse import urlparse
from urllib.request import urlopen
SSDP_ADDR = "239.255.255.250"
SSDP_PORT = 1900
SSDP_MX = 3
SSDP_ST = "ssdp:all"
M_SEARCH = f"""M-SEARCH * HTTP/1.1
HOST: {SSDP_ADDR}:{SSDP_PORT}
MAN: "ssdp:discover"
MX: {SSDP_MX}
ST: {SSDP_ST}
USER-AGENT: PMOMusic UPnP Discovery Tool
"""
def discover_upnp_devices(timeout=5):
"""Découvre les devices UPnP sur le réseau local"""
devices = {}
# Créer le socket UDP
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.settimeout(timeout)
# Envoyer la requête M-SEARCH
print(f"🔍 Envoi de la requête M-SEARCH sur {SSDP_ADDR}:{SSDP_PORT}...")
print(f"⏱️ Timeout: {timeout}s\n")
message = M_SEARCH.replace('\n', '\r\n').encode('utf-8')
sock.sendto(message, (SSDP_ADDR, SSDP_PORT))
# Collecter les réponses
start_time = time.time()
while time.time() - start_time < timeout:
try:
data, addr = sock.recvfrom(65507)
response = data.decode('utf-8', errors='ignore')
# Parser la réponse
location = None
server = None
st = None
usn = None
for line in response.split('\r\n'):
if line.lower().startswith('location:'):
location = line.split(':', 1)[1].strip()
elif line.lower().startswith('server:'):
server = line.split(':', 1)[1].strip()
elif line.lower().startswith('st:'):
st = line.split(':', 1)[1].strip()
elif line.lower().startswith('usn:'):
usn = line.split(':', 1)[1].strip()
if location and location not in devices:
devices[location] = {
'location': location,
'server': server,
'st': st,
'usn': usn,
'from': addr[0]
}
except socket.timeout:
break
except Exception as e:
print(f"⚠️ Erreur lors de la réception: {e}")
sock.close()
return devices
def fetch_device_description(location):
"""Récupère la description XML du device"""
try:
response = urlopen(location, timeout=3)
return response.read().decode('utf-8')
except Exception as e:
return f"Error: {e}"
def main():
print("=" * 70)
print(" 🔍 UPnP Device Discovery Tool")
print("=" * 70)
print()
devices = discover_upnp_devices(timeout=5)
# Filtrer pour ne garder que les MediaServers
media_servers = {}
for loc, info in devices.items():
if 'MediaServer' in str(info.get('st', '')):
media_servers[loc] = info
print(f"\n📊 Résultats:")
print(f" Total devices trouvés: {len(devices)}")
print(f" MediaServers trouvés: {len(media_servers)}\n")
if not media_servers:
print("❌ Aucun MediaServer trouvé!\n")
print("📋 Tous les devices trouvés:")
for loc, info in devices.items():
print(f"\n - Location: {loc}")
print(f" ST: {info.get('st', 'N/A')}")
print(f" Server: {info.get('server', 'N/A')}")
return
# Analyser chaque MediaServer
for idx, (location, info) in enumerate(media_servers.items(), 1):
print("=" * 70)
print(f"📡 MediaServer #{idx}")
print("=" * 70)
print(f"Location: {location}")
print(f"From IP: {info['from']}")
print(f"Server: {info.get('server', 'N/A')}")
print(f"USN: {info.get('usn', 'N/A')}")
print()
# Récupérer la description
print("📄 Fetching device description...")
desc = fetch_device_description(location)
# Analyser la description
if desc and not desc.startswith("Error"):
print("\n📝 Device Description XML:")
print("-" * 70)
# Afficher les premières lignes
lines = desc.split('\n')
for line in lines[:50]: # Limiter à 50 lignes
print(line)
if len(lines) > 50:
print(f"... ({len(lines) - 50} more lines)")
print("-" * 70)
# Extraire les infos importantes
import re
friendly_name = re.search(r'<friendlyName>([^<]+)</friendlyName>', desc)
manufacturer = re.search(r'<manufacturer>([^<]+)</manufacturer>', desc)
model_name = re.search(r'<modelName>([^<]+)</modelName>', desc)
udn = re.search(r'<UDN>([^<]+)</UDN>', desc)
print("\n📋 Device Info:")
if friendly_name:
print(f" Friendly Name: {friendly_name.group(1)}")
if manufacturer:
print(f" Manufacturer: {manufacturer.group(1)}")
if model_name:
print(f" Model Name: {model_name.group(1)}")
if udn:
print(f" UDN: {udn.group(1)}")
# Vérifier le format de l'UDN
udn_value = udn.group(1)
if not udn_value.startswith('uuid:'):
print(f" ⚠️ WARNING: UDN ne commence pas par 'uuid:' !")
else:
print(f"❌ Erreur lors de la récupération: {desc}")
print("\n")
if __name__ == "__main__":
main()

149
tools/test_soap.py Normal file
View File

@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""
Test SOAP Services for UPnP MediaServers
"""
from urllib.request import Request, urlopen
# SOAP request pour GetProtocolInfo
GET_PROTOCOL_INFO = """<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetProtocolInfo xmlns:u="urn:schemas-upnp-org:service:ConnectionManager:1"/>
</s:Body>
</s:Envelope>"""
# SOAP request pour Browse
BROWSE_REQUEST = """<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">
<ObjectID>0</ObjectID>
<BrowseFlag>BrowseDirectChildren</BrowseFlag>
<Filter>*</Filter>
<StartingIndex>0</StartingIndex>
<RequestedCount>10</RequestedCount>
<SortCriteria></SortCriteria>
</u:Browse>
</s:Body>
</s:Envelope>"""
SERVERS = {
"PMO Music": {
"base": "http://192.168.0.138:8080",
"content_control": "/device/8b8e9b19-9c65-4d59-b127-b34717658085/service/ContentDirectory/control",
"conn_control": "/device/8b8e9b19-9c65-4d59-b127-b34717658085/service/ConnectionManager/control",
"scpd_content": "/device/8b8e9b19-9c65-4d59-b127-b34717658085/service/ContentDirectory/desc.xml",
},
"Upmpdcli": {
"base": "http://192.168.0.200:49152",
"content_control": "/uuid-c110358f-d885-b44a-d6d3-dca6329ead0d/ctl-urn-schemas-upnp-org-service-ContentDirectory-1",
"conn_control": "/uuid-c110358f-d885-b44a-d6d3-dca6329ead0d/ctl-urn-schemas-upnp-org-service-ConnectionManager-1",
"scpd_content": "/uuid-c110358f-d885-b44a-d6d3-dca6329ead0d/urn-schemas-upnp-org-service-ContentDirectory-1.xml",
},
}
def send_soap_request(url, soap_action, soap_body):
"""Envoie une requête SOAP"""
try:
req = Request(
url,
data=soap_body.encode('utf-8'),
headers={
'Content-Type': 'text/xml; charset="utf-8"',
'SOAPAction': f'"{soap_action}"',
'User-Agent': 'PMOMusic/1.0',
}
)
response = urlopen(req, timeout=5)
return response.read().decode('utf-8'), response.status, dict(response.headers)
except Exception as e:
return f"Error: {e}", None, None
def main():
print("=" * 100)
print(" 🧪 SOAP Services Testing")
print("=" * 100)
print()
for server_name, server_info in SERVERS.items():
print("\n" + "=" * 100)
print(f" 📡 Testing {server_name}")
print("=" * 100)
# Test 1: GetProtocolInfo
print("\n🔌 Test 1: ConnectionManager::GetProtocolInfo")
print("-" * 100)
url = server_info["base"] + server_info["conn_control"]
soap_action = "urn:schemas-upnp-org:service:ConnectionManager:1#GetProtocolInfo"
print(f"URL: {url}")
print(f"SOAPAction: {soap_action}")
response, status, headers = send_soap_request(url, soap_action, GET_PROTOCOL_INFO)
if status:
print(f"\n✅ Status: {status}")
if headers:
print(f"Content-Type: {headers.get('Content-Type', 'N/A')}")
print(f"\n📄 Response ({len(response)} bytes):")
print(response[:1000])
if len(response) > 1000:
print(f"... ({len(response) - 1000} more bytes)")
else:
print(f"\n❌ Error: {response}")
# Test 2: Browse
print("\n\n📁 Test 2: ContentDirectory::Browse")
print("-" * 100)
url = server_info["base"] + server_info["content_control"]
soap_action = "urn:schemas-upnp-org:service:ContentDirectory:1#Browse"
print(f"URL: {url}")
print(f"SOAPAction: {soap_action}")
response, status, headers = send_soap_request(url, soap_action, BROWSE_REQUEST)
if status:
print(f"\n✅ Status: {status}")
if headers:
print(f"Content-Type: {headers.get('Content-Type', 'N/A')}")
print(f"\n📄 Response ({len(response)} bytes):")
print(response[:2000])
if len(response) > 2000:
print(f"... ({len(response) - 2000} more bytes)")
else:
print(f"\n❌ Error: {response}")
print("\n")
# Test 3: Vérifier les SCPD
print("\n" + "=" * 100)
print(" 📋 SCPD (Service Control Protocol Description) Verification")
print("=" * 100)
for server_name, server_info in SERVERS.items():
print(f"\n{server_name}:")
# ContentDirectory SCPD
scpd_url = server_info["base"] + server_info["scpd_content"]
print(f" ContentDirectory SCPD: {scpd_url}")
try:
req = Request(scpd_url, headers={'User-Agent': 'PMOMusic/1.0'})
response = urlopen(req, timeout=3)
scpd_xml = response.read().decode('utf-8')
print(f" ✅ Fetched ({len(scpd_xml)} bytes)")
# Vérifier les actions
import re
actions = re.findall(r'<action>.*?<name>([^<]+)</name>', scpd_xml, re.DOTALL)
print(f" Actions: {', '.join(actions)}")
except Exception as e:
print(f" ❌ Error: {e}")
if __name__ == "__main__":
main()