Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions base_versions.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
aiohttp==3.8.3,<5
biplist==1.0.3
async-timeout==4.0.2;python_version<'3.11'
cryptography==44.0.1
chacha20poly1305-reuseable==0.13.2
Expand Down
32 changes: 16 additions & 16 deletions docs/documentation/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -1205,7 +1205,7 @@ AirPlay uses two services, one for audio and one for video. They are described h
| osvers | 14.5 | Operating system version
| pi | UUID4 | Group ID
| vv | 2 | ?
| srcvers | 540.31.41 | AirPlay version
| srcvers | 870.14.1 | AirPlay version
| psi | UUID4 | Public AirPlay Pairing Identifier
| gid | UUID4 | Group UUID
| pk | UUID4 | Public key
Expand Down Expand Up @@ -1234,7 +1234,7 @@ Sender asks receiver what methods it supports:
```raw
OPTIONS * RTSP/1.0
CSeq: 0
nUser-Agent: AirPlay/540.31
nUser-Agent: AirPlay/870.14
DACP-ID: A851074254310A45
Active-Remote: 4019753970
Client-Instance: A851074254310A45
Expand All @@ -1246,7 +1246,7 @@ RTSP/1.0 200 OK
Date: Tue, 11 May 2021 17:35:10 GMT
Content-Length: 0
Public: ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN, OPTIONS, GET_PARAMETER, SET_PARAMETER, POST, GET, PUT
Server: AirTunes/540.31.41
Server: AirTunes/870.14.1
CSeq: 0
```

Expand All @@ -1258,7 +1258,7 @@ Sender tells the receiver about properties for an upcoming stream.
```raw
ANNOUNCE rtsp://10.0.10.254/4018537194 RTSP/1.0
CSeq: 0
User-Agent: AirPlay/540.31
User-Agent: AirPlay/870.14
DACP-ID: 9D881F7AED72DB4A
Active-Remote: 3630929274
Client-Instance: 9D881F7AED72DB4A
Expand Down Expand Up @@ -1287,7 +1287,7 @@ Some observations (might not be true):
RTSP/1.0 200 OK
Date: Tue, 11 May 2021 17:25:54 GMT
Content-Length: 0
Server: AirTunes/540.31.41
Server: AirTunes/870.14.1
CSeq: 0
```

Expand All @@ -1305,7 +1305,7 @@ Sender requests initialization of a (Airplay v1) session (but does not start it)
```raw
SETUP rtsp://10.0.10.254/1085946124 RTSP/1.0
CSeq: 2
User-Agent: AirPlay/540.31
User-Agent: AirPlay/870.14
DACP-ID: A851074254310A45
Active-Remote: 4019753970
Client-Instance: A851074254310A45
Expand All @@ -1320,7 +1320,7 @@ Content-Length: 0
Transport: RTP/AVP/UDP;unicast;mode=record;server_port=55801;control_port=50367;timing_port=0
Session: 1
Audio-Jack-Status: connected
Server: AirTunes/540.31.41
Server: AirTunes/870.14.1
CSeq: 2
```

Expand All @@ -1347,7 +1347,7 @@ randomized.
```raw
RECORD rtsp://10.0.10.254/1085946124 RTSP/1.0
CSeq: 6
User-Agent: AirPlay/540.31
User-Agent: AirPlay/870.14
DACP-ID: A851074254310A45
Active-Remote: 4019753970
Client-Instance: A851074254310A45
Expand All @@ -1362,7 +1362,7 @@ RTSP/1.0 200 OK
Date: Tue, 11 May 2021 07:35:11 GMT
Content-Length: 0
Audio-Latency: 3035
Server: AirTunes/540.31.41
Server: AirTunes/870.14.1
CSeq: 6
```

Expand All @@ -1374,7 +1374,7 @@ Requests to flush the receivers buffer and pause/stop what is playing.
```raw
FLUSH rtsp://10.0.10.254/1085946124 RTSP/1.0
CSeq: 7
User-Agent: AirPlay/540.31
User-Agent: AirPlay/870.14
DACP-ID: A851074254310A45
Active-Remote: 4019753970
Client-Instance: A851074254310A45
Expand All @@ -1385,7 +1385,7 @@ Client-Instance: A851074254310A45
RTSP/1.0 200 OK
Date: Tue, 11 May 2021 17:35:11 GMT
Content-Length: 0
Server: AirTunes/540.31.41
Server: AirTunes/870.14.1
CSeq: 7
```

Expand All @@ -1397,7 +1397,7 @@ End the active session.
```raw
TEARDOWN rtsp://10.0.10.254/1085946124 RTSP/1.0
CSeq: 8
User-Agent: AirPlay/540.31
User-Agent: AirPlay/870.14
DACP-ID: A851074254310A45
Active-Remote: 4019753970
Client-Instance: A851074254310A45
Expand All @@ -1408,7 +1408,7 @@ Client-Instance: A851074254310A45
RTSP/1.0 200 OK
Date: Tue, 11 May 2021 17:35:19 GMT
Content-Length: 0
Server: AirTunes/540.31.41
Server: AirTunes/870.14.1
CSeq: 8
```

