From a234248436c828e5ecfd1e00b6a2f43ba780d539 Mon Sep 17 00:00:00 2001 From: Sergei Fedorov Date: Mon, 5 Oct 2020 16:47:12 +0300 Subject: [PATCH] fix rtsp reconnect --- build.gradle | 4 +- .../com/github/serezhka/jap2lib/AirPlay.java | 38 ++++- .../github/serezhka/jap2lib/ModifiedMD5.java | 2 +- .../com/github/serezhka/jap2lib/OmgHax.java | 2 +- .../com/github/serezhka/jap2lib/Pairing.java | 64 +-------- .../com/github/serezhka/jap2lib/RTSP.java | 132 +++++++++--------- .../jap2lib/rtsp/AudioStreamInfo.java | 72 ++++++++++ .../jap2lib/rtsp/MediaStreamInfo.java | 11 ++ .../jap2lib/rtsp/VideoStreamInfo.java | 19 +++ src/main/resources/info-response.xml | 89 ++++++++++++ .../serezhka/jap2lib/AirPlayFairPlayTest.java | 7 +- 11 files changed, 305 insertions(+), 135 deletions(-) create mode 100644 src/main/java/com/github/serezhka/jap2lib/rtsp/AudioStreamInfo.java create mode 100644 src/main/java/com/github/serezhka/jap2lib/rtsp/MediaStreamInfo.java create mode 100644 src/main/java/com/github/serezhka/jap2lib/rtsp/VideoStreamInfo.java create mode 100644 src/main/resources/info-response.xml diff --git a/build.gradle b/build.gradle index a66abd2..dbe0850 100644 --- a/build.gradle +++ b/build.gradle @@ -4,9 +4,9 @@ plugins { } group 'com.github.serezhka' -version '1.0.3' +version '1.0.4' -sourceCompatibility = 1.9 +sourceCompatibility = JavaVersion.VERSION_1_9 repositories { mavenCentral() diff --git a/src/main/java/com/github/serezhka/jap2lib/AirPlay.java b/src/main/java/com/github/serezhka/jap2lib/AirPlay.java index ee1e13b..2432a7e 100644 --- a/src/main/java/com/github/serezhka/jap2lib/AirPlay.java +++ b/src/main/java/com/github/serezhka/jap2lib/AirPlay.java @@ -1,5 +1,7 @@ package com.github.serezhka.jap2lib; +import com.github.serezhka.jap2lib.rtsp.MediaStreamInfo; + import java.io.InputStream; import java.io.OutputStream; @@ -66,13 +68,39 @@ public void fairPlaySetup(InputStream in, OutputStream out) throws Exception { } /** - * {@code RTSP SETUP} + * Retrieves information about media stream from RTSP SETUP request + * + * @return null if there's no stream info + */ + public MediaStreamInfo rtspGetMediaStreamInfo(InputStream in) throws Exception { + return rtsp.getMediaStreamInfo(in); + } + + /** + * {@code RTSP SETUP ENCRYPTION} + *

+ * Retrieves encrypted EAS key and IV + */ + public void rtspSetupEncryption(InputStream in) throws Exception { + rtsp.setup(in); + } + + /** + * {@code RTSP SETUP VIDEO} + *

+ * Writes video event, data and timing ports info to output stream + */ + public void rtspSetupVideo(OutputStream out, int videoDataPort, int videoEventPort, int videoTimingPort) throws Exception { + rtsp.setupVideo(out, videoDataPort, videoEventPort, videoTimingPort); + } + + /** + * {@code RTSP SETUP AUDIO} *

- * Writes RSTP SETUP response bytes to output stream, returns stream data type: 110 - video, 96 - audio, 0 - no stream assigned + * Writes audio control and data ports info to output stream */ - public void rtspSetup(InputStream in, OutputStream out, - int videoDataPort, int videoEventPort, int videoTimingPort, int audioDataPort, int audioControlPort) throws Exception { - rtsp.rtspSetup(in, out, videoDataPort, videoEventPort, videoTimingPort, audioDataPort, audioControlPort); + public void rtspSetupAudio(OutputStream out, int audioDataPort, int audioControlPort) throws Exception { + rtsp.setupAudio(out, audioDataPort, audioControlPort); } public byte[] getFairPlayAesKey() { diff --git a/src/main/java/com/github/serezhka/jap2lib/ModifiedMD5.java b/src/main/java/com/github/serezhka/jap2lib/ModifiedMD5.java index 32cf5d8..300e17d 100644 --- a/src/main/java/com/github/serezhka/jap2lib/ModifiedMD5.java +++ b/src/main/java/com/github/serezhka/jap2lib/ModifiedMD5.java @@ -5,7 +5,7 @@ class ModifiedMD5 { - private int[] shift = {7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + private final int[] shift = {7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21}; diff --git a/src/main/java/com/github/serezhka/jap2lib/OmgHax.java b/src/main/java/com/github/serezhka/jap2lib/OmgHax.java index 1625c6d..972919f 100644 --- a/src/main/java/com/github/serezhka/jap2lib/OmgHax.java +++ b/src/main/java/com/github/serezhka/jap2lib/OmgHax.java @@ -247,7 +247,7 @@ void generate_session_key(byte[] oldSap, byte[] messageIn, byte[] sessionKey) { } } - void cycle(byte[] block, int key_schedule[][]) { + void cycle(byte[] block, int[][] key_schedule) { int ptr1, ptr2, ptr3, ptr4, ab; ByteBuffer bWords = ByteBuffer.wrap(block); diff --git a/src/main/java/com/github/serezhka/jap2lib/Pairing.java b/src/main/java/com/github/serezhka/jap2lib/Pairing.java index 0be0165..23a2ea7 100644 --- a/src/main/java/com/github/serezhka/jap2lib/Pairing.java +++ b/src/main/java/com/github/serezhka/jap2lib/Pairing.java @@ -1,8 +1,8 @@ package com.github.serezhka.jap2lib; import com.dd.plist.BinaryPropertyListWriter; -import com.dd.plist.NSArray; -import com.dd.plist.NSDictionary; +import com.dd.plist.NSObject; +import com.dd.plist.PropertyListParser; import net.i2p.crypto.eddsa.EdDSAEngine; import net.i2p.crypto.eddsa.EdDSAPublicKey; import net.i2p.crypto.eddsa.KeyPairGenerator; @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.*; import java.util.Arrays; @@ -44,59 +45,9 @@ class Pairing { this.keyPair = new KeyPairGenerator().generateKeyPair(); } - void info(OutputStream out) throws IOException { - NSArray audioFormats = new NSArray(2); - NSDictionary audioFormat1 = new NSDictionary(); - audioFormat1.put("audioInputFormats", 67108860); - audioFormat1.put("audioOutputFormats", 67108860); - audioFormat1.put("type", 100); - audioFormats.setValue(0, audioFormat1); - NSDictionary audioFormat2 = new NSDictionary(); - audioFormat2.put("audioInputFormats", 67108860); - audioFormat2.put("audioOutputFormats", 67108860); - audioFormat2.put("type", 101); - audioFormats.setValue(1, audioFormat2); - - NSArray audioLatencies = new NSArray(2); - NSDictionary audioLatency1 = new NSDictionary(); - audioLatency1.put("audioType", "default"); - audioLatency1.put("inputLatencyMicros", false); - audioLatency1.put("type", 100); - audioLatencies.setValue(0, audioLatency1); - NSDictionary audioLatency2 = new NSDictionary(); - audioLatency2.put("audioType", "default"); - audioLatency2.put("inputLatencyMicros", false); - audioLatency2.put("type", 101); - audioLatencies.setValue(1, audioLatency2); - - NSArray displays = new NSArray(1); - NSDictionary display = new NSDictionary(); - display.put("features", 14); - display.put("height", 1080); - display.put("heightPhysical", false); - display.put("heightPixels", 1080); - display.put("maxFPS", 30); - display.put("overscanned", false); - display.put("refreshRate", 60); - display.put("rotation", false); - display.put("uuid", "e5f7a68d-7b0f-4305-984b-974f677a150b"); - display.put("width", 1920); - display.put("widthPhysical", false); - display.put("widthPixels", 1920); - displays.setValue(0, display); - - NSDictionary serverInfo = new NSDictionary(); - serverInfo.put("audioFormats", audioFormats); - serverInfo.put("audioLatencies", audioLatencies); - serverInfo.put("displays", displays); - serverInfo.put("features", 130367356919L); - serverInfo.put("keepAliveSendStatsAsBody", 1); - serverInfo.put("model", "AppleTV2,1"); - serverInfo.put("name", "Apple TV"); - serverInfo.put("pi", "b08f5a79-db29-4384-b456-a4784d9e6055"); - serverInfo.put("sourceVersion", "220.68"); - serverInfo.put("statusFlags", 68); - serverInfo.put("vv", 2); + void info(OutputStream out) throws Exception { + URL response = Pairing.class.getResource("/info-response.xml"); + NSObject serverInfo = PropertyListParser.parse(response.openStream()); BinaryPropertyListWriter.write(out, serverInfo); } @@ -107,8 +58,8 @@ void pairSetup(OutputStream out) throws IOException { @SuppressWarnings("ResultOfMethodCallIgnored") void pairVerify(InputStream request, OutputStream response) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, SignatureException, BadPaddingException, IllegalBlockSizeException, IOException { int flag = request.read(); + request.skip(3); if (flag > 0) { - request.skip(3); request.read(ecdhTheirs = new byte[32]); request.read(edTheirs = new byte[32]); @@ -137,7 +88,6 @@ void pairVerify(InputStream request, OutputStream response) throws NoSuchAlgorit response.write(responseContent); } else { - request.skip(3); byte[] signature = new byte[64]; request.read(signature); diff --git a/src/main/java/com/github/serezhka/jap2lib/RTSP.java b/src/main/java/com/github/serezhka/jap2lib/RTSP.java index 9e2fd63..94f3ebc 100644 --- a/src/main/java/com/github/serezhka/jap2lib/RTSP.java +++ b/src/main/java/com/github/serezhka/jap2lib/RTSP.java @@ -4,9 +4,14 @@ import com.dd.plist.BinaryPropertyListWriter; import com.dd.plist.NSArray; import com.dd.plist.NSDictionary; +import com.github.serezhka.jap2lib.rtsp.AudioStreamInfo; +import com.github.serezhka.jap2lib.rtsp.MediaStreamInfo; +import com.github.serezhka.jap2lib.rtsp.VideoStreamInfo; +import net.i2p.crypto.eddsa.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.HashMap; @@ -19,67 +24,78 @@ class RTSP { private byte[] encryptedAESKey; private byte[] eiv; - void rtspSetup(InputStream in, OutputStream out, - int videoDataPort, int videoEventPort, int videoTimingPort, int audioDataPort, int audioControlPort) throws Exception { - NSDictionary request = (NSDictionary) BinaryPropertyListParser.parse(in); - - log.debug("Binary property list parsed:\n{}", request.toXMLPropertyList()); + MediaStreamInfo getMediaStreamInfo(InputStream rtspSetupPayload) throws Exception { + NSDictionary rtspSetup = (NSDictionary) BinaryPropertyListParser.parse(rtspSetupPayload); - if (request.containsKey("ekey")) { - encryptedAESKey = (byte[]) request.get("ekey").toJavaObject(); - } - - if (request.containsKey("eiv")) { - eiv = (byte[]) request.get("eiv").toJavaObject(); - } - - if (request.containsKey("streams")) { - HashMap stream = (HashMap) ((Object[]) request.get("streams").toJavaObject())[0]; // iter + log.debug("Binary property list parsed:\n{}", rtspSetup.toXMLPropertyList()); + if (rtspSetup.containsKey("streams")) { + // assume one stream info per RTSP SETUP request + HashMap stream = (HashMap) ((Object[]) rtspSetup.get("streams").toJavaObject())[0]; int type = (int) stream.get("type"); - switch (type) { - // mirror - case 110: { - streamConnectionID = Long.toUnsignedString((long) stream.get("streamConnectionID")); - - NSArray streams = new NSArray(1); - NSDictionary dataStream = new NSDictionary(); - dataStream.put("dataPort", videoDataPort); - dataStream.put("type", 110); - streams.setValue(0, dataStream); - - NSDictionary response = new NSDictionary(); - response.put("streams", streams); - response.put("eventPort", videoEventPort); - response.put("timingPort", videoTimingPort); - BinaryPropertyListWriter.write(out, response); - break; - } + // video + case 110: + if (stream.containsKey("streamConnectionID")) { + streamConnectionID = Long.toUnsignedString((long) stream.get("streamConnectionID")); + } + return new VideoStreamInfo(streamConnectionID); // audio - case 96: { - - log.debug("Audio format: {}", getAudioFormatDescription((int) stream.get("audioFormat"))); - - NSArray streams = new NSArray(1); - NSDictionary dataStream = new NSDictionary(); - dataStream.put("dataPort", audioDataPort); - dataStream.put("type", 96); - dataStream.put("controlPort", audioControlPort); - streams.setValue(0, dataStream); - - NSDictionary response = new NSDictionary(); - response.put("streams", streams); - BinaryPropertyListWriter.write(out, response); - break; - } + case 96: + if (stream.containsKey("audioFormat")) { + long audioFormatCode = (int) stream.get("audioFormat"); // FIXME int or long ?! + return new AudioStreamInfo(audioFormatCode); + } + return new AudioStreamInfo(); default: log.warn("Unknown stream type: {}", type); } } + return null; + } + + void setup(InputStream in) throws Exception { + NSDictionary request = (NSDictionary) BinaryPropertyListParser.parse(in); + + if (request.containsKey("ekey")) { + encryptedAESKey = (byte[]) request.get("ekey").toJavaObject(); + log.info("Encrypted AES key: " + Utils.bytesToHex(encryptedAESKey)); + } + + if (request.containsKey("eiv")) { + eiv = (byte[]) request.get("eiv").toJavaObject(); + log.info("AES eiv: " + Utils.bytesToHex(eiv)); + } + } + + void setupVideo(OutputStream out, int videoDataPort, int videoEventPort, int videoTimingPort) throws IOException { + NSArray streams = new NSArray(1); + NSDictionary dataStream = new NSDictionary(); + dataStream.put("dataPort", videoDataPort); + dataStream.put("type", 110); + streams.setValue(0, dataStream); + + NSDictionary response = new NSDictionary(); + response.put("streams", streams); + response.put("eventPort", videoEventPort); + response.put("timingPort", videoTimingPort); + BinaryPropertyListWriter.write(out, response); + } + + void setupAudio(OutputStream out, int audioDataPort, int audioControlPort) throws IOException { + NSArray streams = new NSArray(1); + NSDictionary dataStream = new NSDictionary(); + dataStream.put("dataPort", audioDataPort); + dataStream.put("type", 96); + dataStream.put("controlPort", audioControlPort); + streams.setValue(0, dataStream); + + NSDictionary response = new NSDictionary(); + response.put("streams", streams); + BinaryPropertyListWriter.write(out, response); } String getStreamConnectionID() { @@ -93,22 +109,4 @@ byte[] getEncryptedAESKey() { byte[] getEiv() { return eiv; } - - private String getAudioFormatDescription(int format) { - String formatDescription; - switch (format) { - case 0x40000: - formatDescription = "96 AppleLossless, 96 352 0 16 40 10 14 2 255 0 0 44100"; - break; - case 0x400000: - formatDescription = "96 mpeg4-generic/44100/2, 96 mode=AAC-main; constantDuration=1024"; - break; - case 0x1000000: - formatDescription = "96 mpeg4-generic/44100/2, 96 mode=AAC-eld; constantDuration=480"; - break; - default: - formatDescription = "Unknown: " + format; - } - return formatDescription; - } } diff --git a/src/main/java/com/github/serezhka/jap2lib/rtsp/AudioStreamInfo.java b/src/main/java/com/github/serezhka/jap2lib/rtsp/AudioStreamInfo.java new file mode 100644 index 0000000..1fd0265 --- /dev/null +++ b/src/main/java/com/github/serezhka/jap2lib/rtsp/AudioStreamInfo.java @@ -0,0 +1,72 @@ +package com.github.serezhka.jap2lib.rtsp; + +public class AudioStreamInfo implements MediaStreamInfo { + + private final AudioFormat audioFormat; + + public AudioStreamInfo() { + audioFormat = null; + } + + public AudioStreamInfo(long audioFormatCode) { + this.audioFormat = AudioFormat.fromCode(audioFormatCode); + } + + @Override + public StreamType getStreamType() { + return StreamType.AUDIO; + } + + public AudioFormat getAudioFormat() { + return audioFormat; + } + + public enum AudioFormat { + PCM_8000_16_1(0x4), + PCM_8000_16_2(0x8), + PCM_16000_16_1(0x10), + PCM_16000_16_2(0x20), + PCM_24000_16_1(0x40), + PCM_24000_16_2(0x80), + PCM_32000_16_1(0x100), + PCM_32000_16_2(0x200), + PCM_44100_16_1(0x400), + PCM_44100_16_2(0x800), + PCM_44100_24_1(0x1000), + PCM_44100_24_2(0x2000), + PCM_48000_16_1(0x4000), + PCM_48000_16_2(0x8000), + PCM_48000_24_1(0x10000), + PCM_48000_24_2(0x20000), + ALAC_44100_16_2(0x40000), + ALAC_44100_24_2(0x80000), + ALAC_48000_16_2(0x100000), + ALAC_48000_24_2(0x200000), + AAC_LC_44100_2(0x400000), + AAC_LC_48000_2(0x800000), + AAC_ELD_44100_2(0x1000000), + AAC_ELD_48000_2(0x2000000), + AAC_ELD_16000_1(0x4000000), + AAC_ELD_24000_1(0x8000000), + OPUS_16000_1(0x10000000), + OPUS_24000_1(0x20000000), + OPUS_48000_1(0x40000000), + AAC_ELD_44100_1(0x80000000), + AAC_ELD_48000_1(0x100000000L); + + private final long code; + + AudioFormat(long code) { + this.code = code; + } + + public static AudioFormat fromCode(long code) { + for (AudioFormat format : AudioFormat.values()) { + if (format.code == code) { + return format; + } + } + throw new IllegalArgumentException("Unknown audio format with code: " + code); + } + } +} diff --git a/src/main/java/com/github/serezhka/jap2lib/rtsp/MediaStreamInfo.java b/src/main/java/com/github/serezhka/jap2lib/rtsp/MediaStreamInfo.java new file mode 100644 index 0000000..8ba501d --- /dev/null +++ b/src/main/java/com/github/serezhka/jap2lib/rtsp/MediaStreamInfo.java @@ -0,0 +1,11 @@ +package com.github.serezhka.jap2lib.rtsp; + +public interface MediaStreamInfo { + + StreamType getStreamType(); + + enum StreamType { + AUDIO, + VIDEO + } +} diff --git a/src/main/java/com/github/serezhka/jap2lib/rtsp/VideoStreamInfo.java b/src/main/java/com/github/serezhka/jap2lib/rtsp/VideoStreamInfo.java new file mode 100644 index 0000000..a2a382a --- /dev/null +++ b/src/main/java/com/github/serezhka/jap2lib/rtsp/VideoStreamInfo.java @@ -0,0 +1,19 @@ +package com.github.serezhka.jap2lib.rtsp; + +public class VideoStreamInfo implements MediaStreamInfo { + + private final String streamConnectionID; + + public VideoStreamInfo(String streamConnectionID) { + this.streamConnectionID = streamConnectionID; + } + + @Override + public StreamType getStreamType() { + return MediaStreamInfo.StreamType.VIDEO; + } + + public String getStreamConnectionID() { + return streamConnectionID; + } +} diff --git a/src/main/resources/info-response.xml b/src/main/resources/info-response.xml new file mode 100644 index 0000000..bf0c93e --- /dev/null +++ b/src/main/resources/info-response.xml @@ -0,0 +1,89 @@ + + + + + audioFormats + + + audioInputFormats + 67108860 + audioOutputFormats + 67108860 + type + 100 + + + audioInputFormats + 67108860 + audioOutputFormats + 67108860 + type + 101 + + + audioLatencies + + + audioType + default + inputLatencyMicros + + type + 100 + + + audioType + default + inputLatencyMicros + + type + 101 + + + displays + + + features + 14 + height + 1080 + heightPhysical + + heightPixels + 1080 + maxFPS + 30 + overscanned + + refreshRate + 60 + rotation + + uuid + e5f7a68d-7b0f-4305-984b-974f677a150b + width + 1920 + widthPhysical + + widthPixels + 1920 + + + features + 130367356919 + keepAliveSendStatsAsBody + 1 + model + AppleTV2,1 + name + Apple TV + pi + b08f5a79-db29-4384-b456-a4784d9e6055 + sourceVersion + 220.68 + statusFlags + 68 + vv + 2 + + \ No newline at end of file diff --git a/src/test/java/com/github/serezhka/jap2lib/AirPlayFairPlayTest.java b/src/test/java/com/github/serezhka/jap2lib/AirPlayFairPlayTest.java index 12e8f25..9a1540d 100644 --- a/src/test/java/com/github/serezhka/jap2lib/AirPlayFairPlayTest.java +++ b/src/test/java/com/github/serezhka/jap2lib/AirPlayFairPlayTest.java @@ -4,6 +4,7 @@ import com.dd.plist.BinaryPropertyListWriter; import com.dd.plist.NSArray; import com.dd.plist.NSDictionary; +import com.github.serezhka.jap2lib.rtsp.MediaStreamInfo; import org.junit.jupiter.api.Test; import java.io.ByteArrayInputStream; @@ -12,6 +13,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -45,7 +47,7 @@ void fairPlayTest() throws Exception { NSDictionary rtspSetup1Request = new NSDictionary(); rtspSetup1Request.put("ekey", encryptedAesKey); byte[] rtspSetup1RequestBytes = BinaryPropertyListWriter.writeToArray(rtspSetup1Request); - airPlay.rtspSetup(new ByteArrayInputStream(rtspSetup1RequestBytes), null, 0, 0, 0, 0, 0); + airPlay.rtspSetupEncryption(new ByteArrayInputStream(rtspSetup1RequestBytes)); // RSTP SETUP 2 request long streamConnectionID = -3907568444900622110L; @@ -58,7 +60,8 @@ void fairPlayTest() throws Exception { rtspSetup2Request.put("streams", streams); byte[] rtspSetup2RequestBytes = BinaryPropertyListWriter.writeToArray(rtspSetup2Request); ByteArrayOutputStream rtspSetup2Response = new ByteArrayOutputStream(); - airPlay.rtspSetup(new ByteArrayInputStream(rtspSetup2RequestBytes), rtspSetup2Response, 7001, 7002, 7003, 0, 0); + airPlay.rtspGetMediaStreamInfo(new ByteArrayInputStream(rtspSetup2RequestBytes)); + airPlay.rtspSetupVideo(rtspSetup2Response, 7001, 7002, 7003); NSDictionary rtsp2Response = (NSDictionary) BinaryPropertyListParser.parse(new ByteArrayInputStream(rtspSetup2Response.toByteArray())); HashMap stream = (HashMap) ((Object[]) rtsp2Response.get("streams").toJavaObject())[0];