openairplay2 0.3.0

Embeddable AirPlay 2 audio receiver library (network in, PCM + events out)
Documentation
//! FairPlay `fp-setup` — a canned compatibility handshake.
//!
//! A stock Apple sender requires `POST /fp-setup` after pairing, before it
//! will send `SETUP`. This is not live crypto: the receiver replies with fixed
//! tables. The tables are well-known reverse-engineered FairPlay interop
//! constants (Apple-derived); they circulate across receivers under differing
//! licenses (shairport-sync is MIT; the openairplay Python receiver's FairPlay
//! code is GPLv2). See `notes/licensing.md` for the licensing/DMCA caveats.
//!
//! Requests are `application/octet-stream` beginning with `FPLY`
//! (`46 50 4c 59`):
//! - byte 4 = version (3), byte 5 = type (1), byte 6 = sequence, byte 14 =
//!   mode (phase 1 only).
//! - Phase 1 (seq 1, 16-byte request): reply with the 142-byte table for the
//!   mode byte (0..=3).
//! - Phase 2 (seq 3, 164-byte request): reply with a 12-byte header followed
//!   by the request's last 20 bytes (32 bytes total).

const MAGIC: &[u8; 4] = b"FPLY";
const VERSION_POS: usize = 4;
const TYPE_POS: usize = 5;
const SEQ_POS: usize = 6;
const MODE_POS: usize = 14;
const SETUP_TYPE: u8 = 1;
const SETUP1_SEQ: u8 = 1;
const SETUP2_SEQ: u8 = 3;
const SETUP2_SUFFIX_LEN: usize = 20;

const REPLY0: [u8; 142] = [
    0x46, 0x50, 0x4c, 0x59, 0x03, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x82, 0x02, 0x00, 0x0f, 0x9f,
    0x3f, 0x9e, 0x0a, 0x25, 0x21, 0xdb, 0xdf, 0x31, 0x2a, 0xb2, 0xbf, 0xb2, 0x9e, 0x8d, 0x23, 0x2b,
    0x63, 0x76, 0xa8, 0xc8, 0x18, 0x70, 0x1d, 0x22, 0xae, 0x93, 0xd8, 0x27, 0x37, 0xfe, 0xaf, 0x9d,
    0xb4, 0xfd, 0xf4, 0x1c, 0x2d, 0xba, 0x9d, 0x1f, 0x49, 0xca, 0xaa, 0xbf, 0x65, 0x91, 0xac, 0x1f,
    0x7b, 0xc6, 0xf7, 0xe0, 0x66, 0x3d, 0x21, 0xaf, 0xe0, 0x15, 0x65, 0x95, 0x3e, 0xab, 0x81, 0xf4,
    0x18, 0xce, 0xed, 0x09, 0x5a, 0xdb, 0x7c, 0x3d, 0x0e, 0x25, 0x49, 0x09, 0xa7, 0x98, 0x31, 0xd4,
    0x9c, 0x39, 0x82, 0x97, 0x34, 0x34, 0xfa, 0xcb, 0x42, 0xc6, 0x3a, 0x1c, 0xd9, 0x11, 0xa6, 0xfe,
    0x94, 0x1a, 0x8a, 0x6d, 0x4a, 0x74, 0x3b, 0x46, 0xc3, 0xa7, 0x64, 0x9e, 0x44, 0xc7, 0x89, 0x55,
    0xe4, 0x9d, 0x81, 0x55, 0x00, 0x95, 0x49, 0xc4, 0xe2, 0xf7, 0xa3, 0xf6, 0xd5, 0xba,
];

