Add an Android TLS hack to fix GFE 2.1.4 and migrate to OkHttp classes for HTTPS

This commit is contained in:
Cameron Gutman 2014-11-10 21:58:03 -08:00
parent 2cc2d05c2f
commit 7bc325fa08
5 changed files with 105 additions and 93 deletions

View File

@ -5,5 +5,7 @@
<classpathentry kind="lib" path="libs/xpp3-1.1.4c.jar"/>
<classpathentry kind="lib" path="libs/tinyrtsp.jar"/>
<classpathentry kind="lib" path="libs/jmdns-fixed.jar"/>
<classpathentry kind="lib" path="libs/okhttp-2.1.0-RC1.jar"/>
<classpathentry kind="lib" path="libs/okio-1.0.1.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>

Binary file not shown.

Binary file not shown.

View File

@ -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<NvApp> 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<NvApp> appList = new LinkedList<NvApp>();
Stack<String> currentTag = new Stack<String>();
@ -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;
}
}

View File

@ -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();