From 83b66b19de03d65e350c804109731b7c9993257e Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sat, 13 Jul 2019 20:43:14 -0700 Subject: [PATCH] Add support for zero configuration IPv6 streaming --- .../computers/ComputerDatabaseManager.java | 63 ++++++++------ .../computers/ComputerManagerService.java | 70 +++++++++------- .../computers/LegacyDatabaseReader2.java | 83 +++++++++++++++++++ .../preferences/AddComputerManually.java | 5 +- moonlight-common | 2 +- 5 files changed, 166 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/com/limelight/computers/LegacyDatabaseReader2.java diff --git a/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java b/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java index d2456bda..515f9220 100644 --- a/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java +++ b/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java @@ -8,6 +8,7 @@ import java.security.cert.X509Certificate; import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Scanner; import com.limelight.nvstream.http.ComputerDetails; @@ -18,16 +19,16 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; public class ComputerDatabaseManager { - private static final String COMPUTER_DB_NAME = "computers2.db"; + private static final String COMPUTER_DB_NAME = "computers3.db"; private static final String COMPUTER_TABLE_NAME = "Computers"; private static final String COMPUTER_UUID_COLUMN_NAME = "UUID"; private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName"; - private static final String LOCAL_ADDRESS_COLUMN_NAME = "LocalAddress"; - private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress"; - private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress"; + private static final String ADDRESSES_COLUMN_NAME = "Addresses"; private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress"; private static final String SERVER_CERT_COLUMN_NAME = "ServerCert"; + private static final char ADDRESS_DELIMITER = ';'; + private SQLiteDatabase computerDb; public ComputerDatabaseManager(Context c) { @@ -47,27 +48,21 @@ public class ComputerDatabaseManager { } private void initializeDb(Context c) { - // Add cert column to the table if not present - try { - computerDb.execSQL(String.format((Locale)null, - "ALTER TABLE %s ADD COLUMN %s TEXT", - COMPUTER_TABLE_NAME, SERVER_CERT_COLUMN_NAME)); - } catch (SQLiteException e) {} - - // Create tables if they aren't already there computerDb.execSQL(String.format((Locale)null, - "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT, %s TEXT, %s TEXT, %s TEXT, %s TEXT)", - COMPUTER_TABLE_NAME, - COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME, - LOCAL_ADDRESS_COLUMN_NAME, REMOTE_ADDRESS_COLUMN_NAME, MANUAL_ADDRESS_COLUMN_NAME, - MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME)); + "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT, %s TEXT)", + COMPUTER_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME, + ADDRESSES_COLUMN_NAME, MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME)); // Move all computers from the old DB (if any) to the new one List oldComputers = LegacyDatabaseReader.migrateAllComputers(c); for (ComputerDetails computer : oldComputers) { updateComputer(computer); } + oldComputers = LegacyDatabaseReader2.migrateAllComputers(c); + for (ComputerDetails computer : oldComputers) { + updateComputer(computer); + } } public void deleteComputer(String name) { @@ -78,9 +73,14 @@ public class ComputerDatabaseManager { ContentValues values = new ContentValues(); values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid); values.put(COMPUTER_NAME_COLUMN_NAME, details.name); - values.put(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress); - values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress); - values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress); + + StringBuilder addresses = new StringBuilder(); + addresses.append(details.localAddress != null ? details.localAddress : "").append(ADDRESS_DELIMITER); + addresses.append(details.remoteAddress != null ? details.remoteAddress : "").append(ADDRESS_DELIMITER); + addresses.append(details.manualAddress != null ? details.manualAddress : "").append(ADDRESS_DELIMITER); + addresses.append(details.ipv6Address != null ? details.ipv6Address : "").append(ADDRESS_DELIMITER); + + values.put(ADDRESSES_COLUMN_NAME, addresses.toString()); values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress); try { if (details.serverCert != null) { @@ -96,18 +96,31 @@ public class ComputerDatabaseManager { return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); } + private static String readNonEmptyString(String input) { + if (input.isEmpty()) { + return null; + } + + return input; + } + private ComputerDetails getComputerFromCursor(Cursor c) { ComputerDetails details = new ComputerDetails(); details.uuid = c.getString(0); details.name = c.getString(1); - details.localAddress = c.getString(2); - details.remoteAddress = c.getString(3); - details.manualAddress = c.getString(4); - details.macAddress = c.getString(5); + + Scanner s = new Scanner(c.getString(2)).useDelimiter(""+ADDRESS_DELIMITER); + + details.localAddress = readNonEmptyString(s.next()); + details.remoteAddress = readNonEmptyString(s.next()); + details.manualAddress = readNonEmptyString(s.next()); + details.ipv6Address = readNonEmptyString(s.next()); + + details.macAddress = c.getString(3); try { - byte[] derCertData = c.getBlob(6); + byte[] derCertData = c.getBlob(4); if (derCertData != null) { details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509") diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index 328c779d..f253b9cd 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -220,8 +220,8 @@ public class ComputerManagerService extends Service { } } - public boolean addComputerBlocking(String addr, boolean manuallyAdded) { - return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded); + public boolean addComputerBlocking(ComputerDetails fakeDetails) { + return ComputerManagerService.this.addComputerBlocking(fakeDetails); } public void removeComputer(String name) { @@ -297,8 +297,25 @@ public class ComputerManagerService extends Service { return new MdnsDiscoveryListener() { @Override public void notifyComputerAdded(MdnsComputer computer) { + ComputerDetails details = new ComputerDetails(); + + // Populate the computer template with mDNS info + if (computer.getAddressV4() != null) { + details.localAddress = computer.getAddressV4().getHostAddress(); + } + if (computer.getAddressV6() != null) { + details.ipv6Address = computer.getAddressV6().getHostAddress(); + } + + // Since we're on the same network, we can use STUN to find + // our WAN address, which is also very likely the WAN address + // of the PC. We can use this later to connect remotely. + details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478); + // Kick off a serverinfo poll on this machine - addComputerBlocking(computer.getAddress().getHostAddress(), false); + if (!addComputerBlocking(details)) { + LimeLog.warning("Auto-discovered PC failed to respond: "+details); + } } @Override @@ -345,24 +362,7 @@ public class ComputerManagerService extends Service { } } - public boolean addComputerBlocking(String addr, boolean manuallyAdded) { - // Setup a placeholder - ComputerDetails fakeDetails = new ComputerDetails(); - - if (manuallyAdded) { - // Add PC UI - fakeDetails.manualAddress = addr; - } - else { - // mDNS - fakeDetails.localAddress = addr; - - // Since we're on the same network, we can use STUN to find - // our WAN address, which is also very likely the WAN address - // of the PC. We can use this later to connect remotely. - fakeDetails.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478); - } - + public boolean addComputerBlocking(ComputerDetails fakeDetails) { // Block while we try to fill the details try { // We cannot use runPoll() here because it will attempt to persist the state of the machine @@ -395,10 +395,6 @@ public class ComputerManagerService extends Service { return true; } else { - if (!manuallyAdded) { - LimeLog.warning("Auto-discovered PC failed to respond: "+addr); - } - return false; } } @@ -514,14 +510,16 @@ public class ComputerManagerService extends Service { t.start(); } - private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress) throws InterruptedException { + private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress, final String ipv6Address) throws InterruptedException { final boolean[] remoteInfo = new boolean[2]; final boolean[] localInfo = new boolean[2]; final boolean[] manualInfo = new boolean[2]; + final boolean[] ipv6Info = new boolean[2]; startFastPollThread(localAddress, localInfo); startFastPollThread(remoteAddress, remoteInfo); startFastPollThread(manualAddress, manualInfo); + startFastPollThread(ipv6Address, ipv6Info); // Check local first synchronized (localInfo) { @@ -545,7 +543,7 @@ public class ComputerManagerService extends Service { } } - // And finally, remote + // Now remote IPv4 synchronized (remoteInfo) { while (!remoteInfo[0]) { remoteInfo.wait(500); @@ -556,6 +554,17 @@ public class ComputerManagerService extends Service { } } + // Now global IPv6 + synchronized (ipv6Info) { + while (!ipv6Info[0]) { + ipv6Info.wait(500); + } + + if (ipv6Info[1]) { + return ipv6Address; + } + } + return null; } @@ -566,8 +575,8 @@ public class ComputerManagerService extends Service { // Do not write this address to details.activeAddress because: // a) it's only a candidate and may be wrong (multiple PCs behind a single router) // b) if it's null, it will be unexpectedly nulling the activeAddress of a possibly online PC - LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress +")"); - String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress); + LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress+", "+details.ipv6Address+")"); + String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress, details.ipv6Address); LimeLog.info("Fast poll for "+details.name+" returned candidate address: "+candidateAddress); // If no connection could be established to either IP address, there's nothing we can do @@ -582,8 +591,9 @@ public class ComputerManagerService extends Service { // already tried HashSet uniqueAddresses = new HashSet<>(); uniqueAddresses.add(details.localAddress); - uniqueAddresses.add(details.remoteAddress); uniqueAddresses.add(details.manualAddress); + uniqueAddresses.add(details.remoteAddress); + uniqueAddresses.add(details.ipv6Address); for (String addr : uniqueAddresses) { if (addr == null || addr.equals(candidateAddress)) { continue; diff --git a/app/src/main/java/com/limelight/computers/LegacyDatabaseReader2.java b/app/src/main/java/com/limelight/computers/LegacyDatabaseReader2.java new file mode 100644 index 00000000..37aef2ba --- /dev/null +++ b/app/src/main/java/com/limelight/computers/LegacyDatabaseReader2.java @@ -0,0 +1,83 @@ +package com.limelight.computers; + +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +import com.limelight.nvstream.http.ComputerDetails; + +import java.io.ByteArrayInputStream; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.LinkedList; +import java.util.List; + +public class LegacyDatabaseReader2 { + private static final String COMPUTER_DB_NAME = "computers2.db"; + private static final String COMPUTER_TABLE_NAME = "Computers"; + + private static ComputerDetails getComputerFromCursor(Cursor c) { + ComputerDetails details = new ComputerDetails(); + + details.uuid = c.getString(0); + details.name = c.getString(1); + details.localAddress = c.getString(2); + details.remoteAddress = c.getString(3); + details.manualAddress = c.getString(4); + details.macAddress = c.getString(5); + + try { + byte[] derCertData = c.getBlob(6); + + if (derCertData != null) { + details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509") + .generateCertificate(new ByteArrayInputStream(derCertData)); + } + } catch (CertificateException e) { + e.printStackTrace(); + } + + // This signifies we don't have dynamic state (like pair state) + details.state = ComputerDetails.State.UNKNOWN; + + return details; + } + + public static List getAllComputers(SQLiteDatabase computerDb) { + Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null); + LinkedList computerList = new LinkedList<>(); + while (c.moveToNext()) { + ComputerDetails details = getComputerFromCursor(c); + + // If a critical field is corrupt or missing, skip the database entry + if (details.uuid == null) { + continue; + } + + computerList.add(details); + } + + c.close(); + + return computerList; + } + + public static List migrateAllComputers(Context c) { + SQLiteDatabase computerDb = null; + try { + // Open the existing database + computerDb = SQLiteDatabase.openDatabase(c.getDatabasePath(COMPUTER_DB_NAME).getPath(), null, SQLiteDatabase.OPEN_READONLY); + return getAllComputers(computerDb); + } catch (SQLiteException e) { + return new LinkedList(); + } finally { + // Close and delete the old DB + if (computerDb != null) { + computerDb.close(); + } + c.deleteDatabase(COMPUTER_DB_NAME); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/limelight/preferences/AddComputerManually.java b/app/src/main/java/com/limelight/preferences/AddComputerManually.java index 5a0e4d51..8fd582da 100644 --- a/app/src/main/java/com/limelight/preferences/AddComputerManually.java +++ b/app/src/main/java/com/limelight/preferences/AddComputerManually.java @@ -11,6 +11,7 @@ import java.util.concurrent.LinkedBlockingQueue; import com.limelight.computers.ComputerManagerService; import com.limelight.R; +import com.limelight.nvstream.http.ComputerDetails; import com.limelight.utils.Dialog; import com.limelight.utils.SpinnerDialog; import com.limelight.utils.UiHelper; @@ -98,7 +99,9 @@ public class AddComputerManually extends Activity { getResources().getString(R.string.msg_add_pc), false); try { - success = managerBinder.addComputerBlocking(host, true); + ComputerDetails details = new ComputerDetails(); + details.manualAddress = host; + success = managerBinder.addComputerBlocking(details); } catch (IllegalArgumentException e) { // This can be thrown from OkHttp if the host fails to canonicalize to a valid name. // https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705 diff --git a/moonlight-common b/moonlight-common index 10346a28..3cfdf4da 160000 --- a/moonlight-common +++ b/moonlight-common @@ -1 +1 @@ -Subproject commit 10346a285be604ca0dcafe96b337734abde89da5 +Subproject commit 3cfdf4dae21bfe38abd32107474f2619fea82078