async def perform_pairing(conf, pin=DEVICE_PIN): pairing = await pair(conf, Protocol.AirPlay, asyncio.get_event_loop()) assert pairing.device_provides_pin await pairing.begin() if pin: pairing.pin(pin) assert not pairing.has_paired await pairing.finish() assert pairing.has_paired assert parse_credentials(conf.get_service( Protocol.AirPlay).credentials) == parse_credentials(DEVICE_CREDENTIALS)
def __init__(self, config: BaseConfig, service: BaseService) -> None: """Initialize a new AirPlayStreamAPI instance.""" self.config = config self.service = service self._credentials: HapCredentials = parse_credentials( self.service.credentials) self._play_task: Optional[asyncio.Future] = None
async def _setup_encryption(self): if self.service.credentials: credentials = parse_credentials(self.service.credentials) pair_verifier = CompanionPairVerifyProcedure( self, self.srp, credentials) try: await pair_verifier.verify_credentials() output_key, input_key = pair_verifier.encryption_keys( SRP_SALT, SRP_OUTPUT_INFO, SRP_INPUT_INFO) self.connection.enable_encryption(output_key, input_key) except Exception as ex: raise exceptions.AuthenticationError(str(ex)) from ex
def extract_credentials(service: BaseService) -> HapCredentials: """Extract credentials from service based on what's supported.""" if service.credentials is not None: return parse_credentials(service.credentials) flags = parse_features(service.properties.get("features", "0x0")) if ( AirPlayFlags.SupportsSystemPairing in flags or AirPlayFlags.SupportsCoreUtilsPairingAndEncryption in flags ): return TRANSIENT_CREDENTIALS return NO_CREDENTIALS
async def start(self, skip_initial_messages: bool = False) -> None: """Connect to device and listen to incoming messages.""" if self._state != ProtocolState.NOT_CONNECTED: raise exceptions.InvalidStateError(self._state.name) self._state = ProtocolState.CONNECTING try: await self.connection.connect() self._state = ProtocolState.CONNECTED # In case credentials have been given externally (i.e. not by pairing # with a device), then use that client id if self.service.credentials: self.srp.pairing_id = parse_credentials( self.service.credentials).client_id # The first message must always be DEVICE_INFORMATION, otherwise the # device will not respond with anything self.device_info = await self.send_and_receive( messages.device_information("pyatv", self.srp.pairing_id.decode())) # Distribute the device information to all listeners (as the # send_and_receive will stop that propagation). self.dispatch(protobuf.DEVICE_INFO_MESSAGE, self.device_info) # This is a hack to support re-use of a protocol object in # proxy (will be removed/refactored later) if skip_initial_messages: return await self._enable_encryption() # This should be the first message sent after encryption has # been enabled await self.send(messages.set_connection_state()) # Subscribe to updates at this stage await self.send_and_receive(messages.client_updates_config()) await self.send_and_receive(messages.get_keyboard_session()) except Exception: # Something went wrong, let's do cleanup self.stop() raise else: # We're now ready self._state = ProtocolState.READY
async def start(self): """Connect to device and listen to incoming messages.""" if self._is_started: raise exceptions.ProtocolError("Already started") self._is_started = True await self.connection.connect() if self.service.credentials: self.srp.pairing_id = parse_credentials( self.service.credentials).client_id _LOGGER.debug("Companion credentials: %s", self.service.credentials) await self._setup_encryption()
async def test_pair_airplay(self): self.user_input(str(DEVICE_PIN)) await self.atvremote( "--address", IP_2, "--protocol", "airplay", "--id", MRP_ID, "pair", ) self.has_output( "Enter PIN", "seems to have succeeded", parse_credentials(DEVICE_CREDENTIALS), ) self.exit(0)
async def _enable_encryption(self) -> None: # Encryption can be enabled whenever credentials are available but only # after DEVICE_INFORMATION has been sent if self.service.credentials is None: return # Verify credentials and generate keys credentials = parse_credentials(self.service.credentials) pair_verifier = MrpPairVerifyProcedure(self, self.srp, credentials) try: await pair_verifier.verify_credentials() output_key, input_key = pair_verifier.encryption_keys( SRP_SALT, SRP_OUTPUT_INFO, SRP_INPUT_INFO) self.connection.enable_encryption(output_key, input_key) except Exception as ex: raise exceptions.AuthenticationError(str(ex)) from ex
async def system_info(self): """Send system information to device.""" _LOGGER.debug("Sending system information") creds = parse_credentials(self.core.service.credentials) # Bunch of semi-random values here... await self._send_command( "_systemInfo", { "_bf": 0, "_cf": 512, "_clFl": 128, "_i": "cafecafecafe", # TODO: Figure out what to put here "_idsID": creds.client_id, "_pubID": "aa:bb:cc:dd:ee:ff", "_sf": 256, # Status flags? "_sv": "170.18", # Software Version (I guess?) "model": "iPhone14,3", "name": "pyatv", }, )
async def finish(self): """Stop pairing process.""" if not self.pin_code: raise exceptions.PairingError("no pin given") credentials = str(await error_handler( self.pairing_procedure.finish_pairing, exceptions.PairingError, "", # username required but not used self.pin_code, )) _LOGGER.debug("Verifying credentials %s", credentials) verifier = MrpPairVerifyProcedure(self.protocol, self.srp, parse_credentials(credentials)) await error_handler(verifier.verify_credentials, exceptions.PairingError) self.service.credentials = credentials self._has_paired = True
def airplay_creds_fixture(): with patch( "pyatv.protocols.airplay.auth.new_credentials") as new_credentials: new_credentials.return_value = parse_credentials(DEVICE_CREDENTIALS) yield
async def stream_file(self, file: Union[str, io.BufferedReader], **kwargs) -> None: """Stream local or remote file to device. Supports either local file paths or a HTTP(s) address. INCUBATING METHOD - MIGHT CHANGE IN THE FUTURE! """ self.playback_manager.acquire() audio_file: Optional[AudioSource] = None takeover_release = self.core.takeover(Audio, Metadata, PushUpdater, RemoteControl) try: client, _, context = await self.playback_manager.setup() client.credentials = parse_credentials( self.core.service.credentials) client.password = self.core.service.password client.listener = self.listener await client.initialize(self.core.service.properties) # Try to load metadata and pass it along if it succeeds metadata: AudioMetadata = EMPTY_METADATA try: # Source must support seeking to read metadata (or point to file) if (isinstance(file, io.BufferedReader) and file.seekable()) or (isinstance(file, str) and path.exists(file)): metadata = await get_metadata(file) else: _LOGGER.debug( "Seeking not supported by source, not loading metadata" ) except Exception as ex: _LOGGER.exception("Failed to extract metadata from %s: %s", file, ex) # After initialize has been called, all the audio properties will be # initialized and can be used in the miniaudio wrapper audio_file = await open_source( file, context.sample_rate, context.channels, context.bytes_per_channel, ) # If the user didn't change volume level prior to streaming, try to extract # volume level from device (if supported). Otherwise set the default level # in pyatv. if not self.audio.has_changed_volume and "initialVolume" in client.info: initial_volume = client.info["initialVolume"] if not isinstance(initial_volume, float): raise exceptions.ProtocolError( f"initial volume {initial_volume} has " "incorrect type {type(initial_volume)}", ) context.volume = initial_volume else: await self.audio.set_volume(self.audio.volume) await client.send_audio(audio_file, metadata) finally: takeover_release() if audio_file: await audio_file.close() await self.playback_manager.teardown()
NO_CREDENTIALS, TRANSIENT_CREDENTIALS, parse_credentials, ) from pyatv.const import PairingRequirement, Protocol from pyatv.core import MutableService from pyatv.protocols.airplay.utils import ( AirPlayFlags, get_pairing_requirement, is_password_required, is_remote_control_supported, parse_features, ) # These are not really valid credentials but parse_credentials accepts them (for now) HAP_CREDS = parse_credentials("aa:bb:cc:dd") LEGACY_CREDS = parse_credentials(":aa::bb") @pytest.mark.parametrize( "flags,output", [ # Single feature flag ("0x00000001", AirPlayFlags.SupportsAirPlayVideoV1), ( "0x40000003", AirPlayFlags.HasUnifiedAdvertiserInfo | AirPlayFlags.SupportsAirPlayPhoto | AirPlayFlags.SupportsAirPlayVideoV1, ), # Dual feature flag