Implement support for non-default ports with Sunshine

Fixes #1115
This commit is contained in:
Cameron Gutman
2022-11-06 17:36:46 -06:00
parent a896f9a28f
commit 7af290b6e1
14 changed files with 277 additions and 77 deletions

View File

@@ -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;

View File

@@ -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(),

View File

@@ -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();
}
}

View File

@@ -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<NvApp> appList = getAppList();
for (NvApp appFromList : appList) {

View File

@@ -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,