Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ val isDevBuild = providers.gradleProperty("waterdog.is-dev-build")
val log4j2Version = "2.25.3"
val jlineVersion = "3.30.6"
val nettyVersion = "4.1.101.Final"
val raklibVersion = "1.0.0.CR3-20260328.145829-29"
val protocolVersion = "1.26.10-R2"
val raklibVersion = "1.0.0.CR3-20260421.213623-35"
val protocolVersion = "1.26.20-R4-SNAPSHOT"

group = "org.allaymc"
version = if (isDevBuild) "$baseVersion-SNAPSHOT" else baseVersion
Expand All @@ -42,6 +42,7 @@ java {

repositories {
mavenCentral()
maven("https://central.sonatype.com/repository/maven-snapshots/")
maven("https://repo.opencollab.dev/maven-releases/")
maven("https://repo.opencollab.dev/maven-snapshots/")
maven("https://repo.waterdog.dev/main")
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true

waterdog.version=1.26.10-R2
waterdog.version=1.26.30-R1
waterdog.is-dev-build=true
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ public class ProtocolCodecs {
HANDLED_PACKETS.add(PlayerUpdateEntityOverridesPacket.class);
HANDLED_PACKETS.add(PlayerLocationPacket.class);
HANDLED_PACKETS.add(CameraPresetsPacket.class);
HANDLED_PACKETS.add(PrimitiveShapesPacket.class);
HANDLED_PACKETS.add(AddVolumeEntityPacket.class);
HANDLED_PACKETS.add(RemoveVolumeEntityPacket.class);
HANDLED_PACKETS.add(PlayerFogPacket.class);
HANDLED_PACKETS.add(UpdateClientInputLocksPacket.class);
HANDLED_PACKETS.add(SetHudPacket.class);
HANDLED_PACKETS.add(ContainerOpenPacket.class);
HANDLED_PACKETS.add(ContainerClosePacket.class);
HANDLED_PACKETS.add(SetTimePacket.class);
}

private static final List<ProtocolCodecUpdater> UPDATERS = new ObjectArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import lombok.Setter;
import lombok.ToString;
import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec;
import org.cloudburstmc.protocol.bedrock.codec.v1001.Bedrock_v1001;
import org.cloudburstmc.protocol.bedrock.codec.v313.Bedrock_v313;
import org.cloudburstmc.protocol.bedrock.codec.v332.Bedrock_v332;
import org.cloudburstmc.protocol.bedrock.codec.v340.Bedrock_v340;
Expand Down Expand Up @@ -83,6 +84,7 @@
import org.cloudburstmc.protocol.bedrock.codec.v898.Bedrock_v898;
import org.cloudburstmc.protocol.bedrock.codec.v924.Bedrock_v924;
import org.cloudburstmc.protocol.bedrock.codec.v944.Bedrock_v944;
import org.cloudburstmc.protocol.bedrock.codec.v975.Bedrock_v975;

@ToString(exclude = {"defaultCodec", "bedrockCodec"})
public enum ProtocolVersion {
Expand Down Expand Up @@ -148,6 +150,8 @@ public enum ProtocolVersion {
MINECRAFT_PE_1_21_130(898, Bedrock_v898.CODEC),
MINECRAFT_PE_1_26_0(924, Bedrock_v924.CODEC),
MINECRAFT_PE_1_26_10(944, Bedrock_v944.CODEC),
MINECRAFT_PE_1_26_20(975, Bedrock_v975.CODEC),
MINECRAFT_PE_1_26_30(1001, Bedrock_v1001.CODEC),
;

private static final ProtocolVersion[] VALUES = values();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData;
import org.cloudburstmc.protocol.bedrock.data.primitiveshape.*;
import org.cloudburstmc.protocol.bedrock.packet.*;
import dev.waterdog.waterdogpe.network.protocol.rewrite.types.RewriteData;
import dev.waterdog.waterdogpe.network.protocol.user.PlayerRewriteUtils;
Expand Down Expand Up @@ -374,6 +375,68 @@ public PacketSignal handle(CameraInstructionPacket packet) {
return signal;
}

@Override
public PacketSignal handle(PrimitiveShapesPacket packet) {
PacketSignal signal = PacketSignal.UNHANDLED;
ListIterator<PrimitiveShape> iterator = packet.getShapes().listIterator();
while (iterator.hasNext()) {
PrimitiveShape shape = iterator.next();
Long attachedEntityId = shape.getAttachedToEntityId();
if (attachedEntityId != null) {
long rewrittenId = PlayerRewriteUtils.rewriteId(attachedEntityId, this.rewrite.getEntityId(), this.rewrite.getOriginalEntityId());
if (rewrittenId != attachedEntityId) {
iterator.set(withAttachedEntityId(shape, rewrittenId));
signal = PacketSignal.HANDLED;
}
}
}
return signal;
}

private static PrimitiveShape withAttachedEntityId(PrimitiveShape shape, Long attachedEntityId) {
return switch (shape) {
case PrimitiveLine line -> new PrimitiveLine(line.getId(), line.getDimension(), line.getPosition(),
line.getScale(), line.getRotation(), line.getTotalTimeLeft(), line.getColor(),
line.getMaximumRenderDistance(), line.getLineEndPosition(), attachedEntityId);
case PrimitiveBox box -> new PrimitiveBox(box.getId(), box.getDimension(), box.getPosition(),
box.getScale(), box.getRotation(), box.getTotalTimeLeft(), box.getColor(),
box.getMaximumRenderDistance(), box.getBoxBounds(), attachedEntityId);
case PrimitiveSphere sphere -> new PrimitiveSphere(sphere.getId(), sphere.getDimension(),
sphere.getPosition(), sphere.getScale(), sphere.getRotation(), sphere.getTotalTimeLeft(),
sphere.getColor(), sphere.getMaximumRenderDistance(), sphere.getSegments(), attachedEntityId);
case PrimitiveCircle circle -> new PrimitiveCircle(circle.getId(), circle.getDimension(),
circle.getPosition(), circle.getScale(), circle.getRotation(), circle.getTotalTimeLeft(),
circle.getColor(), circle.getMaximumRenderDistance(), circle.getSegments(), attachedEntityId);
case PrimitiveText text -> new PrimitiveText(text.getId(), text.getDimension(), text.getPosition(),
text.getScale(), text.getRotation(), text.getTotalTimeLeft(), text.getColor(), text.getText(),
text.isUseRotation(), text.getBackgroundColor(), text.isDepthTest(), text.isShowBackface(),
text.isShowTextBackface(), text.getMaximumRenderDistance(), attachedEntityId);
case PrimitiveArrow arrow -> new PrimitiveArrow(arrow.getId(), arrow.getDimension(), arrow.getPosition(),
arrow.getScale(), arrow.getRotation(), arrow.getTotalTimeLeft(), arrow.getColor(),
arrow.getMaximumRenderDistance(), arrow.getArrowEndPosition(), arrow.getArrowHeadLength(),
arrow.getArrowHeadRadius(), arrow.getArrowHeadSegments(), attachedEntityId);
case PrimitiveCylinder cylinder -> new PrimitiveCylinder(cylinder.getId(), cylinder.getDimension(),
cylinder.getPosition(), cylinder.getScale(), cylinder.getRotation(), cylinder.getTotalTimeLeft(),
cylinder.getColor(), cylinder.getMaximumRenderDistance(), cylinder.getHeight(),
cylinder.getSegments(), cylinder.getRadiusX(), cylinder.getRadiusZ(), attachedEntityId);
case PrimitivePyramid pyramid -> new PrimitivePyramid(pyramid.getId(), pyramid.getDimension(),
pyramid.getPosition(), pyramid.getScale(), pyramid.getRotation(), pyramid.getTotalTimeLeft(),
pyramid.getColor(), pyramid.getMaximumRenderDistance(), pyramid.getHeight(), pyramid.getWidth(),
pyramid.getDepth(), attachedEntityId);
case PrimitiveEllipsoid ellipsoid -> new PrimitiveEllipsoid(ellipsoid.getId(), ellipsoid.getDimension(),
ellipsoid.getPosition(), ellipsoid.getScale(), ellipsoid.getRotation(),
ellipsoid.getTotalTimeLeft(), ellipsoid.getColor(), ellipsoid.getMaximumRenderDistance(),
ellipsoid.getSegments(), ellipsoid.getRadii(), attachedEntityId);
case PrimitiveCone cone -> new PrimitiveCone(cone.getId(), cone.getDimension(), cone.getPosition(),
cone.getScale(), cone.getRotation(), cone.getTotalTimeLeft(), cone.getColor(),
cone.getMaximumRenderDistance(), cone.getHeight(), cone.getSegments(), cone.getRadii(),
attachedEntityId);
default -> new PrimitiveShape(shape.getId(), shape.getDimension(), shape.getPosition(), shape.getScale(),
shape.getRotation(), shape.getTotalTimeLeft(), shape.getColor(), shape.getMaximumRenderDistance(),
attachedEntityId);
};
}

private PacketSignal rewriteMetadata(EntityDataMap metadata) {
PacketSignal signal = PacketSignal.UNHANDLED;
for (EntityDataType<Long> data : ENTITY_DATA_FIELDS) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,24 @@ public class HandshakeEntry {
private final String xuid;
private final UUID uuid;
private final String displayName;
private final String minecraftId;
private final boolean xboxAuthed;
private final boolean isChainPayload;
private final boolean shouldSendCertificateChain;
private final boolean netEaseClient;
private final LoginData.NetEaseData netEaseData;
@Setter
private ProtocolVersion protocol;

public HandshakeEntry(ECPublicKey identityPublicKey, JsonObject clientData, String xuid, UUID uuid, String displayName, boolean xboxAuthed, ProtocolVersion protocol, boolean isChainPayload, boolean netEaseClient, LoginData.NetEaseData netEaseData) {
public HandshakeEntry(ECPublicKey identityPublicKey, JsonObject clientData, String xuid, UUID uuid, String displayName, String minecraftId, boolean xboxAuthed, ProtocolVersion protocol, boolean shouldSendCertificateChain, boolean netEaseClient, LoginData.NetEaseData netEaseData) {
this.identityPublicKey = identityPublicKey;
this.clientData = clientData;
this.xuid = xuid;
this.uuid = uuid;
this.displayName = displayName;
this.minecraftId = minecraftId;
this.xboxAuthed = xboxAuthed;
this.protocol = protocol;
this.isChainPayload = isChainPayload;
this.shouldSendCertificateChain = shouldSendCertificateChain;
this.netEaseClient = netEaseClient;
this.netEaseData = netEaseData;
}
Expand All @@ -69,13 +71,14 @@ public LoginData buildData(BedrockServerSession session, ProxyServer proxy) {
builder.displayName(this.displayName);
builder.uuid(this.uuid);
builder.xuid(this.xuid);
builder.minecraftId(this.minecraftId);
builder.xboxAuthed(this.xboxAuthed);
builder.protocol(this.protocol);
builder.joinHostname(this.clientData.get("ServerAddress").getAsString().split(":")[0]);
builder.address(session.getSocketAddress());
builder.keyPair(event.getKeyPair());
builder.clientData(this.clientData);
builder.isChainPayload(this.isChainPayload);
builder.shouldSendCertificateChain(this.shouldSendCertificateChain);
builder.netEaseClient(this.netEaseClient);
builder.netEaseData(this.netEaseData);
if (this.clientData.has("DeviceModel")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import lombok.extern.log4j.Log4j2;
import org.cloudburstmc.protocol.bedrock.BedrockSession;
import org.cloudburstmc.protocol.bedrock.data.auth.CertificateChainPayload;
import org.cloudburstmc.protocol.bedrock.data.auth.TokenPayload;
import org.cloudburstmc.protocol.bedrock.packet.LoginPacket;
import org.cloudburstmc.protocol.bedrock.packet.ServerToClientHandshakePacket;
import org.cloudburstmc.protocol.bedrock.util.ChainValidationResult;
Expand Down Expand Up @@ -71,6 +72,14 @@ public class HandshakeUtils {
}
}

public static JsonObject createChainExtraData(String displayName, String xuid, UUID uuid) {
JsonObject extraData = new JsonObject();
extraData.addProperty("displayName", displayName);
extraData.addProperty("XUID", xuid);
extraData.addProperty("identity", uuid.toString());
return extraData;
}

public static SignedJWT createClientDataChain(KeyPair pair, JsonObject extraData) {
String publicKeyBase64 = Base64.getEncoder().encodeToString(pair.getPublic().getEncoded());
long timestamp = System.currentTimeMillis() / 1000;
Expand All @@ -87,17 +96,21 @@ public static SignedJWT createClientDataChain(KeyPair pair, JsonObject extraData
return encodeJWT(pair, dataChain);
}

public static SignedJWT createClientDataToken(KeyPair pair, String displayName, String xuid) {
public static SignedJWT createClientDataToken(KeyPair pair, String displayName, String xuid, UUID uuid, String minecraftId) {
String publicKeyBase64 = Base64.getEncoder().encodeToString(pair.getPublic().getEncoded());
long timestamp = System.currentTimeMillis() / 1000;

JsonObject dataChain = new JsonObject();
dataChain.addProperty("cpk", publicKeyBase64);
dataChain.addProperty("leguuid", uuid.toString());
dataChain.addProperty("iat", timestamp);
dataChain.addProperty("xname", displayName);
dataChain.addProperty("exp", timestamp + 24 * 3600);
dataChain.addProperty("mid", minecraftId);
dataChain.addProperty("ap", 7);
dataChain.addProperty("iss", "self");
dataChain.addProperty("cpk", publicKeyBase64);
dataChain.addProperty("aud", "api://auth-minecraft-services/multiplayer");
dataChain.addProperty("xid", xuid);
dataChain.addProperty("xname", displayName);
return encodeJWT(pair, dataChain);
}

Expand Down Expand Up @@ -140,6 +153,7 @@ public static HandshakeEntry processHandshake(BedrockSession session, LoginPacke
String xuid = identityData.xuid;
//UUID uuid = UUID.nameUUIDFromBytes(("pocket-auth-1-xuid:" + xuid).getBytes(StandardCharsets.UTF_8));
UUID uuid = identityData.identity;
String minecraftId = identityData.minecraftId;

SignedJWT clientDataJwt = SignedJWT.parse(packet.getClientJwt());
JsonObject clientData = HandshakeUtils.parseClientData(clientDataJwt, xuid, session);
Expand All @@ -166,8 +180,12 @@ public static HandshakeEntry processHandshake(BedrockSession session, LoginPacke
netEaseData = extractNetEaseData(result.rawIdentityClaims());
}

return new HandshakeEntry(identityPublicKey, clientData, xuid, uuid, displayName, xboxAuth, protocol,
packet.getAuthPayload() instanceof CertificateChainPayload, neteaseClient, netEaseData);
boolean shouldSendCertificateChain = packet.getAuthPayload() instanceof CertificateChainPayload ||
protocol.isBefore(ProtocolVersion.MINECRAFT_PE_1_26_20) ||
(neteaseClient && !(packet.getAuthPayload() instanceof TokenPayload));

return new HandshakeEntry(identityPublicKey, clientData, xuid, uuid, displayName, minecraftId, xboxAuth, protocol,
shouldSendCertificateChain, neteaseClient, netEaseData);
}

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -218,11 +236,4 @@ public static void processEncryption(BedrockSession session, PublicKey key) thro
});
}

public static JsonObject createChainExtraData(String displayName, String xuid, UUID uuid) {
JsonObject extraData = new JsonObject();
extraData.addProperty("displayName", displayName);
extraData.addProperty("XUID", xuid);
extraData.addProperty("identity", uuid.toString());
return extraData;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import com.google.gson.JsonObject;
import com.nimbusds.jwt.SignedJWT;
import dev.waterdog.waterdogpe.ProxyServer;
import dev.waterdog.waterdogpe.network.protocol.ProtocolVersion;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -46,6 +45,7 @@ public class LoginData {
private final String displayName;
private final UUID uuid;
private final String xuid;
private final String minecraftId;
private final boolean xboxAuthed;
private final SocketAddress address;
private final ProtocolVersion protocol;
Expand All @@ -62,7 +62,7 @@ public class LoginData {
private final JsonObject clientData;
private final NetEaseData netEaseData;
private final boolean netEaseClient;
private final boolean isChainPayload;
private final boolean shouldSendCertificateChain;
private LoginPacket loginPacket;
@Setter
@Builder.Default
Expand All @@ -82,7 +82,7 @@ public LoginPacket rebuildLoginPacket() {
SignedJWT signedClientData = HandshakeUtils.encodeJWT(this.keyPair, this.clientData);
loginPacket.setClientJwt(signedClientData.serialize());
loginPacket.setProtocolVersion(this.protocol.getProtocol());
if (isChainPayload || ProxyServer.getInstance().getConfiguration().useCertificatePayload()) {
if (shouldSendCertificateChain) {
JsonObject extraData = HandshakeUtils.createChainExtraData(displayName, xuid, uuid);
// Add NetEase-specific fields if this is a NetEase client
if (this.netEaseData != null) {
Expand All @@ -98,7 +98,7 @@ public LoginPacket rebuildLoginPacket() {
SignedJWT signedPayload = HandshakeUtils.createClientDataChain(this.keyPair, extraData);
loginPacket.setAuthPayload(new CertificateChainPayload(Collections.singletonList(signedPayload.serialize()), AuthType.SELF_SIGNED));
} else {
SignedJWT signedPayload = HandshakeUtils.createClientDataToken(this.keyPair, displayName, xuid);
SignedJWT signedPayload = HandshakeUtils.createClientDataToken(this.keyPair, displayName, xuid, uuid, minecraftId);
loginPacket.setAuthPayload(new TokenPayload(signedPayload.serialize(), AuthType.SELF_SIGNED));
}
this.loginPacket = loginPacket;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,6 @@ public class ProxyConfig extends YamlConfig {
@Comment("If enabled, the proxy will pass information like XUID or IP to the downstream server using custom fields in the LoginPacket")
private boolean useLoginExtras = false;

@Path("use_certificate_payload")
@Accessors(fluent = true)
@Comment("If enabled, the proxy will always send Certificate payload in the LoginPacket")
private boolean useCertificatePayload = true;

@Path("replace_username_spaces")
@Comment("Replaces username spaces with underscores if enabled")
private boolean replaceUsernameSpaces = false;
Expand Down Expand Up @@ -195,7 +190,7 @@ public class ProxyConfig extends YamlConfig {

@Path("netease_client_support")
@Comments({
"Enable support for NetEase (China) Minecraft clients. Only protocol v766 (1.21.50) is supported.",
"Enable support for NetEase (China) Minecraft clients. Supported protocols: v630, v686, v766, v819.",
"Warning: Enabling this will treat all RakNet v8 clients as NetEase clients"
})
private boolean neteaseClientSupport = false;
Expand Down