@basmilius/apple-airplay 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/baseStream.d.ts +12 -0
- package/dist/{rtsp.d.ts → controlStream.d.ts} +5 -4
- package/dist/dataStream.d.ts +8 -6
- package/dist/dataStreamMessages.d.ts +16 -16
- package/dist/eventStream.d.ts +5 -4
- package/dist/index.d.ts +6 -7
- package/dist/index.js +809 -921
- package/dist/pairing.d.ts +11 -4
- package/dist/protocol.d.ts +12 -11
- package/dist/utils.d.ts +11 -18
- package/package.json +3 -3
- package/dist/stream.d.ts +0 -13
- package/dist/types.d.ts +0 -11
- package/dist/verify.d.ts +0 -9
package/dist/index.js
CHANGED
|
@@ -9,30 +9,12 @@ var __export = (target, all) => {
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// src/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
setReadyState: () => setReadyState,
|
|
19
|
-
setConnectionState: () => setConnectionState,
|
|
20
|
-
sendHIDEvent: () => sendHIDEvent,
|
|
21
|
-
sendCommand: () => sendCommand,
|
|
22
|
-
sendButtonEvent: () => sendButtonEvent,
|
|
23
|
-
protocol: () => protocol,
|
|
24
|
-
playbackQueueRequest: () => playbackQueueRequest,
|
|
25
|
-
notification: () => notification,
|
|
26
|
-
getVolumeMuted: () => getVolumeMuted,
|
|
27
|
-
getVolume: () => getVolume,
|
|
28
|
-
getState: () => getState,
|
|
29
|
-
getExtension: () => getExtension,
|
|
30
|
-
deviceInfo: () => deviceInfo,
|
|
31
|
-
configureConnection: () => configureConnection,
|
|
32
|
-
clientUpdatesConfig: () => clientUpdatesConfig
|
|
33
|
-
});
|
|
34
|
-
import { uint16ToBE, uuid } from "@basmilius/apple-common";
|
|
35
|
-
import { create, getExtension as _getExtension, setExtension } from "@bufbuild/protobuf";
|
|
12
|
+
// src/baseStream.ts
|
|
13
|
+
import { Chacha20, ENCRYPTION, EncryptionAwareConnection } from "@basmilius/apple-common";
|
|
14
|
+
|
|
15
|
+
// src/utils.ts
|
|
16
|
+
import { Plist } from "@basmilius/apple-encoding";
|
|
17
|
+
import { fromBinary } from "@bufbuild/protobuf";
|
|
36
18
|
|
|
37
19
|
// src/proto/index.ts
|
|
38
20
|
var exports_proto = {};
|
|
@@ -1498,381 +1480,304 @@ import { extDesc as extDesc63, fileDesc as fileDesc90, messageDesc as messageDes
|
|
|
1498
1480
|
var file_WakeDeviceMessage = /* @__PURE__ */ fileDesc90("ChdXYWtlRGV2aWNlTWVzc2FnZS5wcm90byITChFXYWtlRGV2aWNlTWVzc2FnZTpSChF3YWtlRGV2aWNlTWVzc2FnZRIQLlByb3RvY29sTWVzc2FnZRgtIAEoCzISLldha2VEZXZpY2VNZXNzYWdlUhF3YWtlRGV2aWNlTWVzc2FnZQ", [file_ProtocolMessage]);
|
|
1499
1481
|
var WakeDeviceMessageSchema = /* @__PURE__ */ messageDesc90(file_WakeDeviceMessage, 0);
|
|
1500
1482
|
var wakeDeviceMessage = /* @__PURE__ */ extDesc63(file_WakeDeviceMessage, 0);
|
|
1501
|
-
// src/dataStreamMessages.ts
|
|
1502
|
-
function protocol(type, errorCode = 0 /* NoError */) {
|
|
1503
|
-
return create(ProtocolMessageSchema, {
|
|
1504
|
-
type,
|
|
1505
|
-
errorCode,
|
|
1506
|
-
identifier: uuid().toUpperCase(),
|
|
1507
|
-
uniqueIdentifier: uuid().toUpperCase()
|
|
1508
|
-
});
|
|
1509
|
-
}
|
|
1510
|
-
function clientUpdatesConfig(artworkUpdates = true, nowPlayingUpdates = true, volumeUpdates = true, keyboardUpdates = false, outputDeviceUpdates = true, systemEndpointUpdates = true) {
|
|
1511
|
-
const protocolMessage = protocol(16 /* CLIENT_UPDATES_CONFIG_MESSAGE */);
|
|
1512
|
-
const message = create(ClientUpdatesConfigMessageSchema, {
|
|
1513
|
-
artworkUpdates,
|
|
1514
|
-
nowPlayingUpdates,
|
|
1515
|
-
volumeUpdates,
|
|
1516
|
-
keyboardUpdates,
|
|
1517
|
-
outputDeviceUpdates,
|
|
1518
|
-
systemEndpointUpdates
|
|
1519
|
-
});
|
|
1520
|
-
setExtension(protocolMessage, clientUpdatesConfigMessage, message);
|
|
1521
|
-
return protocolMessage;
|
|
1522
|
-
}
|
|
1523
|
-
function configureConnection(groupId) {
|
|
1524
|
-
const protocolMessage = protocol(120 /* CONFIGURE_CONNECTION_MESSAGE */);
|
|
1525
|
-
const message = create(ConfigureConnectionMessageSchema, {
|
|
1526
|
-
groupID: groupId
|
|
1527
|
-
});
|
|
1528
|
-
setExtension(protocolMessage, configureConnectionMessage, message);
|
|
1529
|
-
return protocolMessage;
|
|
1530
|
-
}
|
|
1531
|
-
function deviceInfo(pairingId) {
|
|
1532
|
-
const protocolMessage = protocol(15 /* DEVICE_INFO_MESSAGE */);
|
|
1533
|
-
const message = create(DeviceInfoMessageSchema, {
|
|
1534
|
-
uniqueIdentifier: pairingId.toString(),
|
|
1535
|
-
name: "iPhone van Bas",
|
|
1536
|
-
localizedModelName: "iPhone",
|
|
1537
|
-
systemBuildVersion: "18C66",
|
|
1538
|
-
applicationBundleIdentifier: "com.apple.TVRemote",
|
|
1539
|
-
applicationBundleVersion: "344.28",
|
|
1540
|
-
protocolVersion: 1,
|
|
1541
|
-
lastSupportedMessageType: 129,
|
|
1542
|
-
supportsSystemPairing: true,
|
|
1543
|
-
allowsPairing: true,
|
|
1544
|
-
systemMediaApplication: "com.apple.TVMusic",
|
|
1545
|
-
supportsACL: true,
|
|
1546
|
-
supportsSharedQueue: true,
|
|
1547
|
-
supportsExtendedMotion: true,
|
|
1548
|
-
sharedQueueVersion: 2,
|
|
1549
|
-
deviceClass: 1 /* iPhone */,
|
|
1550
|
-
logicalDeviceCount: 1
|
|
1551
|
-
});
|
|
1552
|
-
setExtension(protocolMessage, deviceInfoMessage, message);
|
|
1553
|
-
return protocolMessage;
|
|
1554
|
-
}
|
|
1555
|
-
function getState() {
|
|
1556
|
-
const protocolMessage = protocol(3 /* GET_STATE_MESSAGE */);
|
|
1557
|
-
const message = create(GetStateMessageSchema, {});
|
|
1558
|
-
setExtension(protocolMessage, getStateMessage, message);
|
|
1559
|
-
return protocolMessage;
|
|
1560
|
-
}
|
|
1561
|
-
function getVolume(outputDeviceUID) {
|
|
1562
|
-
const protocolMessage = protocol(49 /* GET_VOLUME_MESSAGE */);
|
|
1563
|
-
const message = create(GetVolumeMessageSchema, {
|
|
1564
|
-
outputDeviceUID
|
|
1565
|
-
});
|
|
1566
|
-
setExtension(protocolMessage, getVolumeMessage, message);
|
|
1567
|
-
return protocolMessage;
|
|
1568
|
-
}
|
|
1569
|
-
function getVolumeMuted(outputDeviceUID) {
|
|
1570
|
-
const protocolMessage = protocol(126 /* GET_VOLUME_MUTED_MESSAGE */);
|
|
1571
|
-
const message = create(GetVolumeMutedMessageSchema, {
|
|
1572
|
-
outputDeviceUID
|
|
1573
|
-
});
|
|
1574
|
-
setExtension(protocolMessage, getVolumeMutedMessage, message);
|
|
1575
|
-
return protocolMessage;
|
|
1576
|
-
}
|
|
1577
|
-
function notification(notification2) {
|
|
1578
|
-
const protocolMessage = protocol(11 /* NOTIFICATION_MESSAGE */);
|
|
1579
|
-
const message = create(NotificationMessageSchema, {
|
|
1580
|
-
notification: notification2
|
|
1581
|
-
});
|
|
1582
|
-
setExtension(protocolMessage, notificationMessage, message);
|
|
1583
|
-
return protocolMessage;
|
|
1584
|
-
}
|
|
1585
|
-
function playbackQueueRequest(location, length, includeMetadata = true, includeLanguageOptions = false) {
|
|
1586
|
-
const protocolMessage = protocol(32 /* PLAYBACK_QUEUE_REQUEST_MESSAGE */);
|
|
1587
|
-
const message = create(PlaybackQueueRequestMessageSchema, {
|
|
1588
|
-
location,
|
|
1589
|
-
length,
|
|
1590
|
-
includeMetadata,
|
|
1591
|
-
includeLanguageOptions,
|
|
1592
|
-
artworkHeight: 600,
|
|
1593
|
-
artworkWidth: 600,
|
|
1594
|
-
includeInfo: true,
|
|
1595
|
-
includeLyrics: false,
|
|
1596
|
-
includeSections: false,
|
|
1597
|
-
includeAlignments: false,
|
|
1598
|
-
includeAvailableArtworkFormats: true,
|
|
1599
|
-
includeParticipants: false,
|
|
1600
|
-
isLegacyNowPlayingInfoRequest: false,
|
|
1601
|
-
returnContentItemAssetsInUserCompletion: true,
|
|
1602
|
-
requestedAnimatedArtworkAssetURLFormats: [
|
|
1603
|
-
"MRContentItemAnimatedArtworkFormatSquare",
|
|
1604
|
-
"MRContentItemAnimatedArtworkFormatTall"
|
|
1605
|
-
]
|
|
1606
|
-
});
|
|
1607
|
-
setExtension(protocolMessage, playbackQueueRequestMessage, message);
|
|
1608
|
-
return protocolMessage;
|
|
1609
|
-
}
|
|
1610
|
-
function sendButtonEvent(usagePage, usage, buttonDown) {
|
|
1611
|
-
const protocolMessage = protocol(39 /* SEND_BUTTON_EVENT_MESSAGE */);
|
|
1612
|
-
const message = create(SendButtonEventMessageSchema, {
|
|
1613
|
-
usagePage,
|
|
1614
|
-
usage,
|
|
1615
|
-
buttonDown
|
|
1616
|
-
});
|
|
1617
|
-
setExtension(protocolMessage, sendButtonEventMessage, message);
|
|
1618
|
-
return protocolMessage;
|
|
1619
|
-
}
|
|
1620
|
-
function sendCommand(command, options) {
|
|
1621
|
-
const protocolMessage = protocol(1 /* SEND_COMMAND_MESSAGE */);
|
|
1622
|
-
const message = create(SendCommandMessageSchema, {
|
|
1623
|
-
command,
|
|
1624
|
-
options
|
|
1625
|
-
});
|
|
1626
|
-
setExtension(protocolMessage, sendCommandMessage, message);
|
|
1627
|
-
return protocolMessage;
|
|
1628
|
-
}
|
|
1629
|
-
function sendHIDEvent(usePage, usage, down) {
|
|
1630
|
-
const protocolMessage = protocol(8 /* SEND_HID_EVENT_MESSAGE */);
|
|
1631
|
-
const message = create(SendHIDEventMessageSchema, {
|
|
1632
|
-
hidEventData: Buffer.concat([
|
|
1633
|
-
Buffer.from("438922cf08020000", "hex"),
|
|
1634
|
-
Buffer.from("00000000000000000100000000000000020" + "00000200000000300000001000000000000", "hex"),
|
|
1635
|
-
Buffer.concat([
|
|
1636
|
-
uint16ToBE(usePage),
|
|
1637
|
-
uint16ToBE(usage),
|
|
1638
|
-
uint16ToBE(down ? 1 : 0)
|
|
1639
|
-
]),
|
|
1640
|
-
Buffer.from("0000000000000001000000", "hex")
|
|
1641
|
-
])
|
|
1642
|
-
});
|
|
1643
|
-
setExtension(protocolMessage, sendHIDEventMessage, message);
|
|
1644
|
-
return protocolMessage;
|
|
1645
|
-
}
|
|
1646
|
-
function setConnectionState(state = 2 /* Connected */) {
|
|
1647
|
-
const protocolMessage = protocol(38 /* SET_CONNECTION_STATE_MESSAGE */);
|
|
1648
|
-
const message = create(SetConnectionStateMessageSchema, {
|
|
1649
|
-
state
|
|
1650
|
-
});
|
|
1651
|
-
setExtension(protocolMessage, setConnectionStateMessage, message);
|
|
1652
|
-
return protocolMessage;
|
|
1653
|
-
}
|
|
1654
|
-
function setReadyState() {
|
|
1655
|
-
const protocolMessage = protocol(36 /* SET_READY_STATE_MESSAGE */);
|
|
1656
|
-
const message = create(SetReadyStateMessageSchema, {});
|
|
1657
|
-
setExtension(protocolMessage, readyStateMessage, message);
|
|
1658
|
-
return protocolMessage;
|
|
1659
|
-
}
|
|
1660
|
-
function setVolume(outputDeviceUID, volume) {
|
|
1661
|
-
const protocolMessage = protocol(51 /* SET_VOLUME_MESSAGE */);
|
|
1662
|
-
const message = create(SetVolumeMessageSchema, {
|
|
1663
|
-
outputDeviceUID,
|
|
1664
|
-
volume
|
|
1665
|
-
});
|
|
1666
|
-
setExtension(protocolMessage, setVolumeMessage, message);
|
|
1667
|
-
return protocolMessage;
|
|
1668
|
-
}
|
|
1669
|
-
function setVolumeMuted(outputDeviceUID, isMuted) {
|
|
1670
|
-
const protocolMessage = protocol(128 /* SET_VOLUME_MUTED_MESSAGE */);
|
|
1671
|
-
const message = create(SetVolumeMutedMessageSchema, {
|
|
1672
|
-
outputDeviceUID,
|
|
1673
|
-
isMuted
|
|
1674
|
-
});
|
|
1675
|
-
setExtension(protocolMessage, setVolumeMutedMessage, message);
|
|
1676
|
-
return protocolMessage;
|
|
1677
|
-
}
|
|
1678
|
-
function wakeDevice() {
|
|
1679
|
-
const protocolMessage = protocol(41 /* WAKE_DEVICE_MESSAGE */);
|
|
1680
|
-
const message = create(WakeDeviceMessageSchema, {});
|
|
1681
|
-
setExtension(protocolMessage, wakeDeviceMessage, message);
|
|
1682
|
-
return protocolMessage;
|
|
1683
|
-
}
|
|
1684
|
-
function getExtension(message, extension) {
|
|
1685
|
-
return _getExtension(message, extension);
|
|
1686
|
-
}
|
|
1687
|
-
// src/protocol.ts
|
|
1688
|
-
import { getMacAddress, reporter as reporter4, uuid as uuid2 } from "@basmilius/apple-common";
|
|
1689
|
-
import { Plist as Plist3 } from "@basmilius/apple-encoding";
|
|
1690
|
-
|
|
1691
1483
|
// src/utils.ts
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
const lines = [];
|
|
1695
|
-
lines.push(`${method} ${path} RTSP/1.0`);
|
|
1696
|
-
lines.push(`CSeq: ${cseq}`);
|
|
1697
|
-
lines.push("User-Agent: AirPlay/320.20");
|
|
1698
|
-
lines.push("X-Apple-ProtocolVersion: 1");
|
|
1699
|
-
lines.push("X-ProtocolVersion: 1");
|
|
1700
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
1701
|
-
lines.push(`${name}: ${value}`);
|
|
1702
|
-
}
|
|
1703
|
-
lines.push("");
|
|
1704
|
-
lines.push("");
|
|
1705
|
-
return lines.join(`\r
|
|
1706
|
-
`);
|
|
1707
|
-
}
|
|
1708
|
-
function makeHttpRequest(buffer) {
|
|
1709
|
-
const headerLength = buffer.indexOf(`\r
|
|
1710
|
-
\r
|
|
1711
|
-
`);
|
|
1712
|
-
const { headers, method, path } = parseRequestHeaders(buffer.subarray(0, headerLength));
|
|
1713
|
-
let contentLength = headers["Content-Length"] ? Number(headers["Content-Length"]) : 0;
|
|
1714
|
-
if (isNaN(contentLength)) {
|
|
1715
|
-
contentLength = 0;
|
|
1716
|
-
}
|
|
1717
|
-
const requestLength = headerLength + 4 + contentLength;
|
|
1718
|
-
if (buffer.byteLength < requestLength) {
|
|
1719
|
-
return null;
|
|
1720
|
-
}
|
|
1721
|
-
const body = buffer.subarray(headerLength + 4, requestLength);
|
|
1722
|
-
return {
|
|
1723
|
-
headers,
|
|
1724
|
-
method,
|
|
1725
|
-
path,
|
|
1726
|
-
body,
|
|
1727
|
-
requestLength
|
|
1728
|
-
};
|
|
1729
|
-
}
|
|
1730
|
-
function makeHttpResponse(buffer) {
|
|
1731
|
-
const headerLength = buffer.indexOf(`\r
|
|
1732
|
-
\r
|
|
1733
|
-
`);
|
|
1734
|
-
const { headers, status, statusText } = parseResponseHeaders(buffer.subarray(0, headerLength));
|
|
1735
|
-
let contentLength = headers["Content-Length"] ? Number(headers["Content-Length"]) : 0;
|
|
1736
|
-
if (isNaN(contentLength)) {
|
|
1737
|
-
contentLength = 0;
|
|
1738
|
-
}
|
|
1739
|
-
const responseLength = headerLength + 4 + contentLength;
|
|
1740
|
-
if (buffer.byteLength < responseLength) {
|
|
1741
|
-
return null;
|
|
1742
|
-
}
|
|
1743
|
-
const body = buffer.subarray(headerLength + 4, responseLength);
|
|
1744
|
-
const response = new Response(body, {
|
|
1745
|
-
status,
|
|
1746
|
-
statusText,
|
|
1747
|
-
headers
|
|
1748
|
-
});
|
|
1749
|
-
return {
|
|
1750
|
-
response,
|
|
1751
|
-
responseLength
|
|
1752
|
-
};
|
|
1484
|
+
function generateActiveRemoteId() {
|
|
1485
|
+
return Math.floor(Math.random() * 2 ** 32).toString(10);
|
|
1753
1486
|
}
|
|
1754
|
-
function
|
|
1755
|
-
return
|
|
1487
|
+
function generateDacpId() {
|
|
1488
|
+
return Math.floor(Math.random() * 2 ** 64).toString(16).toUpperCase();
|
|
1756
1489
|
}
|
|
1757
|
-
function
|
|
1758
|
-
return
|
|
1490
|
+
function generateSessionId() {
|
|
1491
|
+
return Math.floor(Math.random() * 2 ** 32).toString(10);
|
|
1759
1492
|
}
|
|
1760
|
-
function
|
|
1761
|
-
const
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
continue;
|
|
1766
|
-
}
|
|
1767
|
-
const name = lines[i].substring(0, colon).trim();
|
|
1768
|
-
headers[name] = lines[i].substring(colon + 1).trim();
|
|
1769
|
-
}
|
|
1770
|
-
return headers;
|
|
1493
|
+
function nonce(counter) {
|
|
1494
|
+
const nonceArray = new Uint8Array(12);
|
|
1495
|
+
const view = new DataView(nonceArray.buffer);
|
|
1496
|
+
view.setBigUint64(4, BigInt(counter), true);
|
|
1497
|
+
return Buffer.from(nonceArray);
|
|
1771
1498
|
}
|
|
1772
|
-
function
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
method,
|
|
1782
|
-
path
|
|
1783
|
-
};
|
|
1499
|
+
function buildHeader(totalSize, seqno) {
|
|
1500
|
+
const buf = Buffer.alloc(32);
|
|
1501
|
+
buf.writeUInt32BE(totalSize, 0);
|
|
1502
|
+
buf.write("sync", 4, "ascii");
|
|
1503
|
+
buf.fill(0, 8, 16);
|
|
1504
|
+
buf.write("comm", 16, "ascii");
|
|
1505
|
+
buf.writeBigUInt64BE(seqno, 20);
|
|
1506
|
+
buf.writeUInt32BE(0, 28);
|
|
1507
|
+
return buf;
|
|
1784
1508
|
}
|
|
1785
|
-
function
|
|
1786
|
-
const
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
};
|
|
1509
|
+
function buildReply(seqno) {
|
|
1510
|
+
const header = Buffer.alloc(32);
|
|
1511
|
+
header.writeUInt32BE(0, 0);
|
|
1512
|
+
header.write("rply", 4, "ascii");
|
|
1513
|
+
header.fill(0, 8, 16);
|
|
1514
|
+
header.writeBigUInt64BE(seqno, 20);
|
|
1515
|
+
header.writeUInt32BE(0, 28);
|
|
1516
|
+
const plist = Buffer.from(Plist.serialize(Buffer.alloc(0)));
|
|
1517
|
+
const total = header.length + plist.length;
|
|
1518
|
+
header.writeUInt32BE(total, 0);
|
|
1519
|
+
return Buffer.concat([header, plist]);
|
|
1797
1520
|
}
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
import { Plist } from "@basmilius/apple-encoding";
|
|
1802
|
-
import { fromBinary, getExtension as getExtension2, toBinary } from "@bufbuild/protobuf";
|
|
1803
|
-
|
|
1804
|
-
// src/stream.ts
|
|
1805
|
-
import { Chacha20, ENCRYPTION, EncryptionAwareConnection } from "@basmilius/apple-common";
|
|
1806
|
-
|
|
1807
|
-
class AirPlayStream extends EncryptionAwareConnection {
|
|
1808
|
-
get #encryption() {
|
|
1809
|
-
return this[ENCRYPTION];
|
|
1810
|
-
}
|
|
1811
|
-
#buffer = Buffer.alloc(0);
|
|
1812
|
-
async decrypt(data) {
|
|
1813
|
-
if (this.#buffer) {
|
|
1814
|
-
data = Buffer.concat([this.#buffer, data]);
|
|
1815
|
-
this.#buffer = undefined;
|
|
1816
|
-
}
|
|
1817
|
-
let result = Buffer.alloc(0);
|
|
1818
|
-
let offset = 0;
|
|
1819
|
-
while (offset + 2 <= data.length) {
|
|
1820
|
-
const length = data.readUInt16LE(offset);
|
|
1821
|
-
const totalChunkLength = 2 + length + 16;
|
|
1822
|
-
if (offset + totalChunkLength > data.length) {
|
|
1823
|
-
this.#buffer = data.subarray(offset);
|
|
1824
|
-
break;
|
|
1825
|
-
}
|
|
1826
|
-
const aad = data.subarray(offset, offset + 2);
|
|
1827
|
-
const ciphertext = data.subarray(offset + 2, offset + 2 + length);
|
|
1828
|
-
const authTag = data.subarray(offset + 2 + length, offset + 2 + length + 16);
|
|
1829
|
-
const nonce = this.nonce(this.#encryption.readCount++);
|
|
1830
|
-
const plaintext = Chacha20.decrypt(this.#encryption.readKey, nonce, aad, ciphertext, authTag);
|
|
1831
|
-
result = Buffer.concat([result, plaintext]);
|
|
1832
|
-
offset += totalChunkLength;
|
|
1833
|
-
}
|
|
1834
|
-
return result;
|
|
1521
|
+
function encodeVarint(value) {
|
|
1522
|
+
if (value < 0) {
|
|
1523
|
+
throw new RangeError("Varint only supports non-negative integers");
|
|
1835
1524
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
const length = Math.min(total - offset, 1024);
|
|
1841
|
-
const leLength = Buffer.alloc(2);
|
|
1842
|
-
leLength.writeUInt16LE(length, 0);
|
|
1843
|
-
const nonce = this.nonce(this.#encryption.writeCount++);
|
|
1844
|
-
const encrypted = Chacha20.encrypt(this.#encryption.writeKey, nonce, leLength, data.subarray(offset, offset + length));
|
|
1845
|
-
offset += length;
|
|
1846
|
-
result = Buffer.concat([result, leLength, encrypted.ciphertext, encrypted.authTag]);
|
|
1847
|
-
}
|
|
1848
|
-
return result;
|
|
1525
|
+
const bytes = [];
|
|
1526
|
+
while (value > 127) {
|
|
1527
|
+
bytes.push(value & 127 | 128);
|
|
1528
|
+
value >>>= 7;
|
|
1849
1529
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1530
|
+
bytes.push(value);
|
|
1531
|
+
return Uint8Array.from(bytes);
|
|
1532
|
+
}
|
|
1533
|
+
function parseHeaderSeqno(header) {
|
|
1534
|
+
if (header.length < 28) {
|
|
1535
|
+
throw new Error("Header too short");
|
|
1536
|
+
}
|
|
1537
|
+
return header.readBigUInt64BE(20);
|
|
1538
|
+
}
|
|
1539
|
+
function parseMessages(content) {
|
|
1540
|
+
const messages = [];
|
|
1541
|
+
let offset = 0;
|
|
1542
|
+
while (offset < content.length) {
|
|
1543
|
+
const firstByte = content[offset];
|
|
1544
|
+
if (firstByte === 8) {
|
|
1545
|
+
const message2 = content.subarray(offset);
|
|
1546
|
+
const decoded2 = fromBinary(ProtocolMessageSchema, message2, { readUnknownFields: true });
|
|
1547
|
+
messages.push(decoded2);
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1550
|
+
const [length, variantLen] = readVariant(content, offset);
|
|
1551
|
+
offset += variantLen;
|
|
1552
|
+
if (offset + length > content.length) {
|
|
1553
|
+
break;
|
|
1554
|
+
}
|
|
1555
|
+
const message = content.subarray(offset, offset + length);
|
|
1556
|
+
offset += length;
|
|
1557
|
+
const decoded = fromBinary(ProtocolMessageSchema, message, { readUnknownFields: true });
|
|
1558
|
+
messages.push(decoded);
|
|
1559
|
+
}
|
|
1560
|
+
return messages;
|
|
1561
|
+
}
|
|
1562
|
+
function readVariant(buf, offset = 0) {
|
|
1563
|
+
let result = 0;
|
|
1564
|
+
let shift = 0;
|
|
1565
|
+
let bytesRead = 0;
|
|
1566
|
+
while (true) {
|
|
1567
|
+
const byte = buf[offset + bytesRead++];
|
|
1568
|
+
result |= (byte & 127) << shift;
|
|
1569
|
+
if ((byte & 128) === 0) {
|
|
1570
|
+
break;
|
|
1571
|
+
}
|
|
1572
|
+
shift += 7;
|
|
1573
|
+
}
|
|
1574
|
+
return [result, bytesRead];
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// src/baseStream.ts
|
|
1578
|
+
class BaseStream extends EncryptionAwareConnection {
|
|
1579
|
+
get #encryptionState() {
|
|
1580
|
+
return this[ENCRYPTION];
|
|
1581
|
+
}
|
|
1582
|
+
decrypt(data) {
|
|
1583
|
+
const result = [];
|
|
1584
|
+
let offset = 0;
|
|
1585
|
+
let readCount = this.#encryptionState.readCount ?? 0;
|
|
1586
|
+
while (offset < data.length) {
|
|
1587
|
+
if (offset + 2 > data.length) {
|
|
1588
|
+
this.context.logger.warn("Expected frame length to be within buffer bounds.");
|
|
1589
|
+
return false;
|
|
1590
|
+
}
|
|
1591
|
+
const frameLength = data.readUInt16LE(offset);
|
|
1592
|
+
offset += 2;
|
|
1593
|
+
const end = offset + frameLength + 16;
|
|
1594
|
+
if (end > data.length) {
|
|
1595
|
+
this.context.logger.warn(`Truncated frame end=${end} length=${data.length}`);
|
|
1596
|
+
return false;
|
|
1597
|
+
}
|
|
1598
|
+
const ciphertext = data.subarray(offset, offset + frameLength);
|
|
1599
|
+
const authTag = data.subarray(offset + frameLength, end);
|
|
1600
|
+
offset = end;
|
|
1601
|
+
const plaintext = Chacha20.decrypt(this.#encryptionState.readKey, nonce(readCount++), Buffer.from(Uint16Array.of(frameLength).buffer.slice(0, 2)), ciphertext, authTag);
|
|
1602
|
+
result.push(plaintext);
|
|
1603
|
+
}
|
|
1604
|
+
this.#encryptionState.readCount = readCount;
|
|
1605
|
+
return Buffer.concat(result);
|
|
1606
|
+
}
|
|
1607
|
+
encrypt(data) {
|
|
1608
|
+
const FRAME_LENGTH = 1024;
|
|
1609
|
+
const result = [];
|
|
1610
|
+
for (let offset = 0;offset < data.length; ) {
|
|
1611
|
+
const frame = data.subarray(offset, offset + FRAME_LENGTH);
|
|
1612
|
+
offset += frame.length;
|
|
1613
|
+
const leLength = Buffer.alloc(2);
|
|
1614
|
+
leLength.writeUInt16LE(frame.length, 0);
|
|
1615
|
+
const encrypted = Chacha20.encrypt(this.#encryptionState.writeKey, nonce(this.#encryptionState.writeCount++), leLength, frame);
|
|
1616
|
+
result.push(leLength, encrypted.ciphertext, encrypted.authTag);
|
|
1617
|
+
}
|
|
1618
|
+
return Buffer.concat(result);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
// src/controlStream.ts
|
|
1622
|
+
import { HTTP_TIMEOUT } from "@basmilius/apple-common";
|
|
1623
|
+
import { RTSP } from "@basmilius/apple-encoding";
|
|
1624
|
+
class ControlStream extends BaseStream {
|
|
1625
|
+
get activeRemoteId() {
|
|
1626
|
+
return this.#activeRemoteId;
|
|
1627
|
+
}
|
|
1628
|
+
get dacpId() {
|
|
1629
|
+
return this.#dacpId;
|
|
1630
|
+
}
|
|
1631
|
+
get sessionId() {
|
|
1632
|
+
return this.#sessionId;
|
|
1633
|
+
}
|
|
1634
|
+
#activeRemoteId;
|
|
1635
|
+
#dacpId;
|
|
1636
|
+
#sessionId;
|
|
1637
|
+
#buffer = Buffer.alloc(0);
|
|
1638
|
+
#cseq = 0;
|
|
1639
|
+
#requesting = false;
|
|
1640
|
+
#requestTimer;
|
|
1641
|
+
#reject;
|
|
1642
|
+
#resolve;
|
|
1643
|
+
constructor(context, address, port) {
|
|
1644
|
+
super(context, address, port);
|
|
1645
|
+
this.#activeRemoteId = generateActiveRemoteId();
|
|
1646
|
+
this.#dacpId = generateDacpId();
|
|
1647
|
+
this.#sessionId = generateSessionId();
|
|
1648
|
+
this.on("close", this.#onClose.bind(this));
|
|
1649
|
+
this.on("data", this.#onData.bind(this));
|
|
1650
|
+
this.on("error", this.#onError.bind(this));
|
|
1651
|
+
this.on("timeout", this.#onTimeout.bind(this));
|
|
1652
|
+
}
|
|
1653
|
+
async get(path, headers = {}, timeout = HTTP_TIMEOUT) {
|
|
1654
|
+
return await this.#request("GET", path, null, headers, timeout);
|
|
1655
|
+
}
|
|
1656
|
+
async post(path, body = null, headers = {}, timeout = HTTP_TIMEOUT) {
|
|
1657
|
+
return await this.#request("POST", path, body, headers, timeout);
|
|
1658
|
+
}
|
|
1659
|
+
async record(path, headers = {}, timeout = HTTP_TIMEOUT) {
|
|
1660
|
+
return await this.#request("RECORD", path, null, headers, timeout);
|
|
1661
|
+
}
|
|
1662
|
+
async setup(path, body = null, headers = {}, timeout = HTTP_TIMEOUT) {
|
|
1663
|
+
return await this.#request("SETUP", path, body, headers, timeout);
|
|
1664
|
+
}
|
|
1665
|
+
#handle(data, err) {
|
|
1666
|
+
if (this.#requestTimer) {
|
|
1667
|
+
clearTimeout(this.#requestTimer);
|
|
1668
|
+
this.#requestTimer = undefined;
|
|
1669
|
+
}
|
|
1670
|
+
if (err) {
|
|
1671
|
+
this.#reject?.(err);
|
|
1672
|
+
} else {
|
|
1673
|
+
this.#resolve?.(data);
|
|
1674
|
+
}
|
|
1675
|
+
this.#reject = undefined;
|
|
1676
|
+
this.#resolve = undefined;
|
|
1677
|
+
this.#requesting = false;
|
|
1678
|
+
}
|
|
1679
|
+
async#request(method, path, body, headers, timeout = HTTP_TIMEOUT) {
|
|
1680
|
+
if (this.#requesting) {
|
|
1681
|
+
return Promise.reject(new Error("Another request is currently being made."));
|
|
1682
|
+
}
|
|
1683
|
+
this.#requesting = true;
|
|
1684
|
+
headers["Active-Remote"] = this.activeRemoteId;
|
|
1685
|
+
headers["Client-Instance"] = this.dacpId;
|
|
1686
|
+
headers["DACP-ID"] = this.dacpId;
|
|
1687
|
+
const cseq = this.#cseq++;
|
|
1688
|
+
let data;
|
|
1689
|
+
if (body) {
|
|
1690
|
+
headers["Content-Length"] = Buffer.byteLength(body);
|
|
1691
|
+
data = Buffer.concat([
|
|
1692
|
+
Buffer.from(RTSP.makeHeader(method, path, headers, cseq)),
|
|
1693
|
+
Buffer.from(body)
|
|
1694
|
+
]);
|
|
1695
|
+
} else {
|
|
1696
|
+
headers["Content-Length"] = 0;
|
|
1697
|
+
data = Buffer.from(RTSP.makeHeader(method, path, headers, cseq));
|
|
1698
|
+
}
|
|
1699
|
+
this.context.logger.net("[control]", method, path, `cseq = ${cseq}`);
|
|
1700
|
+
if (this.isEncrypted) {
|
|
1701
|
+
data = this.encrypt(data);
|
|
1702
|
+
}
|
|
1703
|
+
return new Promise(async (resolve, reject) => {
|
|
1704
|
+
this.#reject = reject;
|
|
1705
|
+
this.#resolve = resolve;
|
|
1706
|
+
this.#requestTimer = setTimeout(() => this.#handle(undefined, new Error("Request timed out")), timeout);
|
|
1707
|
+
await this.write(data).catch((err) => this.#handle(undefined, err));
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
#onClose() {
|
|
1711
|
+
this.#buffer = Buffer.alloc(0);
|
|
1712
|
+
this.#handle(undefined, new Error("Connection closed."));
|
|
1713
|
+
this.context.logger.net("[control]", "#onClose()");
|
|
1714
|
+
}
|
|
1715
|
+
#onData(data) {
|
|
1716
|
+
try {
|
|
1717
|
+
this.#buffer = Buffer.concat([this.#buffer, data]);
|
|
1718
|
+
if (this.isEncrypted) {
|
|
1719
|
+
const decrypted = this.decrypt(this.#buffer);
|
|
1720
|
+
if (!decrypted) {
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
this.#buffer = decrypted;
|
|
1724
|
+
}
|
|
1725
|
+
while (this.#buffer.byteLength > 0) {
|
|
1726
|
+
const result = RTSP.makeResponse(this.#buffer);
|
|
1727
|
+
if (result === null) {
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
this.#buffer = this.#buffer.subarray(result.responseLength);
|
|
1731
|
+
this.#handle(result.response, undefined);
|
|
1732
|
+
}
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
this.context.logger.error("[control]", "#onData()", err);
|
|
1735
|
+
this.emit("error", err);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
#onError(err) {
|
|
1739
|
+
this.#handle(undefined, err);
|
|
1740
|
+
this.context.logger.error("[control]", "#onError()", err);
|
|
1741
|
+
}
|
|
1742
|
+
#onTimeout() {
|
|
1743
|
+
this.#handle(undefined, new Error("Request timed out."));
|
|
1744
|
+
this.context.logger.net("[control]", "#onTimeout()");
|
|
1855
1745
|
}
|
|
1856
1746
|
}
|
|
1857
|
-
|
|
1858
1747
|
// src/dataStream.ts
|
|
1748
|
+
import { hkdf, randomInt32 } from "@basmilius/apple-common";
|
|
1749
|
+
import { Plist as Plist2 } from "@basmilius/apple-encoding";
|
|
1750
|
+
import { getExtension, toBinary } from "@bufbuild/protobuf";
|
|
1859
1751
|
var DATA_HEADER_LENGTH = 32;
|
|
1860
1752
|
|
|
1861
|
-
class
|
|
1862
|
-
get #encryption() {
|
|
1863
|
-
return this[ENCRYPTION2];
|
|
1864
|
-
}
|
|
1865
|
-
#bindings;
|
|
1753
|
+
class DataStream extends BaseStream {
|
|
1866
1754
|
#buffer = Buffer.alloc(0);
|
|
1867
1755
|
#seqno;
|
|
1868
1756
|
#handler;
|
|
1869
|
-
|
|
1870
|
-
|
|
1757
|
+
#handlers = {};
|
|
1758
|
+
constructor(context, address, port) {
|
|
1759
|
+
super(context, address, port);
|
|
1871
1760
|
this.#seqno = 0x100000000n + BigInt(randomInt32());
|
|
1872
|
-
this.#
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
this
|
|
1761
|
+
this.on("data", this.#onData.bind(this));
|
|
1762
|
+
this.#handlers[15 /* DEVICE_INFO_MESSAGE */] = [deviceInfoMessage, this.#onDeviceInfoMessage.bind(this)];
|
|
1763
|
+
this.#handlers[37 /* DEVICE_INFO_UPDATE_MESSAGE */] = [deviceInfoMessage, this.#onDeviceInfoUpdateMessage.bind(this)];
|
|
1764
|
+
this.#handlers[105 /* ORIGIN_CLIENT_PROPERTIES_MESSAGE */] = [originClientPropertiesMessage, this.#onOriginClientPropertiesMessage.bind(this)];
|
|
1765
|
+
this.#handlers[104 /* PLAYER_CLIENT_PROPERTIES_MESSAGE */] = [playerClientPropertiesMessage, this.#onPlayerClientPropertiesMessage.bind(this)];
|
|
1766
|
+
this.#handlers[2 /* SEND_COMMAND_RESULT_MESSAGE */] = [sendCommandResultMessage, this.#onSendCommandResultMessage.bind(this)];
|
|
1767
|
+
this.#handlers[5 /* SET_ARTWORK_MESSAGE */] = [setArtworkMessage, this.#onSetArtworkMessage.bind(this)];
|
|
1768
|
+
this.#handlers[72 /* SET_DEFAULT_SUPPORTED_COMMANDS_MESSAGE */] = [setDefaultSupportedCommandsMessage, this.#onSetDefaultSupportedCommandsMessage.bind(this)];
|
|
1769
|
+
this.#handlers[46 /* SET_NOW_PLAYING_CLIENT_MESSAGE */] = [setNowPlayingClientMessage, this.#onSetNowPlayingClientMessage.bind(this)];
|
|
1770
|
+
this.#handlers[47 /* SET_NOW_PLAYING_PLAYER_MESSAGE */] = [setNowPlayingPlayerMessage, this.#onSetNowPlayingPlayerMessage.bind(this)];
|
|
1771
|
+
this.#handlers[4 /* SET_STATE_MESSAGE */] = [setStateMessage, this.#onSetStateMessage.bind(this)];
|
|
1772
|
+
this.#handlers[53 /* REMOVE_CLIENT_MESSAGE */] = [removeClientMessage, this.#onRemoveClientMessage.bind(this)];
|
|
1773
|
+
this.#handlers[55 /* UPDATE_CLIENT_MESSAGE */] = [updateClientMessage, this.#onUpdateClientMessage.bind(this)];
|
|
1774
|
+
this.#handlers[56 /* UPDATE_CONTENT_ITEM_MESSAGE */] = [updateContentItemMessage, this.#onUpdateContentItemMessage.bind(this)];
|
|
1775
|
+
this.#handlers[57 /* UPDATE_CONTENT_ITEM_ARTWORK_MESSAGE */] = [updateContentItemArtworkMessage, this.#onUpdateContentItemArtworkMessage.bind(this)];
|
|
1776
|
+
this.#handlers[58 /* UPDATE_PLAYER_MESSAGE */] = [updatePlayerMessage, this.#onUpdatePlayerMessage.bind(this)];
|
|
1777
|
+
this.#handlers[65 /* UPDATE_OUTPUT_DEVICE_MESSAGE */] = [updateOutputDeviceMessage, this.#onUpdateOutputDeviceMessage.bind(this)];
|
|
1778
|
+
this.#handlers[17 /* VOLUME_CONTROL_AVAILABILITY_MESSAGE */] = [volumeControlAvailabilityMessage, this.#onVolumeControlAvailabilityMessage.bind(this)];
|
|
1779
|
+
this.#handlers[64 /* VOLUME_CONTROL_CAPABILITIES_DID_CHANGE_MESSAGE */] = [volumeControlCapabilitiesDidChangeMessage, this.#onVolumeControlCapabilitiesDidChangeMessage.bind(this)];
|
|
1780
|
+
this.#handlers[52 /* VOLUME_DID_CHANGE_MESSAGE */] = [volumeDidChangeMessage, this.#onVolumeDidChangeMessage.bind(this)];
|
|
1876
1781
|
}
|
|
1877
1782
|
async exchange(message) {
|
|
1878
1783
|
return new Promise(async (resolve, reject) => {
|
|
@@ -1882,25 +1787,32 @@ class AirPlayDataStream extends AirPlayStream {
|
|
|
1882
1787
|
}
|
|
1883
1788
|
async reply(seqno) {
|
|
1884
1789
|
const rply = buildReply(seqno);
|
|
1885
|
-
|
|
1886
|
-
await this.write(
|
|
1790
|
+
this.context.logger.raw("[data]", `Sending reply packet seqno=${seqno}`);
|
|
1791
|
+
await this.write(this.encrypt(rply));
|
|
1887
1792
|
}
|
|
1888
1793
|
async send(message) {
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1794
|
+
let extension;
|
|
1795
|
+
if (Array.isArray(message)) {
|
|
1796
|
+
extension = message[1];
|
|
1797
|
+
message = message[0];
|
|
1798
|
+
}
|
|
1799
|
+
const bytes = toBinary(ProtocolMessageSchema, message, { writeUnknownFields: false });
|
|
1800
|
+
const lengthPrefix = Buffer.from(encodeVarint(bytes.byteLength));
|
|
1801
|
+
const payload = Buffer.concat([lengthPrefix, bytes]);
|
|
1802
|
+
const plistPayload = Buffer.from(Plist2.serialize({
|
|
1893
1803
|
params: {
|
|
1894
|
-
data:
|
|
1804
|
+
data: payload.buffer.slice(payload.byteOffset, payload.byteOffset + payload.byteLength)
|
|
1895
1805
|
}
|
|
1896
1806
|
}));
|
|
1897
1807
|
const header = buildHeader(DATA_HEADER_LENGTH + plistPayload.byteLength, this.#seqno++);
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1808
|
+
let frame = Buffer.concat([header, plistPayload]);
|
|
1809
|
+
if (this.isEncrypted) {
|
|
1810
|
+
frame = this.encrypt(frame);
|
|
1811
|
+
}
|
|
1812
|
+
this.context.logger.raw("[data]", "Sending message.", message.type, extension ? getExtension(message, extension) : message);
|
|
1813
|
+
await this.write(frame);
|
|
1902
1814
|
}
|
|
1903
|
-
|
|
1815
|
+
setup(sharedSecret, seed) {
|
|
1904
1816
|
const readKey = hkdf({
|
|
1905
1817
|
hash: "sha512",
|
|
1906
1818
|
key: sharedSecret,
|
|
@@ -1915,103 +1827,32 @@ class AirPlayDataStream extends AirPlayStream {
|
|
|
1915
1827
|
salt: Buffer.from(`DataStream-Salt${seed}`),
|
|
1916
1828
|
info: Buffer.from("DataStream-Output-Encryption-Key")
|
|
1917
1829
|
});
|
|
1918
|
-
|
|
1919
|
-
}
|
|
1920
|
-
async#onDeviceInfoMessage(message) {
|
|
1921
|
-
reporter.info("Connected to device", message.name);
|
|
1922
|
-
this.emit("deviceInfo", message);
|
|
1923
|
-
}
|
|
1924
|
-
async#onDeviceInfoUpdateMessage(message) {
|
|
1925
|
-
reporter.info("Device info update", message);
|
|
1926
|
-
this.emit("deviceInfoUpdate", message);
|
|
1927
|
-
}
|
|
1928
|
-
async#onOriginClientPropertiesMessage(message) {
|
|
1929
|
-
reporter.raw("Origin client properties", message);
|
|
1930
|
-
this.emit("originClientProperties", message);
|
|
1830
|
+
this.enableEncryption(readKey, writeKey);
|
|
1931
1831
|
}
|
|
1932
|
-
async#
|
|
1933
|
-
reporter.raw("Player client properties", message);
|
|
1934
|
-
this.emit("playerClientProperties", message);
|
|
1935
|
-
}
|
|
1936
|
-
async#onRemoveClientMessage(message) {
|
|
1937
|
-
reporter.info("Remove client", message);
|
|
1938
|
-
this.emit("removeClient", message);
|
|
1939
|
-
}
|
|
1940
|
-
async#onSendCommandResultMessage(message) {
|
|
1941
|
-
reporter.info("Send command result", message);
|
|
1942
|
-
this.emit("sendCommandResult", message);
|
|
1943
|
-
}
|
|
1944
|
-
async#onSetArtworkMessage(message) {
|
|
1945
|
-
reporter.info("Set artwork", message);
|
|
1946
|
-
this.emit("setArtwork", message);
|
|
1947
|
-
}
|
|
1948
|
-
async#onSetDefaultSupportedCommandsMessage(message) {
|
|
1949
|
-
reporter.info("Set default supported commands", message);
|
|
1950
|
-
this.emit("setDefaultSupportedCommands", message);
|
|
1951
|
-
}
|
|
1952
|
-
async#onSetNowPlayingClientMessage(message) {
|
|
1953
|
-
reporter.info("Set now playing client", message);
|
|
1954
|
-
this.emit("setNowPlayingClient", message);
|
|
1955
|
-
}
|
|
1956
|
-
async#onSetNowPlayingPlayerMessage(message) {
|
|
1957
|
-
reporter.info("Set now playing player", message);
|
|
1958
|
-
this.emit("setNowPlayingPlayer", message);
|
|
1959
|
-
}
|
|
1960
|
-
async#onSetStateMessage(message) {
|
|
1961
|
-
reporter.info("Set state", message);
|
|
1962
|
-
this.emit("setState", message);
|
|
1963
|
-
}
|
|
1964
|
-
async#onUpdateClientMessage(message) {
|
|
1965
|
-
reporter.info("Update client", message);
|
|
1966
|
-
this.emit("updateClient", message);
|
|
1967
|
-
}
|
|
1968
|
-
async#onUpdateContentItemMessage(message) {
|
|
1969
|
-
reporter.info("Update content item", message);
|
|
1970
|
-
this.emit("updateContentItem", message);
|
|
1971
|
-
}
|
|
1972
|
-
async#onUpdateContentItemArtworkMessage(message) {
|
|
1973
|
-
reporter.info("Update content artwork", message);
|
|
1974
|
-
this.emit("updateContentItemArtwork", message);
|
|
1975
|
-
}
|
|
1976
|
-
async#onUpdatePlayerMessage(message) {
|
|
1977
|
-
reporter.info("Update player", message);
|
|
1978
|
-
this.emit("updatePlayer", message);
|
|
1979
|
-
}
|
|
1980
|
-
async#onUpdateOutputDeviceMessage(message) {
|
|
1981
|
-
reporter.info("Update output device", message);
|
|
1982
|
-
this.emit("updateOutputDevice", message);
|
|
1983
|
-
}
|
|
1984
|
-
async#onVolumeControlAvailabilityMessage(message) {
|
|
1985
|
-
reporter.info("Volume control availability", message);
|
|
1986
|
-
this.emit("volumeControlAvailability", message);
|
|
1987
|
-
}
|
|
1988
|
-
async#onVolumeControlCapabilitiesDidChangeMessage(message) {
|
|
1989
|
-
reporter.info("Volume control capabilities did change", message);
|
|
1990
|
-
this.emit("volumeControlCapabilitiesDidChange", message);
|
|
1991
|
-
}
|
|
1992
|
-
async#onVolumeDidChangeMessage(message) {
|
|
1993
|
-
reporter.info("VolumeDidChange message", message);
|
|
1994
|
-
this.emit("volumeDidChange", message);
|
|
1995
|
-
}
|
|
1996
|
-
async#onData(buffer) {
|
|
1832
|
+
async#onData(data) {
|
|
1997
1833
|
try {
|
|
1998
|
-
this.#buffer = Buffer.concat([this.#buffer,
|
|
1999
|
-
|
|
1834
|
+
this.#buffer = Buffer.concat([this.#buffer, data]);
|
|
1835
|
+
if (this.isEncrypted) {
|
|
1836
|
+
const decrypted = this.decrypt(this.#buffer);
|
|
1837
|
+
if (!decrypted) {
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
this.#buffer = decrypted;
|
|
1841
|
+
}
|
|
2000
1842
|
while (this.#buffer.byteLength > DATA_HEADER_LENGTH) {
|
|
2001
1843
|
const header = this.#buffer.subarray(0, DATA_HEADER_LENGTH);
|
|
2002
1844
|
const totalLength = header.readUint32BE();
|
|
2003
1845
|
if (this.#buffer.byteLength < totalLength) {
|
|
2004
|
-
|
|
1846
|
+
this.context.logger.warn("[data]", `Data packet is too short needed=${totalLength} available=${this.#buffer.byteLength} receivedLength=${data.byteLength}`);
|
|
2005
1847
|
return;
|
|
2006
1848
|
}
|
|
2007
1849
|
const frame = this.#buffer.subarray(DATA_HEADER_LENGTH, totalLength);
|
|
2008
|
-
const plist =
|
|
1850
|
+
const plist = Plist2.parse(frame.buffer.slice(frame.byteOffset, frame.byteOffset + frame.byteLength));
|
|
2009
1851
|
const command = header.toString("ascii", 4, 8);
|
|
2010
|
-
reporter.raw("Raw data received", header.toString());
|
|
2011
1852
|
this.#buffer = this.#buffer.subarray(totalLength);
|
|
2012
1853
|
if (!plist || !plist.params || !plist.params.data) {
|
|
2013
1854
|
if (command === "rply") {
|
|
2014
|
-
|
|
1855
|
+
this.context.logger.raw("[data]", "Received reply packet.");
|
|
2015
1856
|
} else if (command === "sync") {
|
|
2016
1857
|
await this.reply(parseHeaderSeqno(header));
|
|
2017
1858
|
}
|
|
@@ -2023,221 +1864,117 @@ class AirPlayDataStream extends AirPlayStream {
|
|
|
2023
1864
|
continue;
|
|
2024
1865
|
}
|
|
2025
1866
|
const content = Buffer.from(plist.params.data);
|
|
2026
|
-
for (const message of
|
|
2027
|
-
|
|
2028
|
-
|
|
1867
|
+
for (const message of parseMessages(content)) {
|
|
1868
|
+
this.context.logger.raw("[data]", `Received message.`, message);
|
|
1869
|
+
this.#handleMessage(message);
|
|
2029
1870
|
}
|
|
2030
1871
|
if (command === "sync") {
|
|
2031
1872
|
await this.reply(parseHeaderSeqno(header));
|
|
2032
1873
|
}
|
|
2033
1874
|
}
|
|
2034
1875
|
} catch (err) {
|
|
2035
|
-
|
|
1876
|
+
this.context.logger.error("[data]", "#onData()", err);
|
|
2036
1877
|
this.emit("error", err);
|
|
2037
1878
|
}
|
|
2038
1879
|
}
|
|
2039
|
-
|
|
2040
|
-
const result = [];
|
|
2041
|
-
let offset = 0;
|
|
2042
|
-
let readCount = this.#encryption.readCount ?? 0;
|
|
2043
|
-
while (offset < data.length) {
|
|
2044
|
-
if (offset + 2 > data.length) {
|
|
2045
|
-
reporter.warn("Truncated frame length");
|
|
2046
|
-
return this.#buffer;
|
|
2047
|
-
}
|
|
2048
|
-
const frameLength = data.readUInt16LE(offset);
|
|
2049
|
-
offset += 2;
|
|
2050
|
-
const nonce = this.nonce(readCount++);
|
|
2051
|
-
const end = offset + frameLength + 16;
|
|
2052
|
-
if (end > data.length) {
|
|
2053
|
-
reporter.warn(`Truncated frame end=${end} length=${data.length}`);
|
|
2054
|
-
return this.#buffer;
|
|
2055
|
-
}
|
|
2056
|
-
const ciphertext = data.subarray(offset, offset + frameLength);
|
|
2057
|
-
const authTag = data.subarray(offset + frameLength, end);
|
|
2058
|
-
offset = end;
|
|
2059
|
-
const plaintext = Chacha202.decrypt(this.#encryption.readKey, nonce, Buffer.from(Uint16Array.of(frameLength).buffer.slice(0, 2)), ciphertext, authTag);
|
|
2060
|
-
result.push(plaintext);
|
|
2061
|
-
}
|
|
2062
|
-
this.#encryption.readCount = readCount;
|
|
2063
|
-
return Buffer.concat(result);
|
|
2064
|
-
}
|
|
2065
|
-
async#encrypt(data) {
|
|
2066
|
-
const FRAME_LENGTH = 1024;
|
|
2067
|
-
const result = [];
|
|
2068
|
-
for (let offset = 0;offset < data.length; ) {
|
|
2069
|
-
const frame = data.subarray(offset, offset + FRAME_LENGTH);
|
|
2070
|
-
offset += frame.length;
|
|
2071
|
-
const leLength = Buffer.alloc(2);
|
|
2072
|
-
leLength.writeUInt16LE(frame.length, 0);
|
|
2073
|
-
const nonce = this.nonce(this.#encryption.writeCount++);
|
|
2074
|
-
const encrypted = Chacha202.encrypt(this.#encryption.writeKey, nonce, leLength, frame);
|
|
2075
|
-
result.push(leLength, encrypted.ciphertext, encrypted.authTag);
|
|
2076
|
-
}
|
|
2077
|
-
return Buffer.concat(result);
|
|
2078
|
-
}
|
|
2079
|
-
async#handleMessage(message) {
|
|
1880
|
+
#handleMessage(message) {
|
|
2080
1881
|
if (this.#handler) {
|
|
2081
1882
|
const [resolve] = this.#handler;
|
|
2082
1883
|
this.#handler = undefined;
|
|
2083
1884
|
resolve(message);
|
|
2084
1885
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
await this.#onDeviceInfoUpdateMessage(getExtension2(message, deviceInfoMessage));
|
|
2091
|
-
break;
|
|
2092
|
-
case 105 /* ORIGIN_CLIENT_PROPERTIES_MESSAGE */:
|
|
2093
|
-
await this.#onOriginClientPropertiesMessage(getExtension2(message, originClientPropertiesMessage));
|
|
2094
|
-
break;
|
|
2095
|
-
case 104 /* PLAYER_CLIENT_PROPERTIES_MESSAGE */:
|
|
2096
|
-
await this.#onPlayerClientPropertiesMessage(getExtension2(message, playerClientPropertiesMessage));
|
|
2097
|
-
break;
|
|
2098
|
-
case 2 /* SEND_COMMAND_RESULT_MESSAGE */:
|
|
2099
|
-
await this.#onSendCommandResultMessage(getExtension2(message, sendCommandResultMessage));
|
|
2100
|
-
break;
|
|
2101
|
-
case 5 /* SET_ARTWORK_MESSAGE */:
|
|
2102
|
-
await this.#onSetArtworkMessage(getExtension2(message, setArtworkMessage));
|
|
2103
|
-
break;
|
|
2104
|
-
case 72 /* SET_DEFAULT_SUPPORTED_COMMANDS_MESSAGE */:
|
|
2105
|
-
await this.#onSetDefaultSupportedCommandsMessage(getExtension2(message, setDefaultSupportedCommandsMessage));
|
|
2106
|
-
break;
|
|
2107
|
-
case 46 /* SET_NOW_PLAYING_CLIENT_MESSAGE */:
|
|
2108
|
-
await this.#onSetNowPlayingClientMessage(getExtension2(message, setNowPlayingClientMessage));
|
|
2109
|
-
break;
|
|
2110
|
-
case 47 /* SET_NOW_PLAYING_PLAYER_MESSAGE */:
|
|
2111
|
-
await this.#onSetNowPlayingPlayerMessage(getExtension2(message, setNowPlayingPlayerMessage));
|
|
2112
|
-
break;
|
|
2113
|
-
case 4 /* SET_STATE_MESSAGE */:
|
|
2114
|
-
await this.#onSetStateMessage(getExtension2(message, setStateMessage));
|
|
2115
|
-
break;
|
|
2116
|
-
case 53 /* REMOVE_CLIENT_MESSAGE */:
|
|
2117
|
-
await this.#onRemoveClientMessage(getExtension2(message, removeClientMessage));
|
|
2118
|
-
break;
|
|
2119
|
-
case 55 /* UPDATE_CLIENT_MESSAGE */:
|
|
2120
|
-
await this.#onUpdateClientMessage(getExtension2(message, updateClientMessage));
|
|
2121
|
-
break;
|
|
2122
|
-
case 56 /* UPDATE_CONTENT_ITEM_MESSAGE */:
|
|
2123
|
-
await this.#onUpdateContentItemMessage(getExtension2(message, updateContentItemMessage));
|
|
2124
|
-
break;
|
|
2125
|
-
case 57 /* UPDATE_CONTENT_ITEM_ARTWORK_MESSAGE */:
|
|
2126
|
-
await this.#onUpdateContentItemArtworkMessage(getExtension2(message, updateContentItemArtworkMessage));
|
|
2127
|
-
break;
|
|
2128
|
-
case 58 /* UPDATE_PLAYER_MESSAGE */:
|
|
2129
|
-
await this.#onUpdatePlayerMessage(getExtension2(message, updatePlayerMessage));
|
|
2130
|
-
break;
|
|
2131
|
-
case 65 /* UPDATE_OUTPUT_DEVICE_MESSAGE */:
|
|
2132
|
-
await this.#onUpdateOutputDeviceMessage(getExtension2(message, updateOutputDeviceMessage));
|
|
2133
|
-
break;
|
|
2134
|
-
case 17 /* VOLUME_CONTROL_AVAILABILITY_MESSAGE */:
|
|
2135
|
-
await this.#onVolumeControlAvailabilityMessage(getExtension2(message, volumeControlAvailabilityMessage));
|
|
2136
|
-
break;
|
|
2137
|
-
case 64 /* VOLUME_CONTROL_CAPABILITIES_DID_CHANGE_MESSAGE */:
|
|
2138
|
-
await this.#onVolumeControlCapabilitiesDidChangeMessage(getExtension2(message, volumeControlCapabilitiesDidChangeMessage));
|
|
2139
|
-
break;
|
|
2140
|
-
case 52 /* VOLUME_DID_CHANGE_MESSAGE */:
|
|
2141
|
-
await this.#onVolumeDidChangeMessage(getExtension2(message, volumeDidChangeMessage));
|
|
2142
|
-
break;
|
|
2143
|
-
case 0 /* UNKNOWN_MESSAGE */:
|
|
2144
|
-
break;
|
|
2145
|
-
default:
|
|
2146
|
-
reporter.warn("Received unknown message.", message);
|
|
2147
|
-
break;
|
|
1886
|
+
if (message.type in this.#handlers) {
|
|
1887
|
+
const [extension, handler] = this.#handlers[message.type];
|
|
1888
|
+
handler(getExtension(message, extension));
|
|
1889
|
+
} else if (message.type !== 0 /* UNKNOWN_MESSAGE */) {
|
|
1890
|
+
this.context.logger.warn("[data]", `Unknown message type ${message.type}.`);
|
|
2148
1891
|
}
|
|
2149
1892
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
buf.writeUInt32BE(totalSize, 0);
|
|
2154
|
-
buf.write("sync", 4, "ascii");
|
|
2155
|
-
buf.fill(0, 8, 16);
|
|
2156
|
-
buf.write("comm", 16, "ascii");
|
|
2157
|
-
buf.writeBigUInt64BE(seqno, 20);
|
|
2158
|
-
buf.writeUInt32BE(0, 28);
|
|
2159
|
-
return buf;
|
|
2160
|
-
}
|
|
2161
|
-
function buildReply(seqno) {
|
|
2162
|
-
const header = Buffer.alloc(32);
|
|
2163
|
-
header.writeUInt32BE(0, 0);
|
|
2164
|
-
header.write("rply", 4, "ascii");
|
|
2165
|
-
header.fill(0, 8, 16);
|
|
2166
|
-
header.writeBigUInt64BE(seqno, 20);
|
|
2167
|
-
header.writeUInt32BE(0, 28);
|
|
2168
|
-
const plist = Buffer.from(Plist.serialize(Buffer.alloc(0)));
|
|
2169
|
-
const total = header.length + plist.length;
|
|
2170
|
-
header.writeUInt32BE(total, 0);
|
|
2171
|
-
return Buffer.concat([header, plist]);
|
|
2172
|
-
}
|
|
2173
|
-
function encodeVarint(value) {
|
|
2174
|
-
if (value < 0) {
|
|
2175
|
-
throw new RangeError("Varint only supports non-negative integers");
|
|
1893
|
+
#onDeviceInfoMessage(message) {
|
|
1894
|
+
this.context.logger.info("[data]", "Connected to device", message.name);
|
|
1895
|
+
this.emit("deviceInfo", message);
|
|
2176
1896
|
}
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
value >>>= 7;
|
|
1897
|
+
#onDeviceInfoUpdateMessage(message) {
|
|
1898
|
+
this.context.logger.info("[data]", "Device info update", message);
|
|
1899
|
+
this.emit("deviceInfoUpdate", message);
|
|
2181
1900
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
function parseHeaderSeqno(header) {
|
|
2186
|
-
if (header.length < 28) {
|
|
2187
|
-
throw new Error("Header too short");
|
|
1901
|
+
#onOriginClientPropertiesMessage(message) {
|
|
1902
|
+
this.context.logger.raw("[data]", "Origin client properties", message);
|
|
1903
|
+
this.emit("originClientProperties", message);
|
|
2188
1904
|
}
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
const messages = [];
|
|
2193
|
-
let offset = 0;
|
|
2194
|
-
while (offset < content.length) {
|
|
2195
|
-
const firstByte = content[offset];
|
|
2196
|
-
if (firstByte === 8) {
|
|
2197
|
-
const message2 = content.subarray(offset);
|
|
2198
|
-
const decoded2 = fromBinary(ProtocolMessageSchema, message2, { readUnknownFields: true });
|
|
2199
|
-
messages.push(decoded2);
|
|
2200
|
-
break;
|
|
2201
|
-
}
|
|
2202
|
-
const [length, variantLen] = readVariant(content, offset);
|
|
2203
|
-
offset += variantLen;
|
|
2204
|
-
if (offset + length > content.length) {
|
|
2205
|
-
break;
|
|
2206
|
-
}
|
|
2207
|
-
const message = content.subarray(offset, offset + length);
|
|
2208
|
-
offset += length;
|
|
2209
|
-
const decoded = fromBinary(ProtocolMessageSchema, message, { readUnknownFields: true });
|
|
2210
|
-
messages.push(decoded);
|
|
1905
|
+
#onPlayerClientPropertiesMessage(message) {
|
|
1906
|
+
this.context.logger.raw("[data]", "Player client properties", message);
|
|
1907
|
+
this.emit("playerClientProperties", message);
|
|
2211
1908
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
1909
|
+
#onRemoveClientMessage(message) {
|
|
1910
|
+
this.context.logger.info("[data]", "Remove client", message);
|
|
1911
|
+
this.emit("removeClient", message);
|
|
1912
|
+
}
|
|
1913
|
+
#onSendCommandResultMessage(message) {
|
|
1914
|
+
this.context.logger.info("[data]", "Send command result", message);
|
|
1915
|
+
this.emit("sendCommandResult", message);
|
|
1916
|
+
}
|
|
1917
|
+
#onSetArtworkMessage(message) {
|
|
1918
|
+
this.context.logger.info("[data]", "Set artwork", message);
|
|
1919
|
+
this.emit("setArtwork", message);
|
|
1920
|
+
}
|
|
1921
|
+
#onSetDefaultSupportedCommandsMessage(message) {
|
|
1922
|
+
this.context.logger.info("[data]", "Set default supported commands", message);
|
|
1923
|
+
this.emit("setDefaultSupportedCommands", message);
|
|
1924
|
+
}
|
|
1925
|
+
#onSetNowPlayingClientMessage(message) {
|
|
1926
|
+
this.context.logger.info("[data]", "Set now playing client", message);
|
|
1927
|
+
this.emit("setNowPlayingClient", message);
|
|
1928
|
+
}
|
|
1929
|
+
#onSetNowPlayingPlayerMessage(message) {
|
|
1930
|
+
this.context.logger.info("[data]", "Set now playing player", message);
|
|
1931
|
+
this.emit("setNowPlayingPlayer", message);
|
|
1932
|
+
}
|
|
1933
|
+
#onSetStateMessage(message) {
|
|
1934
|
+
this.context.logger.info("[data]", "Set state", message);
|
|
1935
|
+
this.emit("setState", message);
|
|
1936
|
+
}
|
|
1937
|
+
#onUpdateClientMessage(message) {
|
|
1938
|
+
this.context.logger.info("[data]", "Update client", message);
|
|
1939
|
+
this.emit("updateClient", message);
|
|
1940
|
+
}
|
|
1941
|
+
#onUpdateContentItemMessage(message) {
|
|
1942
|
+
this.context.logger.info("[data]", "Update content item", message);
|
|
1943
|
+
this.emit("updateContentItem", message);
|
|
1944
|
+
}
|
|
1945
|
+
#onUpdateContentItemArtworkMessage(message) {
|
|
1946
|
+
this.context.logger.info("[data]", "Update content artwork", message);
|
|
1947
|
+
this.emit("updateContentItemArtwork", message);
|
|
1948
|
+
}
|
|
1949
|
+
#onUpdatePlayerMessage(message) {
|
|
1950
|
+
this.context.logger.info("[data]", "Update player", message);
|
|
1951
|
+
this.emit("updatePlayer", message);
|
|
1952
|
+
}
|
|
1953
|
+
#onUpdateOutputDeviceMessage(message) {
|
|
1954
|
+
this.context.logger.info("[data]", "Update output device", message);
|
|
1955
|
+
this.emit("updateOutputDevice", message);
|
|
1956
|
+
}
|
|
1957
|
+
#onVolumeControlAvailabilityMessage(message) {
|
|
1958
|
+
this.context.logger.info("[data]", "Volume control availability", message);
|
|
1959
|
+
this.emit("volumeControlAvailability", message);
|
|
1960
|
+
}
|
|
1961
|
+
#onVolumeControlCapabilitiesDidChangeMessage(message) {
|
|
1962
|
+
this.context.logger.info("[data]", "Volume control capabilities did change", message);
|
|
1963
|
+
this.emit("volumeControlCapabilitiesDidChange", message);
|
|
1964
|
+
}
|
|
1965
|
+
#onVolumeDidChangeMessage(message) {
|
|
1966
|
+
this.context.logger.info("[data]", "VolumeDidChange message", message);
|
|
1967
|
+
this.emit("volumeDidChange", message);
|
|
2225
1968
|
}
|
|
2226
|
-
return [result, bytesRead];
|
|
2227
1969
|
}
|
|
2228
|
-
|
|
2229
1970
|
// src/eventStream.ts
|
|
2230
|
-
import { hkdf as hkdf2
|
|
2231
|
-
import { Plist as
|
|
2232
|
-
class
|
|
2233
|
-
#bindings;
|
|
1971
|
+
import { hkdf as hkdf2 } from "@basmilius/apple-common";
|
|
1972
|
+
import { Plist as Plist3, RTSP as RTSP2 } from "@basmilius/apple-encoding";
|
|
1973
|
+
class EventStream extends BaseStream {
|
|
2234
1974
|
#buffer = Buffer.alloc(0);
|
|
2235
|
-
constructor(address, port) {
|
|
2236
|
-
super(address, port);
|
|
2237
|
-
this.#
|
|
2238
|
-
onData: this.#onData.bind(this)
|
|
2239
|
-
};
|
|
2240
|
-
this.on("data", this.#bindings.onData);
|
|
1975
|
+
constructor(context, address, port) {
|
|
1976
|
+
super(context, address, port);
|
|
1977
|
+
this.on("data", this.#onData.bind(this));
|
|
2241
1978
|
}
|
|
2242
1979
|
async respond(response) {
|
|
2243
1980
|
const body = Buffer.from(await response.arrayBuffer());
|
|
@@ -2265,11 +2002,11 @@ class AirPlayEventStream extends AirPlayStream {
|
|
|
2265
2002
|
data = Buffer.from(headers);
|
|
2266
2003
|
}
|
|
2267
2004
|
if (this.isEncrypted) {
|
|
2268
|
-
data =
|
|
2005
|
+
data = this.encrypt(data);
|
|
2269
2006
|
}
|
|
2270
2007
|
await this.write(data);
|
|
2271
2008
|
}
|
|
2272
|
-
|
|
2009
|
+
setup(sharedSecret) {
|
|
2273
2010
|
const readKey = hkdf2({
|
|
2274
2011
|
hash: "sha512",
|
|
2275
2012
|
key: sharedSecret,
|
|
@@ -2284,14 +2021,14 @@ class AirPlayEventStream extends AirPlayStream {
|
|
|
2284
2021
|
salt: Buffer.from("Events-Salt"),
|
|
2285
2022
|
info: Buffer.from("Events-Write-Encryption-Key")
|
|
2286
2023
|
});
|
|
2287
|
-
|
|
2024
|
+
this.enableEncryption(writeKey, readKey);
|
|
2288
2025
|
}
|
|
2289
2026
|
async#handle(method, path, headers, body) {
|
|
2290
2027
|
const key = `${method} ${path}`;
|
|
2291
2028
|
switch (key) {
|
|
2292
2029
|
case "POST /command":
|
|
2293
|
-
const data =
|
|
2294
|
-
|
|
2030
|
+
const data = Plist3.parse(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength));
|
|
2031
|
+
this.context.logger.info("[event]", "Received event stream request.", data);
|
|
2295
2032
|
const response = new Response(null, {
|
|
2296
2033
|
status: 200,
|
|
2297
2034
|
statusText: "OK",
|
|
@@ -2303,224 +2040,102 @@ class AirPlayEventStream extends AirPlayStream {
|
|
|
2303
2040
|
await this.respond(response);
|
|
2304
2041
|
break;
|
|
2305
2042
|
default:
|
|
2306
|
-
|
|
2307
|
-
break;
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
async#onData(buffer) {
|
|
2311
|
-
try {
|
|
2312
|
-
this.#buffer = Buffer.concat([this.#buffer, buffer]);
|
|
2313
|
-
if (this.isEncrypted) {
|
|
2314
|
-
this.#buffer = await this.decrypt(this.#buffer);
|
|
2315
|
-
}
|
|
2316
|
-
while (this.#buffer.byteLength > 0) {
|
|
2317
|
-
const result = makeHttpRequest(this.#buffer);
|
|
2318
|
-
if (result === null) {
|
|
2319
|
-
return;
|
|
2320
|
-
}
|
|
2321
|
-
this.#buffer = this.#buffer.subarray(result.requestLength);
|
|
2322
|
-
await this.#handle(result.method, result.path, result.headers, result.body);
|
|
2323
|
-
}
|
|
2324
|
-
} catch (err) {
|
|
2325
|
-
reporter2.error("Error in event stream #onData()", err);
|
|
2326
|
-
this.emit("error", err);
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
// src/pairing.ts
|
|
2332
|
-
import { AccessoryPair } from "@basmilius/apple-common";
|
|
2333
|
-
|
|
2334
|
-
class AirPlayPairing {
|
|
2335
|
-
get internal() {
|
|
2336
|
-
return this.#internal;
|
|
2337
|
-
}
|
|
2338
|
-
get rtsp() {
|
|
2339
|
-
return this.#protocol.rtsp;
|
|
2340
|
-
}
|
|
2341
|
-
#internal;
|
|
2342
|
-
#protocol;
|
|
2343
|
-
#hkp;
|
|
2344
|
-
constructor(protocol2) {
|
|
2345
|
-
this.#internal = new AccessoryPair(this.#request.bind(this));
|
|
2346
|
-
this.#protocol = protocol2;
|
|
2347
|
-
}
|
|
2348
|
-
async start() {
|
|
2349
|
-
await this.#internal.start();
|
|
2350
|
-
}
|
|
2351
|
-
async pin(askPin) {
|
|
2352
|
-
this.#hkp = 3;
|
|
2353
|
-
await this.#pinStart();
|
|
2354
|
-
return this.#internal.pin(askPin);
|
|
2355
|
-
}
|
|
2356
|
-
async pinStart() {
|
|
2357
|
-
this.#hkp = 3;
|
|
2358
|
-
await this.#pinStart();
|
|
2359
|
-
}
|
|
2360
|
-
async transient() {
|
|
2361
|
-
this.#hkp = 4;
|
|
2362
|
-
await this.#pinStart();
|
|
2363
|
-
return this.#internal.transient();
|
|
2364
|
-
}
|
|
2365
|
-
async#pinStart() {
|
|
2366
|
-
const response = await this.rtsp.post("/pair-pin-start", null, {
|
|
2367
|
-
"Content-Type": "application/octet-stream",
|
|
2368
|
-
"X-Apple-HKP": this.#hkp.toString()
|
|
2369
|
-
});
|
|
2370
|
-
if (response.status !== 200) {
|
|
2371
|
-
throw new Error(`Cannot start pairing session. ${response.status} ${response.statusText} ${await response.text()}`);
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
async#request(_, data) {
|
|
2375
|
-
const response = await this.rtsp.post("/pair-setup", data, {
|
|
2376
|
-
"Content-Type": "application/octet-stream",
|
|
2377
|
-
"X-Apple-HKP": this.#hkp.toString()
|
|
2378
|
-
});
|
|
2379
|
-
return Buffer.from(await response.arrayBuffer());
|
|
2380
|
-
}
|
|
2381
|
-
}
|
|
2382
|
-
|
|
2383
|
-
// src/rtsp.ts
|
|
2384
|
-
import { HTTP_TIMEOUT, reporter as reporter3 } from "@basmilius/apple-common";
|
|
2385
|
-
class AirPlayRTSP extends AirPlayStream {
|
|
2386
|
-
get activeRemote() {
|
|
2387
|
-
return this.#activeRemote;
|
|
2388
|
-
}
|
|
2389
|
-
get dacpId() {
|
|
2390
|
-
return this.#dacpId;
|
|
2391
|
-
}
|
|
2392
|
-
get sessionId() {
|
|
2393
|
-
return this.#sessionId;
|
|
2394
|
-
}
|
|
2395
|
-
#activeRemote;
|
|
2396
|
-
#bindings;
|
|
2397
|
-
#dacpId;
|
|
2398
|
-
#sessionId;
|
|
2399
|
-
#buffer = Buffer.alloc(0);
|
|
2400
|
-
#cseq = 0;
|
|
2401
|
-
#requesting = false;
|
|
2402
|
-
#requestTimer;
|
|
2403
|
-
#reject;
|
|
2404
|
-
#resolve;
|
|
2405
|
-
constructor(address, port) {
|
|
2406
|
-
super(address, port);
|
|
2407
|
-
this.#activeRemote = Math.floor(Math.random() * 2 ** 32).toString(10);
|
|
2408
|
-
this.#dacpId = Math.floor(Math.random() * 2 ** 64).toString(16).toUpperCase();
|
|
2409
|
-
this.#sessionId = Math.floor(Math.random() * 2 ** 32).toString(10);
|
|
2410
|
-
this.#bindings = {
|
|
2411
|
-
onClose: this.#onClose.bind(this),
|
|
2412
|
-
onData: this.#onData.bind(this),
|
|
2413
|
-
onError: this.#onError.bind(this),
|
|
2414
|
-
onTimeout: this.#onTimeout.bind(this)
|
|
2415
|
-
};
|
|
2416
|
-
this.on("close", this.#bindings.onClose);
|
|
2417
|
-
this.on("data", this.#bindings.onData);
|
|
2418
|
-
this.on("error", this.#bindings.onError);
|
|
2419
|
-
this.on("timeout", this.#bindings.onTimeout);
|
|
2420
|
-
}
|
|
2421
|
-
async get(path, headers = {}, timeout = HTTP_TIMEOUT) {
|
|
2422
|
-
return await this.#request("GET", path, null, headers, timeout);
|
|
2423
|
-
}
|
|
2424
|
-
async post(path, body = null, headers = {}, timeout = HTTP_TIMEOUT) {
|
|
2425
|
-
return await this.#request("POST", path, body, headers, timeout);
|
|
2426
|
-
}
|
|
2427
|
-
async record(path, headers = {}, timeout = HTTP_TIMEOUT) {
|
|
2428
|
-
return await this.#request("RECORD", path, null, headers, timeout);
|
|
2429
|
-
}
|
|
2430
|
-
async setup(path, body = null, headers = {}, timeout = HTTP_TIMEOUT) {
|
|
2431
|
-
return await this.#request("SETUP", path, body, headers, timeout);
|
|
2432
|
-
}
|
|
2433
|
-
async#handle(data, err) {
|
|
2434
|
-
if (this.#requestTimer) {
|
|
2435
|
-
clearTimeout(this.#requestTimer);
|
|
2436
|
-
this.#requestTimer = undefined;
|
|
2437
|
-
}
|
|
2438
|
-
if (err) {
|
|
2439
|
-
this.#reject?.(err);
|
|
2440
|
-
} else {
|
|
2441
|
-
this.#resolve?.(data);
|
|
2442
|
-
}
|
|
2443
|
-
this.#reject = undefined;
|
|
2444
|
-
this.#resolve = undefined;
|
|
2445
|
-
this.#requesting = false;
|
|
2446
|
-
}
|
|
2447
|
-
async#request(method, path, body, headers, timeout = HTTP_TIMEOUT) {
|
|
2448
|
-
if (!this.isConnected) {
|
|
2449
|
-
return Promise.reject(new Error("Accessory not connected."));
|
|
2450
|
-
}
|
|
2451
|
-
if (this.#requesting) {
|
|
2452
|
-
return Promise.reject(new Error("Another request is currently being made."));
|
|
2453
|
-
}
|
|
2454
|
-
this.#requesting = true;
|
|
2455
|
-
headers["Active-Remote"] = this.activeRemote;
|
|
2456
|
-
headers["Client-Instance"] = this.dacpId;
|
|
2457
|
-
headers["DACP-ID"] = this.dacpId;
|
|
2458
|
-
const cseq = this.#cseq++;
|
|
2459
|
-
let data;
|
|
2460
|
-
if (body) {
|
|
2461
|
-
headers["Content-Length"] = Buffer.byteLength(body).toString();
|
|
2462
|
-
data = Buffer.concat([
|
|
2463
|
-
Buffer.from(makeHttpHeader(method, path, headers, cseq)),
|
|
2464
|
-
Buffer.from(body)
|
|
2465
|
-
]);
|
|
2466
|
-
} else {
|
|
2467
|
-
headers["Content-Length"] = "0";
|
|
2468
|
-
data = Buffer.from(makeHttpHeader(method, path, headers, cseq));
|
|
2469
|
-
}
|
|
2470
|
-
reporter3.net("[rtsp]", method, path, `cseq = ${cseq}`);
|
|
2471
|
-
if (this.isEncrypted) {
|
|
2472
|
-
data = await this.encrypt(data);
|
|
2043
|
+
this.context.logger.warn("[event]", "No handler for url", key);
|
|
2044
|
+
break;
|
|
2473
2045
|
}
|
|
2474
|
-
return new Promise(async (resolve, reject) => {
|
|
2475
|
-
this.#reject = reject;
|
|
2476
|
-
this.#resolve = resolve;
|
|
2477
|
-
this.#requestTimer = setTimeout(() => this.#handle(undefined, new Error("Request timed out")), timeout);
|
|
2478
|
-
await this.write(data).catch((err) => this.#handle(undefined, err));
|
|
2479
|
-
});
|
|
2480
|
-
}
|
|
2481
|
-
async#onClose() {
|
|
2482
|
-
this.#buffer = Buffer.alloc(0);
|
|
2483
|
-
await this.#handle(undefined, new Error("Connection closed."));
|
|
2484
2046
|
}
|
|
2485
|
-
async#onData(
|
|
2047
|
+
async#onData(data) {
|
|
2486
2048
|
try {
|
|
2487
|
-
this.#buffer = Buffer.concat([this.#buffer,
|
|
2049
|
+
this.#buffer = Buffer.concat([this.#buffer, data]);
|
|
2488
2050
|
if (this.isEncrypted) {
|
|
2489
|
-
|
|
2051
|
+
const decrypted = this.decrypt(this.#buffer);
|
|
2052
|
+
if (!decrypted) {
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
this.#buffer = decrypted;
|
|
2490
2056
|
}
|
|
2491
2057
|
while (this.#buffer.byteLength > 0) {
|
|
2492
|
-
const result =
|
|
2058
|
+
const result = RTSP2.makeRequest(this.#buffer);
|
|
2493
2059
|
if (result === null) {
|
|
2494
2060
|
return;
|
|
2495
2061
|
}
|
|
2496
|
-
this.#buffer = this.#buffer.subarray(result.
|
|
2497
|
-
await this.#handle(result.
|
|
2062
|
+
this.#buffer = this.#buffer.subarray(result.requestLength);
|
|
2063
|
+
await this.#handle(result.method, result.path, result.headers, result.body);
|
|
2498
2064
|
}
|
|
2499
2065
|
} catch (err) {
|
|
2500
|
-
|
|
2066
|
+
this.context.logger.error("[event]", "#onData()", err);
|
|
2501
2067
|
this.emit("error", err);
|
|
2502
2068
|
}
|
|
2503
2069
|
}
|
|
2504
|
-
|
|
2505
|
-
|
|
2070
|
+
}
|
|
2071
|
+
// src/protocol.ts
|
|
2072
|
+
import { AIRPLAY_SERVICE, Context, getMacAddress, randomInt64, uuid } from "@basmilius/apple-common";
|
|
2073
|
+
import { Plist as Plist4 } from "@basmilius/apple-encoding";
|
|
2074
|
+
|
|
2075
|
+
// src/pairing.ts
|
|
2076
|
+
import { AccessoryPair, AccessoryVerify, hkdf as hkdf3 } from "@basmilius/apple-common";
|
|
2077
|
+
|
|
2078
|
+
class Pairing {
|
|
2079
|
+
get controlStream() {
|
|
2080
|
+
return this.#controlStream;
|
|
2081
|
+
}
|
|
2082
|
+
get internal() {
|
|
2083
|
+
return this.#internal;
|
|
2084
|
+
}
|
|
2085
|
+
#controlStream;
|
|
2086
|
+
#internal;
|
|
2087
|
+
#hkp;
|
|
2088
|
+
constructor(protocol) {
|
|
2089
|
+
this.#controlStream = protocol.controlStream;
|
|
2090
|
+
this.#internal = new AccessoryPair(protocol.context, this.#request.bind(this));
|
|
2091
|
+
}
|
|
2092
|
+
async start() {
|
|
2093
|
+
await this.#internal.start();
|
|
2094
|
+
}
|
|
2095
|
+
async pin(askPin) {
|
|
2096
|
+
this.#hkp = 3;
|
|
2097
|
+
await this.#pinStart();
|
|
2098
|
+
return this.#internal.pin(askPin);
|
|
2099
|
+
}
|
|
2100
|
+
async pinStart() {
|
|
2101
|
+
this.#hkp = 3;
|
|
2102
|
+
await this.#pinStart();
|
|
2103
|
+
}
|
|
2104
|
+
async transient() {
|
|
2105
|
+
this.#hkp = 4;
|
|
2106
|
+
await this.#pinStart();
|
|
2107
|
+
return this.#internal.transient();
|
|
2108
|
+
}
|
|
2109
|
+
async#pinStart() {
|
|
2110
|
+
const response = await this.#controlStream.post("/pair-pin-start", null, {
|
|
2111
|
+
"Content-Type": "application/octet-stream",
|
|
2112
|
+
"X-Apple-HKP": this.#hkp.toString()
|
|
2113
|
+
});
|
|
2114
|
+
if (response.status !== 200) {
|
|
2115
|
+
throw new Error(`Cannot start pairing session. ${response.status} ${response.statusText} ${await response.text()}`);
|
|
2116
|
+
}
|
|
2506
2117
|
}
|
|
2507
|
-
async#
|
|
2508
|
-
await this.#
|
|
2118
|
+
async#request(_, data) {
|
|
2119
|
+
const response = await this.#controlStream.post("/pair-setup", data, {
|
|
2120
|
+
"Content-Type": "application/octet-stream",
|
|
2121
|
+
"X-Apple-HKP": this.#hkp.toString()
|
|
2122
|
+
});
|
|
2123
|
+
return Buffer.from(await response.arrayBuffer());
|
|
2509
2124
|
}
|
|
2510
2125
|
}
|
|
2511
2126
|
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
class AirPlayVerify {
|
|
2516
|
-
get rtsp() {
|
|
2517
|
-
return this.#protocol.rtsp;
|
|
2127
|
+
class Verify {
|
|
2128
|
+
get controlStream() {
|
|
2129
|
+
return this.#controlStream;
|
|
2518
2130
|
}
|
|
2131
|
+
get internal() {
|
|
2132
|
+
return this.#internal;
|
|
2133
|
+
}
|
|
2134
|
+
#controlStream;
|
|
2519
2135
|
#internal;
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
this.#internal = new AccessoryVerify(this.#request.bind(this));
|
|
2523
|
-
this.#protocol = protocol2;
|
|
2136
|
+
constructor(protocol) {
|
|
2137
|
+
this.#controlStream = protocol.controlStream;
|
|
2138
|
+
this.#internal = new AccessoryVerify(protocol.context, this.#request.bind(this));
|
|
2524
2139
|
}
|
|
2525
2140
|
async start(credentials) {
|
|
2526
2141
|
const keys = await this.#internal.start(credentials);
|
|
@@ -2546,7 +2161,7 @@ class AirPlayVerify {
|
|
|
2546
2161
|
};
|
|
2547
2162
|
}
|
|
2548
2163
|
async#request(_, data) {
|
|
2549
|
-
const response = await this.
|
|
2164
|
+
const response = await this.controlStream.post("/pair-verify", data, {
|
|
2550
2165
|
"Content-Type": "application/octet-stream",
|
|
2551
2166
|
"X-Apple-HKP": "3"
|
|
2552
2167
|
});
|
|
@@ -2555,79 +2170,89 @@ class AirPlayVerify {
|
|
|
2555
2170
|
}
|
|
2556
2171
|
|
|
2557
2172
|
// src/protocol.ts
|
|
2558
|
-
class
|
|
2559
|
-
get
|
|
2560
|
-
return this.#
|
|
2173
|
+
class Protocol {
|
|
2174
|
+
get context() {
|
|
2175
|
+
return this.#context;
|
|
2176
|
+
}
|
|
2177
|
+
get controlStream() {
|
|
2178
|
+
return this.#controlStream;
|
|
2561
2179
|
}
|
|
2562
2180
|
get dataStream() {
|
|
2563
2181
|
return this.#dataStream;
|
|
2564
2182
|
}
|
|
2183
|
+
get discoveryResult() {
|
|
2184
|
+
return this.#discoveryResult;
|
|
2185
|
+
}
|
|
2565
2186
|
get eventStream() {
|
|
2566
2187
|
return this.#eventStream;
|
|
2567
2188
|
}
|
|
2568
2189
|
get pairing() {
|
|
2569
2190
|
return this.#pairing;
|
|
2570
2191
|
}
|
|
2571
|
-
get rtsp() {
|
|
2572
|
-
return this.#rtsp;
|
|
2573
|
-
}
|
|
2574
2192
|
get sessionUUID() {
|
|
2575
2193
|
return this.#sessionUUID;
|
|
2576
2194
|
}
|
|
2577
2195
|
get verify() {
|
|
2578
2196
|
return this.#verify;
|
|
2579
2197
|
}
|
|
2580
|
-
#
|
|
2198
|
+
#context;
|
|
2199
|
+
#controlStream;
|
|
2200
|
+
#discoveryResult;
|
|
2581
2201
|
#pairing;
|
|
2582
|
-
#rtsp;
|
|
2583
|
-
#verify;
|
|
2584
2202
|
#sessionUUID;
|
|
2203
|
+
#verify;
|
|
2585
2204
|
#dataStream;
|
|
2586
2205
|
#eventStream;
|
|
2587
2206
|
#timingServer;
|
|
2588
|
-
constructor(
|
|
2589
|
-
this.#
|
|
2590
|
-
this.#
|
|
2591
|
-
this.#
|
|
2592
|
-
this.#
|
|
2593
|
-
this.#
|
|
2207
|
+
constructor(deviceId, discoveryResult) {
|
|
2208
|
+
this.#context = new Context(deviceId.replace(`.${AIRPLAY_SERVICE}`, "").replace(".local", ""));
|
|
2209
|
+
this.#discoveryResult = discoveryResult;
|
|
2210
|
+
this.#sessionUUID = uuid();
|
|
2211
|
+
this.#controlStream = new ControlStream(this.#context, discoveryResult.address, discoveryResult.service.port);
|
|
2212
|
+
this.#pairing = new Pairing(this);
|
|
2213
|
+
this.#verify = new Verify(this);
|
|
2594
2214
|
}
|
|
2595
2215
|
async connect() {
|
|
2596
|
-
await this.#
|
|
2216
|
+
await this.#controlStream.connect();
|
|
2217
|
+
}
|
|
2218
|
+
async destroy() {
|
|
2219
|
+
await this.#controlStream.destroy();
|
|
2597
2220
|
}
|
|
2598
2221
|
async disconnect() {
|
|
2599
|
-
await this.#
|
|
2222
|
+
await this.#dataStream.disconnect();
|
|
2600
2223
|
}
|
|
2601
2224
|
async feedback() {
|
|
2602
|
-
await this.#
|
|
2225
|
+
await this.#controlStream.post("/feedback", undefined, undefined, 1900);
|
|
2603
2226
|
}
|
|
2604
2227
|
async setupDataStream(sharedSecret, onBeforeConnect) {
|
|
2605
2228
|
const seed = randomInt64();
|
|
2606
|
-
const request =
|
|
2607
|
-
streams: [
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
}
|
|
2617
|
-
]
|
|
2229
|
+
const request = Plist4.serialize({
|
|
2230
|
+
streams: [{
|
|
2231
|
+
controlType: 2,
|
|
2232
|
+
channelID: uuid().toUpperCase(),
|
|
2233
|
+
seed,
|
|
2234
|
+
clientUUID: uuid().toUpperCase(),
|
|
2235
|
+
type: 130,
|
|
2236
|
+
wantsDedicatedSocket: true,
|
|
2237
|
+
clientTypeUUID: "1910A70F-DBC0-4242-AF95-115DB30604E1"
|
|
2238
|
+
}]
|
|
2618
2239
|
});
|
|
2619
|
-
const response = await this.#
|
|
2240
|
+
const response = await this.#controlStream.setup(`/${this.#controlStream.sessionId}`, Buffer.from(request), {
|
|
2620
2241
|
"Content-Type": "application/x-apple-binary-plist"
|
|
2621
2242
|
});
|
|
2622
|
-
|
|
2243
|
+
if (response.status !== 200) {
|
|
2244
|
+
this.context.logger.error("[protocol]", "Failed to setup data stream.", response.status, response.statusText, await response.text());
|
|
2245
|
+
throw new Error("Failed to setup data stream.");
|
|
2246
|
+
}
|
|
2247
|
+
const plist = Plist4.parse(await response.arrayBuffer());
|
|
2623
2248
|
const dataPort = plist.streams[0].dataPort & 65535;
|
|
2624
|
-
|
|
2625
|
-
this.#dataStream = new
|
|
2626
|
-
|
|
2627
|
-
|
|
2249
|
+
this.context.logger.net("[protocol]", `Connecting to data stream on port ${dataPort}...`);
|
|
2250
|
+
this.#dataStream = new DataStream(this.context, this.#controlStream.address, dataPort);
|
|
2251
|
+
this.#dataStream.setup(sharedSecret, seed);
|
|
2252
|
+
onBeforeConnect?.();
|
|
2628
2253
|
await this.#dataStream.connect();
|
|
2629
2254
|
}
|
|
2630
|
-
async setupEventStream(
|
|
2255
|
+
async setupEventStream(sharedSecret, pairingId) {
|
|
2631
2256
|
const body = {
|
|
2632
2257
|
deviceID: pairingId.toString(),
|
|
2633
2258
|
macAddress: getMacAddress().toUpperCase(),
|
|
@@ -2645,28 +2270,291 @@ class AirPlay {
|
|
|
2645
2270
|
body.timingPort = this.#timingServer.port;
|
|
2646
2271
|
body.timingProtocol = "NTP";
|
|
2647
2272
|
}
|
|
2648
|
-
const request =
|
|
2649
|
-
const response = await this.#
|
|
2273
|
+
const request = Plist4.serialize(body);
|
|
2274
|
+
const response = await this.#controlStream.setup(`/${this.#controlStream.sessionId}`, Buffer.from(request), {
|
|
2650
2275
|
"Content-Type": "application/x-apple-binary-plist"
|
|
2651
2276
|
});
|
|
2652
2277
|
if (response.status !== 200) {
|
|
2653
|
-
|
|
2654
|
-
throw new Error("
|
|
2278
|
+
this.context.logger.error("[protocol]", "Failed to setup event stream.", response.status, response.statusText, await response.text());
|
|
2279
|
+
throw new Error("Failed to setup event stream.");
|
|
2655
2280
|
}
|
|
2656
|
-
const plist =
|
|
2281
|
+
const plist = Plist4.parse(await response.arrayBuffer());
|
|
2657
2282
|
const eventPort = plist.eventPort & 65535;
|
|
2658
|
-
|
|
2659
|
-
this.#eventStream = new
|
|
2660
|
-
|
|
2283
|
+
this.context.logger.net("[protocol]", `Connecting to event stream on port ${eventPort}...`);
|
|
2284
|
+
this.#eventStream = new EventStream(this.#context, this.#controlStream.address, eventPort);
|
|
2285
|
+
this.#eventStream.setup(sharedSecret);
|
|
2661
2286
|
await this.#eventStream.connect();
|
|
2662
|
-
await this.#
|
|
2287
|
+
await this.#controlStream.record(`/${this.#controlStream.sessionId}`);
|
|
2663
2288
|
}
|
|
2664
|
-
|
|
2665
|
-
this.#timingServer =
|
|
2289
|
+
useTimingServer(timingServer) {
|
|
2290
|
+
this.#timingServer = timingServer;
|
|
2666
2291
|
}
|
|
2667
2292
|
}
|
|
2293
|
+
// src/dataStreamMessages.ts
|
|
2294
|
+
var exports_dataStreamMessages = {};
|
|
2295
|
+
__export(exports_dataStreamMessages, {
|
|
2296
|
+
wakeDevice: () => wakeDevice,
|
|
2297
|
+
setVolumeMuted: () => setVolumeMuted,
|
|
2298
|
+
setVolume: () => setVolume,
|
|
2299
|
+
setReadyState: () => setReadyState,
|
|
2300
|
+
setConnectionState: () => setConnectionState,
|
|
2301
|
+
sendHIDEvent: () => sendHIDEvent,
|
|
2302
|
+
sendCommand: () => sendCommand,
|
|
2303
|
+
sendButtonEvent: () => sendButtonEvent,
|
|
2304
|
+
protocol: () => protocol,
|
|
2305
|
+
playbackQueueRequest: () => playbackQueueRequest,
|
|
2306
|
+
notification: () => notification,
|
|
2307
|
+
getVolumeMuted: () => getVolumeMuted,
|
|
2308
|
+
getVolume: () => getVolume,
|
|
2309
|
+
getState: () => getState,
|
|
2310
|
+
getExtension: () => getExtension2,
|
|
2311
|
+
deviceInfo: () => deviceInfo,
|
|
2312
|
+
configureConnection: () => configureConnection,
|
|
2313
|
+
clientUpdatesConfig: () => clientUpdatesConfig
|
|
2314
|
+
});
|
|
2315
|
+
import { uint16ToBE, uuid as uuid2 } from "@basmilius/apple-common";
|
|
2316
|
+
import { create, getExtension as _getExtension, setExtension } from "@bufbuild/protobuf";
|
|
2317
|
+
function protocol(type, errorCode = 0 /* NoError */) {
|
|
2318
|
+
return create(ProtocolMessageSchema, {
|
|
2319
|
+
type,
|
|
2320
|
+
errorCode,
|
|
2321
|
+
identifier: uuid2().toUpperCase(),
|
|
2322
|
+
uniqueIdentifier: uuid2().toUpperCase()
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
function clientUpdatesConfig(artworkUpdates = true, nowPlayingUpdates = true, volumeUpdates = true, keyboardUpdates = false, outputDeviceUpdates = true, systemEndpointUpdates = true) {
|
|
2326
|
+
const protocolMessage = protocol(16 /* CLIENT_UPDATES_CONFIG_MESSAGE */);
|
|
2327
|
+
const message = create(ClientUpdatesConfigMessageSchema, {
|
|
2328
|
+
artworkUpdates,
|
|
2329
|
+
nowPlayingUpdates,
|
|
2330
|
+
volumeUpdates,
|
|
2331
|
+
keyboardUpdates,
|
|
2332
|
+
outputDeviceUpdates,
|
|
2333
|
+
systemEndpointUpdates
|
|
2334
|
+
});
|
|
2335
|
+
setExtension(protocolMessage, clientUpdatesConfigMessage, message);
|
|
2336
|
+
return [
|
|
2337
|
+
protocolMessage,
|
|
2338
|
+
clientUpdatesConfigMessage
|
|
2339
|
+
];
|
|
2340
|
+
}
|
|
2341
|
+
function configureConnection(groupId) {
|
|
2342
|
+
const protocolMessage = protocol(120 /* CONFIGURE_CONNECTION_MESSAGE */);
|
|
2343
|
+
const message = create(ConfigureConnectionMessageSchema, {
|
|
2344
|
+
groupID: groupId
|
|
2345
|
+
});
|
|
2346
|
+
setExtension(protocolMessage, configureConnectionMessage, message);
|
|
2347
|
+
return [
|
|
2348
|
+
protocolMessage,
|
|
2349
|
+
configureConnectionMessage
|
|
2350
|
+
];
|
|
2351
|
+
}
|
|
2352
|
+
function deviceInfo(pairingId) {
|
|
2353
|
+
const protocolMessage = protocol(15 /* DEVICE_INFO_MESSAGE */);
|
|
2354
|
+
const message = create(DeviceInfoMessageSchema, {
|
|
2355
|
+
uniqueIdentifier: pairingId.toString(),
|
|
2356
|
+
name: "iPhone van Bas",
|
|
2357
|
+
localizedModelName: "iPhone",
|
|
2358
|
+
systemBuildVersion: "18C66",
|
|
2359
|
+
applicationBundleIdentifier: "com.apple.TVRemote",
|
|
2360
|
+
applicationBundleVersion: "344.28",
|
|
2361
|
+
protocolVersion: 1,
|
|
2362
|
+
lastSupportedMessageType: 129,
|
|
2363
|
+
supportsSystemPairing: true,
|
|
2364
|
+
allowsPairing: true,
|
|
2365
|
+
systemMediaApplication: "com.apple.TVMusic",
|
|
2366
|
+
supportsACL: true,
|
|
2367
|
+
supportsSharedQueue: true,
|
|
2368
|
+
supportsExtendedMotion: true,
|
|
2369
|
+
sharedQueueVersion: 2,
|
|
2370
|
+
deviceClass: 1 /* iPhone */,
|
|
2371
|
+
logicalDeviceCount: 1
|
|
2372
|
+
});
|
|
2373
|
+
setExtension(protocolMessage, deviceInfoMessage, message);
|
|
2374
|
+
return [
|
|
2375
|
+
protocolMessage,
|
|
2376
|
+
deviceInfoMessage
|
|
2377
|
+
];
|
|
2378
|
+
}
|
|
2379
|
+
function getState() {
|
|
2380
|
+
const protocolMessage = protocol(3 /* GET_STATE_MESSAGE */);
|
|
2381
|
+
const message = create(GetStateMessageSchema, {});
|
|
2382
|
+
setExtension(protocolMessage, getStateMessage, message);
|
|
2383
|
+
return [
|
|
2384
|
+
protocolMessage,
|
|
2385
|
+
getStateMessage
|
|
2386
|
+
];
|
|
2387
|
+
}
|
|
2388
|
+
function getVolume(outputDeviceUID) {
|
|
2389
|
+
const protocolMessage = protocol(49 /* GET_VOLUME_MESSAGE */);
|
|
2390
|
+
const message = create(GetVolumeMessageSchema, {
|
|
2391
|
+
outputDeviceUID
|
|
2392
|
+
});
|
|
2393
|
+
setExtension(protocolMessage, getVolumeMessage, message);
|
|
2394
|
+
return [
|
|
2395
|
+
protocolMessage,
|
|
2396
|
+
getVolumeMessage
|
|
2397
|
+
];
|
|
2398
|
+
}
|
|
2399
|
+
function getVolumeMuted(outputDeviceUID) {
|
|
2400
|
+
const protocolMessage = protocol(126 /* GET_VOLUME_MUTED_MESSAGE */);
|
|
2401
|
+
const message = create(GetVolumeMutedMessageSchema, {
|
|
2402
|
+
outputDeviceUID
|
|
2403
|
+
});
|
|
2404
|
+
setExtension(protocolMessage, getVolumeMutedMessage, message);
|
|
2405
|
+
return [
|
|
2406
|
+
protocolMessage,
|
|
2407
|
+
getVolumeMutedMessage
|
|
2408
|
+
];
|
|
2409
|
+
}
|
|
2410
|
+
function notification(notification2) {
|
|
2411
|
+
const protocolMessage = protocol(11 /* NOTIFICATION_MESSAGE */);
|
|
2412
|
+
const message = create(NotificationMessageSchema, {
|
|
2413
|
+
notification: notification2
|
|
2414
|
+
});
|
|
2415
|
+
setExtension(protocolMessage, notificationMessage, message);
|
|
2416
|
+
return [
|
|
2417
|
+
protocolMessage,
|
|
2418
|
+
notificationMessage
|
|
2419
|
+
];
|
|
2420
|
+
}
|
|
2421
|
+
function playbackQueueRequest(location, length, includeMetadata = true, includeLanguageOptions = false) {
|
|
2422
|
+
const protocolMessage = protocol(32 /* PLAYBACK_QUEUE_REQUEST_MESSAGE */);
|
|
2423
|
+
const message = create(PlaybackQueueRequestMessageSchema, {
|
|
2424
|
+
location,
|
|
2425
|
+
length,
|
|
2426
|
+
includeMetadata,
|
|
2427
|
+
includeLanguageOptions,
|
|
2428
|
+
artworkHeight: 600,
|
|
2429
|
+
artworkWidth: 600,
|
|
2430
|
+
includeInfo: true,
|
|
2431
|
+
includeLyrics: false,
|
|
2432
|
+
includeSections: false,
|
|
2433
|
+
includeAlignments: false,
|
|
2434
|
+
includeAvailableArtworkFormats: true,
|
|
2435
|
+
includeParticipants: false,
|
|
2436
|
+
isLegacyNowPlayingInfoRequest: false,
|
|
2437
|
+
returnContentItemAssetsInUserCompletion: true,
|
|
2438
|
+
requestedAnimatedArtworkAssetURLFormats: [
|
|
2439
|
+
"MRContentItemAnimatedArtworkFormatSquare",
|
|
2440
|
+
"MRContentItemAnimatedArtworkFormatTall"
|
|
2441
|
+
]
|
|
2442
|
+
});
|
|
2443
|
+
setExtension(protocolMessage, playbackQueueRequestMessage, message);
|
|
2444
|
+
return [
|
|
2445
|
+
protocolMessage,
|
|
2446
|
+
playbackQueueRequestMessage
|
|
2447
|
+
];
|
|
2448
|
+
}
|
|
2449
|
+
function sendButtonEvent(usagePage, usage, buttonDown) {
|
|
2450
|
+
const protocolMessage = protocol(39 /* SEND_BUTTON_EVENT_MESSAGE */);
|
|
2451
|
+
const message = create(SendButtonEventMessageSchema, {
|
|
2452
|
+
usagePage,
|
|
2453
|
+
usage,
|
|
2454
|
+
buttonDown
|
|
2455
|
+
});
|
|
2456
|
+
setExtension(protocolMessage, sendButtonEventMessage, message);
|
|
2457
|
+
return [
|
|
2458
|
+
protocolMessage,
|
|
2459
|
+
sendButtonEventMessage
|
|
2460
|
+
];
|
|
2461
|
+
}
|
|
2462
|
+
function sendCommand(command, options) {
|
|
2463
|
+
const protocolMessage = protocol(1 /* SEND_COMMAND_MESSAGE */);
|
|
2464
|
+
const message = create(SendCommandMessageSchema, {
|
|
2465
|
+
command,
|
|
2466
|
+
options
|
|
2467
|
+
});
|
|
2468
|
+
setExtension(protocolMessage, sendCommandMessage, message);
|
|
2469
|
+
return [
|
|
2470
|
+
protocolMessage,
|
|
2471
|
+
sendCommandMessage
|
|
2472
|
+
];
|
|
2473
|
+
}
|
|
2474
|
+
function sendHIDEvent(usePage, usage, down) {
|
|
2475
|
+
const protocolMessage = protocol(8 /* SEND_HID_EVENT_MESSAGE */);
|
|
2476
|
+
const message = create(SendHIDEventMessageSchema, {
|
|
2477
|
+
hidEventData: Buffer.concat([
|
|
2478
|
+
Buffer.from("438922cf08020000", "hex"),
|
|
2479
|
+
Buffer.from("00000000000000000100000000000000020" + "00000200000000300000001000000000000", "hex"),
|
|
2480
|
+
Buffer.concat([
|
|
2481
|
+
uint16ToBE(usePage),
|
|
2482
|
+
uint16ToBE(usage),
|
|
2483
|
+
uint16ToBE(down ? 1 : 0)
|
|
2484
|
+
]),
|
|
2485
|
+
Buffer.from("0000000000000001000000", "hex")
|
|
2486
|
+
])
|
|
2487
|
+
});
|
|
2488
|
+
setExtension(protocolMessage, sendHIDEventMessage, message);
|
|
2489
|
+
return [
|
|
2490
|
+
protocolMessage,
|
|
2491
|
+
sendHIDEventMessage
|
|
2492
|
+
];
|
|
2493
|
+
}
|
|
2494
|
+
function setConnectionState(state = 2 /* Connected */) {
|
|
2495
|
+
const protocolMessage = protocol(38 /* SET_CONNECTION_STATE_MESSAGE */);
|
|
2496
|
+
const message = create(SetConnectionStateMessageSchema, {
|
|
2497
|
+
state
|
|
2498
|
+
});
|
|
2499
|
+
setExtension(protocolMessage, setConnectionStateMessage, message);
|
|
2500
|
+
return [
|
|
2501
|
+
protocolMessage,
|
|
2502
|
+
setConnectionStateMessage
|
|
2503
|
+
];
|
|
2504
|
+
}
|
|
2505
|
+
function setReadyState() {
|
|
2506
|
+
const protocolMessage = protocol(36 /* SET_READY_STATE_MESSAGE */);
|
|
2507
|
+
const message = create(SetReadyStateMessageSchema, {});
|
|
2508
|
+
setExtension(protocolMessage, readyStateMessage, message);
|
|
2509
|
+
return [
|
|
2510
|
+
protocolMessage,
|
|
2511
|
+
readyStateMessage
|
|
2512
|
+
];
|
|
2513
|
+
}
|
|
2514
|
+
function setVolume(outputDeviceUID, volume) {
|
|
2515
|
+
const protocolMessage = protocol(51 /* SET_VOLUME_MESSAGE */);
|
|
2516
|
+
const message = create(SetVolumeMessageSchema, {
|
|
2517
|
+
outputDeviceUID,
|
|
2518
|
+
volume
|
|
2519
|
+
});
|
|
2520
|
+
setExtension(protocolMessage, setVolumeMessage, message);
|
|
2521
|
+
return [
|
|
2522
|
+
protocolMessage,
|
|
2523
|
+
setVolumeMessage
|
|
2524
|
+
];
|
|
2525
|
+
}
|
|
2526
|
+
function setVolumeMuted(outputDeviceUID, isMuted) {
|
|
2527
|
+
const protocolMessage = protocol(128 /* SET_VOLUME_MUTED_MESSAGE */);
|
|
2528
|
+
const message = create(SetVolumeMutedMessageSchema, {
|
|
2529
|
+
outputDeviceUID,
|
|
2530
|
+
isMuted
|
|
2531
|
+
});
|
|
2532
|
+
setExtension(protocolMessage, setVolumeMutedMessage, message);
|
|
2533
|
+
return [
|
|
2534
|
+
protocolMessage,
|
|
2535
|
+
setVolumeMutedMessage
|
|
2536
|
+
];
|
|
2537
|
+
}
|
|
2538
|
+
function wakeDevice() {
|
|
2539
|
+
const protocolMessage = protocol(41 /* WAKE_DEVICE_MESSAGE */);
|
|
2540
|
+
const message = create(WakeDeviceMessageSchema, {});
|
|
2541
|
+
setExtension(protocolMessage, wakeDeviceMessage, message);
|
|
2542
|
+
return [
|
|
2543
|
+
protocolMessage,
|
|
2544
|
+
wakeDeviceMessage
|
|
2545
|
+
];
|
|
2546
|
+
}
|
|
2547
|
+
function getExtension2(message, extension) {
|
|
2548
|
+
return _getExtension(message, extension);
|
|
2549
|
+
}
|
|
2668
2550
|
export {
|
|
2551
|
+
Verify,
|
|
2552
|
+
Protocol,
|
|
2669
2553
|
exports_proto as Proto,
|
|
2554
|
+
Pairing,
|
|
2555
|
+
EventStream,
|
|
2670
2556
|
exports_dataStreamMessages as DataStreamMessage,
|
|
2671
|
-
|
|
2557
|
+
DataStream,
|
|
2558
|
+
ControlStream,
|
|
2559
|
+
BaseStream
|
|
2672
2560
|
};
|