Make sure that the time we use is always the same across servers

This commit is contained in:
Tim203 2021-05-26 01:55:58 +02:00
parent 5c76bd8544
commit cfa2805e00
No known key found for this signature in database
GPG key ID: 064EE9F5BF7C3EE8
6 changed files with 179 additions and 11 deletions

View file

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

View file

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

View file

@ -28,6 +28,7 @@ package org.geysermc.floodgate.util;
import lombok.AccessLevel;
import lombok.Getter;
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
@ -56,20 +57,28 @@ public final class BedrockData implements Cloneable {
private final long timestamp;
private final int dataLength;
public static BedrockData of(String version, String username, String xuid, int deviceOs,
String languageCode, int uiProfile, int inputMode, String ip,
LinkedPlayer linkedPlayer, boolean fromProxy, int subscribeId,
String verifyCode) {
public static BedrockData of(
String version, String username, String xuid, int deviceOs,
String languageCode, int uiProfile, int inputMode, String ip,
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,
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,
String languageCode, int uiProfile, int inputMode, String ip,
int subscribeId, String verifyCode) {
public static BedrockData of(
String version, String username, String xuid, int deviceOs,
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,
false, subscribeId, verifyCode);
false, subscribeId, verifyCode, timeSyncer);
}
public static BedrockData fromString(String data) {

View file

@ -62,6 +62,7 @@ import org.geysermc.floodgate.crypto.AesCipher;
import org.geysermc.floodgate.crypto.AesKeyProducer;
import org.geysermc.floodgate.crypto.Base64Topping;
import org.geysermc.floodgate.crypto.FloodgateCipher;
import org.geysermc.floodgate.time.TimeSyncer;
import org.jetbrains.annotations.Contract;
import javax.naming.directory.Attribute;
@ -79,7 +80,6 @@ import java.util.concurrent.TimeUnit;
@Getter
public class GeyserConnector {
public static final ObjectMapper JSON_MAPPER = new ObjectMapper()
.enable(JsonParser.Feature.IGNORE_UNDEFINED)
.enable(JsonParser.Feature.ALLOW_COMMENTS)
@ -106,6 +106,7 @@ public class GeyserConnector {
@Setter
private AuthType defaultAuthType;
private TimeSyncer timeSyncer;
private FloodgateCipher cipher;
private FloodgateSkinUploader skinUploader;
@ -198,6 +199,7 @@ public class GeyserConnector {
defaultAuthType = AuthType.getByName(config.getRemote().getAuthType());
if (defaultAuthType == AuthType.FLOODGATE) {
timeSyncer = new TimeSyncer(Constants.NTP_SERVER);
try {
Key key = new AesKeyProducer().produceFrom(config.getFloodgateKeyPath());
cipher = new AesCipher(new Base64Topping());
@ -353,6 +355,7 @@ public class GeyserConnector {
generalThreadPool.shutdown();
bedrockServer.close();
timeSyncer.shutdown();
players.clear();
defaultAuthType = null;
this.getCommandManager().getCommands().clear();
@ -431,6 +434,10 @@ public class GeyserConnector {
return bootstrap.getWorldManager();
}
public TimeSyncer getTimeSyncer() {
return timeSyncer;
}
/**
* 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.

View file

@ -691,7 +691,8 @@ public class GeyserSession implements CommandSender {
clientData.getCurrentInputMode().ordinal(),
upstream.getAddress().getAddress().getHostAddress(),
skinUploader.getId(),
skinUploader.getVerifyCode()
skinUploader.getVerifyCode(),
connector.getTimeSyncer()
).toString());
} catch (Exception e) {
connector.getLogger().error(LanguageUtils.getLocaleStringLog("geyser.auth.floodgate.encrypt_fail"), e);

View file

@ -30,6 +30,7 @@ import java.net.URISyntaxException;
public final class Constants {
public static final URI SKIN_UPLOAD_URI;
public static final String NTP_SERVER = "time.cloudflare.com";
static {
URI skinUploadUri = null;