diff --git a/moonlight-common/.classpath b/moonlight-common/.classpath index 0ac3498e..be0236b0 100644 --- a/moonlight-common/.classpath +++ b/moonlight-common/.classpath @@ -5,5 +5,7 @@ + + diff --git a/moonlight-common/libs/okhttp-2.1.0-RC1.jar b/moonlight-common/libs/okhttp-2.1.0-RC1.jar new file mode 100644 index 00000000..b2b9bc50 Binary files /dev/null and b/moonlight-common/libs/okhttp-2.1.0-RC1.jar differ diff --git a/moonlight-common/libs/okio-1.0.1.jar b/moonlight-common/libs/okio-1.0.1.jar new file mode 100644 index 00000000..11b709a4 Binary files /dev/null and b/moonlight-common/libs/okio-1.0.1.jar differ diff --git a/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java b/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java index 4eda6af0..0a60cb45 100644 --- a/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java +++ b/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java @@ -8,14 +8,25 @@ import java.io.StringReader; import java.net.Inet6Address; import java.net.InetAddress; import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; import java.util.LinkedList; import java.util.Scanner; import java.util.Stack; import java.util.UUID; +import java.util.concurrent.TimeUnit; import javax.crypto.SecretKey; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -23,6 +34,9 @@ import org.xmlpull.v1.XmlPullParserFactory; import com.limelight.nvstream.StreamConfiguration; import com.limelight.nvstream.http.PairingManager.PairState; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.ResponseBody; public class NvHTTP { @@ -38,6 +52,51 @@ public class NvHTTP { public String baseUrl; + private OkHttpClient httpClient = new OkHttpClient(); + private OkHttpClient httpClientWithReadTimeout; + + private TrustManager[] trustAllCerts; + private KeyManager[] ourKeyman; + + private void initializeHttpState(final LimelightCryptoProvider cryptoProvider) { + trustAllCerts = new TrustManager[] { + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + public void checkClientTrusted(X509Certificate[] certs, String authType) {} + public void checkServerTrusted(X509Certificate[] certs, String authType) {} + }}; + + ourKeyman = new KeyManager[] { + new X509KeyManager() { + public String chooseClientAlias(String[] keyTypes, + Principal[] issuers, Socket socket) { return "Limelight-RSA"; } + public String chooseServerAlias(String keyType, Principal[] issuers, + Socket socket) { return null; } + public X509Certificate[] getCertificateChain(String alias) { + return new X509Certificate[] {cryptoProvider.getClientCertificate()}; + } + public String[] getClientAliases(String keyType, Principal[] issuers) { return null; } + public PrivateKey getPrivateKey(String alias) { + return cryptoProvider.getClientPrivateKey(); + } + public String[] getServerAliases(String keyType, Principal[] issuers) { return null; } + } + }; + + // Ignore differences between given hostname and certificate hostname + HostnameVerifier hv = new HostnameVerifier() { + public boolean verify(String hostname, SSLSession session) { return true; } + }; + + httpClient.setHostnameVerifier(hv); + httpClient.setConnectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS); + + httpClientWithReadTimeout = httpClient.clone(); + httpClientWithReadTimeout.setReadTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS); + } + public NvHTTP(InetAddress host, String uniqueId, String deviceName, LimelightCryptoProvider cryptoProvider) { this.uniqueId = uniqueId; this.address = host; @@ -51,6 +110,8 @@ public class NvHTTP { safeAddress = host.getHostAddress(); } + initializeHttpState(cryptoProvider); + this.baseUrl = "https://" + safeAddress + ":" + PORT; this.pm = new PairingManager(this, cryptoProvider); } @@ -147,28 +208,42 @@ public class NvHTTP { return details; } + + // This hack is Android-specific but we do it on all platforms + // because it doesn't really matter + private void performAndroidTlsHack(OkHttpClient client) { + // Doing this each time we create a socket is required + // to avoid the SSLv3 fallback that causes connection failures + try { + SSLContext sc = SSLContext.getInstance("TLSv1"); + sc.init(ourKeyman, trustAllCerts, new SecureRandom()); + + client.setSslSocketFactory(sc.getSocketFactory()); + } catch (Exception e) { + e.printStackTrace(); + } + } // Read timeout should be enabled for any HTTP query that requires no outside action // on the GFE server. Examples of queries that DO require outside action are launch, resume, and quit. // The initial pair query does require outside action (user entering a PIN) but subsequent pairing // queries do not. - private InputStream openHttpConnection(String url, boolean enableReadTimeout) throws IOException { - URLConnection conn = new URL(url).openConnection(); - if (verbose) { - System.out.println(url); - } - conn.setConnectTimeout(CONNECTION_TIMEOUT); - if (enableReadTimeout) { - conn.setReadTimeout(READ_TIMEOUT); - } + private ResponseBody openHttpConnection(String url, boolean enableReadTimeout) throws IOException { + Request request = new Request.Builder().url(url).build(); - conn.setUseCaches(false); - conn.connect(); - return conn.getInputStream(); + if (enableReadTimeout) { + performAndroidTlsHack(httpClientWithReadTimeout); + return httpClientWithReadTimeout.newCall(request).execute().body(); + } + else { + performAndroidTlsHack(httpClient); + return httpClient.newCall(request).execute().body(); + } } String openHttpConnectionToString(String url, boolean enableReadTimeout) throws MalformedURLException, IOException { - Scanner s = new Scanner(openHttpConnection(url, enableReadTimeout)); + ResponseBody resp = openHttpConnection(url, enableReadTimeout); + Scanner s = new Scanner(resp.byteStream()); String str = ""; while (s.hasNext()) { @@ -176,6 +251,7 @@ public class NvHTTP { } s.close(); + resp.close(); if (verbose) { System.out.println(str); @@ -216,20 +292,13 @@ public class NvHTTP { return pm.pair(uniqueId, pin); } - public InputStream getBoxArtPng(NvApp app) throws IOException { - // FIXME: Investigate whether this should be subject to the 2 second read timeout - // or not. - return openHttpConnection(baseUrl + "/appasset?uniqueid="+uniqueId+"&appid="+ - app.getAppId()+"&AssetType=2&AssetIdx=0", false); - } - public LinkedList getAppList() throws GfeHttpResponseException, IOException, XmlPullParserException { - InputStream in = openHttpConnection(baseUrl + "/applist?uniqueid=" + uniqueId, true); + ResponseBody resp = openHttpConnection(baseUrl + "/applist?uniqueid=" + uniqueId, true); XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); factory.setNamespaceAware(true); XmlPullParser xpp = factory.newPullParser(); - xpp.setInput(new InputStreamReader(in)); + xpp.setInput(new InputStreamReader(resp.byteStream())); int eventType = xpp.getEventType(); LinkedList appList = new LinkedList(); Stack currentTag = new Stack(); @@ -261,11 +330,14 @@ public class NvHTTP { } eventType = xpp.next(); } + + resp.close(); + return appList; } public void unpair() throws IOException { - openHttpConnection(baseUrl + "/unpair?uniqueid=" + uniqueId, true); + openHttpConnectionToString(baseUrl + "/unpair?uniqueid=" + uniqueId, true); } final private static char[] hexArray = "0123456789ABCDEF".toCharArray(); @@ -280,7 +352,7 @@ public class NvHTTP { } public int launchApp(int appId, SecretKey inputKey, int riKeyId, StreamConfiguration config) throws IOException, XmlPullParserException { - InputStream in = openHttpConnection(baseUrl + + String xmlStr = openHttpConnectionToString(baseUrl + "/launch?uniqueid=" + uniqueId + "&appid=" + appId + "&mode=" + config.getWidth() + "x" + config.getHeight() + "x" + config.getRefreshRate() + @@ -288,21 +360,21 @@ public class NvHTTP { "&rikey="+bytesToHex(inputKey.getEncoded()) + "&rikeyid="+riKeyId + "&localAudioPlayMode=" + (config.getPlayLocalAudio() ? 1 : 0), false); - String gameSession = getXmlString(in, "gamesession"); + String gameSession = getXmlString(xmlStr, "gamesession"); return Integer.parseInt(gameSession); } public boolean resumeApp(SecretKey inputKey, int riKeyId) throws IOException, XmlPullParserException { - InputStream in = openHttpConnection(baseUrl + "/resume?uniqueid=" + uniqueId + + String xmlStr = openHttpConnectionToString(baseUrl + "/resume?uniqueid=" + uniqueId + "&rikey="+bytesToHex(inputKey.getEncoded()) + "&rikeyid="+riKeyId, false); - String resume = getXmlString(in, "resume"); + String resume = getXmlString(xmlStr, "resume"); return Integer.parseInt(resume) != 0; } public boolean quitApp() throws IOException, XmlPullParserException { - InputStream in = openHttpConnection(baseUrl + "/cancel?uniqueid=" + uniqueId, false); - String cancel = getXmlString(in, "cancel"); + String xmlStr = openHttpConnectionToString(baseUrl + "/cancel?uniqueid=" + uniqueId, false); + String cancel = getXmlString(xmlStr, "cancel"); return Integer.parseInt(cancel) != 0; } } diff --git a/moonlight-common/src/com/limelight/nvstream/http/PairingManager.java b/moonlight-common/src/com/limelight/nvstream/http/PairingManager.java index 660ff6b3..a8329bb3 100644 --- a/moonlight-common/src/com/limelight/nvstream/http/PairingManager.java +++ b/moonlight-common/src/com/limelight/nvstream/http/PairingManager.java @@ -7,7 +7,6 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.ShortBufferException; import javax.crypto.spec.SecretKeySpec; -import javax.net.ssl.*; import org.xmlpull.v1.XmlPullParserException; @@ -15,7 +14,6 @@ import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.io.*; import java.net.MalformedURLException; -import java.net.Socket; import java.security.*; import java.security.cert.*; import java.util.Arrays; @@ -43,66 +41,6 @@ public class PairingManager { this.cert = cryptoProvider.getClientCertificate(); this.pemCertBytes = cryptoProvider.getPemEncodedClientCertificate(); this.pk = cryptoProvider.getClientPrivateKey(); - - // Update the trust manager and key manager to use our certificate and PK - installSslKeysAndTrust(); - } - - private void installSslKeysAndTrust() { - // Create a trust manager that does not validate certificate chains - TrustManager[] trustAllCerts = new TrustManager[] { - new X509TrustManager() { - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - public void checkClientTrusted(X509Certificate[] certs, String authType) {} - public void checkServerTrusted(X509Certificate[] certs, String authType) {} - }}; - - KeyManager[] ourKeyman = new KeyManager[] { - new X509KeyManager() { - public String chooseClientAlias(String[] keyTypes, - Principal[] issuers, Socket socket) { - return "Limelight-RSA"; - } - - public String chooseServerAlias(String keyType, Principal[] issuers, - Socket socket) { - return null; - } - - public X509Certificate[] getCertificateChain(String alias) { - return new X509Certificate[] {cert}; - } - - public String[] getClientAliases(String keyType, Principal[] issuers) { - return null; - } - - public PrivateKey getPrivateKey(String alias) { - return pk; - } - - public String[] getServerAliases(String keyType, Principal[] issuers) { - return null; - } - } - }; - - // Ignore differences between given hostname and certificate hostname - HostnameVerifier hv = new HostnameVerifier() { - public boolean verify(String hostname, SSLSession session) { return true; } - }; - - // Install the all-trusting trust manager - try { - SSLContext sc = SSLContext.getInstance("SSL"); - sc.init(ourKeyman, trustAllCerts, new SecureRandom()); - HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); - HttpsURLConnection.setDefaultHostnameVerifier(hv); - } catch (Exception e) { - e.printStackTrace(); - } } final private static char[] hexArray = "0123456789ABCDEF".toCharArray();