const REPLY1: [u8; 142] = [
    0x46, 0x50, 0x4c, 0x59, 0x03, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x82, 0x02, 0x01, 0xcf, 0x32,
    0xa2, 0x57, 0x14, 0xb2, 0x52, 0x4f, 0x8a, 0xa0, 0xad, 0x7a, 0xf1, 0x64, 0xe3, 0x7b, 0xcf, 0x44,
    0x24, 0xe2, 0x00, 0x04, 0x7e, 0xfc, 0x0a, 0xd6, 0x7a, 0xfc, 0xd9, 0x5d, 0xed, 0x1c, 0x27, 0x30,
    0xbb, 0x59, 0x1b, 0x96, 0x2e, 0xd6, 0x3a, 0x9c, 0x4d, 0xed, 0x88, 0xba, 0x8f, 0xc7, 0x8d, 0xe6,
    0x4d, 0x91, 0xcc, 0xfd, 0x5c, 0x7b, 0x56, 0xda, 0x88, 0xe3, 0x1f, 0x5c, 0xce, 0xaf, 0xc7, 0x43,
    0x19, 0x95, 0xa0, 0x16, 0x65, 0xa5, 0x4e, 0x19, 0x39, 0xd2, 0x5b, 0x94, 0xdb, 0x64, 0xb9, 0xe4,
    0x5d, 0x8d, 0x06, 0x3e, 0x1e, 0x6a, 0xf0, 0x7e, 0x96, 0x56, 0x16, 0x2b, 0x0e, 0xfa, 0x40, 0x42,
    0x75, 0xea, 0x5a, 0x44, 0xd9, 0x59, 0x1c, 0x72, 0x56, 0xb9, 0xfb, 0xe6, 0x51, 0x38, 0x98, 0xb8,
    0x02, 0x27, 0x72, 0x19, 0x88, 0x57, 0x16, 0x50, 0x94, 0x2a, 0xd9, 0x46, 0x68, 0x8a,
];

const REPLY2: [u8; 142] = [
    0x46, 0x50, 0x4c, 0x59, 0x03, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x82, 0x02, 0x02, 0xc1, 0x69,
    0xa3, 0x52, 0xee, 0xed, 0x35, 0xb1, 0x8c, 0xdd, 0x9c, 0x58, 0xd6, 0x4f, 0x16, 0xc1, 0x51, 0x9a,
    0x89, 0xeb, 0x53, 0x17, 0xbd, 0x0d, 0x43, 0x36, 0xcd, 0x68, 0xf6, 0x38, 0xff, 0x9d, 0x01, 0x6a,
    0x5b, 0x52, 0xb7, 0xfa, 0x92, 0x16, 0xb2, 0xb6, 0x54, 0x82, 0xc7, 0x84, 0x44, 0x11, 0x81, 0x21,
    0xa2, 0xc7, 0xfe, 0xd8, 0x3d, 0xb7, 0x11, 0x9e, 0x91, 0x82, 0xaa, 0xd7, 0xd1, 0x8c, 0x70, 0x63,
    0xe2, 0xa4, 0x57, 0x55, 0x59, 0x10, 0xaf, 0x9e, 0x0e, 0xfc, 0x76, 0x34, 0x7d, 0x16, 0x40, 0x43,
    0x80, 0x7f, 0x58, 0x1e, 0xe4, 0xfb, 0xe4, 0x2c, 0xa9, 0xde, 0xdc, 0x1b, 0x5e, 0xb2, 0xa3, 0xaa,
    0x3d, 0x2e, 0xcd, 0x59, 0xe7, 0xee, 0xe7, 0x0b, 0x36, 0x29, 0xf2, 0x2a, 0xfd, 0x16, 0x1d, 0x87,
    0x73, 0x53, 0xdd, 0xb9, 0x9a, 0xdc, 0x8e, 0x07, 0x00, 0x6e, 0x56, 0xf8, 0x50, 0xce,
];

const REPLY3: [u8; 142] = [
    0x46, 0x50, 0x4c, 0x59, 0x03, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x82, 0x02, 0x03, 0x90, 0x01,
    0xe1, 0x72, 0x7e, 0x0f, 0x57, 0xf9, 0xf5, 0x88, 0x0d, 0xb1, 0x04, 0xa6, 0x25, 0x7a, 0x23, 0xf5,
    0xcf, 0xff, 0x1a, 0xbb, 0xe1, 0xe9, 0x30, 0x45, 0x25, 0x1a, 0xfb, 0x97, 0xeb, 0x9f, 0xc0, 0x01,
    0x1e, 0xbe, 0x0f, 0x3a, 0x81, 0xdf, 0x5b, 0x69, 0x1d, 0x76, 0xac, 0xb2, 0xf7, 0xa5, 0xc7, 0x08,
    0xe3, 0xd3, 0x28, 0xf5, 0x6b, 0xb3, 0x9d, 0xbd, 0xe5, 0xf2, 0x9c, 0x8a, 0x17, 0xf4, 0x81, 0x48,
    0x7e, 0x3a, 0xe8, 0x63, 0xc6, 0x78, 0x32, 0x54, 0x22, 0xe6, 0xf7, 0x8e, 0x16, 0x6d, 0x18, 0xaa,
    0x7f, 0xd6, 0x36, 0x25, 0x8b, 0xce, 0x28, 0x72, 0x6f, 0x66, 0x1f, 0x73, 0x88, 0x93, 0xce, 0x44,
    0x31, 0x1e, 0x4b, 0xe6, 0xc0, 0x53, 0x51, 0x93, 0xe5, 0xef, 0x72, 0xe8, 0x68, 0x62, 0x33, 0x72,
    0x9c, 0x22, 0x7d, 0x82, 0x0c, 0x99, 0x94, 0x45, 0xd8, 0x92, 0x46, 0xc8, 0xc3, 0x59,
];

