Ejemplo n.º 1
0
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)
Ejemplo n.º 2
0
 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
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
    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
Ejemplo n.º 6
0
    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()
Ejemplo n.º 7
0
 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)
Ejemplo n.º 8
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
Ejemplo n.º 9
0
    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",
            },
        )
Ejemplo n.º 10
0
    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
Ejemplo n.º 11
0
def airplay_creds_fixture():
    with patch(
            "pyatv.protocols.airplay.auth.new_credentials") as new_credentials:
        new_credentials.return_value = parse_credentials(DEVICE_CREDENTIALS)
        yield
Ejemplo n.º 12
0
    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()
Ejemplo n.º 13
0
    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