debug du upnp mediaserver
This commit is contained in:
166
tools/compare_upnp.py
Executable file
166
tools/compare_upnp.py
Executable 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
175
tools/discover_upnp.py
Executable 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
149
tools/test_soap.py
Normal 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()
|
||||
Reference in New Issue
Block a user