mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-04-08 08:56:10 +00:00
Migrate all files to AAR project
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package com.limelight.nvstream.http;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.UUID;
|
||||
|
||||
|
||||
public class ComputerDetails {
|
||||
public enum State {
|
||||
ONLINE, OFFLINE, UNKNOWN
|
||||
}
|
||||
public enum Reachability {
|
||||
LOCAL, REMOTE, OFFLINE, UNKNOWN
|
||||
}
|
||||
|
||||
public State state;
|
||||
public Reachability reachability;
|
||||
public String name;
|
||||
public UUID uuid;
|
||||
public InetAddress localIp;
|
||||
public InetAddress remoteIp;
|
||||
public PairingManager.PairState pairState;
|
||||
public String macAddress;
|
||||
public int runningGameId;
|
||||
public String rawAppList;
|
||||
|
||||
public ComputerDetails() {
|
||||
// Use defaults
|
||||
state = State.UNKNOWN;
|
||||
reachability = Reachability.UNKNOWN;
|
||||
}
|
||||
|
||||
public ComputerDetails(ComputerDetails details) {
|
||||
// Copy details from the other computer
|
||||
update(details);
|
||||
}
|
||||
|
||||
public void update(ComputerDetails details) {
|
||||
this.state = details.state;
|
||||
this.reachability = details.reachability;
|
||||
this.name = details.name;
|
||||
this.uuid = details.uuid;
|
||||
this.localIp = details.localIp;
|
||||
this.remoteIp = details.remoteIp;
|
||||
this.macAddress = details.macAddress;
|
||||
this.pairState = details.pairState;
|
||||
this.runningGameId = details.runningGameId;
|
||||
this.rawAppList = details.rawAppList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder str = new StringBuilder();
|
||||
str.append("State: ").append(state).append("\n");
|
||||
str.append("Reachability: ").append(reachability).append("\n");
|
||||
str.append("Name: ").append(name).append("\n");
|
||||
str.append("UUID: ").append(uuid).append("\n");
|
||||
str.append("Local IP: ").append(localIp).append("\n");
|
||||
str.append("Remote IP: ").append(remoteIp).append("\n");
|
||||
str.append("MAC Address: ").append(macAddress).append("\n");
|
||||
str.append("Pair State: ").append(pairState).append("\n");
|
||||
str.append("Running Game ID: ").append(runningGameId).append("\n");
|
||||
return str.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.limelight.nvstream.http;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class GfeHttpResponseException extends IOException {
|
||||
private static final long serialVersionUID = 1543508830807804222L;
|
||||
|
||||
private int errorCode;
|
||||
private String errorMsg;
|
||||
|
||||
public GfeHttpResponseException(int errorCode, String errorMsg) {
|
||||
this.errorCode = errorCode;
|
||||
this.errorMsg = errorMsg;
|
||||
}
|
||||
|
||||
public int getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMsg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return "GFE error: "+errorMsg+" (Error code: "+errorCode+")";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.limelight.nvstream.http;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
|
||||
public interface LimelightCryptoProvider {
|
||||
public X509Certificate getClientCertificate();
|
||||
public RSAPrivateKey getClientPrivateKey();
|
||||
public byte[] getPemEncodedClientCertificate();
|
||||
public String encodeBase64String(byte[] data);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.limelight.nvstream.http;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
public class NvApp {
|
||||
private String appName = "";
|
||||
private int appId;
|
||||
private boolean initialized;
|
||||
|
||||
public NvApp() {}
|
||||
|
||||
public NvApp(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public NvApp(String appName, int appId) {
|
||||
this.appName = appName;
|
||||
this.appId = appId;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public void setAppName(String appName) {
|
||||
this.appName = appName;
|
||||
}
|
||||
|
||||
public void setAppId(String appId) {
|
||||
try {
|
||||
this.appId = Integer.parseInt(appId);
|
||||
this.initialized = true;
|
||||
} catch (NumberFormatException e) {
|
||||
LimeLog.warning("Malformed app ID: "+appId);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAppId(int appId) {
|
||||
this.appId = appId;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
public String getAppName() {
|
||||
return this.appName;
|
||||
}
|
||||
|
||||
public int getAppId() {
|
||||
return this.appId;
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return this.initialized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,652 @@
|
||||
package com.limelight.nvstream.http;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.MalformedURLException;
|
||||
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.ListIterator;
|
||||
import java.util.Scanner;
|
||||
import java.util.Stack;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
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;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.ConnectionContext;
|
||||
import com.limelight.nvstream.http.PairingManager.PairState;
|
||||
import com.squareup.okhttp.ConnectionPool;
|
||||
import com.squareup.okhttp.OkHttpClient;
|
||||
import com.squareup.okhttp.Request;
|
||||
import com.squareup.okhttp.Response;
|
||||
import com.squareup.okhttp.ResponseBody;
|
||||
|
||||
|
||||
public class NvHTTP {
|
||||
private String uniqueId;
|
||||
private PairingManager pm;
|
||||
private InetAddress address;
|
||||
|
||||
public static final int HTTPS_PORT = 47984;
|
||||
public static final int HTTP_PORT = 47989;
|
||||
public static final int CONNECTION_TIMEOUT = 3000;
|
||||
public static final int READ_TIMEOUT = 5000;
|
||||
|
||||
private static boolean verbose = false;
|
||||
|
||||
public String baseUrlHttps;
|
||||
public String baseUrlHttp;
|
||||
|
||||
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.setConnectionPool(new ConnectionPool(0, 0));
|
||||
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;
|
||||
|
||||
String safeAddress;
|
||||
if (host instanceof Inet6Address) {
|
||||
// RFC2732-formatted IPv6 address for use in URL
|
||||
safeAddress = "["+host.getHostAddress()+"]";
|
||||
}
|
||||
else {
|
||||
safeAddress = host.getHostAddress();
|
||||
}
|
||||
|
||||
initializeHttpState(cryptoProvider);
|
||||
|
||||
this.baseUrlHttps = "https://" + safeAddress + ":" + HTTPS_PORT;
|
||||
this.baseUrlHttp = "http://" + safeAddress + ":" + HTTP_PORT;
|
||||
this.pm = new PairingManager(this, cryptoProvider);
|
||||
}
|
||||
|
||||
String buildUniqueIdUuidString() {
|
||||
return "uniqueid="+uniqueId+"&uuid="+UUID.randomUUID();
|
||||
}
|
||||
|
||||
static String getXmlString(Reader r, String tagname) throws XmlPullParserException, IOException {
|
||||
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
XmlPullParser xpp = factory.newPullParser();
|
||||
|
||||
xpp.setInput(r);
|
||||
int eventType = xpp.getEventType();
|
||||
Stack<String> currentTag = new Stack<String>();
|
||||
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
switch (eventType) {
|
||||
case (XmlPullParser.START_TAG):
|
||||
if (xpp.getName().equals("root")) {
|
||||
verifyResponseStatus(xpp);
|
||||
}
|
||||
currentTag.push(xpp.getName());
|
||||
break;
|
||||
case (XmlPullParser.END_TAG):
|
||||
currentTag.pop();
|
||||
break;
|
||||
case (XmlPullParser.TEXT):
|
||||
if (currentTag.peek().equals(tagname)) {
|
||||
return xpp.getText().trim();
|
||||
}
|
||||
break;
|
||||
}
|
||||
eventType = xpp.next();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static String getXmlString(String str, String tagname) throws XmlPullParserException, IOException {
|
||||
return getXmlString(new StringReader(str), tagname);
|
||||
}
|
||||
|
||||
static String getXmlString(InputStream in, String tagname) throws XmlPullParserException, IOException {
|
||||
return getXmlString(new InputStreamReader(in), tagname);
|
||||
}
|
||||
|
||||
private static void verifyResponseStatus(XmlPullParser xpp) throws GfeHttpResponseException {
|
||||
int statusCode = Integer.parseInt(xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code"));
|
||||
if (statusCode != 200) {
|
||||
throw new GfeHttpResponseException(statusCode, xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message"));
|
||||
}
|
||||
}
|
||||
|
||||
public String getServerInfo() throws MalformedURLException, IOException, XmlPullParserException {
|
||||
String resp;
|
||||
|
||||
//
|
||||
// TODO: Shield Hub uses HTTP for this and is able to get an accurate PairStatus with HTTP.
|
||||
// For some reason, we always see PairStatus is 0 over HTTP and only 1 over HTTPS. It looks
|
||||
// like there are extra request headers required to make this stuff work over HTTP.
|
||||
//
|
||||
|
||||
try {
|
||||
resp = openHttpConnectionToString(baseUrlHttps + "/serverinfo?"+buildUniqueIdUuidString(), true);
|
||||
|
||||
// This will throw an exception if the request came back with a failure status.
|
||||
// We want this because it will throw us into the HTTP case if the client is unpaired.
|
||||
getServerVersion(resp);
|
||||
}
|
||||
catch (GfeHttpResponseException e) {
|
||||
if (e.getErrorCode() == 401) {
|
||||
// Cert validation error - fall back to HTTP
|
||||
return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true);
|
||||
}
|
||||
|
||||
// If it's not a cert validation error, throw it
|
||||
throw e;
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
public ComputerDetails getComputerDetails() throws MalformedURLException, IOException, XmlPullParserException {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
String serverInfo = getServerInfo();
|
||||
|
||||
details.name = getXmlString(serverInfo, "hostname");
|
||||
details.uuid = UUID.fromString(getXmlString(serverInfo, "uniqueid"));
|
||||
details.macAddress = getXmlString(serverInfo, "mac");
|
||||
|
||||
// If there's no LocalIP field, use the address we hit the server on
|
||||
String localIpStr = getXmlString(serverInfo, "LocalIP");
|
||||
if (localIpStr == null) {
|
||||
localIpStr = address.getHostAddress();
|
||||
}
|
||||
|
||||
// If there's no ExternalIP field, use the address we hit the server on
|
||||
String externalIpStr = getXmlString(serverInfo, "ExternalIP");
|
||||
if (externalIpStr == null) {
|
||||
externalIpStr = address.getHostAddress();
|
||||
}
|
||||
|
||||
details.localIp = InetAddress.getByName(localIpStr);
|
||||
details.remoteIp = InetAddress.getByName(externalIpStr);
|
||||
|
||||
try {
|
||||
details.pairState = Integer.parseInt(getXmlString(serverInfo, "PairStatus")) == 1 ?
|
||||
PairState.PAIRED : PairState.NOT_PAIRED;
|
||||
} catch (NumberFormatException e) {
|
||||
details.pairState = PairState.FAILED;
|
||||
}
|
||||
|
||||
try {
|
||||
details.runningGameId = getCurrentGame(serverInfo);
|
||||
} catch (NumberFormatException e) {
|
||||
details.runningGameId = 0;
|
||||
}
|
||||
|
||||
// We could reach it so it's online
|
||||
details.state = ComputerDetails.State.ONLINE;
|
||||
|
||||
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 ResponseBody openHttpConnection(String url, boolean enableReadTimeout) throws IOException {
|
||||
Request request = new Request.Builder().url(url).build();
|
||||
Response response;
|
||||
|
||||
if (enableReadTimeout) {
|
||||
performAndroidTlsHack(httpClientWithReadTimeout);
|
||||
response = httpClientWithReadTimeout.newCall(request).execute();
|
||||
}
|
||||
else {
|
||||
performAndroidTlsHack(httpClient);
|
||||
response = httpClient.newCall(request).execute();
|
||||
}
|
||||
|
||||
ResponseBody body = response.body();
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
return body;
|
||||
}
|
||||
|
||||
// Unsuccessful, so close the response body
|
||||
try {
|
||||
if (body != null) {
|
||||
body.close();
|
||||
}
|
||||
} catch (IOException e) {}
|
||||
|
||||
if (response.code() == 404) {
|
||||
throw new FileNotFoundException(url);
|
||||
}
|
||||
else {
|
||||
throw new IOException("HTTP request failed: "+response.code());
|
||||
}
|
||||
}
|
||||
|
||||
String openHttpConnectionToString(String url, boolean enableReadTimeout) throws MalformedURLException, IOException {
|
||||
if (verbose) {
|
||||
LimeLog.info("Requesting URL: "+url);
|
||||
}
|
||||
|
||||
ResponseBody resp;
|
||||
try {
|
||||
resp = openHttpConnection(url, enableReadTimeout);
|
||||
} catch (IOException e) {
|
||||
if (verbose) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
StringBuilder strb = new StringBuilder();
|
||||
try {
|
||||
Scanner s = new Scanner(resp.byteStream());
|
||||
try {
|
||||
while (s.hasNext()) {
|
||||
strb.append(s.next());
|
||||
strb.append(' ');
|
||||
}
|
||||
} finally {
|
||||
s.close();
|
||||
}
|
||||
} finally {
|
||||
resp.close();
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
LimeLog.info(url+" -> "+strb.toString());
|
||||
}
|
||||
|
||||
return strb.toString();
|
||||
}
|
||||
|
||||
public String getServerVersion(String serverInfo) throws XmlPullParserException, IOException {
|
||||
return getXmlString(serverInfo, "appversion");
|
||||
}
|
||||
|
||||
public PairingManager.PairState getPairState() throws IOException, XmlPullParserException {
|
||||
return pm.getPairState(getServerInfo());
|
||||
}
|
||||
|
||||
public PairingManager.PairState getPairState(String serverInfo) throws IOException, XmlPullParserException {
|
||||
return pm.getPairState(serverInfo);
|
||||
}
|
||||
|
||||
public long getMaxLumaPixelsH264(String serverInfo) throws XmlPullParserException, IOException {
|
||||
String str = getXmlString(serverInfo, "MaxLumaPixelsH264");
|
||||
if (str != null) {
|
||||
try {
|
||||
return Long.parseLong(str);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public long getMaxLumaPixelsHEVC(String serverInfo) throws XmlPullParserException, IOException {
|
||||
String str = getXmlString(serverInfo, "MaxLumaPixelsHEVC");
|
||||
if (str != null) {
|
||||
try {
|
||||
return Long.parseLong(str);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public String getGpuType(String serverInfo) throws XmlPullParserException, IOException {
|
||||
return getXmlString(serverInfo, "gputype");
|
||||
}
|
||||
|
||||
public boolean supports4K(String serverInfo) throws XmlPullParserException, IOException {
|
||||
// serverinfo returns supported resolutions in descending order, so getting the first
|
||||
// height will give us whether we support 4K. If this is not present, we don't support
|
||||
// 4K.
|
||||
String heightStr = getXmlString(serverInfo, "Height");
|
||||
if (heightStr == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only allow 4K on GFE 3.x
|
||||
String gfeVersionStr = getXmlString(serverInfo, "GfeVersion");
|
||||
if (gfeVersionStr == null || gfeVersionStr.startsWith("2.")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (Integer.parseInt(heightStr) >= 2160) {
|
||||
// Found a 4K resolution in the list
|
||||
return true;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean supports4K60(String serverInfo) throws XmlPullParserException, IOException {
|
||||
// If we don't support 4K at all, bail early
|
||||
if (!supports4K(serverInfo)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// serverinfo returns supported resolutions in descending order, so getting the first
|
||||
// refresh rate will give us whether we support 4K60. If this is 30, we don't support
|
||||
// 4K 60 FPS.
|
||||
String fpsStr = getXmlString(serverInfo, "RefreshRate");
|
||||
if (fpsStr == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (Integer.parseInt(fpsStr) >= 60) {
|
||||
// 4K supported and 60 FPS is the first entry
|
||||
return true;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getCurrentGame(String serverInfo) throws IOException, XmlPullParserException {
|
||||
// GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer
|
||||
// has the semantics that its name would indicate. To contain the effects of this change as much
|
||||
// as possible, we'll force the current game to zero if the server isn't in a streaming session.
|
||||
String serverState = getXmlString(serverInfo, "state");
|
||||
if (serverState != null && !serverState.endsWith("_SERVER_AVAILABLE")) {
|
||||
String game = getXmlString(serverInfo, "currentgame");
|
||||
return Integer.parseInt(game);
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCurrentClient(String serverInfo) throws XmlPullParserException, IOException {
|
||||
String currentClient = getXmlString(serverInfo, "CurrentClient");
|
||||
if (currentClient != null) {
|
||||
return !currentClient.equals("0");
|
||||
}
|
||||
else {
|
||||
// For versions of GFE that lack this field, we'll assume we are
|
||||
// the current client. If we're not, we'll get a response error that
|
||||
// will let us know.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public NvApp getAppById(int appId) throws IOException, XmlPullParserException {
|
||||
LinkedList<NvApp> appList = getAppList();
|
||||
for (NvApp appFromList : appList) {
|
||||
if (appFromList.getAppId() == appId) {
|
||||
return appFromList;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/* NOTE: Only use this function if you know what you're doing.
|
||||
* It's totally valid to have two apps named the same thing,
|
||||
* or even nothing at all! Look apps up by ID if at all possible
|
||||
* using the above function */
|
||||
public NvApp getAppByName(String appName) throws IOException, XmlPullParserException {
|
||||
LinkedList<NvApp> appList = getAppList();
|
||||
for (NvApp appFromList : appList) {
|
||||
if (appFromList.getAppName().equalsIgnoreCase(appName)) {
|
||||
return appFromList;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public PairingManager.PairState pair(String serverInfo, String pin) throws Exception {
|
||||
return pm.pair(serverInfo, pin);
|
||||
}
|
||||
|
||||
public static LinkedList<NvApp> getAppListByReader(Reader r) throws XmlPullParserException, IOException {
|
||||
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||
factory.setNamespaceAware(true);
|
||||
XmlPullParser xpp = factory.newPullParser();
|
||||
|
||||
xpp.setInput(r);
|
||||
int eventType = xpp.getEventType();
|
||||
LinkedList<NvApp> appList = new LinkedList<NvApp>();
|
||||
Stack<String> currentTag = new Stack<String>();
|
||||
boolean rootTerminated = false;
|
||||
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
switch (eventType) {
|
||||
case (XmlPullParser.START_TAG):
|
||||
if (xpp.getName().equals("root")) {
|
||||
verifyResponseStatus(xpp);
|
||||
}
|
||||
currentTag.push(xpp.getName());
|
||||
if (xpp.getName().equals("App")) {
|
||||
appList.addLast(new NvApp());
|
||||
}
|
||||
break;
|
||||
case (XmlPullParser.END_TAG):
|
||||
currentTag.pop();
|
||||
if (xpp.getName().equals("root")) {
|
||||
rootTerminated = true;
|
||||
}
|
||||
break;
|
||||
case (XmlPullParser.TEXT):
|
||||
NvApp app = appList.getLast();
|
||||
if (currentTag.peek().equals("AppTitle")) {
|
||||
app.setAppName(xpp.getText().trim());
|
||||
} else if (currentTag.peek().equals("ID")) {
|
||||
app.setAppId(xpp.getText().trim());
|
||||
}
|
||||
break;
|
||||
}
|
||||
eventType = xpp.next();
|
||||
}
|
||||
|
||||
// Throw a malformed XML exception if we've not seen the root tag ended
|
||||
if (!rootTerminated) {
|
||||
throw new XmlPullParserException("Malformed XML: Root tag was not terminated");
|
||||
}
|
||||
|
||||
// Ensure that all apps in the list are initialized
|
||||
ListIterator<NvApp> i = appList.listIterator();
|
||||
while (i.hasNext()) {
|
||||
NvApp app = i.next();
|
||||
|
||||
// Remove uninitialized apps
|
||||
if (!app.isInitialized()) {
|
||||
LimeLog.warning("GFE returned incomplete app: "+app.getAppId()+" "+app.getAppName());
|
||||
i.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return appList;
|
||||
}
|
||||
|
||||
public String getAppListRaw() throws MalformedURLException, IOException {
|
||||
return openHttpConnectionToString(baseUrlHttps + "/applist?"+buildUniqueIdUuidString(), true);
|
||||
}
|
||||
|
||||
public LinkedList<NvApp> getAppList() throws GfeHttpResponseException, IOException, XmlPullParserException {
|
||||
if (verbose) {
|
||||
// Use the raw function so the app list is printed
|
||||
return getAppListByReader(new StringReader(getAppListRaw()));
|
||||
}
|
||||
else {
|
||||
ResponseBody resp = openHttpConnection(baseUrlHttps + "/applist?" + buildUniqueIdUuidString(), true);
|
||||
LinkedList<NvApp> appList = getAppListByReader(new InputStreamReader(resp.byteStream()));
|
||||
resp.close();
|
||||
return appList;
|
||||
}
|
||||
}
|
||||
|
||||
public void unpair() throws IOException {
|
||||
openHttpConnectionToString(baseUrlHttps + "/unpair?"+buildUniqueIdUuidString(), true);
|
||||
}
|
||||
|
||||
public InputStream getBoxArt(NvApp app) throws IOException {
|
||||
ResponseBody resp = openHttpConnection(baseUrlHttps + "/appasset?"+ buildUniqueIdUuidString() +
|
||||
"&appid=" + app.getAppId() + "&AssetType=2&AssetIdx=0", true);
|
||||
return resp.byteStream();
|
||||
}
|
||||
|
||||
public int getServerMajorVersion(String serverInfo) throws XmlPullParserException, IOException {
|
||||
int[] appVersionQuad = getServerAppVersionQuad(serverInfo);
|
||||
if (appVersionQuad != null) {
|
||||
return appVersionQuad[0];
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int[] getServerAppVersionQuad(String serverInfo) throws XmlPullParserException, IOException {
|
||||
try {
|
||||
String serverVersion = getServerVersion(serverInfo);
|
||||
if (serverVersion == null) {
|
||||
LimeLog.warning("Missing server version field");
|
||||
return null;
|
||||
}
|
||||
String[] serverVersionSplit = serverVersion.split("\\.");
|
||||
if (serverVersionSplit.length != 4) {
|
||||
LimeLog.warning("Malformed server version field");
|
||||
return null;
|
||||
}
|
||||
int[] ret = new int[serverVersionSplit.length];
|
||||
for (int i = 0; i < ret.length; i++) {
|
||||
ret[i] = Integer.parseInt(serverVersionSplit[i]);
|
||||
}
|
||||
return ret;
|
||||
} catch (NumberFormatException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for ( int j = 0; j < bytes.length; j++ ) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = hexArray[v >>> 4];
|
||||
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
public boolean launchApp(ConnectionContext context, int appId) throws IOException, XmlPullParserException {
|
||||
String xmlStr = openHttpConnectionToString(baseUrlHttps +
|
||||
"/launch?" + buildUniqueIdUuidString() +
|
||||
"&appid=" + appId +
|
||||
"&mode=" + context.negotiatedWidth + "x" + context.negotiatedHeight + "x" + context.negotiatedFps +
|
||||
"&additionalStates=1&sops=" + (context.streamConfig.getSops() ? 1 : 0) +
|
||||
"&rikey="+bytesToHex(context.riKey.getEncoded()) +
|
||||
"&rikeyid="+context.riKeyId +
|
||||
"&localAudioPlayMode=" + (context.streamConfig.getPlayLocalAudio() ? 1 : 0) +
|
||||
"&surroundAudioInfo=" + ((context.streamConfig.getAudioChannelMask() << 16) + context.streamConfig.getAudioChannelCount()),
|
||||
false);
|
||||
String gameSession = getXmlString(xmlStr, "gamesession");
|
||||
return gameSession != null && !gameSession.equals("0");
|
||||
}
|
||||
|
||||
public boolean resumeApp(ConnectionContext context) throws IOException, XmlPullParserException {
|
||||
String xmlStr = openHttpConnectionToString(baseUrlHttps + "/resume?" + buildUniqueIdUuidString() +
|
||||
"&rikey="+bytesToHex(context.riKey.getEncoded()) +
|
||||
"&rikeyid="+context.riKeyId, false);
|
||||
String resume = getXmlString(xmlStr, "resume");
|
||||
return Integer.parseInt(resume) != 0;
|
||||
}
|
||||
|
||||
public boolean quitApp() throws IOException, XmlPullParserException {
|
||||
// First check if this client is allowed to quit the app. Newer GFE versions
|
||||
// will just return success even if quitting fails if we're not the original requestor.
|
||||
if (!isCurrentClient(getServerInfo())) {
|
||||
// Generate a synthetic GfeResponseException letting the caller know
|
||||
// that they can't kill someone else's stream.
|
||||
throw new GfeHttpResponseException(599, "");
|
||||
}
|
||||
|
||||
String xmlStr = openHttpConnectionToString(baseUrlHttps + "/cancel?" + buildUniqueIdUuidString(), false);
|
||||
String cancel = getXmlString(xmlStr, "cancel");
|
||||
return Integer.parseInt(cancel) != 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
package com.limelight.nvstream.http;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.io.*;
|
||||
import java.net.MalformedURLException;
|
||||
import java.security.*;
|
||||
import java.security.cert.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
|
||||
public class PairingManager {
|
||||
|
||||
private NvHTTP http;
|
||||
|
||||
private PrivateKey pk;
|
||||
private X509Certificate cert;
|
||||
private SecretKey aesKey;
|
||||
private byte[] pemCertBytes;
|
||||
|
||||
public enum PairState {
|
||||
NOT_PAIRED,
|
||||
PAIRED,
|
||||
PIN_WRONG,
|
||||
FAILED,
|
||||
ALREADY_IN_PROGRESS
|
||||
}
|
||||
|
||||
public PairingManager(NvHTTP http, LimelightCryptoProvider cryptoProvider) {
|
||||
this.http = http;
|
||||
this.cert = cryptoProvider.getClientCertificate();
|
||||
this.pemCertBytes = cryptoProvider.getPemEncodedClientCertificate();
|
||||
this.pk = cryptoProvider.getClientPrivateKey();
|
||||
}
|
||||
|
||||
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for ( int j = 0; j < bytes.length; j++ ) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = hexArray[v >>> 4];
|
||||
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
private static byte[] hexToBytes(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||
+ Character.digit(s.charAt(i+1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException, CertificateException
|
||||
{
|
||||
String certText = NvHTTP.getXmlString(text, "plaincert");
|
||||
if (certText != null) {
|
||||
byte[] certBytes = hexToBytes(certText);
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] generateRandomBytes(int length)
|
||||
{
|
||||
byte[] rand = new byte[length];
|
||||
new SecureRandom().nextBytes(rand);
|
||||
return rand;
|
||||
}
|
||||
|
||||
private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodingException {
|
||||
byte[] saltedPin = new byte[salt.length + pin.length()];
|
||||
System.arraycopy(salt, 0, saltedPin, 0, salt.length);
|
||||
System.arraycopy(pin.getBytes("UTF-8"), 0, saltedPin, salt.length, pin.length());
|
||||
return saltedPin;
|
||||
}
|
||||
|
||||
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initVerify(cert.getPublicKey());
|
||||
sig.update(data);
|
||||
return sig.verify(signature);
|
||||
}
|
||||
|
||||
private static byte[] signData(byte[] data, PrivateKey key) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException {
|
||||
Signature sig = Signature.getInstance("SHA256withRSA");
|
||||
sig.initSign(key);
|
||||
sig.update(data);
|
||||
byte[] signature = new byte[256];
|
||||
sig.sign(signature, 0, signature.length);
|
||||
return signature;
|
||||
}
|
||||
|
||||
private static byte[] decryptAes(byte[] encryptedData, SecretKey secretKey) throws NoSuchAlgorithmException, SignatureException,
|
||||
InvalidKeyException, ShortBufferException, IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||
|
||||
int blockRoundedSize = ((encryptedData.length + 15) / 16) * 16;
|
||||
byte[] blockRoundedEncrypted = Arrays.copyOf(encryptedData, blockRoundedSize);
|
||||
byte[] fullDecrypted = new byte[blockRoundedSize];
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey);
|
||||
cipher.doFinal(blockRoundedEncrypted, 0,
|
||||
blockRoundedSize, fullDecrypted);
|
||||
return fullDecrypted;
|
||||
}
|
||||
|
||||
private static byte[] encryptAes(byte[] data, SecretKey secretKey) throws NoSuchAlgorithmException, SignatureException,
|
||||
InvalidKeyException, ShortBufferException, IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException {
|
||||
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
|
||||
|
||||
int blockRoundedSize = ((data.length + 15) / 16) * 16;
|
||||
byte[] blockRoundedData = Arrays.copyOf(data, blockRoundedSize);
|
||||
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
return cipher.doFinal(blockRoundedData);
|
||||
}
|
||||
|
||||
private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
|
||||
byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16);
|
||||
return new SecretKeySpec(aesTruncated, "AES");
|
||||
}
|
||||
|
||||
private static byte[] concatBytes(byte[] a, byte[] b) {
|
||||
byte[] c = new byte[a.length + b.length];
|
||||
System.arraycopy(a, 0, c, 0, a.length);
|
||||
System.arraycopy(b, 0, c, a.length, b.length);
|
||||
return c;
|
||||
}
|
||||
|
||||
public static String generatePinString() {
|
||||
Random r = new Random();
|
||||
return String.format((Locale)null, "%d%d%d%d",
|
||||
r.nextInt(10), r.nextInt(10),
|
||||
r.nextInt(10), r.nextInt(10));
|
||||
}
|
||||
|
||||
public PairState getPairState(String serverInfo) throws MalformedURLException, IOException, XmlPullParserException {
|
||||
if (!NvHTTP.getXmlString(serverInfo, "PairStatus").equals("1")) {
|
||||
return PairState.NOT_PAIRED;
|
||||
}
|
||||
|
||||
return PairState.PAIRED;
|
||||
}
|
||||
|
||||
public PairState pair(String serverInfo, String pin) throws MalformedURLException, IOException, XmlPullParserException, CertificateException, InvalidKeyException, NoSuchAlgorithmException, SignatureException, ShortBufferException, IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException {
|
||||
PairingHashAlgorithm hashAlgo;
|
||||
|
||||
int serverMajorVersion = http.getServerMajorVersion(serverInfo);
|
||||
LimeLog.info("Pairing with server generation: "+serverMajorVersion);
|
||||
if (serverMajorVersion >= 7) {
|
||||
// Gen 7+ uses SHA-256 hashing
|
||||
hashAlgo = new Sha256PairingHash();
|
||||
}
|
||||
else {
|
||||
// Prior to Gen 7, SHA-1 is used
|
||||
hashAlgo = new Sha1PairingHash();
|
||||
}
|
||||
|
||||
// Generate a salt for hashing the PIN
|
||||
byte[] salt = generateRandomBytes(16);
|
||||
|
||||
// Combine the salt and pin, then create an AES key from them
|
||||
byte[] saltAndPin = saltPin(salt, pin);
|
||||
aesKey = generateAesKey(hashAlgo, saltAndPin);
|
||||
|
||||
// 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.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=getservercert&salt="+
|
||||
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
|
||||
false);
|
||||
if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) {
|
||||
return PairState.FAILED;
|
||||
}
|
||||
X509Certificate serverCert = extractPlainCert(getCert);
|
||||
if (serverCert == null) {
|
||||
// Attempting to pair while another device is pairing will cause GFE
|
||||
// to give an empty cert in the response.
|
||||
return PairState.ALREADY_IN_PROGRESS;
|
||||
}
|
||||
|
||||
// Generate a random challenge and encrypt it with our AES key
|
||||
byte[] randomChallenge = generateRandomBytes(16);
|
||||
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);
|
||||
|
||||
// Send the encrypted challenge to the server
|
||||
String challengeResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Decode the server's response and subsequent challenge
|
||||
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse"));
|
||||
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
|
||||
|
||||
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
|
||||
byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, hashAlgo.getHashLength(), hashAlgo.getHashLength() + 16);
|
||||
|
||||
// Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge
|
||||
byte[] clientSecret = generateRandomBytes(16);
|
||||
byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
|
||||
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
|
||||
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Get the server's signed secret
|
||||
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret"));
|
||||
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
|
||||
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272);
|
||||
|
||||
// Ensure the authenticity of the data
|
||||
if (!verifySignature(serverSecret, serverSignature, serverCert)) {
|
||||
// Cancel the pairing process
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
|
||||
// Looks like a MITM
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Ensure the server challenge matched what we expected (aka the PIN was correct)
|
||||
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
|
||||
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
|
||||
// Cancel the pairing process
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
|
||||
// Probably got the wrong PIN
|
||||
return PairState.PIN_WRONG;
|
||||
}
|
||||
|
||||
// Send the server our signed secret
|
||||
byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk));
|
||||
String clientSecretResp = http.openHttpConnectionToString(http.baseUrlHttp +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret),
|
||||
true);
|
||||
if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
// Do the initial challenge (seems neccessary for us to show as paired)
|
||||
String pairChallenge = http.openHttpConnectionToString(http.baseUrlHttps +
|
||||
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=pairchallenge", true);
|
||||
if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) {
|
||||
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
|
||||
return PairState.FAILED;
|
||||
}
|
||||
|
||||
return PairState.PAIRED;
|
||||
}
|
||||
|
||||
private static interface PairingHashAlgorithm {
|
||||
public int getHashLength();
|
||||
public byte[] hashData(byte[] data);
|
||||
}
|
||||
|
||||
private static class Sha1PairingHash implements PairingHashAlgorithm {
|
||||
public int getHashLength() {
|
||||
return 20;
|
||||
}
|
||||
|
||||
public byte[] hashData(byte[] data) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
return md.digest(data);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
// Shouldn't ever happen
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class Sha256PairingHash implements PairingHashAlgorithm {
|
||||
public int getHashLength() {
|
||||
return 32;
|
||||
}
|
||||
|
||||
public byte[] hashData(byte[] data) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
return md.digest(data);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
// Shouldn't ever happen
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user