Expand All @@ -1420,7 +1420,7 @@ Change a parameter, e.g. metadata or progress, on the receiver.
```raw
SET_PARAMETER rtsp://10.0.10.254/1085946124 RTSP/1.0
CSeq: 3
User-Agent: AirPlay/540.31
User-Agent: AirPlay/870.14
DACP-ID: A851074254310A45
Active-Remote: 4019753970
Client-Instance: A851074254310A45
Expand All @@ -1435,7 +1435,7 @@ volume: -20
RTSP/1.0 200 OK
Date: Tue, 11 May 2021 17:35:11 GMT
Content-Length: 0
Server: AirTunes/540.31.41
Server: AirTunes/870.14.1
CSeq: 3
```

Expand Down Expand Up @@ -1463,7 +1463,7 @@ from owntone [here](https://github.com/owntone/owntone-server/blob/c1db4d914f5cd
```raw
POST /auth-setup RTSP/1.0
CSeq: 0
User-Agent: AirPlay/540.31
User-Agent: AirPlay/870.14
DACP-ID: BFAA2A9155BD093C
Active-Remote: 347218209
Client-Instance: BFAA2A9155BD093C
Expand Down
4 changes: 2 additions & 2 deletions pyatv/protocols/airplay/ap2_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from pyatv.protocols.airplay.auth import verify_connection
from pyatv.protocols.airplay.channels import DataStreamChannel, EventChannel
from pyatv.settings import InfoSettings
from pyatv.support.http import HttpConnection, decode_bplist_from_body, http_connect
from pyatv.support.http import HttpConnection, decode_plist_body, http_connect
from pyatv.support.rtsp import RtspSession
from pyatv.support.state_producer import StateProducer

Expand Down Expand Up @@ -110,7 +110,7 @@ async def _send_feedback(message: Optional[Any]) -> None:
async def _setup(self, body: Dict[str, Any]) -> Dict[str, Any]:
assert self.rtsp
resp = await self.rtsp.setup(body=body)
return decode_bplist_from_body(resp)
return decode_plist_body(resp.body)

async def _setup_event_channel(self, address: str) -> None:
if self.verifier is None:
Expand Down
88 changes: 62 additions & 26 deletions pyatv/protocols/airplay/auth/hap.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,17 @@
from pyatv.auth.hap_srp import SRPAuthHandler
from pyatv.exceptions import InvalidResponseError
from pyatv.support import log_binary
from pyatv.support.http import HttpConnection, HttpResponse
from pyatv.support.http import HttpConnection, HttpResponse, HttpRequest

_LOGGER = logging.getLogger(__name__)

_AIRPLAY_HEADERS = {
"User-Agent": "AirPlay/320.20",
"User-Agent": "AirPlay/870.14.1",
"Connection": "keep-alive",
"X-Apple-HKP": 3,
"Content-Type": "application/octet-stream",
"Content-Type": "application/octet-stream"
}


def _get_pairing_data(resp: HttpResponse):
if not isinstance(resp.body, bytes):
raise InvalidResponseError(f"got unexpected response: {resp.body}")
Expand Down Expand Up @@ -107,45 +106,82 @@ def __init__(
self.http = http
self.srp = auth_handler
self.credentials = credentials
self.pairing_data = None
self._sequence = 0

async def verify_credentials(self) -> bool:
"""Verify if device is allowed to use AirPlau."""
"""Verify if device is allowed to use AirPlay."""
await self.verify_credentials_seq1()
await self.verify_credentials_seq2()

_, public_key = self.srp.initialize()
return True

def sequence(self) -> int:
return self._sequence

resp = await self._send(
{
hap_tlv8.TlvValue.SeqNo: b"\x01",
hap_tlv8.TlvValue.PublicKey: public_key,
}
)
async def verify_credentials_seq1(self, request:HttpRequest=None):
_, public_key = self.srp.initialize()

pairing_data = _get_pairing_data(resp)
session_pub_key = pairing_data[hap_tlv8.TlvValue.PublicKey]
encrypted = pairing_data[hap_tlv8.TlvValue.EncryptedData]
# copy data from template request
data = hap_tlv8.read_tlv(
request.body
if isinstance(request.body, bytes)
else request.body.encode("utf-8")
) if request else {}

# override critical values
data[hap_tlv8.TlvValue.SeqNo] = b"\x01"
# data[hap_tlv8.TlvValue.Method] = b"\x07"
data[hap_tlv8.TlvValue.PublicKey] = public_key

response = await self._send(data, copy(request.headers if request else _AIRPLAY_HEADERS))

self.pairing_data = _get_pairing_data(response)

async def verify_credentials_seq2(self, request:HttpRequest=None):
session_pub_key = self.pairing_data[hap_tlv8.TlvValue.PublicKey]
encrypted = self.pairing_data[hap_tlv8.TlvValue.EncryptedData]
log_binary(_LOGGER, "Device", Public=self.credentials.ltpk, Encrypted=encrypted)

encrypted_data = self.srp.verify1(self.credentials, session_pub_key, encrypted)
await self._send(
{
hap_tlv8.TlvValue.SeqNo: b"\x03",
hap_tlv8.TlvValue.EncryptedData: encrypted_data,
}
)

# TODO: check status code
# copy data from template request
data = hap_tlv8.read_tlv(
request.body
if isinstance(request.body, bytes)
else request.body.encode("utf-8")
) if request else {}

data[hap_tlv8.TlvValue.SeqNo] = b"\x03"
data[hap_tlv8.TlvValue.EncryptedData] = encrypted_data

response = await self._send(data, copy(request.headers if request else _AIRPLAY_HEADERS))

if response.code != 200:
raise (f"HAP Verification Sequence 2 failed with %s", response.code)

return True

async def _send(self, data: Dict[Any, Any]) -> HttpResponse:
headers = copy(_AIRPLAY_HEADERS)
async def _send(self, data: Dict[Any, Any], headers=copy(_AIRPLAY_HEADERS)) -> HttpResponse:
# data[hap_tlv8.TlvValue.Method] = b"\x07"
body = hap_tlv8.write_tlv(data)
headers["Content-Type"] = "application/octet-stream"
return await self.http.post(
"/pair-verify", body=hap_tlv8.write_tlv(data), headers=headers
headers["Content-Length"] = len(body)

self._sequence = data[hap_tlv8.TlvValue.SeqNo][0]

resp = await self.http.post(
"/pair-verify", body=body, headers=headers
)

if (resp.code == 200):
self._sequence = hap_tlv8.read_tlv(resp.body)[hap_tlv8.TlvValue.SeqNo][0]

return resp

def encryption_keys(
self, salt: str, output_info: str, input_info: str
) -> Tuple[bytes, bytes]:
"""Return derived encryption keys."""

return self.srp.verify2(salt, output_info, input_info)
4 changes: 2 additions & 2 deletions pyatv/protocols/airplay/auth/legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
PairVerifyProcedure,
)
from pyatv.protocols.airplay.srp import LegacySRPAuthHandler
from pyatv.support.http import HttpConnection, HttpResponse, decode_bplist_from_body
from pyatv.support.http import HttpConnection, HttpResponse, decode_plist_body

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -53,7 +53,7 @@ async def finish_pairing(
)
self.srp.step1(client_id, pin_code)
resp = await self._send_plist(method="pin", user=client_id)
body = decode_bplist_from_body(resp)
body = decode_plist_body(resp.body)
if not isinstance(body, dict):
raise exceptions.ProtocolError(f"exoected dict, got {type(body).__name__}")

Expand Down
33 changes: 32 additions & 1 deletion pyatv/protocols/airplay/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from abc import ABC
import logging
from random import randrange
from typing import Any, List, NamedTuple, Optional, Tuple
from typing import Any, List, NamedTuple, Optional, Tuple, Dict

from pyatv.auth.hap_channel import AbstractHAPChannel
from pyatv.protocols.airplay.utils import decode_plist_body, encode_plist_body
Expand All @@ -21,6 +21,7 @@
)
from pyatv.support.packet import defpacket
from pyatv.support.variant import read_variant, write_variant
import asyncio

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -60,6 +61,23 @@ def parse_response(data: bytes) -> Tuple[Optional[HttpResponse], bytes, bytes]:
class EventChannel(BaseEventChannel):
"""Connection used to handle the event channel."""

def __init__(self, output_key, input_key):
super().__init__(output_key, input_key)
self._requests = {}
self._responses = {}
self._listener = None

async def responseFor(self, messageId: int) -> Dict:
event = asyncio.Event()
self._requests[str(messageId)] = event
await event.wait()
response = self._responses[str(messageId)]
del self._responses[str(messageId)]
return response

def listener(self, listener):
self._listener = listener

def handle_received(self) -> None:
"""Handle received data that was put in buffer."""
self.buffer: bytes
Expand All @@ -72,6 +90,19 @@ def handle_received(self) -> None:

_LOGGER.debug("Got message on event channel: %s", request)

plist = decode_plist_body(request.body)
if plist and 'params' in plist and 'data' in plist['params']:
plist = decode_plist_body(plist['params']['data'])
if plist and 'kind' in plist:
if plist['kind'] == "response":
ID = str(plist["messageID"])
if ID in self._requests:
self._responses[ID] = plist
self._requests[ID].set()
elif plist["type"] == "playbackState":
# Update state
self._listener(plist)

# Send a positive response to satisfy the other end of the channel
headers = {
"Content-Length": "0",
Expand Down
Loading