You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

156 lines
4.0 KiB

4 years ago
  1. """Xbox One SmartGlass device discovery."""
  2. import socket
  3. import struct
  4. import binascii
  5. from datetime import timedelta
  6. DISCOVERY_PORT = 5050
  7. DISCOVERY_ADDRESS_BCAST = '<broadcast>'
  8. DISCOVERY_ADDRESS_MCAST = '239.255.255.250'
  9. DISCOVERY_REQUEST = 0xDD00
  10. DISCOVERY_RESPONSE = 0xDD01
  11. DISCOVERY_TIMEOUT = timedelta(seconds=2)
  12. """
  13. SmartGlass Client type
  14. XboxOne = 1
  15. Xbox360 = 2
  16. WindowsDesktop = 3
  17. WindowsStore = 4
  18. WindowsPhone = 5
  19. iPhone = 6
  20. iPad = 7
  21. Android = 8
  22. """
  23. DISCOVERY_CLIENT_TYPE = 4
  24. class XboxSmartGlass:
  25. """Base class to discover Xbox SmartGlass devices."""
  26. def __init__(self):
  27. """Initialize the Xbox SmartGlass discovery."""
  28. self.entries = []
  29. self._discovery_payload = self.discovery_packet()
  30. @staticmethod
  31. def discovery_packet():
  32. """Assemble discovery payload."""
  33. version = 0
  34. flags = 0
  35. min_version = 0
  36. max_version = 2
  37. payload = struct.pack(
  38. '>IHHH',
  39. flags, DISCOVERY_CLIENT_TYPE, min_version, max_version
  40. )
  41. header = struct.pack(
  42. '>HHH',
  43. DISCOVERY_REQUEST, len(payload), version
  44. )
  45. return header + payload
  46. @staticmethod
  47. def parse_discovery_response(data):
  48. """Parse console's discovery response."""
  49. pos = 0
  50. # Header
  51. # pkt_type, payload_len, version = struct.unpack_from(
  52. # '>HHH',
  53. # data, pos
  54. # )
  55. pos += 6
  56. # Payload
  57. flags, type_, name_len = struct.unpack_from(
  58. '>IHH',
  59. data, pos
  60. )
  61. pos += 8
  62. name = data[pos:pos + name_len]
  63. pos += name_len + 1 # including null terminator
  64. uuid_len = struct.unpack_from(
  65. '>H',
  66. data, pos
  67. )[0]
  68. pos += 2
  69. uuid = data[pos:pos + uuid_len]
  70. pos += uuid_len + 1 # including null terminator
  71. last_error, cert_len = struct.unpack_from(
  72. '>IH',
  73. data, pos
  74. )
  75. pos += 6
  76. cert = data[pos:pos + cert_len]
  77. return {
  78. 'device_type': type_,
  79. 'flags': flags,
  80. 'name': name.decode('utf-8'),
  81. 'uuid': uuid.decode('utf-8'),
  82. 'last_error': last_error,
  83. 'certificate': binascii.hexlify(cert).decode('utf-8')
  84. }
  85. def scan(self):
  86. """Scan the network."""
  87. self.update()
  88. def all(self):
  89. """Scan and return all found entries."""
  90. self.scan()
  91. return self.entries
  92. @staticmethod
  93. def verify_packet(data):
  94. """Parse packet if it has correct magic"""
  95. if len(data) < 2:
  96. return None
  97. pkt_type = struct.unpack_from('>H', data)[0]
  98. if pkt_type != DISCOVERY_RESPONSE:
  99. return None
  100. return XboxSmartGlass.parse_discovery_response(data)
  101. def update(self):
  102. """Scan network for Xbox SmartGlass devices."""
  103. entries = []
  104. sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  105. sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
  106. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  107. sock.settimeout(DISCOVERY_TIMEOUT.seconds)
  108. sock.sendto(self._discovery_payload,
  109. (DISCOVERY_ADDRESS_BCAST, DISCOVERY_PORT))
  110. sock.sendto(self._discovery_payload,
  111. (DISCOVERY_ADDRESS_MCAST, DISCOVERY_PORT))
  112. while True:
  113. try:
  114. data, (address, _) = sock.recvfrom(1024)
  115. response = self.verify_packet(data)
  116. if response:
  117. entries.append((address, response))
  118. except socket.timeout:
  119. break
  120. self.entries = entries
  121. sock.close()
  122. def main():
  123. """Test XboxOne discovery."""
  124. from pprint import pprint
  125. xbsmartglass = XboxSmartGlass()
  126. pprint("Scanning for Xbox One SmartGlass consoles devices..")
  127. xbsmartglass.update()
  128. pprint(xbsmartglass.entries)
  129. if __name__ == "__main__":
  130. main()