mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-04-05 07:26:28 +00:00
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user