diff --git a/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java b/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java index b14758a3..0f7e57ee 100644 --- a/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java +++ b/moonlight-common/src/com/limelight/nvstream/http/NvHTTP.java @@ -31,6 +31,7 @@ public class NvHTTP { public static final int PORT = 47984; public static final int CONNECTION_TIMEOUT = 2000; + public static final int READ_TIMEOUT = 2000; private final boolean verbose = false; @@ -101,7 +102,7 @@ public class NvHTTP { } public String getServerInfo(String uniqueId) throws MalformedURLException, IOException { - return openHttpConnectionToString(baseUrl + "/serverinfo?uniqueid=" + uniqueId); + return openHttpConnectionToString(baseUrl + "/serverinfo?uniqueid=" + uniqueId, true); } public ComputerDetails getComputerDetails() throws MalformedURLException, IOException, XmlPullParserException { @@ -146,19 +147,27 @@ public class NvHTTP { return details; } - private InputStream openHttpConnection(String url) throws IOException { + // 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); + } + conn.setUseCaches(false); conn.connect(); return conn.getInputStream(); } - String openHttpConnectionToString(String url) throws MalformedURLException, IOException { - Scanner s = new Scanner(openHttpConnection(url)); + String openHttpConnectionToString(String url, boolean enableReadTimeout) throws MalformedURLException, IOException { + Scanner s = new Scanner(openHttpConnection(url, enableReadTimeout)); String str = ""; while (s.hasNext()) { @@ -207,12 +216,14 @@ public class NvHTTP { } 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 + "/applist?uniqueid="+uniqueId+"&appid="+ - app.getAppId()+"&AssetType=2&AssetIdx=0"); + app.getAppId()+"&AssetType=2&AssetIdx=0", false); } public LinkedList getAppList() throws GfeHttpResponseException, IOException, XmlPullParserException { - InputStream in = openHttpConnection(baseUrl + "/applist?uniqueid=" + uniqueId); + InputStream in = openHttpConnection(baseUrl + "/applist?uniqueid=" + uniqueId, true); XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); factory.setNamespaceAware(true); XmlPullParser xpp = factory.newPullParser(); @@ -253,7 +264,7 @@ public class NvHTTP { } public void unpair() throws IOException { - openHttpConnection(baseUrl + "/unpair?uniqueid=" + uniqueId); + openHttpConnection(baseUrl + "/unpair?uniqueid=" + uniqueId, true); } final private static char[] hexArray = "0123456789ABCDEF".toCharArray(); @@ -274,7 +285,7 @@ public class NvHTTP { "&mode=" + width + "x" + height + "x" + refreshRate + "&additionalStates=1&sops=" + (sops ? 1 : 0) + "&rikey="+bytesToHex(inputKey.getEncoded()) + - "&rikeyid="+riKeyId); + "&rikeyid="+riKeyId, false); String gameSession = getXmlString(in, "gamesession"); return Integer.parseInt(gameSession); } @@ -282,13 +293,13 @@ public class NvHTTP { public boolean resumeApp(SecretKey inputKey, int riKeyId) throws IOException, XmlPullParserException { InputStream in = openHttpConnection(baseUrl + "/resume?uniqueid=" + uniqueId + "&rikey="+bytesToHex(inputKey.getEncoded()) + - "&rikeyid="+riKeyId); + "&rikeyid="+riKeyId, false); String resume = getXmlString(in, "resume"); return Integer.parseInt(resume) != 0; } public boolean quitApp() throws IOException, XmlPullParserException { - InputStream in = openHttpConnection(baseUrl + "/cancel?uniqueid=" + uniqueId); + InputStream in = openHttpConnection(baseUrl + "/cancel?uniqueid=" + uniqueId, false); String cancel = getXmlString(in, "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 61a4c293..f0737725 100644 --- a/moonlight-common/src/com/limelight/nvstream/http/PairingManager.java +++ b/moonlight-common/src/com/limelight/nvstream/http/PairingManager.java @@ -235,11 +235,14 @@ public class PairingManager { byte[] saltAndPin = saltPin(salt, pin); aesKey = generateAesKey(saltAndPin); - // Send the salt and get the server cert + // Send the salt and get the server cert. This doesn't have a read timeout + // because the user must enter the PIN before the server responds String getCert = http.openHttpConnectionToString(http.baseUrl + - "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&phrase=getservercert&salt="+bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes)); + "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&phrase=getservercert&salt="+ + bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes), + false); if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) { - http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId); + http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId, true); return PairState.FAILED; } X509Certificate serverCert = extractPlainCert(getCert); @@ -250,9 +253,10 @@ public class PairingManager { // Send the encrypted challenge to the server String challengeResp = http.openHttpConnectionToString(http.baseUrl + - "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge)); + "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge), + true); if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) { - http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId); + http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId, true); return PairState.FAILED; } @@ -268,9 +272,10 @@ public class PairingManager { byte[] challengeRespHash = toSHA1Bytes(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret)); byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey); String secretResp = http.openHttpConnectionToString(http.baseUrl + - "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted)); + "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted), + true); if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) { - http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId); + http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId, true); return PairState.FAILED; } @@ -282,7 +287,7 @@ public class PairingManager { // Ensure the authenticity of the data if (!verifySignature(serverSecret, serverSignature, serverCert)) { // Cancel the pairing process - http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId); + http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId, true); // Looks like a MITM return PairState.FAILED; @@ -292,7 +297,7 @@ public class PairingManager { byte[] serverChallengeRespHash = toSHA1Bytes(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret)); if (!Arrays.equals(serverChallengeRespHash, serverResponse)) { // Cancel the pairing process - http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId); + http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId, true); // Probably got the wrong PIN return PairState.PIN_WRONG; @@ -301,16 +306,18 @@ public class PairingManager { // Send the server our signed secret byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk)); String clientSecretResp = http.openHttpConnectionToString(http.baseUrl + - "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret)); + "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret), + true); if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) { - http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId); + http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId, true); return PairState.FAILED; } // Do the initial challenge (seems neccessary for us to show as paired) - String pairChallenge = http.openHttpConnectionToString(http.baseUrl + "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&phrase=pairchallenge"); + String pairChallenge = http.openHttpConnectionToString(http.baseUrl + + "/pair?uniqueid="+uniqueId+"&devicename=roth&updateState=1&phrase=pairchallenge", true); if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) { - http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId); + http.openHttpConnectionToString(http.baseUrl + "/unpair?uniqueid="+uniqueId, true); return PairState.FAILED; }