mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-08-14 23:57:35 +00:00
Make sure that the time we use is always the same across servers
This commit is contained in:
parent
5c76bd8544
commit
cfa2805e00
6 changed files with 179 additions and 11 deletions
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
* @author GeyserMC
|
||||||
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.geysermc.floodgate.time;
|
||||||
|
|
||||||
|
import java.net.DatagramPacket;
|
||||||
|
import java.net.DatagramSocket;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Thanks:
|
||||||
|
* https://datatracker.ietf.org/doc/html/rfc1769
|
||||||
|
* https://github.com/jonsagara/SimpleNtpClient
|
||||||
|
* https://stackoverflow.com/a/29138806
|
||||||
|
*/
|
||||||
|
public final class SntpClientUtils {
|
||||||
|
private static final int NTP_PORT = 123;
|
||||||
|
|
||||||
|
private static final int NTP_PACKET_SIZE = 48;
|
||||||
|
private static final int NTP_MODE = 3; // client
|
||||||
|
private static final int NTP_VERSION = 3;
|
||||||
|
private static final int RECEIVE_TIME_POSITION = 32;
|
||||||
|
|
||||||
|
private static final long NTP_TIME_OFFSET = ((365L * 70L) + 17L) * 24L * 60L * 60L;
|
||||||
|
|
||||||
|
public static long requestTimeOffset(String host, int timeout) {
|
||||||
|
try (DatagramSocket socket = new DatagramSocket()) {
|
||||||
|
socket.setSoTimeout(timeout);
|
||||||
|
|
||||||
|
InetAddress address = InetAddress.getByName(host);
|
||||||
|
|
||||||
|
ByteBuffer buff = ByteBuffer.allocate(NTP_PACKET_SIZE);
|
||||||
|
|
||||||
|
DatagramPacket request = new DatagramPacket(
|
||||||
|
buff.array(), NTP_PACKET_SIZE, address, NTP_PORT
|
||||||
|
);
|
||||||
|
|
||||||
|
// mode is in the least signification 3 bits
|
||||||
|
// version is in bits 3-5
|
||||||
|
buff.put((byte) (NTP_MODE | (NTP_VERSION << 3)));
|
||||||
|
|
||||||
|
long originateTime = System.currentTimeMillis();
|
||||||
|
socket.send(request);
|
||||||
|
|
||||||
|
DatagramPacket response = new DatagramPacket(buff.array(), NTP_PACKET_SIZE);
|
||||||
|
socket.receive(response);
|
||||||
|
|
||||||
|
long responseTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// everything before isn't important for us
|
||||||
|
buff.position(RECEIVE_TIME_POSITION);
|
||||||
|
|
||||||
|
long receiveTime = readTimestamp(buff);
|
||||||
|
long transmitTime = readTimestamp(buff);
|
||||||
|
|
||||||
|
return ((receiveTime - originateTime) + (transmitTime - responseTime)) / 2;
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return Long.MIN_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long readTimestamp(ByteBuffer buffer) {
|
||||||
|
//todo look into the ntp 2036 problem
|
||||||
|
long seconds = buffer.getInt() & 0xffffffffL;
|
||||||
|
long fraction = buffer.getInt() & 0xffffffffL;
|
||||||
|
return ((seconds - NTP_TIME_OFFSET) * 1000) + ((fraction * 1000) / 0x100000000L);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
* @author GeyserMC
|
||||||
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.geysermc.floodgate.time;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public final class TimeSyncer {
|
||||||
|
private final ExecutorService executorService;
|
||||||
|
private long timeOffset = Long.MIN_VALUE; // value when it failed to get the offset
|
||||||
|
|
||||||
|
public TimeSyncer(String timeServer) {
|
||||||
|
ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
|
||||||
|
service.scheduleWithFixedDelay(() -> {
|
||||||
|
// 5 tries to get the time offset
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
long offset = SntpClientUtils.requestTimeOffset(timeServer, 3000);
|
||||||
|
if (offset != Long.MIN_VALUE) {
|
||||||
|
timeOffset = offset;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 0, 30, TimeUnit.MINUTES);
|
||||||
|
executorService = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
executorService.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimeOffset() {
|
||||||
|
return timeOffset;
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ package org.geysermc.floodgate.util;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.geysermc.floodgate.time.TimeSyncer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class contains the raw data send by Geyser to Floodgate or from Floodgate to Floodgate. This
|
* This class contains the raw data send by Geyser to Floodgate or from Floodgate to Floodgate. This
|
||||||
|
@ -56,20 +57,28 @@ public final class BedrockData implements Cloneable {
|
||||||
private final long timestamp;
|
private final long timestamp;
|
||||||
private final int dataLength;
|
private final int dataLength;
|
||||||
|
|
||||||
public static BedrockData of(String version, String username, String xuid, int deviceOs,
|
public static BedrockData of(
|
||||||
String languageCode, int uiProfile, int inputMode, String ip,
|
String version, String username, String xuid, int deviceOs,
|
||||||
LinkedPlayer linkedPlayer, boolean fromProxy, int subscribeId,
|
String languageCode, int uiProfile, int inputMode, String ip,
|
||||||
String verifyCode) {
|
LinkedPlayer linkedPlayer, boolean fromProxy, int subscribeId,
|
||||||
|
String verifyCode, TimeSyncer timeSyncer) {
|
||||||
|
|
||||||
|
long realMillis = System.currentTimeMillis();
|
||||||
|
if (timeSyncer.getTimeOffset() != Long.MIN_VALUE) {
|
||||||
|
realMillis += timeSyncer.getTimeOffset();
|
||||||
|
}
|
||||||
|
|
||||||
return new BedrockData(version, username, xuid, deviceOs, languageCode, inputMode,
|
return new BedrockData(version, username, xuid, deviceOs, languageCode, inputMode,
|
||||||
uiProfile, ip, linkedPlayer, fromProxy, subscribeId, verifyCode,
|
uiProfile, ip, linkedPlayer, fromProxy, subscribeId, verifyCode,
|
||||||
System.currentTimeMillis(), EXPECTED_LENGTH);
|
realMillis, EXPECTED_LENGTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BedrockData of(String version, String username, String xuid, int deviceOs,
|
public static BedrockData of(
|
||||||
String languageCode, int uiProfile, int inputMode, String ip,
|
String version, String username, String xuid, int deviceOs,
|
||||||
int subscribeId, String verifyCode) {
|
String languageCode, int uiProfile, int inputMode, String ip,
|
||||||
|
int subscribeId, String verifyCode, TimeSyncer timeSyncer) {
|
||||||
return of(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null,
|
return of(version, username, xuid, deviceOs, languageCode, uiProfile, inputMode, ip, null,
|
||||||
false, subscribeId, verifyCode);
|
false, subscribeId, verifyCode, timeSyncer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BedrockData fromString(String data) {
|
public static BedrockData fromString(String data) {
|
||||||
|
|
|
@ -62,6 +62,7 @@ import org.geysermc.floodgate.crypto.AesCipher;
|
||||||
import org.geysermc.floodgate.crypto.AesKeyProducer;
|
import org.geysermc.floodgate.crypto.AesKeyProducer;
|
||||||
import org.geysermc.floodgate.crypto.Base64Topping;
|
import org.geysermc.floodgate.crypto.Base64Topping;
|
||||||
import org.geysermc.floodgate.crypto.FloodgateCipher;
|
import org.geysermc.floodgate.crypto.FloodgateCipher;
|
||||||
|
import org.geysermc.floodgate.time.TimeSyncer;
|
||||||
import org.jetbrains.annotations.Contract;
|
import org.jetbrains.annotations.Contract;
|
||||||
|
|
||||||
import javax.naming.directory.Attribute;
|
import javax.naming.directory.Attribute;
|
||||||
|
@ -79,7 +80,6 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public class GeyserConnector {
|
public class GeyserConnector {
|
||||||
|
|
||||||
public static final ObjectMapper JSON_MAPPER = new ObjectMapper()
|
public static final ObjectMapper JSON_MAPPER = new ObjectMapper()
|
||||||
.enable(JsonParser.Feature.IGNORE_UNDEFINED)
|
.enable(JsonParser.Feature.IGNORE_UNDEFINED)
|
||||||
.enable(JsonParser.Feature.ALLOW_COMMENTS)
|
.enable(JsonParser.Feature.ALLOW_COMMENTS)
|
||||||
|
@ -106,6 +106,7 @@ public class GeyserConnector {
|
||||||
@Setter
|
@Setter
|
||||||
private AuthType defaultAuthType;
|
private AuthType defaultAuthType;
|
||||||
|
|
||||||
|
private TimeSyncer timeSyncer;
|
||||||
private FloodgateCipher cipher;
|
private FloodgateCipher cipher;
|
||||||
private FloodgateSkinUploader skinUploader;
|
private FloodgateSkinUploader skinUploader;
|
||||||
|
|
||||||
|
@ -198,6 +199,7 @@ public class GeyserConnector {
|
||||||
defaultAuthType = AuthType.getByName(config.getRemote().getAuthType());
|
defaultAuthType = AuthType.getByName(config.getRemote().getAuthType());
|
||||||
|
|
||||||
if (defaultAuthType == AuthType.FLOODGATE) {
|
if (defaultAuthType == AuthType.FLOODGATE) {
|
||||||
|
timeSyncer = new TimeSyncer(Constants.NTP_SERVER);
|
||||||
try {
|
try {
|
||||||
Key key = new AesKeyProducer().produceFrom(config.getFloodgateKeyPath());
|
Key key = new AesKeyProducer().produceFrom(config.getFloodgateKeyPath());
|
||||||
cipher = new AesCipher(new Base64Topping());
|
cipher = new AesCipher(new Base64Topping());
|
||||||
|
@ -353,6 +355,7 @@ public class GeyserConnector {
|
||||||
|
|
||||||
generalThreadPool.shutdown();
|
generalThreadPool.shutdown();
|
||||||
bedrockServer.close();
|
bedrockServer.close();
|
||||||
|
timeSyncer.shutdown();
|
||||||
players.clear();
|
players.clear();
|
||||||
defaultAuthType = null;
|
defaultAuthType = null;
|
||||||
this.getCommandManager().getCommands().clear();
|
this.getCommandManager().getCommands().clear();
|
||||||
|
@ -431,6 +434,10 @@ public class GeyserConnector {
|
||||||
return bootstrap.getWorldManager();
|
return bootstrap.getWorldManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TimeSyncer getTimeSyncer() {
|
||||||
|
return timeSyncer;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to use XML reflections in the jar or manually find the reflections.
|
* Whether to use XML reflections in the jar or manually find the reflections.
|
||||||
* Will return true if the version number is not 'DEV' and the platform is not Fabric.
|
* Will return true if the version number is not 'DEV' and the platform is not Fabric.
|
||||||
|
|
|
@ -691,7 +691,8 @@ public class GeyserSession implements CommandSender {
|
||||||
clientData.getCurrentInputMode().ordinal(),
|
clientData.getCurrentInputMode().ordinal(),
|
||||||
upstream.getAddress().getAddress().getHostAddress(),
|
upstream.getAddress().getAddress().getHostAddress(),
|
||||||
skinUploader.getId(),
|
skinUploader.getId(),
|
||||||
skinUploader.getVerifyCode()
|
skinUploader.getVerifyCode(),
|
||||||
|
connector.getTimeSyncer()
|
||||||
).toString());
|
).toString());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);
|
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);
|
||||||
|
|
|
@ -30,6 +30,7 @@ import java.net.URISyntaxException;
|
||||||
|
|
||||||
public final class Constants {
|
public final class Constants {
|
||||||
public static final URI SKIN_UPLOAD_URI;
|
public static final URI SKIN_UPLOAD_URI;
|
||||||
|
public static final String NTP_SERVER = "time.cloudflare.com";
|
||||||
|
|
||||||
static {
|
static {
|
||||||
URI skinUploadUri = null;
|
URI skinUploadUri = null;
|
||||||
|
|
Loading…
Reference in a new issue