diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index 76d4d487..eafcdbce 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -22,6 +22,7 @@ import com.limelight.nvstream.NvConnectionListener; import com.limelight.nvstream.StreamConfiguration; import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.NvApp; +import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.input.KeyboardPacket; import com.limelight.nvstream.input.MouseButtonPacket; import com.limelight.nvstream.jni.MoonBridge; @@ -166,6 +167,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, }; public static final String EXTRA_HOST = "Host"; + public static final String EXTRA_PORT = "Port"; public static final String EXTRA_APP_NAME = "AppName"; public static final String EXTRA_APP_ID = "AppId"; public static final String EXTRA_UNIQUEID = "UniqueId"; @@ -311,6 +313,7 @@ public class Game extends Activity implements SurfaceHolder.Callback, pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME); String host = Game.this.getIntent().getStringExtra(EXTRA_HOST); + int port = Game.this.getIntent().getIntExtra(EXTRA_PORT, NvHTTP.DEFAULT_HTTP_PORT); int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID); String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID); String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID); @@ -472,7 +475,11 @@ public class Game extends Activity implements SurfaceHolder.Callback, .build(); // Initialize the connection - conn = new NvConnection(getApplicationContext(), host, uniqueId, config, PlatformBinding.getCryptoProvider(this), serverCert, needsInputBatching); + conn = new NvConnection(getApplicationContext(), + new ComputerDetails.AddressTuple(host, port), + uniqueId, config, + PlatformBinding.getCryptoProvider(this), serverCert, + needsInputBatching); controllerHandler = new ControllerHandler(this, conn, this, prefConfig); keyboardTranslator = new KeyboardTranslator(); diff --git a/app/src/main/java/com/limelight/PcView.java b/app/src/main/java/com/limelight/PcView.java index dbb8af8f..a9ea5dc8 100644 --- a/app/src/main/java/com/limelight/PcView.java +++ b/app/src/main/java/com/limelight/PcView.java @@ -400,8 +400,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { stopComputerUpdates(true); httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), - managerBinder.getUniqueId(), - computer.serverCert, + computer.httpsPort, managerBinder.getUniqueId(), computer.serverCert, PlatformBinding.getCryptoProvider(PcView.this)); if (httpConn.getPairState() == PairState.PAIRED) { // Don't display any toast, but open the app list @@ -534,8 +533,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks { String message; try { httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), - managerBinder.getUniqueId(), - computer.serverCert, + computer.httpsPort, managerBinder.getUniqueId(), computer.serverCert, PlatformBinding.getCryptoProvider(PcView.this)); if (httpConn.getPairState() == PairingManager.PairState.PAIRED) { httpConn.unpair(); diff --git a/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java b/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java index 67c3ae02..e3d23c3f 100644 --- a/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java +++ b/app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Locale; import com.limelight.nvstream.http.ComputerDetails; +import com.limelight.nvstream.http.NvHTTP; import android.content.ContentValues; import android.content.Context; @@ -27,6 +28,7 @@ public class ComputerDatabaseManager { private static final String SERVER_CERT_COLUMN_NAME = "ServerCert"; private static final char ADDRESS_DELIMITER = ';'; + private static final char PORT_DELIMITER = '_'; private SQLiteDatabase computerDb; @@ -74,10 +76,10 @@ public class ComputerDatabaseManager { values.put(COMPUTER_NAME_COLUMN_NAME, details.name); StringBuilder addresses = new StringBuilder(); - addresses.append(details.localAddress != null ? details.localAddress : ""); - addresses.append(ADDRESS_DELIMITER).append(details.remoteAddress != null ? details.remoteAddress : ""); - addresses.append(ADDRESS_DELIMITER).append(details.manualAddress != null ? details.manualAddress : ""); - addresses.append(ADDRESS_DELIMITER).append(details.ipv6Address != null ? details.ipv6Address : ""); + addresses.append(details.localAddress != null ? splitTupleToAddress(details.localAddress) : ""); + addresses.append(ADDRESS_DELIMITER).append(details.remoteAddress != null ? splitTupleToAddress(details.remoteAddress) : ""); + addresses.append(ADDRESS_DELIMITER).append(details.manualAddress != null ? splitTupleToAddress(details.manualAddress) : ""); + addresses.append(ADDRESS_DELIMITER).append(details.ipv6Address != null ? splitTupleToAddress(details.ipv6Address) : ""); values.put(ADDRESSES_COLUMN_NAME, addresses.toString()); values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress); @@ -103,6 +105,24 @@ public class ComputerDatabaseManager { return input; } + private static ComputerDetails.AddressTuple splitAddressToTuple(String input) { + if (input == null) { + return null; + } + + String[] parts = input.split(""+PORT_DELIMITER, -1); + if (parts.length == 1) { + return new ComputerDetails.AddressTuple(parts[0], NvHTTP.DEFAULT_HTTP_PORT); + } + else { + return new ComputerDetails.AddressTuple(parts[0], Integer.parseInt(parts[1])); + } + } + + private static String splitTupleToAddress(ComputerDetails.AddressTuple tuple) { + return tuple.address+PORT_DELIMITER+tuple.port; + } + private ComputerDetails getComputerFromCursor(Cursor c) { ComputerDetails details = new ComputerDetails(); @@ -111,10 +131,18 @@ public class ComputerDatabaseManager { String[] addresses = c.getString(2).split(""+ADDRESS_DELIMITER, -1); - details.localAddress = readNonEmptyString(addresses[0]); - details.remoteAddress = readNonEmptyString(addresses[1]); - details.manualAddress = readNonEmptyString(addresses[2]); - details.ipv6Address = readNonEmptyString(addresses[3]); + details.localAddress = splitAddressToTuple(readNonEmptyString(addresses[0])); + details.remoteAddress = splitAddressToTuple(readNonEmptyString(addresses[1])); + details.manualAddress = splitAddressToTuple(readNonEmptyString(addresses[2])); + details.ipv6Address = splitAddressToTuple(readNonEmptyString(addresses[3])); + + // External port is persisted in the remote address field + if (details.remoteAddress != null) { + details.externalPort = details.remoteAddress.port; + } + else { + details.externalPort = NvHTTP.DEFAULT_HTTP_PORT; + } details.macAddress = c.getString(3); diff --git a/app/src/main/java/com/limelight/computers/ComputerManagerService.java b/app/src/main/java/com/limelight/computers/ComputerManagerService.java index f298b744..18f1d383 100644 --- a/app/src/main/java/com/limelight/computers/ComputerManagerService.java +++ b/app/src/main/java/com/limelight/computers/ComputerManagerService.java @@ -139,7 +139,7 @@ public class ComputerManagerService extends Service { // then use STUN to populate the external address field if // it's not set already. if (details.remoteAddress == null) { - InetAddress addr = InetAddress.getByName(details.activeAddress); + InetAddress addr = InetAddress.getByName(details.activeAddress.address); if (addr.isSiteLocalAddress()) { populateExternalAddress(details); } @@ -369,7 +369,12 @@ public class ComputerManagerService extends Service { // Perform the STUN request if we're not on a VPN or if we bound to a network if (!activeNetworkIsVpn || boundToNetwork) { - details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478); + String stunResolvedAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478); + if (stunResolvedAddress != null) { + // We don't know for sure what the external port is, so we will have to guess. + // When we contact the PC (if we haven't already), it will update the port. + details.remoteAddress = new ComputerDetails.AddressTuple(stunResolvedAddress, details.guessExternalPort()); + } } // Unbind from the network @@ -396,7 +401,7 @@ public class ComputerManagerService extends Service { // Populate the computer template with mDNS info if (computer.getLocalAddress() != null) { - details.localAddress = computer.getLocalAddress().getHostAddress(); + details.localAddress = new ComputerDetails.AddressTuple(computer.getLocalAddress().getHostAddress(), computer.getPort()); // Since we're on the same network, we can use STUN to find // our WAN address, which is also very likely the WAN address @@ -406,7 +411,7 @@ public class ComputerManagerService extends Service { } } if (computer.getIpv6Address() != null) { - details.ipv6Address = computer.getIpv6Address().getHostAddress(); + details.ipv6Address = new ComputerDetails.AddressTuple(computer.getIpv6Address().getHostAddress(), computer.getPort()); } try { @@ -543,9 +548,9 @@ public class ComputerManagerService extends Service { } } - private ComputerDetails tryPollIp(ComputerDetails details, String address) { + private ComputerDetails tryPollIp(ComputerDetails details, ComputerDetails.AddressTuple address) { try { - NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), details.serverCert, + NvHTTP http = new NvHTTP(address, 0, idManager.getUniqueId(), details.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this)); ComputerDetails newDetails = http.getComputerDetails(); @@ -572,14 +577,14 @@ public class ComputerManagerService extends Service { } private static class ParallelPollTuple { - public String address; + public ComputerDetails.AddressTuple address; public ComputerDetails existingDetails; public boolean complete; public Thread pollingThread; public ComputerDetails returnedDetails; - public ParallelPollTuple(String address, ComputerDetails existingDetails) { + public ParallelPollTuple(ComputerDetails.AddressTuple address, ComputerDetails existingDetails) { this.address = address; this.existingDetails = existingDetails; } @@ -591,7 +596,7 @@ public class ComputerManagerService extends Service { } } - private void startParallelPollThread(ParallelPollTuple tuple, HashSet uniqueAddresses) { + private void startParallelPollThread(ParallelPollTuple tuple, HashSet uniqueAddresses) { // Don't bother starting a polling thread for an address that doesn't exist // or if the address has already been polled with an earlier tuple if (tuple.address == null || !uniqueAddresses.add(tuple.address)) { @@ -625,7 +630,7 @@ public class ComputerManagerService extends Service { // These must be started in order of precedence for the deduplication algorithm // to result in the correct behavior. - HashSet uniqueAddresses = new HashSet<>(); + HashSet uniqueAddresses = new HashSet<>(); startParallelPollThread(localInfo, uniqueAddresses); startParallelPollThread(manualInfo, uniqueAddresses); startParallelPollThread(remoteInfo, uniqueAddresses); @@ -821,7 +826,7 @@ public class ComputerManagerService extends Service { PollingTuple tuple = getPollingTuple(computer); try { - NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(), + NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), computer.httpsPort, idManager.getUniqueId(), computer.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this)); String appList; diff --git a/app/src/main/java/com/limelight/computers/LegacyDatabaseReader.java b/app/src/main/java/com/limelight/computers/LegacyDatabaseReader.java index 86e9db8c..27a7f1a3 100644 --- a/app/src/main/java/com/limelight/computers/LegacyDatabaseReader.java +++ b/app/src/main/java/com/limelight/computers/LegacyDatabaseReader.java @@ -7,6 +7,7 @@ import android.database.sqlite.SQLiteException; import com.limelight.LimeLog; import com.limelight.nvstream.http.ComputerDetails; +import com.limelight.nvstream.http.NvHTTP; import java.net.InetAddress; import java.net.UnknownHostException; @@ -30,26 +31,26 @@ public class LegacyDatabaseReader { // too. To disambiguate, we'll need to prefix them with a string // greater than the allowable IP address length. try { - details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress(); + details.localAddress = new ComputerDetails.AddressTuple(InetAddress.getByAddress(c.getBlob(2)).getHostAddress(), NvHTTP.DEFAULT_HTTP_PORT); LimeLog.warning("DB: Legacy local address for " + details.name); } catch (UnknownHostException e) { // This is probably a hostname/address with the prefix string String stringData = c.getString(2); if (stringData.startsWith(ADDRESS_PREFIX)) { - details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length()); + details.localAddress = new ComputerDetails.AddressTuple(c.getString(2).substring(ADDRESS_PREFIX.length()), NvHTTP.DEFAULT_HTTP_PORT); } else { LimeLog.severe("DB: Corrupted local address for " + details.name); } } try { - details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress(); + details.remoteAddress = new ComputerDetails.AddressTuple(InetAddress.getByAddress(c.getBlob(3)).getHostAddress(), NvHTTP.DEFAULT_HTTP_PORT); LimeLog.warning("DB: Legacy remote address for " + details.name); } catch (UnknownHostException e) { // This is probably a hostname/address with the prefix string String stringData = c.getString(3); if (stringData.startsWith(ADDRESS_PREFIX)) { - details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length()); + details.remoteAddress = new ComputerDetails.AddressTuple(c.getString(3).substring(ADDRESS_PREFIX.length()), NvHTTP.DEFAULT_HTTP_PORT); } else { LimeLog.severe("DB: Corrupted remote address for " + details.name); } diff --git a/app/src/main/java/com/limelight/computers/LegacyDatabaseReader2.java b/app/src/main/java/com/limelight/computers/LegacyDatabaseReader2.java index 8f461b28..55ad59a4 100644 --- a/app/src/main/java/com/limelight/computers/LegacyDatabaseReader2.java +++ b/app/src/main/java/com/limelight/computers/LegacyDatabaseReader2.java @@ -6,6 +6,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import com.limelight.nvstream.http.ComputerDetails; +import com.limelight.nvstream.http.NvHTTP; import java.io.ByteArrayInputStream; import java.security.cert.CertificateException; @@ -23,9 +24,9 @@ public class LegacyDatabaseReader2 { 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.localAddress = new ComputerDetails.AddressTuple(c.getString(2), NvHTTP.DEFAULT_HTTP_PORT); + details.remoteAddress = new ComputerDetails.AddressTuple(c.getString(3), NvHTTP.DEFAULT_HTTP_PORT); + details.manualAddress = new ComputerDetails.AddressTuple(c.getString(4), NvHTTP.DEFAULT_HTTP_PORT); details.macAddress = c.getString(5); // This column wasn't always present in the old schema diff --git a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java index fa31f67c..d75b4c54 100644 --- a/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java +++ b/app/src/main/java/com/limelight/grid/assets/NetworkAssetLoader.java @@ -22,8 +22,9 @@ public class NetworkAssetLoader { public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) { InputStream in = null; try { - NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId, - tuple.computer.serverCert, PlatformBinding.getCryptoProvider(context)); + NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), + tuple.computer.httpsPort, uniqueId, tuple.computer.serverCert, + PlatformBinding.getCryptoProvider(context)); in = http.getBoxArt(tuple.app); } catch (IOException ignored) {} diff --git a/app/src/main/java/com/limelight/nvstream/ConnectionContext.java b/app/src/main/java/com/limelight/nvstream/ConnectionContext.java index 36fe92b7..0bbf5918 100644 --- a/app/src/main/java/com/limelight/nvstream/ConnectionContext.java +++ b/app/src/main/java/com/limelight/nvstream/ConnectionContext.java @@ -1,11 +1,13 @@ package com.limelight.nvstream; +import com.limelight.nvstream.http.ComputerDetails; + import java.security.cert.X509Certificate; import javax.crypto.SecretKey; public class ConnectionContext { - public String serverAddress; + public ComputerDetails.AddressTuple serverAddress; public X509Certificate serverCert; public StreamConfiguration streamConfig; public NvConnectionListener connListener; diff --git a/app/src/main/java/com/limelight/nvstream/NvConnection.java b/app/src/main/java/com/limelight/nvstream/NvConnection.java index 3b4fdabd..ccc0a5e4 100644 --- a/app/src/main/java/com/limelight/nvstream/NvConnection.java +++ b/app/src/main/java/com/limelight/nvstream/NvConnection.java @@ -32,6 +32,7 @@ import org.xmlpull.v1.XmlPullParserException; import com.limelight.LimeLog; import com.limelight.nvstream.av.audio.AudioRenderer; import com.limelight.nvstream.av.video.VideoDecoderRenderer; +import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.GfeHttpResponseException; import com.limelight.nvstream.http.LimelightCryptoProvider; import com.limelight.nvstream.http.NvApp; @@ -42,7 +43,7 @@ import com.limelight.nvstream.jni.MoonBridge; public class NvConnection { // Context parameters - private String host; + private ComputerDetails.AddressTuple host; private LimelightCryptoProvider cryptoProvider; private String uniqueId; private ConnectionContext context; @@ -57,7 +58,7 @@ public class NvConnection { private short relMouseX, relMouseY, relMouseWidth, relMouseHeight; private short absMouseX, absMouseY, absMouseWidth, absMouseHeight; - public NvConnection(Context appContext, String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert, boolean batchMouseInput) + public NvConnection(Context appContext, ComputerDetails.AddressTuple host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert, boolean batchMouseInput) { this.appContext = appContext; this.host = host; @@ -134,11 +135,11 @@ public class NvConnection { private InetAddress resolveServerAddress() throws IOException { // Try to find an address that works for this host - InetAddress[] addrs = InetAddress.getAllByName(context.serverAddress); + InetAddress[] addrs = InetAddress.getAllByName(context.serverAddress.address); for (InetAddress addr : addrs) { try (Socket s = new Socket()) { s.setSoLinger(true, 0); - s.connect(new InetSocketAddress(addr, 47989), 1000); + s.connect(new InetSocketAddress(addr, context.serverAddress.port), 1000); return addr; } catch (IOException e) { e.printStackTrace(); @@ -252,7 +253,7 @@ public class NvConnection { private boolean startApp() throws XmlPullParserException, IOException { - NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider); + NvHTTP h = new NvHTTP(context.serverAddress, 0, uniqueId, context.serverCert, cryptoProvider); String serverInfo = h.getServerInfo(); @@ -452,7 +453,7 @@ public class NvConnection { // we must not invoke that functionality in parallel. synchronized (MoonBridge.class) { MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener); - int ret = MoonBridge.startConnection(context.serverAddress, + int ret = MoonBridge.startConnection(context.serverAddress.address, context.serverAppVersion, context.serverGfeVersion, context.rtspSessionUrl, context.negotiatedWidth, context.negotiatedHeight, context.streamConfig.getRefreshRate(), context.streamConfig.getBitrate(), diff --git a/app/src/main/java/com/limelight/nvstream/http/ComputerDetails.java b/app/src/main/java/com/limelight/nvstream/http/ComputerDetails.java index dcf2d7d0..65c9d388 100644 --- a/app/src/main/java/com/limelight/nvstream/http/ComputerDetails.java +++ b/app/src/main/java/com/limelight/nvstream/http/ComputerDetails.java @@ -8,19 +8,57 @@ public class ComputerDetails { ONLINE, OFFLINE, UNKNOWN } + public static class AddressTuple { + public String address; + public int port; + + public AddressTuple(String address, int port) { + if (address == null) { + throw new IllegalArgumentException("Address cannot be null"); + } + if (port <= 0) { + throw new IllegalArgumentException("Invalid port"); + } + + this.address = address; + this.port = port; + } + + @Override + public int hashCode() { + return address.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AddressTuple)) { + return false; + } + + AddressTuple that = (AddressTuple) obj; + return address.equals(that.address) && port == that.port; + } + + public String toString() { + return address + ":" + port; + } + } + // Persistent attributes public String uuid; public String name; - public String localAddress; - public String remoteAddress; - public String manualAddress; - public String ipv6Address; + public AddressTuple localAddress; + public AddressTuple remoteAddress; + public AddressTuple manualAddress; + public AddressTuple ipv6Address; public String macAddress; public X509Certificate serverCert; // Transient attributes public State state; - public String activeAddress; + public AddressTuple activeAddress; + public int httpsPort; + public int externalPort; public PairingManager.PairState pairState; public int runningGameId; public String rawAppList; @@ -35,6 +73,27 @@ public class ComputerDetails { update(details); } + public int guessExternalPort() { + if (externalPort != 0) { + return externalPort; + } + else if (remoteAddress != null) { + return remoteAddress.port; + } + else if (activeAddress != null) { + return activeAddress.port; + } + else if (ipv6Address != null) { + return ipv6Address.port; + } + else if (localAddress != null) { + return localAddress.port; + } + else { + return NvHTTP.DEFAULT_HTTP_PORT; + } + } + public void update(ComputerDetails details) { this.state = details.state; this.name = details.name; @@ -43,11 +102,16 @@ public class ComputerDetails { this.activeAddress = details.activeAddress; } // We can get IPv4 loopback addresses with GS IPv6 Forwarder - if (details.localAddress != null && !details.localAddress.startsWith("127.")) { + if (details.localAddress != null && !details.localAddress.address.startsWith("127.")) { this.localAddress = details.localAddress; } if (details.remoteAddress != null) { this.remoteAddress = details.remoteAddress; + + // If the port is unknown, populate it from the external port field + if (this.remoteAddress.port == 0) { + this.remoteAddress.port = externalPort; + } } if (details.manualAddress != null) { this.manualAddress = details.manualAddress; @@ -61,6 +125,8 @@ public class ComputerDetails { if (details.serverCert != null) { this.serverCert = details.serverCert; } + this.externalPort = details.externalPort; + this.httpsPort = details.httpsPort; this.pairState = details.pairState; this.runningGameId = details.runningGameId; this.rawAppList = details.rawAppList; @@ -80,6 +146,7 @@ public class ComputerDetails { 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"); + str.append("HTTPS Port: ").append(httpsPort).append("\n"); return str.toString(); } } diff --git a/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java b/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java index 312bb227..c328a119 100644 --- a/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java +++ b/app/src/main/java/com/limelight/nvstream/http/NvHTTP.java @@ -63,7 +63,7 @@ public class NvHTTP { private PairingManager pm; private static final int DEFAULT_HTTPS_PORT = 47984; - public static final int HTTP_PORT = 47989; + public static final int DEFAULT_HTTP_PORT = 47989; public static final int CONNECTION_TIMEOUT = 3000; public static final int READ_TIMEOUT = 5000; @@ -190,7 +190,7 @@ public class NvHTTP { return new HttpUrl.Builder().scheme("https").host(baseUrlHttp.host()).port(httpsPort).build(); } - public NvHTTP(String address, String uniqueId, X509Certificate serverCert, LimelightCryptoProvider cryptoProvider) throws IOException { + public NvHTTP(ComputerDetails.AddressTuple address, int httpsPort, String uniqueId, X509Certificate serverCert, LimelightCryptoProvider cryptoProvider) throws IOException { // Use the same UID for all Moonlight clients so we can quit games // started by other Moonlight clients. this.uniqueId = "0123456789ABCDEF"; @@ -199,11 +199,13 @@ public class NvHTTP { initializeHttpState(cryptoProvider); + this.httpsPort = httpsPort; + try { this.baseUrlHttp = new HttpUrl.Builder() .scheme("http") - .host(address) - .port(HTTP_PORT) + .host(address.address) + .port(address.port) .build(); } catch (IllegalArgumentException e) { // Encapsulate IllegalArgumentException into IOException for callers to handle more easily @@ -322,6 +324,14 @@ public class NvHTTP { return openHttpConnectionToString(baseUrlHttp, "serverinfo", true); } } + + private static ComputerDetails.AddressTuple makeTuple(String address, int port) { + if (address == null) { + return null; + } + + return new ComputerDetails.AddressTuple(address, port); + } public ComputerDetails getComputerDetails() throws IOException, XmlPullParserException { ComputerDetails details = new ComputerDetails(); @@ -335,11 +345,16 @@ public class NvHTTP { // UUID is mandatory to determine which machine is responding details.uuid = getXmlString(serverInfo, "uniqueid", true); - details.macAddress = getXmlString(serverInfo, "mac", false); - details.localAddress = getXmlString(serverInfo, "LocalIP", false); + details.httpsPort = getHttpsPort(serverInfo); - // This is missing on on recent GFE versions - details.remoteAddress = getXmlString(serverInfo, "ExternalIP", false); + details.macAddress = getXmlString(serverInfo, "mac", false); + + // FIXME: Do we want to use the current port? + details.localAddress = makeTuple(getXmlString(serverInfo, "LocalIP", false), baseUrlHttp.port()); + + // This is missing on on recent GFE versions, but it's present on Sunshine + details.externalPort = getExternalPort(serverInfo); + details.remoteAddress = makeTuple(getXmlString(serverInfo, "ExternalIP", false), details.externalPort); details.pairState = getPairState(serverInfo); details.runningGameId = getCurrentGame(serverInfo); @@ -543,6 +558,20 @@ public class NvHTTP { } } + public int getExternalPort(String serverInfo) { + // This is an extension which is not present in GFE. It is present for Sunshine to be able + // to support dynamic HTTP WAN ports without requiring the user to manually enter the port. + try { + return Integer.parseInt(getXmlString(serverInfo, "ExternalPort", true)); + } catch (XmlPullParserException e) { + // Expected on non-Sunshine servers + return DEFAULT_HTTP_PORT; + } catch (IOException e) { + e.printStackTrace(); + return DEFAULT_HTTP_PORT; + } + } + public NvApp getAppById(int appId) throws IOException, XmlPullParserException { LinkedList appList = getAppList(); for (NvApp appFromList : appList) { diff --git a/app/src/main/java/com/limelight/nvstream/wol/WakeOnLanSender.java b/app/src/main/java/com/limelight/nvstream/wol/WakeOnLanSender.java index afecede5..62fd1978 100644 --- a/app/src/main/java/com/limelight/nvstream/wol/WakeOnLanSender.java +++ b/app/src/main/java/com/limelight/nvstream/wol/WakeOnLanSender.java @@ -10,11 +10,53 @@ import com.limelight.LimeLog; import com.limelight.nvstream.http.ComputerDetails; public class WakeOnLanSender { - private static final int[] PORTS_TO_TRY = new int[] { + // These ports will always be tried as-is. + private static final int[] STATIC_PORTS_TO_TRY = new int[] { 9, // Standard WOL port (privileged port) - 47998, 47999, 48000, 48002, 48010, // Ports opened by GFE 47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port) }; + + // These ports will be offset by the base port number (47989) to support alternate ports. + private static final int[] DYNAMIC_PORTS_TO_TRY = new int[] { + 47998, 47999, 48000, 48002, 48010, // Ports opened by GFE + }; + + private static void sendPacketsForAddress(InetAddress address, int httpPort, DatagramSocket sock, byte[] payload) throws IOException { + IOException lastException = null; + boolean sentWolPacket = false; + + // Try the static ports + for (int port : STATIC_PORTS_TO_TRY) { + try { + DatagramPacket dp = new DatagramPacket(payload, payload.length); + dp.setAddress(address); + dp.setPort(port); + sock.send(dp); + sentWolPacket = true; + } catch (IOException e) { + e.printStackTrace(); + lastException = e; + } + } + + // Try the dynamic ports + for (int port : DYNAMIC_PORTS_TO_TRY) { + try { + DatagramPacket dp = new DatagramPacket(payload, payload.length); + dp.setAddress(address); + dp.setPort((port - 47989) + httpPort); + sock.send(dp); + sentWolPacket = true; + } catch (IOException e) { + e.printStackTrace(); + lastException = e; + } + } + + if (!sentWolPacket) { + throw lastException; + } + } public static void sendWolPacket(ComputerDetails computer) throws IOException { byte[] payload = createWolPayload(computer); @@ -22,26 +64,21 @@ public class WakeOnLanSender { boolean sentWolPacket = false; try (final DatagramSocket sock = new DatagramSocket(0)) { - // Try all resolved remote and local addresses and IPv4 broadcast address. + // Try all resolved remote and local addresses and broadcast addresses. // The broadcast address is required to avoid stale ARP cache entries // making the sleeping machine unreachable. - for (String unresolvedAddress : new String[] { - computer.localAddress, computer.remoteAddress, computer.manualAddress, computer.ipv6Address, "255.255.255.255" + for (ComputerDetails.AddressTuple address : new ComputerDetails.AddressTuple[] { + computer.localAddress, computer.remoteAddress, + computer.manualAddress, computer.ipv6Address, }) { - if (unresolvedAddress == null) { + if (address == null) { continue; } try { - for (InetAddress resolvedAddress : InetAddress.getAllByName(unresolvedAddress)) { - // Try all the ports for each resolved address - for (int port : PORTS_TO_TRY) { - DatagramPacket dp = new DatagramPacket(payload, payload.length); - dp.setAddress(resolvedAddress); - dp.setPort(port); - sock.send(dp); - sentWolPacket = true; - } + sendPacketsForAddress(InetAddress.getByName("255.255.255.255"), address.port, sock, payload); + for (InetAddress resolvedAddress : InetAddress.getAllByName(address.address)) { + sendPacketsForAddress(resolvedAddress, address.port, sock, payload); } } catch (IOException e) { // We may have addresses that don't resolve on this subnet, diff --git a/app/src/main/java/com/limelight/preferences/AddComputerManually.java b/app/src/main/java/com/limelight/preferences/AddComputerManually.java index 837de8f8..e2d02fb2 100644 --- a/app/src/main/java/com/limelight/preferences/AddComputerManually.java +++ b/app/src/main/java/com/limelight/preferences/AddComputerManually.java @@ -6,6 +6,8 @@ import java.net.InetAddress; import java.net.InterfaceAddress; import java.net.NetworkInterface; import java.net.SocketException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.UnknownHostException; import java.util.Collections; import java.util.concurrent.LinkedBlockingQueue; @@ -96,7 +98,7 @@ public class AddComputerManually extends Activity { } } - private void doAddPc(String host) throws InterruptedException { + private void doAddPc(String rawUserInput) throws InterruptedException { boolean wrongSiteLocal = false; boolean success; int portTestResult; @@ -106,8 +108,28 @@ public class AddComputerManually extends Activity { try { ComputerDetails details = new ComputerDetails(); - details.manualAddress = host; + + // Use URI-style parsing for the host address input + URI uri = new URI("moonlight://" + rawUserInput); + + String host = uri.getHost(); + int port = uri.getPort(); + + // URI allows empty hosts, but we don't want that + if (host == null || host.isEmpty()) { + throw new URISyntaxException(rawUserInput, "Host failed to parse"); + } + + // If a port was not specified, use the default + if (port == -1) { + port = NvHTTP.DEFAULT_HTTP_PORT; + } + + details.manualAddress = new ComputerDetails.AddressTuple(host, port); success = managerBinder.addComputerBlocking(details); + if (!success){ + wrongSiteLocal = isWrongSubnetSiteLocalAddress(host); + } } catch (InterruptedException e) { // Propagate the InterruptedException to the caller for proper handling dialog.dismiss(); @@ -117,12 +139,12 @@ public class AddComputerManually extends Activity { // https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705 e.printStackTrace(); success = false; + } catch (URISyntaxException e) { + e.printStackTrace(); + success = false; } // Keep the SpinnerDialog open while testing connectivity - if (!success){ - wrongSiteLocal = isWrongSubnetSiteLocalAddress(host); - } if (!success && !wrongSiteLocal) { // Run the test before dismissing the spinner because it can take a few seconds. portTestResult = MoonBridge.testClientConnectivity(ServerHelper.CONNECTION_TEST_SERVER, 443, diff --git a/app/src/main/java/com/limelight/utils/ServerHelper.java b/app/src/main/java/com/limelight/utils/ServerHelper.java index 980e0604..86bf4528 100644 --- a/app/src/main/java/com/limelight/utils/ServerHelper.java +++ b/app/src/main/java/com/limelight/utils/ServerHelper.java @@ -27,7 +27,7 @@ import java.security.cert.CertificateEncodingException; public class ServerHelper { public static final String CONNECTION_TEST_SERVER = "android.conntest.moonlight-stream.org"; - public static String getCurrentAddressFromComputer(ComputerDetails computer) throws IOException { + public static ComputerDetails.AddressTuple getCurrentAddressFromComputer(ComputerDetails computer) throws IOException { if (computer.activeAddress == null) { throw new IOException("No active address for "+computer.name); } @@ -56,7 +56,8 @@ public class ServerHelper { public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer, ComputerManagerService.ComputerManagerBinder managerBinder) { Intent intent = new Intent(parent, Game.class); - intent.putExtra(Game.EXTRA_HOST, computer.activeAddress); + intent.putExtra(Game.EXTRA_HOST, computer.activeAddress.address); + intent.putExtra(Game.EXTRA_PORT, computer.activeAddress.port); intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName()); intent.putExtra(Game.EXTRA_APP_ID, app.getAppId()); intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported()); @@ -126,7 +127,7 @@ public class ServerHelper { NvHTTP httpConn; String message; try { - httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), + httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), computer.httpsPort, managerBinder.getUniqueId(), computer.serverCert, PlatformBinding.getCryptoProvider(parent)); if (httpConn.quitApp()) { message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();