Add support for zero configuration IPv6 streaming

This commit is contained in:
Cameron Gutman 2019-07-13 20:43:14 -07:00
parent ba0171221c
commit 83b66b19de
5 changed files with 166 additions and 57 deletions

View File

@ -8,6 +8,7 @@ import java.security.cert.X509Certificate;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Scanner;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
@ -18,16 +19,16 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteException;
public class ComputerDatabaseManager { 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_TABLE_NAME = "Computers";
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID"; private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName"; private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
private static final String LOCAL_ADDRESS_COLUMN_NAME = "LocalAddress"; private static final String ADDRESSES_COLUMN_NAME = "Addresses";
private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress";
private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress";
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress"; private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert"; private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
private static final char ADDRESS_DELIMITER = ';';
private SQLiteDatabase computerDb; private SQLiteDatabase computerDb;
public ComputerDatabaseManager(Context c) { public ComputerDatabaseManager(Context c) {
@ -47,27 +48,21 @@ public class ComputerDatabaseManager {
} }
private void initializeDb(Context c) { 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 // Create tables if they aren't already there
computerDb.execSQL(String.format((Locale)null, 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)", "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_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME, ADDRESSES_COLUMN_NAME, MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
LOCAL_ADDRESS_COLUMN_NAME, REMOTE_ADDRESS_COLUMN_NAME, MANUAL_ADDRESS_COLUMN_NAME,
MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
// Move all computers from the old DB (if any) to the new one // Move all computers from the old DB (if any) to the new one
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c); List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
for (ComputerDetails computer : oldComputers) { for (ComputerDetails computer : oldComputers) {
updateComputer(computer); updateComputer(computer);
} }
oldComputers = LegacyDatabaseReader2.migrateAllComputers(c);
for (ComputerDetails computer : oldComputers) {
updateComputer(computer);
}
} }
public void deleteComputer(String name) { public void deleteComputer(String name) {
@ -78,9 +73,14 @@ public class ComputerDatabaseManager {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid); values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid);
values.put(COMPUTER_NAME_COLUMN_NAME, details.name); values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress);
values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress); StringBuilder addresses = new StringBuilder();
values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress); 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); values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
try { try {
if (details.serverCert != null) { if (details.serverCert != null) {
@ -96,18 +96,31 @@ public class ComputerDatabaseManager {
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); 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) { private ComputerDetails getComputerFromCursor(Cursor c) {
ComputerDetails details = new ComputerDetails(); ComputerDetails details = new ComputerDetails();
details.uuid = c.getString(0); details.uuid = c.getString(0);
details.name = c.getString(1); details.name = c.getString(1);
details.localAddress = c.getString(2);
details.remoteAddress = c.getString(3); Scanner s = new Scanner(c.getString(2)).useDelimiter(""+ADDRESS_DELIMITER);
details.manualAddress = c.getString(4);
details.macAddress = c.getString(5); 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 { try {
byte[] derCertData = c.getBlob(6); byte[] derCertData = c.getBlob(4);
if (derCertData != null) { if (derCertData != null) {
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509") details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")

View File

@ -220,8 +220,8 @@ public class ComputerManagerService extends Service {
} }
} }
public boolean addComputerBlocking(String addr, boolean manuallyAdded) { public boolean addComputerBlocking(ComputerDetails fakeDetails) {
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded); return ComputerManagerService.this.addComputerBlocking(fakeDetails);
} }
public void removeComputer(String name) { public void removeComputer(String name) {
@ -297,8 +297,25 @@ public class ComputerManagerService extends Service {
return new MdnsDiscoveryListener() { return new MdnsDiscoveryListener() {
@Override @Override
public void notifyComputerAdded(MdnsComputer computer) { 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 // 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 @Override
@ -345,24 +362,7 @@ public class ComputerManagerService extends Service {
} }
} }
public boolean addComputerBlocking(String addr, boolean manuallyAdded) { public boolean addComputerBlocking(ComputerDetails fakeDetails) {
// 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);
}
// Block while we try to fill the details // Block while we try to fill the details
try { try {
// We cannot use runPoll() here because it will attempt to persist the state of the machine // 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; return true;
} }
else { else {
if (!manuallyAdded) {
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
}
return false; return false;
} }
} }
@ -514,14 +510,16 @@ public class ComputerManagerService extends Service {
t.start(); 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[] remoteInfo = new boolean[2];
final boolean[] localInfo = new boolean[2]; final boolean[] localInfo = new boolean[2];
final boolean[] manualInfo = new boolean[2]; final boolean[] manualInfo = new boolean[2];
final boolean[] ipv6Info = new boolean[2];
startFastPollThread(localAddress, localInfo); startFastPollThread(localAddress, localInfo);
startFastPollThread(remoteAddress, remoteInfo); startFastPollThread(remoteAddress, remoteInfo);
startFastPollThread(manualAddress, manualInfo); startFastPollThread(manualAddress, manualInfo);
startFastPollThread(ipv6Address, ipv6Info);
// Check local first // Check local first
synchronized (localInfo) { synchronized (localInfo) {
@ -545,7 +543,7 @@ public class ComputerManagerService extends Service {
} }
} }
// And finally, remote // Now remote IPv4
synchronized (remoteInfo) { synchronized (remoteInfo) {
while (!remoteInfo[0]) { while (!remoteInfo[0]) {
remoteInfo.wait(500); 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; return null;
} }
@ -566,8 +575,8 @@ public class ComputerManagerService extends Service {
// Do not write this address to details.activeAddress because: // 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) // 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 // 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 +")"); 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); String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress, details.ipv6Address);
LimeLog.info("Fast poll for "+details.name+" returned candidate address: "+candidateAddress); 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 // 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 // already tried
HashSet<String> uniqueAddresses = new HashSet<>(); HashSet<String> uniqueAddresses = new HashSet<>();
uniqueAddresses.add(details.localAddress); uniqueAddresses.add(details.localAddress);
uniqueAddresses.add(details.remoteAddress);
uniqueAddresses.add(details.manualAddress); uniqueAddresses.add(details.manualAddress);
uniqueAddresses.add(details.remoteAddress);
uniqueAddresses.add(details.ipv6Address);
for (String addr : uniqueAddresses) { for (String addr : uniqueAddresses) {
if (addr == null || addr.equals(candidateAddress)) { if (addr == null || addr.equals(candidateAddress)) {
continue; continue;

View File

@ -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<ComputerDetails> getAllComputers(SQLiteDatabase computerDb) {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> 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<ComputerDetails> 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<ComputerDetails>();
} finally {
// Close and delete the old DB
if (computerDb != null) {
computerDb.close();
}
c.deleteDatabase(COMPUTER_DB_NAME);
}
}
}

View File

@ -11,6 +11,7 @@ import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService; import com.limelight.computers.ComputerManagerService;
import com.limelight.R; import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog; import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
@ -98,7 +99,9 @@ public class AddComputerManually extends Activity {
getResources().getString(R.string.msg_add_pc), false); getResources().getString(R.string.msg_add_pc), false);
try { try {
success = managerBinder.addComputerBlocking(host, true); ComputerDetails details = new ComputerDetails();
details.manualAddress = host;
success = managerBinder.addComputerBlocking(details);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name. // 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 // https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705

@ -1 +1 @@
Subproject commit 10346a285be604ca0dcafe96b337734abde89da5 Subproject commit 3cfdf4dae21bfe38abd32107474f2619fea82078