const HEADER: [u8; 12] = [
    0x46, 0x50, 0x4c, 0x59, 0x03, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00, 0x14,
];

/// Compute the `fp-setup` reply for a `FPLY` request, or `None` if the request
/// is malformed / unsupported.
pub fn fp_setup(request: &[u8]) -> Option<Vec<u8>> {
    if request.len() <= MODE_POS
        || &request[0..4] != MAGIC
        || request[VERSION_POS] != 3
        || request[TYPE_POS] != SETUP_TYPE
    {
        return None;
    }
    match request[SEQ_POS] {
        SETUP1_SEQ => {
            let table: &[u8; 142] = match request[MODE_POS] {
                0 => &REPLY0,
                1 => &REPLY1,
                2 => &REPLY2,
                3 => &REPLY3,
                _ => return None,
            };
            Some(table.to_vec())
        }
        SETUP2_SEQ => {
            if request.len() < SETUP2_SUFFIX_LEN {
                return None;
            }
            let mut reply = HEADER.to_vec();
            reply.extend_from_slice(&request[request.len() - SETUP2_SUFFIX_LEN..]);
            Some(reply)
        }
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn phase1(mode: u8) -> Vec<u8> {
        // 16-byte FPLY phase-1 request with the given mode byte.
        let mut req = vec![0u8; 16];
        req[0..4].copy_from_slice(MAGIC);
        req[VERSION_POS] = 3;
        req[TYPE_POS] = 1;
        req[SEQ_POS] = 1;
        req[MODE_POS] = mode;
        req
    }

    #[test]
    fn phase1_returns_table_for_each_mode() {
        assert_eq!(fp_setup(&phase1(0)).unwrap(), REPLY0.to_vec());
        assert_eq!(fp_setup(&phase1(1)).unwrap(), REPLY1.to_vec());
        assert_eq!(fp_setup(&phase1(2)).unwrap(), REPLY2.to_vec());
        assert_eq!(fp_setup(&phase1(3)).unwrap(), REPLY3.to_vec());
        assert_eq!(fp_setup(&phase1(0)).unwrap().len(), 142);
    }

    #[test]
    fn matches_captured_mac_request() {
        // The exact phase-1 request a real macOS sender sent (mode 1).
        let req = hex_literal(b"46504c590301010000000004020001bb");
        assert_eq!(req.len(), 16);
        assert_eq!(fp_setup(&req).unwrap(), REPLY1.to_vec());
    }

    #[test]
    fn phase2_echoes_last_20_bytes() {
        let mut req = vec![0u8; 164];
        req[0..4].copy_from_slice(MAGIC);
        req[VERSION_POS] = 3;
        req[TYPE_POS] = 1;
        req[SEQ_POS] = 3;
        for (i, b) in req.iter_mut().enumerate().skip(144) {
            *b = i as u8; // last 20 bytes are 144..164
        }
        let reply = fp_setup(&req).unwrap();
        assert_eq!(reply.len(), 32);
        assert_eq!(&reply[0..12], &HEADER);
        assert_eq!(&reply[12..32], &req[144..164]);
    }

    #[test]
    fn rejects_non_fply_and_short() {
        assert!(fp_setup(b"NOPE").is_none());
        assert!(fp_setup(&[]).is_none());
        assert!(fp_setup(&phase1(9)).is_none()); // unknown mode
    }

    fn hex_literal(h: &[u8]) -> Vec<u8> {
        // Parse an ASCII-hex byte string in tests.
        h.chunks(2)
            .map(|c| u8::from_str_radix(std::str::from_utf8(c).unwrap(), 16).unwrap())
            .collect()
    }
}