1919from biplist import readPlistFromString , writePlistToString
2020
2121from ap2 .connections .audio import RTPBuffer
22- from ap2 .playfair import PlayFair
22+ from ap2 .playfair import PlayFair , FPAES
2323from ap2 .utils import get_volume , set_volume , set_volume_pid
2424from ap2 .pairing .hap import Hap , HAPSocket
2525from ap2 .connections .event import Event
2626from ap2 .connections .stream import Stream
2727from ap2 .dxxp import parse_dxxp
28- from enum import IntFlag
28+ from enum import IntFlag , Enum
29+ from ap2 .connections .ntp_time import NTP
2930
3031
3132"""
@@ -216,6 +217,8 @@ def get_pub_bytes(self):
216217HTTP_X_A_HKP = "X-Apple-HKP"
217218HTTP_X_A_CN = "X-Apple-Client-Name"
218219HTTP_X_A_PD = "X-Apple-PD"
220+ HTTP_X_A_AT = "X-Apple-AbsoluteTime" # Unix timestamp for current system date/time.
221+ HTTP_X_A_ET = "X-Apple-ET" # Encryption Type
219222LTPK = LTPK ()
220223
221224
@@ -342,9 +345,139 @@ def setup_global_structs(args):
342345 }
343346
344347
345- class AP2Handler (http .server .BaseHTTPRequestHandler ):
348+ class SDPHandler ():
349+ # systemcrash 2021
350+ class SDPAudioFormat (Enum ):
351+ (
352+ UNSUPPORTED ,
353+ PCM ,
354+ ALAC ,
355+ AAC ,
356+ AAC_ELD ,
357+ OPUS ,
358+ ) = range (6 )
359+
360+ def __init__ (self , sdp = '' ):
361+ from ap2 .connections .audio import AirplayAudFmt
362+
363+ self .sdp = sdp .splitlines ()
364+ self .has_mfi = False
365+ self .has_rsa = False
366+ self .has_fp = False
367+ self .last_media = ''
368+ self .has_audio = False
369+ self .has_video = False
370+ self .audio_format = self .SDPAudioFormat .UNSUPPORTED
371+ self .minlatency = 11025
372+ self .maxlatency = 11025
373+ self .spf = 0
374+ for k in self .sdp :
375+ if 'v=' in k :
376+ self .ver_line = k
377+ elif 'o=' in k :
378+ self .o_line = k
379+ elif 's=' in k :
380+ self .subj_line = k
381+ elif 'c=' in k :
382+ self .conn_line = k
383+ elif 't=' in k :
384+ self .t_line = k
385+ elif 'm=audio' in k :
386+ self .has_audio = True
387+ self .last_media = 'audio'
388+ self .m_aud_line = k
389+ start = self .m_aud_line .find ('AVP ' ) + 4
390+ self .audio_media_type = int (self .m_aud_line [start :])
391+ elif 'a=rtpmap:' in k and self .last_media == 'audio' :
392+ self .audio_rtpmap = k .split (':' )[1 ]
393+ start = self .audio_rtpmap .find (':' ) + 1
394+ mid = self .audio_rtpmap .find (' ' ) + 1
395+ self .payload_type = self .audio_rtpmap [start :mid - 1 ] # coerce to int later
396+ self .audio_encoding = self .audio_rtpmap [mid :]
397+ if self .audio_encoding == 'AppleLossless' :
398+ self .audio_format = self .SDPAudioFormat .ALAC
399+ elif 'mpeg4-generic/' in self .audio_encoding :
400+ self .audio_format = self .SDPAudioFormat .AAC
401+ discard , self .audio_format_sr , self .audio_format_ch = self .audio_encoding .split ('/' )
402+ self .audio_format_bd = 16
403+ else :
404+ self .audio_format = self .SDPAudioFormat .PCM
405+ self .audio_format_bd , self .audio_format_sr , self .audio_format_ch = self .audio_encoding .split ('/' )
406+ self .audio_format_bd = '' .join (filter (str .isdigit , self .audio_format_bd ))
407+ elif 'a=fmtp:' in k and self .payload_type in k :
408+ self .audio_fmtp = k .split (':' )[1 ]
409+ self .audio_format_params = self .audio_fmtp .split (' ' )
410+ if self .audio_format == self .SDPAudioFormat .ALAC :
411+ self .spf = self .audio_format_params [1 ] # samples per frame
412+ self .audio_format_bd = self .audio_format_params [3 ]
413+ self .audio_format_ch = self .audio_format_params [7 ]
414+ self .audio_format_sr = self .audio_format_params [11 ]
415+ self .audio_desc = 'ALAC'
416+ elif self .audio_format == self .SDPAudioFormat .AAC :
417+ self .audio_desc = 'AAC_LC'
418+ elif self .audio_format == self .SDPAudioFormat .PCM :
419+ self .audio_desc = 'PCM'
420+ elif self .audio_format == self .SDPAudioFormat .OPUS :
421+ self .audio_desc = 'OPUS'
422+ if 'mode=' in self .audio_fmtp :
423+ self .audio_format = self .SDPAudioFormat .AAC_ELD
424+ for x in self .audio_format_params :
425+ if 'constantDuration=' in x :
426+ start = x .find ('constantDuration=' ) + len ('constantDuration=' )
427+ self .constantDuration = int (x [start :].rstrip (';' ))
428+ self .spf = self .constantDuration
429+ elif 'mode=' in x :
430+ start = x .find ('mode=' ) + len ('mode=' )
431+ self .aac_mode = x [start :].rstrip (';' )
432+ self .audio_desc = 'AAC_ELD'
433+ for f in AirplayAudFmt :
434+ if (self .audio_desc in f .name
435+ and self .audio_format_bd in f .name
436+ and self .audio_format_sr in f .name
437+ and self .audio_format_ch in f .name
438+ ):
439+ self .AirplayAudFmt = f .value
440+ self .audio_format_bd = int (self .audio_format_bd )
441+ self .audio_format_ch = int (self .audio_format_ch )
442+ self .audio_format_sr = int (self .audio_format_sr )
443+ break
444+ # video fmtp not needed, it seems.
445+ elif 'a=mfiaeskey:' in k :
446+ self .has_mfi = True
447+ self .aeskey = k .split (':' )[1 ]
448+ elif 'a=rsaaeskey:' in k :
449+ self .has_rsa = True
450+ # RSA - Use Feat.Ft12FPSAPv2p5_AES_GCM
451+ self .aeskey = k .split (':' )[1 ]
452+ elif 'a=fpaeskey:' in k :
453+ self .has_fp = True
454+ # FairPlay (v3?) AES key
455+ self .aeskey = k .split (':' )[1 ]
456+ elif 'a=aesiv:' in k :
457+ self .aesiv = k .split (':' )[1 ]
458+ elif 'a=min-latency:' in k :
459+ self .minlatency = k .split (':' )[1 ]
460+ elif 'a=max-latency:' in k :
461+ self .maxlatency = k .split (':' )[1 ]
462+ elif 'm=video' in k :
463+ self .has_video = True
464+ self .last_media = 'video'
465+ self .m_video_line = k
466+ start = self .m_video_line .find ('AVP ' ) + 4
467+ self .video_media_type = int (self .m_video_line [start :])
468+ elif 'a=rtpmap:' in k and self .last_media == 'video' :
469+ self .video_rtpmap = k .split (':' )[1 ]
470+ start = self .video_rtpmap .find (':' ) + 1
471+ mid = self .video_rtpmap .find (' ' ) + 1
472+ self .video_payload = int (self .video_rtpmap [start :mid - 1 ])
473+ self .video_encoding = self .video_rtpmap [mid :]
346474
475+
476+ class AP2Handler (http .server .BaseHTTPRequestHandler ):
477+ aeskeys = None
347478 pp = pprint .PrettyPrinter ()
479+ ntp_port , ptp_port = 0 , 0
480+ ntp_proc , ptp_proc = None , None
348481
349482 # Maps paths to methods a la HAP-python
350483 HANDLERS = {
@@ -432,6 +565,53 @@ def do_OPTIONS(self):
432565 )
433566 self .end_headers ()
434567
568+ def do_ANNOUNCE (self ):
569+ # Enable Feature bit 12: Ft12FPSAPv2p5_AES_GCM: this uses only RSA
570+ # Enabling Feat bit 25 and iTunes4win attempts AES - cannot yet decrypt.
571+ print ("ANNOUNCE %s" % self .path )
572+ print (self .headers )
573+
574+ # dacp_id = self.headers.get("DACP-ID")
575+ # active_remote = self.headers.get("Active-Remote")
576+
577+ if self .headers ["Content-Type" ] == 'application/sdp' :
578+ content_len = int (self .headers ["Content-Length" ])
579+ if content_len > 0 :
580+ sdp_body = self .rfile .read (content_len ).decode ('utf-8' )
581+ print (sdp_body )
582+ sdp = SDPHandler (sdp_body )
583+ if sdp .has_mfi :
584+ print ("Mfi not possible on this hardware." )
585+ self .send_response (404 )
586+ self .server .hap = None
587+ else :
588+ if (sdp .audio_format is SDPHandler .SDPAudioFormat .ALAC
589+ and int ((FEATURES & Feat .Ft19RcvAudALAC )) == 0 ):
590+ print ("This receiver not configured for ALAC (set flag 19)." )
591+ self .send_response (404 )
592+ self .server .hap = None
593+ elif (sdp .audio_format is SDPHandler .SDPAudioFormat .AAC
594+ and int ((FEATURES & Feat .Ft20RcvAudAAC_LC )) == 0 ):
595+ print ("This receiver not configured for AAC (set flag 20)." )
596+ self .send_response (404 )
597+ self .server .hap = None
598+ elif (sdp .audio_format is SDPHandler .SDPAudioFormat .AAC_ELD
599+ and int ((FEATURES & Feat .Ft20RcvAudAAC_LC )) == 0 ):
600+ print ("This receiver not configured for AAC (set flag 20/21)." )
601+ self .send_response (404 )
602+ self .server .hap = None
603+ else :
604+ if sdp .has_fp :
605+ # print('Got FP AES Key from SDP')
606+ self .aeskeys = FPAES (fpaeskey = sdp .aeskey , aesiv = sdp .aesiv )
607+ elif sdp .has_rsa :
608+ self .aeskeys = FPAES (rsaaeskey = sdp .aeskey , aesiv = sdp .aesiv )
609+ self .send_response (200 )
610+ self .send_header ("Server" , self .version_string ())
611+ self .send_header ("CSeq" , self .headers ["CSeq" ])
612+ self .end_headers ()
613+ self .sdp = sdp
614+
435615 def do_FLUSHBUFFERED (self ):
436616 print ("FLUSHBUFFERED" )
437617 self .send_response (200 )
@@ -462,6 +642,53 @@ def do_SETUP(self):
462642 ua = self .headers .get ("User-Agent" )
463643 print ("SETUP %s" % self .path )
464644 print (self .headers )
645+ # Found in SETUP after ANNOUNCE:
646+ if self .headers ["Transport" ]:
647+ # print(self.headers["Transport"])
648+ buff = int (self .sdp .maxlatency ) # determines how many CODEC frame size 1024 we can hold
649+
650+ # Set up a stream to receive.
651+ stream = {
652+ 'audioFormat' : self .sdp .AirplayAudFmt ,
653+ 'latencyMin' : int (self .sdp .minlatency ),
654+ 'latencyMax' : int (self .sdp .maxlatency ),
655+ 'ct' : 0 , # Compression Type(?)
656+ 'shk' : self .aeskeys .aeskey ,
657+ 'shiv' : self .aeskeys .aesiv ,
658+ 'spf' : int (self .sdp .spf ), # sample frames per pkt
659+ 'type' : int (self .sdp .payload_type ),
660+ 'controlPort' : 0 ,
661+ }
662+
663+ streamobj = Stream (stream , buff )
664+
665+ self .server .streams .append (streamobj )
666+
667+ event_port , self .event_proc = Event .spawn (self .server .server_address )
668+ timing_port , self .timing_proc = NTP .spawn (self .server .server_address )
669+ transport = self .headers ["Transport" ].split (';' )
670+ res = []
671+ res .append ("RTP/AVP/UDP" )
672+ res .append ("unicast" )
673+ res .append ("mode=record" )
674+ res .append ("control_port=%d" % streamobj .control_port )
675+ print ("control_port=%d" % streamobj .control_port )
676+ res .append ("server_port=%d" % streamobj .data_port )
677+ print ("server_port=%d" % streamobj .data_port )
678+ res .append ("timing_port=%d" % timing_port )
679+ print ("timing_port=%d" % timing_port )
680+ string = ';'
681+
682+ self .send_response (200 )
683+ self .send_header ("Transport" , string .join (res ))
684+ self .send_header ("Session" , "1" )
685+ self .send_header ("Audio-Jack-Status" , 'connected; type=analog' )
686+ self .send_header ("Server" , self .version_string ())
687+ self .send_header ("CSeq" , self .headers ["CSeq" ])
688+ self .end_headers ()
689+
690+ return
691+
465692 if self .headers ["Content-Type" ] == HTTP_CT_BPLIST :
466693 content_len = int (self .headers ["Content-Length" ])
467694 if content_len > 0 :
@@ -632,10 +859,6 @@ def do_TEARDOWN(self):
632859 stream = self .server .streams [stream_id ]
633860 stream .teardown ()
634861 del self .server .streams [stream_id ]
635- else :
636- for stream in self .server .streams :
637- stream .teardown ()
638- self .server .streams .clear ()
639862 self .pp .pprint (plist )
640863 self .send_response (200 )
641864 self .send_header ("Server" , self .version_string ())
@@ -647,8 +870,23 @@ def do_TEARDOWN(self):
647870
648871 # terminate the forked event_proc, otherwise a zombie process consumes 100% cpu
649872 self .event_proc .terminate ()
873+ if (self .ntp_proc ):
874+ self .ntp_proc .terminate ()
875+ # When changing from RTP_BUFFERED to REALTIME, must clean up:
876+ for stream in self .server .streams :
877+ stream .teardown ()
878+ self .server .streams .clear ()
650879
651880 def do_SETPEERS (self ):
881+ """
882+ A shorter format to set timing (PTP clock) peers.
883+
884+ Content-Type: /peer-list-changed
885+ Contains [] array of IP{4|6}addrs:
886+ ['...::...',
887+ '...::...',
888+ '...']
889+ """
652890 print ("SETPEERS %s" % self .path )
653891 print (self .headers )
654892 content_len = int (self .headers ["Content-Length" ])
@@ -663,14 +901,14 @@ def do_SETPEERS(self):
663901 self .end_headers ()
664902
665903 def do_SETPEERSX (self ):
666- # extended message format for setting PTP clock peers
904+ # Extended format for setting timing ( PTP clock) peers
667905 # Requires Ft52PeersExtMsg (bit 52)
668906 # Note: this method does not require defining in do_OPTIONS
669907
670908 # Content-Type: /peer-list-changed-x
671909 # Contains [] array of:
672- # {'Addresses': ['fe80::fb:97fb:2fb3:34bc ',
673- # '192.168.19.110 '],
910+ # {'Addresses': ['fe80::... ',
911+ # '... '],
674912 # 'ClockID': 000000000000000000,
675913 # 'ClockPorts': {GUID1: port,
676914 # GUID2: port,
0 commit comments