diff --git a/common/src/main/java/org/geysermc/floodgate/time/SntpClientUtils.java b/common/src/main/java/org/geysermc/floodgate/time/SntpClientUtils.java new file mode 100644 index 000000000..9e7f2c1b0 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/time/SntpClientUtils.java @@ -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); + } +} diff --git a/common/src/main/java/org/geysermc/floodgate/time/TimeSyncer.java b/common/src/main/java/org/geysermc/floodgate/time/TimeSyncer.java new file mode 100644 index 000000000..2d74e4b77 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/time/TimeSyncer.java @@ -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; + } +} diff --git a/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java index 81a6307a2..46c1d3d1a 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java +++ b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java @@ -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) { diff --git a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java index bac915437..d1122b65f 100644 --- a/connector/src/main/java/org/geysermc/connector/GeyserConnector.java +++ b/connector/src/main/java/org/geysermc/connector/GeyserConnector.java @@ -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. diff --git a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java index b922903b6..15d246fee 100644 --- a/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java +++ b/connector/src/main/java/org/geysermc/connector/network/session/GeyserSession.java @@ -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); diff --git a/connector/src/main/java/org/geysermc/connector/utils/Constants.java b/connector/src/main/java/org/geysermc/connector/utils/Constants.java index 8dcd63d3e..5b21d045c 100644 --- a/connector/src/main/java/org/geysermc/connector/utils/Constants.java +++ b/connector/src/main/java/org/geysermc/connector/utils/Constants.java @@ -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;