Convert into a multi-module project.

For now, the API module only contains a few assorted utilities. More
will be added later.
This commit is contained in:
Andrew Steinborn
2018-07-31 16:12:41 -04:00
parent f9fd58eea5
commit bbf861d3bc
97 changed files with 119 additions and 118 deletions

View File

@@ -0,0 +1,150 @@
package com.velocitypowered.network;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder;
import com.velocitypowered.proxy.protocol.netty.LegacyPingEncoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.ReadTimeoutHandler;
import java.net.InetSocketAddress;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import static com.velocitypowered.network.Connections.CLIENT_READ_TIMEOUT_SECONDS;
import static com.velocitypowered.network.Connections.FRAME_DECODER;
import static com.velocitypowered.network.Connections.FRAME_ENCODER;
import static com.velocitypowered.network.Connections.LEGACY_PING_DECODER;
import static com.velocitypowered.network.Connections.LEGACY_PING_ENCODER;
import static com.velocitypowered.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.network.Connections.MINECRAFT_ENCODER;
import static com.velocitypowered.network.Connections.READ_TIMEOUT;
public final class ConnectionManager {
private static final Logger logger = LogManager.getLogger(ConnectionManager.class);
private static final String DISABLE_EPOLL_PROPERTY = "velocity.connection.disable-epoll";
private static final boolean DISABLE_EPOLL = Boolean.getBoolean(DISABLE_EPOLL_PROPERTY);
private final Set<Channel> endpoints = new HashSet<>();
private final Class<? extends ServerSocketChannel> serverSocketChannelClass;
private final Class<? extends SocketChannel> socketChannelClass;
private final EventLoopGroup bossGroup;
private final EventLoopGroup workerGroup;
public ConnectionManager() {
final boolean epoll = canUseEpoll();
if (epoll) {
this.serverSocketChannelClass = EpollServerSocketChannel.class;
this.socketChannelClass = EpollSocketChannel.class;
this.bossGroup = new EpollEventLoopGroup(0, createThreadFactory("Netty Epoll Boss #%d"));
this.workerGroup = new EpollEventLoopGroup(0, createThreadFactory("Netty Epoll Worker #%d"));
} else {
this.serverSocketChannelClass = NioServerSocketChannel.class;
this.socketChannelClass = NioSocketChannel.class;
this.bossGroup = new NioEventLoopGroup(0, createThreadFactory("Netty Nio Boss #%d"));
this.workerGroup = new NioEventLoopGroup(0, createThreadFactory("Netty Nio Worker #%d"));
}
this.logChannelInformation(epoll);
}
private void logChannelInformation(final boolean epoll) {
final StringBuilder sb = new StringBuilder();
sb.append("Using channel type ");
sb.append(epoll ? "epoll": "nio");
if(DISABLE_EPOLL) {
sb.append(String.format(" - epoll explicitly disabled using -D%s=true", DISABLE_EPOLL_PROPERTY));
}
logger.info(sb.toString()); // TODO: move to logger
}
public void bind(final InetSocketAddress address) {
final ServerBootstrap bootstrap = new ServerBootstrap()
.channel(this.serverSocketChannelClass)
.group(this.bossGroup, this.workerGroup)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(final Channel ch) {
ch.pipeline()
.addLast(READ_TIMEOUT, new ReadTimeoutHandler(CLIENT_READ_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.addLast(LEGACY_PING_DECODER, new LegacyPingDecoder())
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(LEGACY_PING_ENCODER, LegacyPingEncoder.INSTANCE)
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
.addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.SERVERBOUND))
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolConstants.Direction.CLIENTBOUND));
final MinecraftConnection connection = new MinecraftConnection(ch);
connection.setState(StateRegistry.HANDSHAKE);
connection.setSessionHandler(new HandshakeSessionHandler(connection));
ch.pipeline().addLast(Connections.HANDLER, connection);
}
})
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.IP_TOS, 0x18)
.localAddress(address);
bootstrap.bind()
.addListener((ChannelFutureListener) future -> {
final Channel channel = future.channel();
if (future.isSuccess()) {
this.endpoints.add(channel);
logger.info("Listening on {}", channel.localAddress());
} else {
logger.error("Can't bind to {}", address, future.cause());
}
});
}
public Bootstrap createWorker() {
return new Bootstrap()
.channel(this.socketChannelClass)
.group(this.workerGroup);
}
public void shutdown() {
for (final Channel endpoint : this.endpoints) {
try {
logger.info("Closing endpoint {}", endpoint.localAddress());
endpoint.close().sync();
} catch (final InterruptedException e) {
logger.info("Interrupted whilst closing endpoint", e);
}
}
}
private static boolean canUseEpoll() {
return Epoll.isAvailable() && !DISABLE_EPOLL;
}
private static ThreadFactory createThreadFactory(final String nameFormat) {
return new ThreadFactoryBuilder()
.setNameFormat(nameFormat)
.setDaemon(true)
.build();
}
}

View File

@@ -0,0 +1,19 @@
package com.velocitypowered.network;
public interface Connections {
String CIPHER_DECODER = "cipher-decoder";
String CIPHER_ENCODER = "cipher-encoder";
String COMPRESSION_DECODER = "compression-decoder";
String COMPRESSION_ENCODER = "compression-encoder";
String FRAME_DECODER = "frame-decoder";
String FRAME_ENCODER = "frame-encoder";
String HANDLER = "handler";
String LEGACY_PING_DECODER = "legacy-ping-decoder";
String LEGACY_PING_ENCODER = "legacy-ping-encoder";
String MINECRAFT_DECODER = "minecraft-decoder";
String MINECRAFT_ENCODER = "minecraft-encoder";
String READ_TIMEOUT = "read-timeout";
int CLIENT_READ_TIMEOUT_SECONDS = 30; // client -> proxy
int SERVER_READ_TIMEOUT_SECONDS = 30; // proxy -> server
}

View File

@@ -0,0 +1,12 @@
package com.velocitypowered.proxy;
public class Velocity {
public static void main(String... args) throws InterruptedException {
final VelocityServer server = VelocityServer.getServer();
server.start();
Runtime.getRuntime().addShutdownHook(new Thread(server::shutdown, "Shutdown thread"));
Thread.currentThread().join();
}
}

View File

@@ -0,0 +1,102 @@
package com.velocitypowered.proxy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.velocitypowered.network.ConnectionManager;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.http.NettyHttpClient;
import com.velocitypowered.api.servers.ServerInfo;
import com.velocitypowered.proxy.util.AddressUtil;
import com.velocitypowered.proxy.util.EncryptionUtils;
import com.velocitypowered.proxy.util.ServerMap;
import io.netty.bootstrap.Bootstrap;
import net.kyori.text.Component;
import net.kyori.text.serializer.GsonComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.util.Map;
public class VelocityServer {
private static final Logger logger = LogManager.getLogger(VelocityServer.class);
private static final VelocityServer INSTANCE = new VelocityServer();
public static final Gson GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer())
.create();
private final ConnectionManager cm = new ConnectionManager();
private VelocityConfiguration configuration;
private NettyHttpClient httpClient;
private KeyPair serverKeyPair;
private final ServerMap servers = new ServerMap();
private VelocityServer() {
}
public static VelocityServer getServer() {
return INSTANCE;
}
public KeyPair getServerKeyPair() {
return serverKeyPair;
}
public VelocityConfiguration getConfiguration() {
return configuration;
}
public void start() {
// Create a key pair
logger.info("Booting up Velocity...");
try {
Path configPath = Paths.get("velocity.toml");
try {
configuration = VelocityConfiguration.read(configPath);
} catch (NoSuchFileException e) {
logger.info("No velocity.toml found, creating one for you...");
Files.copy(VelocityServer.class.getResourceAsStream("/velocity.toml"), configPath);
configuration = VelocityConfiguration.read(configPath);
}
if (!configuration.validate()) {
logger.error("Your configuration is invalid. Velocity will refuse to start up until the errors are resolved.");
System.exit(1);
}
} catch (IOException e) {
logger.error("Unable to load your velocity.toml. The server will shut down.", e);
System.exit(1);
}
for (Map.Entry<String, String> entry : configuration.getServers().entrySet()) {
servers.register(new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue())));
}
serverKeyPair = EncryptionUtils.createRsaKeyPair(1024);
httpClient = new NettyHttpClient(this);
this.cm.bind(configuration.getBind());
}
public ServerMap getServers() {
return servers;
}
public Bootstrap initializeGenericBootstrap() {
return this.cm.createWorker();
}
public void shutdown() {
this.cm.shutdown();
}
public NettyHttpClient getHttpClient() {
return httpClient;
}
}

View File

@@ -0,0 +1,7 @@
package com.velocitypowered.proxy.config;
public enum IPForwardingMode {
NONE,
LEGACY,
MODERN
}

View File

@@ -0,0 +1,185 @@
package com.velocitypowered.proxy.config;
import com.google.common.collect.ImmutableMap;
import com.moandjiezana.toml.Toml;
import com.velocitypowered.proxy.util.AddressUtil;
import com.velocitypowered.api.util.LegacyChatColorUtils;
import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializers;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.io.Reader;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class VelocityConfiguration {
private static final Logger logger = LogManager.getLogger(VelocityConfiguration.class);
private final String bind;
private final String motd;
private final int showMaxPlayers;
private final boolean onlineMode;
private final IPForwardingMode ipForwardingMode;
private final Map<String, String> servers;
private final List<String> attemptConnectionOrder;
private Component motdAsComponent;
private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode,
IPForwardingMode ipForwardingMode, Map<String, String> servers,
List<String> attemptConnectionOrder) {
this.bind = bind;
this.motd = motd;
this.showMaxPlayers = showMaxPlayers;
this.onlineMode = onlineMode;
this.ipForwardingMode = ipForwardingMode;
this.servers = servers;
this.attemptConnectionOrder = attemptConnectionOrder;
}
public boolean validate() {
boolean valid = true;
if (bind.isEmpty()) {
logger.error("'bind' option is empty.");
valid = false;
}
try {
AddressUtil.parseAddress(bind);
} catch (IllegalArgumentException e) {
logger.error("'bind' option does not specify a valid IP address.", e);
valid = false;
}
if (!onlineMode) {
logger.info("Proxy is running in offline mode!");
}
switch (ipForwardingMode) {
case NONE:
logger.info("IP forwarding is disabled! All players will appear to be connecting from the proxy and will have offline-mode UUIDs.");
break;
}
if (servers.isEmpty()) {
logger.error("You have no servers configured. :(");
valid = false;
} else {
if (attemptConnectionOrder.isEmpty()) {
logger.error("No fallback servers are configured!");
valid = false;
}
for (Map.Entry<String, String> entry : servers.entrySet()) {
try {
AddressUtil.parseAddress(entry.getValue());
} catch (IllegalArgumentException e) {
logger.error("Server {} does not have a valid IP address.", entry.getKey(), e);
valid = false;
}
}
for (String s : attemptConnectionOrder) {
if (!servers.containsKey(s)) {
logger.error("Fallback server " + s + " doesn't exist!");
valid = false;
}
}
}
try {
getMotdComponent();
} catch (Exception e) {
logger.error("Can't parse your MOTD", e);
valid = false;
}
return valid;
}
public InetSocketAddress getBind() {
return AddressUtil.parseAddress(bind);
}
public String getMotd() {
return motd;
}
public Component getMotdComponent() {
if (motdAsComponent == null) {
if (motd.startsWith("{")) {
motdAsComponent = ComponentSerializers.JSON.deserialize(motd);
} else {
motdAsComponent = ComponentSerializers.LEGACY.deserialize(LegacyChatColorUtils.translate('&', motd));
}
}
return motdAsComponent;
}
public int getShowMaxPlayers() {
return showMaxPlayers;
}
public boolean isOnlineMode() {
return onlineMode;
}
public IPForwardingMode getIpForwardingMode() {
return ipForwardingMode;
}
public Map<String, String> getServers() {
return servers;
}
public List<String> getAttemptConnectionOrder() {
return attemptConnectionOrder;
}
@Override
public String toString() {
return "VelocityConfiguration{" +
"bind='" + bind + '\'' +
", motd='" + motd + '\'' +
", showMaxPlayers=" + showMaxPlayers +
", onlineMode=" + onlineMode +
", ipForwardingMode=" + ipForwardingMode +
", servers=" + servers +
", attemptConnectionOrder=" + attemptConnectionOrder +
'}';
}
public static VelocityConfiguration read(Path path) throws IOException {
try (Reader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
Toml toml = new Toml().read(reader);
Map<String, String> servers = new HashMap<>();
for (Map.Entry<String, Object> entry : toml.getTable("servers").entrySet()) {
if (entry.getValue() instanceof String) {
servers.put(entry.getKey(), (String) entry.getValue());
} else {
if (!entry.getKey().equalsIgnoreCase("try")) {
throw new IllegalArgumentException("Server entry " + entry.getKey() + " is not a string!");
}
}
}
return new VelocityConfiguration(
toml.getString("bind"),
toml.getString("motd"),
toml.getLong("show-max-players").intValue(),
toml.getBoolean("online-mode"),
IPForwardingMode.valueOf(toml.getString("ip-forwarding").toUpperCase()),
ImmutableMap.copyOf(servers),
toml.getTable("servers").getList("try"));
}
}
}

View File

@@ -0,0 +1,216 @@
package com.velocitypowered.proxy.connection;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.protocol.PacketWrapper;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.compression.JavaVelocityCompressor;
import com.velocitypowered.proxy.protocol.encryption.JavaVelocityCipher;
import com.velocitypowered.proxy.protocol.encryption.VelocityCipher;
import com.velocitypowered.proxy.protocol.netty.*;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.GeneralSecurityException;
import static com.velocitypowered.network.Connections.CIPHER_DECODER;
import static com.velocitypowered.network.Connections.CIPHER_ENCODER;
import static com.velocitypowered.network.Connections.COMPRESSION_DECODER;
import static com.velocitypowered.network.Connections.COMPRESSION_ENCODER;
import static com.velocitypowered.network.Connections.FRAME_DECODER;
import static com.velocitypowered.network.Connections.FRAME_ENCODER;
import static com.velocitypowered.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.network.Connections.MINECRAFT_ENCODER;
/**
* A utility class to make working with the pipeline a little less painful and transparently handles certain Minecraft
* protocol mechanics.
*/
public class MinecraftConnection extends ChannelInboundHandlerAdapter {
private static final Logger logger = LogManager.getLogger(MinecraftConnection.class);
private final Channel channel;
private boolean closed;
private StateRegistry state;
private MinecraftSessionHandler sessionHandler;
private int protocolVersion;
private MinecraftConnectionAssociation association;
public MinecraftConnection(Channel channel) {
this.channel = channel;
this.closed = false;
this.state = StateRegistry.HANDSHAKE;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
if (sessionHandler != null) {
sessionHandler.connected();
}
if (association != null) {
logger.info("{} has connected", association);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (sessionHandler != null) {
sessionHandler.disconnected();
}
if (association != null) {
logger.info("{} has disconnected", association);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof PacketWrapper) {
PacketWrapper pw = (PacketWrapper) msg;
try {
if (sessionHandler != null) {
if (pw.getPacket() == null) {
sessionHandler.handleUnknown(pw.getBuffer());
} else {
sessionHandler.handle(pw.getPacket());
}
}
} finally {
ReferenceCountUtil.release(pw.getBuffer());
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (ctx.channel().isActive()) {
if (sessionHandler != null) {
sessionHandler.exception(cause);
}
if (association != null) {
logger.error("{}: exception encountered", association, cause);
} else {
logger.error("{} encountered an exception", ctx.channel().remoteAddress(), cause);
}
closed = true;
ctx.close();
}
}
public void write(Object msg) {
ensureOpen();
channel.writeAndFlush(msg, channel.voidPromise());
}
public void delayedWrite(Object msg) {
ensureOpen();
channel.write(msg, channel.voidPromise());
}
public void flush() {
ensureOpen();
channel.flush();
}
public void closeWith(Object msg) {
ensureOpen();
teardown();
channel.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
public void close() {
ensureOpen();
teardown();
channel.close();
}
public void teardown() {
closed = true;
}
public Channel getChannel() {
return channel;
}
public boolean isClosed() {
return closed;
}
public StateRegistry getState() {
return state;
}
public void setState(StateRegistry state) {
this.state = state;
this.channel.pipeline().get(MinecraftEncoder.class).setState(state);
this.channel.pipeline().get(MinecraftDecoder.class).setState(state);
}
public int getProtocolVersion() {
return protocolVersion;
}
public void setProtocolVersion(int protocolVersion) {
this.protocolVersion = protocolVersion;
this.channel.pipeline().get(MinecraftEncoder.class).setProtocolVersion(protocolVersion);
this.channel.pipeline().get(MinecraftDecoder.class).setProtocolVersion(protocolVersion);
}
public MinecraftSessionHandler getSessionHandler() {
return sessionHandler;
}
public void setSessionHandler(MinecraftSessionHandler sessionHandler) {
if (this.sessionHandler != null) {
this.sessionHandler.deactivated();
}
this.sessionHandler = sessionHandler;
sessionHandler.activated();
}
private void ensureOpen() {
Preconditions.checkState(!closed, "Connection is closed.");
}
public void setCompressionThreshold(int threshold) {
if (threshold == -1) {
channel.pipeline().remove(COMPRESSION_DECODER);
channel.pipeline().remove(COMPRESSION_ENCODER);
return;
}
JavaVelocityCompressor compressor = new JavaVelocityCompressor();
MinecraftCompressEncoder encoder = new MinecraftCompressEncoder(threshold, compressor);
MinecraftCompressDecoder decoder = new MinecraftCompressDecoder(threshold, compressor);
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
}
public void enableEncryption(byte[] secret) throws GeneralSecurityException {
SecretKey key = new SecretKeySpec(secret, "AES");
VelocityCipher decryptionCipher = new JavaVelocityCipher(false, key);
VelocityCipher encryptionCipher = new JavaVelocityCipher(true, key);
channel.pipeline().addBefore(FRAME_DECODER, CIPHER_DECODER, new MinecraftCipherDecoder(decryptionCipher));
channel.pipeline().addBefore(FRAME_ENCODER, CIPHER_ENCODER, new MinecraftCipherEncoder(encryptionCipher));
}
public MinecraftConnectionAssociation getAssociation() {
return association;
}
public void setAssociation(MinecraftConnectionAssociation association) {
this.association = association;
}
}

View File

@@ -0,0 +1,4 @@
package com.velocitypowered.proxy.connection;
public interface MinecraftConnectionAssociation {
}

View File

@@ -0,0 +1,32 @@
package com.velocitypowered.proxy.connection;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import io.netty.buffer.ByteBuf;
public interface MinecraftSessionHandler {
void handle(MinecraftPacket packet) throws Exception;
default void handleUnknown(ByteBuf buf) {
// No-op: we'll release the buffer later.
}
default void connected() {
}
default void disconnected() {
}
default void activated() {
}
default void deactivated() {
}
default void exception(Throwable throwable) {
}
}

View File

@@ -0,0 +1,9 @@
package com.velocitypowered.proxy.connection;
public class VelocityConstants {
private VelocityConstants() {
throw new AssertionError();
}
public static final String VELOCITY_IP_FORWARDING_CHANNEL = "velocity:player_info";
}

View File

@@ -0,0 +1,103 @@
package com.velocitypowered.proxy.connection.backend;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.packets.*;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf;
import io.netty.util.ReferenceCountUtil;
public class BackendPlaySessionHandler implements MinecraftSessionHandler {
private final ServerConnection connection;
public BackendPlaySessionHandler(ServerConnection connection) {
this.connection = connection;
}
@Override
public void handle(MinecraftPacket packet) {
ClientPlaySessionHandler playerHandler =
(ClientPlaySessionHandler) connection.getProxyPlayer().getConnection().getSessionHandler();
if (packet instanceof KeepAlive) {
// Forward onto the server
connection.getMinecraftConnection().write(packet);
} else if (packet instanceof Disconnect) {
Disconnect original = (Disconnect) packet;
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), original);
} else if (packet instanceof JoinGame) {
playerHandler.handleBackendJoinGame((JoinGame) packet);
} else if (packet instanceof Respawn) {
// Record the dimension switch, and then forward the packet on.
playerHandler.setCurrentDimension(((Respawn) packet).getDimension());
connection.getProxyPlayer().getConnection().write(packet);
} else if (packet instanceof BossBar) {
BossBar bossBar = (BossBar) packet;
switch (bossBar.getAction()) {
case 0: // add
playerHandler.getServerBossBars().add(bossBar.getUuid());
break;
case 1: // remove
playerHandler.getServerBossBars().remove(bossBar.getUuid());
break;
}
connection.getProxyPlayer().getConnection().write(packet);
} else if (packet instanceof PluginMessage) {
PluginMessage pm = (PluginMessage) packet;
try {
PluginMessage newPacket = pm;
if (!canForwardPluginMessage(newPacket)) {
return;
}
if (newPacket.getChannel().equals("MC|Brand")) {
newPacket = PluginMessageUtil.rewriteMCBrand(pm);
}
if (newPacket == pm) {
// we'll decrement this thrice: once when writing to the server, once just below this block,
// and once in the MinecraftConnection (since this is a slice)
pm.getData().retain();
}
connection.getProxyPlayer().getConnection().write(newPacket);
} finally {
ReferenceCountUtil.release(pm.getData());
}
} else {
// Just forward the packet on. We don't have anything to handle at this time.
if (packet instanceof ScoreboardTeam ||
packet instanceof ScoreboardObjective ||
packet instanceof ScoreboardSetScore ||
packet instanceof ScoreboardDisplay) {
playerHandler.handleServerScoreboardPacket(packet);
}
connection.getProxyPlayer().getConnection().write(packet);
}
}
@Override
public void handleUnknown(ByteBuf buf) {
ClientPlaySessionHandler playerHandler =
(ClientPlaySessionHandler) connection.getProxyPlayer().getConnection().getSessionHandler();
ByteBuf remapped = playerHandler.getIdRemapper().remap(buf, ProtocolConstants.Direction.CLIENTBOUND);
connection.getProxyPlayer().getConnection().write(remapped);
}
@Override
public void exception(Throwable throwable) {
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), throwable);
}
private boolean canForwardPluginMessage(PluginMessage message) {
ClientPlaySessionHandler playerHandler =
(ClientPlaySessionHandler) connection.getProxyPlayer().getConnection().getSessionHandler();
if (connection.getMinecraftConnection().getProtocolVersion() <= ProtocolConstants.MINECRAFT_1_12_2) {
return message.getChannel().startsWith("MC|") ||
playerHandler.getClientPluginMsgChannels().contains(message.getChannel());
} else {
return message.getChannel().startsWith("minecraft:") ||
playerHandler.getClientPluginMsgChannels().contains(message.getChannel());
}
}
}

View File

@@ -0,0 +1,124 @@
package com.velocitypowered.proxy.connection.backend;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.IPForwardingMode;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packets.*;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import net.kyori.text.TextComponent;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class LoginSessionHandler implements MinecraftSessionHandler {
private final ServerConnection connection;
private ScheduledFuture<?> forwardingCheckTask;
public LoginSessionHandler(ServerConnection connection) {
this.connection = connection;
}
@Override
public void activated() {
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.MODERN) {
forwardingCheckTask = connection.getMinecraftConnection().getChannel().eventLoop().schedule(() -> {
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(),
TextComponent.of("Your server did not send the forwarding request in time. Is it set up correctly?"));
}, 1, TimeUnit.SECONDS);
}
}
@Override
public void handle(MinecraftPacket packet) {
if (packet instanceof EncryptionRequest) {
throw new IllegalStateException("Backend server is online-mode!");
} else if (packet instanceof LoginPluginMessage) {
LoginPluginMessage message = (LoginPluginMessage) packet;
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.MODERN &&
message.getChannel().equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
LoginPluginResponse response = new LoginPluginResponse();
response.setSuccess(true);
response.setId(message.getId());
response.setData(createForwardingData(connection.getProxyPlayer().getRemoteAddress().getHostString(),
connection.getProxyPlayer().getProfile()));
connection.getMinecraftConnection().write(response);
cancelForwardingCheck();
ServerLogin login = new ServerLogin();
login.setUsername(connection.getProxyPlayer().getUsername());
connection.getMinecraftConnection().write(login);
} else {
// Don't understand
LoginPluginResponse response = new LoginPluginResponse();
response.setSuccess(false);
response.setId(message.getId());
response.setData(Unpooled.EMPTY_BUFFER);
connection.getMinecraftConnection().write(response);
}
} else if (packet instanceof Disconnect) {
Disconnect disconnect = (Disconnect) packet;
connection.disconnect();
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), disconnect);
} else if (packet instanceof SetCompression) {
SetCompression sc = (SetCompression) packet;
connection.getMinecraftConnection().setCompressionThreshold(sc.getThreshold());
} else if (packet instanceof ServerLoginSuccess) {
// The player has been logged on to the backend server.
connection.getMinecraftConnection().setState(StateRegistry.PLAY);
ServerConnection existingConnection = connection.getProxyPlayer().getConnectedServer();
if (existingConnection == null) {
// Strap on the play session handler
connection.getProxyPlayer().getConnection().setSessionHandler(new ClientPlaySessionHandler(connection.getProxyPlayer()));
} else {
// The previous server connection should become obsolete.
existingConnection.disconnect();
}
connection.getMinecraftConnection().setSessionHandler(new BackendPlaySessionHandler(connection));
connection.getProxyPlayer().setConnectedServer(connection);
}
}
@Override
public void deactivated() {
cancelForwardingCheck();
}
@Override
public void exception(Throwable throwable) {
connection.getProxyPlayer().handleConnectionException(connection.getServerInfo(), throwable);
}
private void cancelForwardingCheck() {
if (forwardingCheckTask != null) {
forwardingCheckTask.cancel(false);
forwardingCheckTask = null;
}
}
private static ByteBuf createForwardingData(String address, GameProfile profile) {
ByteBuf buf = Unpooled.buffer();
ProtocolUtils.writeString(buf, address);
ProtocolUtils.writeUuid(buf, profile.idAsUuid());
ProtocolUtils.writeString(buf, profile.getName());
ProtocolUtils.writeVarInt(buf, profile.getProperties().size());
for (GameProfile.Property property : profile.getProperties()) {
ProtocolUtils.writeString(buf, property.getName());
ProtocolUtils.writeString(buf, property.getValue());
String signature = property.getSignature();
if (signature != null) {
buf.writeBoolean(true);
ProtocolUtils.writeString(buf, signature);
} else {
buf.writeBoolean(false);
}
}
return buf;
}
}

View File

@@ -0,0 +1,134 @@
package com.velocitypowered.proxy.connection.backend;
import com.velocitypowered.proxy.config.IPForwardingMode;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder;
import com.velocitypowered.proxy.protocol.packets.Handshake;
import com.velocitypowered.proxy.protocol.packets.ServerLogin;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.api.servers.ServerInfo;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import io.netty.channel.*;
import io.netty.handler.timeout.ReadTimeoutHandler;
import java.util.concurrent.TimeUnit;
import static com.velocitypowered.network.Connections.FRAME_DECODER;
import static com.velocitypowered.network.Connections.FRAME_ENCODER;
import static com.velocitypowered.network.Connections.HANDLER;
import static com.velocitypowered.network.Connections.MINECRAFT_DECODER;
import static com.velocitypowered.network.Connections.MINECRAFT_ENCODER;
import static com.velocitypowered.network.Connections.READ_TIMEOUT;
import static com.velocitypowered.network.Connections.SERVER_READ_TIMEOUT_SECONDS;
public class ServerConnection implements MinecraftConnectionAssociation {
private final ServerInfo serverInfo;
private final ConnectedPlayer proxyPlayer;
private final VelocityServer server;
private MinecraftConnection minecraftConnection;
public ServerConnection(ServerInfo target, ConnectedPlayer proxyPlayer, VelocityServer server) {
this.serverInfo = target;
this.proxyPlayer = proxyPlayer;
this.server = server;
}
public void connect() {
server.initializeGenericBootstrap()
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
.addLast(READ_TIMEOUT, new ReadTimeoutHandler(SERVER_READ_TIMEOUT_SECONDS, TimeUnit.SECONDS))
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder())
.addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE)
.addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.CLIENTBOUND))
.addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolConstants.Direction.SERVERBOUND));
MinecraftConnection connection = new MinecraftConnection(ch);
connection.setState(StateRegistry.HANDSHAKE);
connection.setAssociation(ServerConnection.this);
ch.pipeline().addLast(HANDLER, connection);
}
})
.connect(serverInfo.getAddress())
.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
minecraftConnection = future.channel().pipeline().get(MinecraftConnection.class);
// Kick off the connection process
minecraftConnection.setSessionHandler(new LoginSessionHandler(ServerConnection.this));
startHandshake();
} else {
proxyPlayer.handleConnectionException(serverInfo, future.cause());
}
}
});
}
private String createBungeeForwardingAddress() {
// BungeeCord IP forwarding is simply a special injection after the "address" in the handshake,
// separated by \0 (the null byte). In order, you send the original host, the player's IP, their
// UUID (undashed), and if you are in online-mode, their login properties (retrieved from Mojang).
return serverInfo.getAddress().getHostString() + "\0" +
proxyPlayer.getRemoteAddress().getHostString() + "\0" +
proxyPlayer.getProfile().getId() + "\0" +
VelocityServer.GSON.toJson(proxyPlayer.getProfile().getProperties());
}
private void startHandshake() {
// Initiate a handshake.
Handshake handshake = new Handshake();
handshake.setNextStatus(StateRegistry.LOGIN_ID);
handshake.setProtocolVersion(proxyPlayer.getConnection().getProtocolVersion());
if (VelocityServer.getServer().getConfiguration().getIpForwardingMode() == IPForwardingMode.LEGACY) {
handshake.setServerAddress(createBungeeForwardingAddress());
} else {
handshake.setServerAddress(serverInfo.getAddress().getHostString());
}
handshake.setPort(serverInfo.getAddress().getPort());
minecraftConnection.write(handshake);
int protocolVersion = proxyPlayer.getConnection().getProtocolVersion();
minecraftConnection.setProtocolVersion(protocolVersion);
minecraftConnection.setState(StateRegistry.LOGIN);
// Send the server login packet for <=1.12.2 and for 1.13+ servers not using "modern" forwarding.
if (protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2 ||
VelocityServer.getServer().getConfiguration().getIpForwardingMode() != IPForwardingMode.MODERN) {
ServerLogin login = new ServerLogin();
login.setUsername(proxyPlayer.getUsername());
minecraftConnection.write(login);
}
}
public ConnectedPlayer getProxyPlayer() {
return proxyPlayer;
}
public MinecraftConnection getMinecraftConnection() {
return minecraftConnection;
}
public ServerInfo getServerInfo() {
return serverInfo;
}
public void disconnect() {
minecraftConnection.close();
minecraftConnection = null;
}
@Override
public String toString() {
return "[server connection] " + proxyPlayer.getProfile().getName() + " -> " + serverInfo.getName();
}
}

View File

@@ -0,0 +1,338 @@
package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.backend.ServerConnection;
import com.velocitypowered.api.servers.ServerInfo;
import com.velocitypowered.proxy.data.scoreboard.Objective;
import com.velocitypowered.proxy.data.scoreboard.Score;
import com.velocitypowered.proxy.data.scoreboard.Scoreboard;
import com.velocitypowered.proxy.data.scoreboard.Team;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.packets.*;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.remap.EntityIdRemapper;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import com.velocitypowered.proxy.util.ThrowableUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.EventLoop;
import io.netty.util.ReferenceCountUtil;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
private static final int MAX_PLUGIN_CHANNELS = 128;
private final ConnectedPlayer player;
private ScheduledFuture<?> pingTask;
private long lastPing = -1;
private boolean spawned = false;
private final List<UUID> serverBossBars = new ArrayList<>();
private final Set<String> clientPluginMsgChannels = new HashSet<>();
private PluginMessage brandMessage;
private int currentDimension;
private Scoreboard serverScoreboard = new Scoreboard();
private EntityIdRemapper idRemapper;
public ClientPlaySessionHandler(ConnectedPlayer player) {
this.player = player;
}
@Override
public void activated() {
EventLoop loop = player.getConnection().getChannel().eventLoop();
pingTask = loop.scheduleAtFixedRate(this::ping, 5, 15, TimeUnit.SECONDS);
}
private void ping() {
long randomId = ThreadLocalRandom.current().nextInt();
lastPing = randomId;
KeepAlive keepAlive = new KeepAlive();
keepAlive.setRandomId(randomId);
player.getConnection().write(keepAlive);
}
@Override
public void handle(MinecraftPacket packet) {
if (packet instanceof KeepAlive) {
KeepAlive keepAlive = (KeepAlive) packet;
if (keepAlive.getRandomId() != lastPing) {
throw new IllegalStateException("Client sent invalid keepAlive; expected " + lastPing + ", got " + keepAlive.getRandomId());
}
// Do not forward the packet to the player's server, because we handle pings for all servers already.
return;
}
if (packet instanceof ClientSettings) {
player.setClientSettings((ClientSettings) packet);
// forward it on
}
if (packet instanceof Chat) {
Chat chat = (Chat) packet;
if (chat.getMessage().equals("/connect")) {
ServerInfo info = new ServerInfo("test", new InetSocketAddress("localhost", 25566));
ServerConnection connection = new ServerConnection(info, player, VelocityServer.getServer());
connection.connect();
}
}
if (packet instanceof PluginMessage) {
handleClientPluginMessage((PluginMessage) packet);
return;
}
// If we don't want to handle this packet, just forward it on.
player.getConnectedServer().getMinecraftConnection().write(packet);
}
@Override
public void handleUnknown(ByteBuf buf) {
ByteBuf remapped = idRemapper.remap(buf, ProtocolConstants.Direction.SERVERBOUND);
player.getConnectedServer().getMinecraftConnection().write(remapped);
}
@Override
public void disconnected() {
player.teardown();
if (pingTask != null && !pingTask.isCancelled()) {
pingTask.cancel(false);
pingTask = null;
}
if (brandMessage != null) {
brandMessage.getData().release();
}
}
@Override
public void exception(Throwable throwable) {
player.close(TextComponent.builder()
.content("An exception occurred in your connection: ")
.color(TextColor.RED)
.append(TextComponent.of(ThrowableUtils.briefDescription(throwable), TextColor.WHITE))
.build());
}
public void handleBackendJoinGame(JoinGame joinGame) {
if (!spawned) {
// nothing special to do here
spawned = true;
currentDimension = joinGame.getDimension();
player.getConnection().delayedWrite(joinGame);
idRemapper = EntityIdRemapper.getMapper(joinGame.getEntityId(), player.getConnection().getProtocolVersion());
} else {
// In order to handle switching to another server we will need send three packets:
// - The join game packet from the backend server
// - A respawn packet with a different dimension
// - Another respawn with the correct dimension
// We can't simply ignore the packet with the different dimension. If you try to be smart about it it doesn't
// work.
idRemapper.setServerEntityId(joinGame.getEntityId());
player.getConnection().delayedWrite(joinGame);
int tempDim = joinGame.getDimension() == 0 ? -1 : 0;
player.getConnection().delayedWrite(new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType()));
player.getConnection().delayedWrite(new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType()));
currentDimension = joinGame.getDimension();
}
// Resend client settings packet to remote server if we have it, this preserves client settings across
// transitions.
if (player.getClientSettings() != null) {
player.getConnectedServer().getMinecraftConnection().delayedWrite(player.getClientSettings());
}
// Remove old boss bars.
for (UUID serverBossBar : serverBossBars) {
BossBar deletePacket = new BossBar();
deletePacket.setUuid(serverBossBar);
deletePacket.setAction(1); // remove
player.getConnection().delayedWrite(deletePacket);
}
serverBossBars.clear();
// Remove scoreboard junk.
clearServerScoreboard();
// Tell the server about this client's plugin messages. Velocity will forward them on to the client.
if (!clientPluginMsgChannels.isEmpty()) {
String channel = player.getConnection().getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13 ?
"minecraft:register" : "REGISTER";
player.getConnectedServer().getMinecraftConnection().delayedWrite(
PluginMessageUtil.constructChannelsPacket(channel, clientPluginMsgChannels));
}
// Tell the server the client's brand
if (brandMessage != null) {
brandMessage.getData().retain();
player.getConnectedServer().getMinecraftConnection().delayedWrite(brandMessage);
}
// Flush everything
player.getConnection().flush();
player.getConnectedServer().getMinecraftConnection().flush();
}
public void setCurrentDimension(int currentDimension) {
this.currentDimension = currentDimension;
}
public List<UUID> getServerBossBars() {
return serverBossBars;
}
public void handleClientPluginMessage(PluginMessage packet) {
logger.info("Got client plugin message packet {}", packet);
PluginMessage original = packet;
try {
if (packet.getChannel().equals("REGISTER") || packet.getChannel().equals("minecraft:register")) {
List<String> actuallyRegistered = new ArrayList<>();
List<String> channels = PluginMessageUtil.getChannels(packet);
for (String channel : channels) {
if (clientPluginMsgChannels.size() >= MAX_PLUGIN_CHANNELS &&
!clientPluginMsgChannels.contains(channel)) {
throw new IllegalStateException("Too many plugin message channels registered");
}
if (clientPluginMsgChannels.add(channel)) {
actuallyRegistered.add(channel);
}
}
if (actuallyRegistered.size() > 0) {
logger.info("Rewritten register packet: {}", actuallyRegistered);
PluginMessage newRegisterPacket = PluginMessageUtil.constructChannelsPacket(packet.getChannel(), actuallyRegistered);
player.getConnectedServer().getMinecraftConnection().write(newRegisterPacket);
}
return;
}
if (packet.getChannel().equals("UNREGISTER") || packet.getChannel().equals("minecraft:unregister")) {
List<String> channels = PluginMessageUtil.getChannels(packet);
clientPluginMsgChannels.removeAll(channels);
}
if (packet.getChannel().equals("MC|Brand") || packet.getChannel().equals("minecraft:brand")) {
if (this.brandMessage != null) {
// Rewrite this packet to indicate that Velocity is running. Hurrah!
packet = PluginMessageUtil.rewriteMCBrand(packet);
this.brandMessage = packet;
} else {
// Already have the brand packet and don't need this one.
return;
}
}
// No other special handling?
if (packet == original) {
// we'll decrement this thrice: once when writing to the server, once just below this block,
// and once in the MinecraftConnection (since this is a slice)
packet.getData().retain();
}
player.getConnectedServer().getMinecraftConnection().write(packet);
} finally {
ReferenceCountUtil.release(original.getData());
}
}
public void handleServerScoreboardPacket(MinecraftPacket packet) {
if (packet instanceof ScoreboardDisplay) {
ScoreboardDisplay sd = (ScoreboardDisplay) packet;
serverScoreboard.setPosition(sd.getPosition());
serverScoreboard.setDisplayName(sd.getDisplayName());
}
if (packet instanceof ScoreboardObjective) {
ScoreboardObjective so = (ScoreboardObjective) packet;
switch (so.getMode()) {
case ScoreboardObjective.ADD:
Objective o = new Objective(so.getId());
o.setDisplayName(so.getDisplayName());
o.setType(so.getType());
serverScoreboard.getObjectives().put(so.getId(), o);
break;
case ScoreboardObjective.REMOVE:
serverScoreboard.getObjectives().remove(so.getId());
break;
}
}
if (packet instanceof ScoreboardSetScore) {
ScoreboardSetScore sss = (ScoreboardSetScore) packet;
Objective objective = serverScoreboard.getObjectives().get(sss.getObjective());
if (objective == null) {
return;
}
switch (sss.getAction()) {
case ScoreboardSetScore.CHANGE:
Score score = new Score(sss.getEntity(), sss.getValue());
objective.getScores().put(sss.getEntity(), score);
break;
case ScoreboardSetScore.REMOVE:
objective.getScores().remove(sss.getEntity());
break;
}
}
if (packet instanceof ScoreboardTeam) {
ScoreboardTeam st = (ScoreboardTeam) packet;
switch (st.getMode()) {
case ScoreboardTeam.ADD:
// TODO: Preserve other team information? We might not need to...
Team team = new Team(st.getId());
serverScoreboard.getTeams().put(st.getId(), team);
break;
case ScoreboardTeam.REMOVE:
serverScoreboard.getTeams().remove(st.getId());
break;
}
}
}
private void clearServerScoreboard() {
for (Objective objective : serverScoreboard.getObjectives().values()) {
for (Score score : objective.getScores().values()) {
ScoreboardSetScore sss = new ScoreboardSetScore();
sss.setObjective(objective.getId());
sss.setAction(ScoreboardSetScore.REMOVE);
sss.setEntity(score.getTarget());
player.getConnection().delayedWrite(sss);
}
ScoreboardObjective so = new ScoreboardObjective();
so.setId(objective.getId());
so.setMode(ScoreboardObjective.REMOVE);
player.getConnection().delayedWrite(so);
}
for (Team team : serverScoreboard.getTeams().values()) {
ScoreboardTeam st = new ScoreboardTeam();
st.setId(team.getId());
st.setMode(ScoreboardTeam.REMOVE);
player.getConnection().delayedWrite(st);
}
serverScoreboard = new Scoreboard();
}
public Set<String> getClientPluginMsgChannels() {
return clientPluginMsgChannels;
}
public EntityIdRemapper getIdRemapper() {
return idRemapper;
}
}

View File

@@ -0,0 +1,158 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.proxy.protocol.packets.Chat;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.backend.ServerConnection;
import com.velocitypowered.proxy.protocol.packets.ClientSettings;
import com.velocitypowered.proxy.util.ThrowableUtils;
import com.velocitypowered.api.servers.ServerInfo;
import com.velocitypowered.proxy.protocol.packets.Disconnect;
import net.kyori.text.Component;
import net.kyori.text.TextComponent;
import net.kyori.text.TranslatableComponent;
import net.kyori.text.format.TextColor;
import net.kyori.text.serializer.ComponentSerializers;
import net.kyori.text.serializer.PlainComponentSerializer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public class ConnectedPlayer implements MinecraftConnectionAssociation {
private static final PlainComponentSerializer PASS_THRU_TRANSLATE = new PlainComponentSerializer((c) -> "", TranslatableComponent::key);
private static final Logger logger = LogManager.getLogger(ConnectedPlayer.class);
private final GameProfile profile;
private final MinecraftConnection connection;
private int tryIndex = 0;
private ServerConnection connectedServer;
private ClientSettings clientSettings;
private ServerConnection connectionInFlight;
public ConnectedPlayer(GameProfile profile, MinecraftConnection connection) {
this.profile = profile;
this.connection = connection;
}
public String getUsername() {
return profile.getName();
}
public UUID getUniqueId() {
return profile.idAsUuid();
}
public GameProfile getProfile() {
return profile;
}
public MinecraftConnection getConnection() {
return connection;
}
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) connection.getChannel().remoteAddress();
}
public ServerConnection getConnectedServer() {
return connectedServer;
}
public ClientSettings getClientSettings() {
return clientSettings;
}
public void setClientSettings(ClientSettings clientSettings) {
this.clientSettings = clientSettings;
}
public void handleConnectionException(ServerInfo info, Throwable throwable) {
String error = ThrowableUtils.briefDescription(throwable);
String userMessage;
if (connectedServer != null && connectedServer.getServerInfo().equals(info)) {
logger.error("{}: exception occurred in connection to {}", this, info.getName(), throwable);
userMessage = "Exception in server " + info.getName();
} else {
logger.error("{}: unable to connect to server {}", this, info.getName(), throwable);
userMessage = "Exception connecting to server " + info.getName();
}
handleConnectionException(info, TextComponent.builder()
.content(userMessage + ": ")
.color(TextColor.RED)
.append(TextComponent.of(error, TextColor.WHITE))
.build());
}
public void handleConnectionException(ServerInfo info, Disconnect disconnect) {
Component disconnectReason = ComponentSerializers.JSON.deserialize(disconnect.getReason());
String plainTextReason = PASS_THRU_TRANSLATE.serialize(disconnectReason);
if (connectedServer != null && connectedServer.getServerInfo().equals(info)) {
logger.error("{}: kicked from server {}: {}", this, info.getName(), plainTextReason);
} else {
logger.error("{}: disconnected while connecting to {}: {}", this, info.getName(), plainTextReason);
}
handleConnectionException(info, disconnectReason);
}
public void handleConnectionException(ServerInfo info, Component disconnectReason) {
if (connectedServer == null || connectedServer.getServerInfo().equals(info)) {
// The player isn't yet connected to a server or they are already connected to the server
// they're disconnected from.
connection.closeWith(Disconnect.create(disconnectReason));
} else {
connection.write(Chat.create(disconnectReason));
}
}
public Optional<ServerInfo> getNextServerToTry() {
List<String> serversToTry = VelocityServer.getServer().getConfiguration().getAttemptConnectionOrder();
if (tryIndex >= serversToTry.size()) {
return Optional.empty();
}
String toTryName = serversToTry.get(tryIndex);
tryIndex++;
return VelocityServer.getServer().getServers().getServer(toTryName);
}
public void connect(ServerInfo info) {
Preconditions.checkNotNull(info, "info");
Preconditions.checkState(connectionInFlight == null, "A connection is already active!");
ServerConnection connection = new ServerConnection(info, this, VelocityServer.getServer());
connectionInFlight = connection;
connection.connect();
}
public void setConnectedServer(ServerConnection serverConnection) {
if (this.connectedServer != null && !serverConnection.getServerInfo().equals(connectedServer.getServerInfo())) {
this.tryIndex = 0;
}
this.connectedServer = serverConnection;
}
public void close(TextComponent reason) {
connection.closeWith(Disconnect.create(reason));
}
public void teardown() {
if (connectionInFlight != null) {
connectionInFlight.disconnect();
}
if (connectedServer != null) {
connectedServer.disconnect();
}
}
@Override
public String toString() {
return "[connected player] " + getProfile().getName() + " (" + getRemoteAddress() + ")";
}
}

View File

@@ -0,0 +1,48 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packets.Disconnect;
import com.velocitypowered.proxy.protocol.packets.Handshake;
import net.kyori.text.TranslatableComponent;
public class HandshakeSessionHandler implements MinecraftSessionHandler {
private final MinecraftConnection connection;
public HandshakeSessionHandler(MinecraftConnection connection) {
this.connection = Preconditions.checkNotNull(connection, "connection");
}
@Override
public void handle(MinecraftPacket packet) {
if (!(packet instanceof Handshake)) {
throw new IllegalArgumentException("Did not expect packet " + packet.getClass().getName());
}
Handshake handshake = (Handshake) packet;
switch (handshake.getNextStatus()) {
case StateRegistry.STATUS_ID:
connection.setState(StateRegistry.STATUS);
connection.setProtocolVersion(handshake.getProtocolVersion());
connection.setSessionHandler(new StatusSessionHandler(connection));
break;
case StateRegistry.LOGIN_ID:
connection.setState(StateRegistry.LOGIN);
connection.setProtocolVersion(handshake.getProtocolVersion());
if (!ProtocolConstants.isSupported(handshake.getProtocolVersion())) {
connection.closeWith(Disconnect.create(TranslatableComponent.of("multiplayer.disconnect.outdated_client")));
return;
} else {
connection.setSessionHandler(new LoginSessionHandler(connection));
}
break;
default:
throw new IllegalArgumentException("Invalid state " + handshake.getNextStatus());
}
}
}

View File

@@ -0,0 +1,23 @@
package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
public class InitialConnectSessionHandler implements MinecraftSessionHandler {
private final ConnectedPlayer player;
public InitialConnectSessionHandler(ConnectedPlayer player) {
this.player = player;
}
@Override
public void handle(MinecraftPacket packet) {
// No-op: will never handle packets
}
@Override
public void disconnected() {
// the user cancelled the login process
player.teardown();
}
}

View File

@@ -0,0 +1,142 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.connection.VelocityConstants;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packets.*;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.api.servers.ServerInfo;
import com.velocitypowered.proxy.util.EncryptionUtils;
import io.netty.buffer.Unpooled;
import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.net.InetSocketAddress;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
public class LoginSessionHandler implements MinecraftSessionHandler {
private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class);
private static final String MOJANG_SERVER_AUTH_URL =
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s";
private final MinecraftConnection inbound;
private ServerLogin login;
private byte[] verify;
private int playerInfoId;
public LoginSessionHandler(MinecraftConnection inbound) {
this.inbound = Preconditions.checkNotNull(inbound, "inbound");
}
@Override
public void activated() {
if (inbound.getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_13) {
LoginPluginMessage message = new LoginPluginMessage();
playerInfoId = ThreadLocalRandom.current().nextInt();
message.setId(playerInfoId);
message.setChannel(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL);
message.setData(Unpooled.EMPTY_BUFFER);
inbound.write(message);
}
}
@Override
public void handle(MinecraftPacket packet) throws Exception {
if (packet instanceof LoginPluginResponse) {
LoginPluginResponse lpr = (LoginPluginResponse) packet;
if (lpr.getId() == playerInfoId && lpr.isSuccess()) {
// Uh oh, someone's trying to run Velocity behind Velocity. We don't want that happening.
inbound.closeWith(Disconnect.create(
TextComponent.of("Running Velocity behind Velocity isn't supported.", TextColor.RED)
));
}
} else if (packet instanceof ServerLogin) {
this.login = (ServerLogin) packet;
if (VelocityServer.getServer().getConfiguration().isOnlineMode()) {
// Request encryption.
EncryptionRequest request = generateRequest();
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
inbound.write(request);
} else {
// Offline-mode, don't try to request encryption.
handleSuccessfulLogin(GameProfile.forOfflinePlayer(login.getUsername()));
}
} else if (packet instanceof EncryptionResponse) {
KeyPair serverKeyPair = VelocityServer.getServer().getServerKeyPair();
EncryptionResponse response = (EncryptionResponse) packet;
byte[] decryptedVerifyToken = EncryptionUtils.decryptRsa(serverKeyPair, response.getVerifyToken());
if (!Arrays.equals(verify, decryptedVerifyToken)) {
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
}
byte[] decryptedSharedSecret = EncryptionUtils.decryptRsa(serverKeyPair, response.getSharedSecret());
String serverId = EncryptionUtils.generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
String playerIp = ((InetSocketAddress) inbound.getChannel().remoteAddress()).getHostString();
VelocityServer.getServer().getHttpClient()
.get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
.thenAcceptAsync(profileResponse -> {
try {
inbound.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
GameProfile profile = VelocityServer.GSON.fromJson(profileResponse, GameProfile.class);
handleSuccessfulLogin(profile);
}, inbound.getChannel().eventLoop())
.exceptionally(exception -> {
logger.error("Unable to enable encryption", exception);
inbound.close();
return null;
});
}
}
private EncryptionRequest generateRequest() {
byte[] verify = new byte[4];
ThreadLocalRandom.current().nextBytes(verify);
EncryptionRequest request = new EncryptionRequest();
request.setPublicKey(VelocityServer.getServer().getServerKeyPair().getPublic().getEncoded());
request.setVerifyToken(verify);
return request;
}
private void handleSuccessfulLogin(GameProfile profile) {
// Initiate a regular connection and move over to it.
ConnectedPlayer player = new ConnectedPlayer(profile, inbound);
Optional<ServerInfo> toTry = player.getNextServerToTry();
if (!toTry.isPresent()) {
player.close(TextComponent.of("No available servers", TextColor.RED));
return;
}
inbound.write(new SetCompression(256));
inbound.setCompressionThreshold(256);
ServerLoginSuccess success = new ServerLoginSuccess();
success.setUsername(profile.getName());
success.setUuid(profile.idAsUuid());
inbound.write(success);
logger.info("{} has connected", player);
inbound.setAssociation(player);
inbound.setState(StateRegistry.PLAY);
inbound.setSessionHandler(new InitialConnectSessionHandler(player));
player.connect(toTry.get());
}
}

View File

@@ -0,0 +1,53 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.packets.KeepAlive;
import com.velocitypowered.proxy.protocol.packets.StatusPing;
import com.velocitypowered.proxy.protocol.packets.StatusRequest;
import com.velocitypowered.proxy.protocol.packets.StatusResponse;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.data.ServerPing;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
public class StatusSessionHandler implements MinecraftSessionHandler {
private final MinecraftConnection connection;
public StatusSessionHandler(MinecraftConnection connection) {
this.connection = connection;
}
@Override
public void handle(MinecraftPacket packet) {
Preconditions.checkArgument(packet instanceof StatusPing|| packet instanceof StatusRequest,
"Unrecognized packet type " + packet.getClass().getName());
if (packet instanceof StatusPing) {
// Just send back the client's packet, no processing to do here.
connection.closeWith(packet);
return;
}
VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration();
// Status request
ServerPing ping = new ServerPing(
new ServerPing.Version(connection.getProtocolVersion(), "Velocity 1.9-1.13"),
new ServerPing.Players(0, configuration.getShowMaxPlayers()),
configuration.getMotdComponent(),
null
);
StatusResponse response = new StatusResponse();
response.setStatus(VelocityServer.GSON.toJson(ping));
connection.write(response);
}
@Override
public void handleUnknown(ByteBuf buf) {
throw new IllegalStateException("Unknown data " + ByteBufUtil.hexDump(buf));
}
}

View File

@@ -0,0 +1,63 @@
package com.velocitypowered.proxy.connection.http;
import com.velocitypowered.proxy.VelocityServer;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import javax.net.ssl.SSLEngine;
import java.net.URL;
import java.util.concurrent.CompletableFuture;
public class NettyHttpClient {
private final VelocityServer server;
public NettyHttpClient(VelocityServer server) {
this.server = server;
}
public CompletableFuture<String> get(URL url) {
String host = url.getHost();
int port = url.getPort();
boolean ssl = url.getProtocol().equals("https");
if (port == -1) {
port = ssl ? 443 : 80;
}
CompletableFuture<String> reply = new CompletableFuture<>();
server.initializeGenericBootstrap()
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
if (ssl) {
SslContext context = SslContextBuilder.forClient().build();
SSLEngine engine = context.newEngine(ch.alloc());
ch.pipeline().addLast(new SslHandler(engine));
}
ch.pipeline().addLast(new HttpClientCodec());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, url.getPath() + "?" + url.getQuery());
request.headers().add(HttpHeaderNames.HOST, url.getHost());
request.headers().add(HttpHeaderNames.USER_AGENT, "Velocity");
ctx.writeAndFlush(request);
}
});
ch.pipeline().addLast(new SimpleHttpResponseCollector(reply));
}
})
.connect(host, port)
.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
reply.completeExceptionally(future.cause());
}
}
});
return reply;
}
}

View File

@@ -0,0 +1,41 @@
package com.velocitypowered.proxy.connection.http;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.LastHttpContent;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
class SimpleHttpResponseCollector extends ChannelInboundHandlerAdapter {
private final StringBuilder buffer = new StringBuilder(1024);
private final CompletableFuture<String> reply;
SimpleHttpResponseCollector(CompletableFuture<String> reply) {
this.reply = reply;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HttpResponse) {
HttpResponseStatus status = ((HttpResponse) msg).status();
if (status != HttpResponseStatus.OK) {
ctx.close();
reply.completeExceptionally(new RuntimeException("Unexpected status code " + status.code()));
}
}
if (msg instanceof HttpContent) {
buffer.append(((HttpContent) msg).content().toString(StandardCharsets.UTF_8));
((HttpContent) msg).release();
if (msg instanceof LastHttpContent) {
ctx.close();
reply.complete(buffer.toString());
}
}
}
}

View File

@@ -0,0 +1,84 @@
package com.velocitypowered.proxy.data;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.proxy.util.UuidUtils;
import java.util.List;
import java.util.UUID;
public class GameProfile {
private final String id;
private final String name;
private final List<Property> properties;
public GameProfile(String id, String name, List<Property> properties) {
this.id = id;
this.name = name;
this.properties = ImmutableList.copyOf(properties);
}
public String getId() {
return id;
}
public UUID idAsUuid() {
return UuidUtils.fromUndashed(id);
}
public String getName() {
return name;
}
public List<Property> getProperties() {
return ImmutableList.copyOf(properties);
}
public static GameProfile forOfflinePlayer(String username) {
Preconditions.checkNotNull(username, "username");
String id = UuidUtils.toUndashed(UuidUtils.generateOfflinePlayerUuid(username));
return new GameProfile(id, username, ImmutableList.of());
}
@Override
public String toString() {
return "GameProfile{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", properties=" + properties +
'}';
}
public class Property {
private final String name;
private final String value;
private final String signature;
public Property(String name, String value, String signature) {
this.name = name;
this.value = value;
this.signature = signature;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
public String getSignature() {
return signature;
}
@Override
public String toString() {
return "Property{" +
"name='" + name + '\'' +
", value='" + value + '\'' +
", signature='" + signature + '\'' +
'}';
}
}
}

View File

@@ -0,0 +1,95 @@
package com.velocitypowered.proxy.data;
import net.kyori.text.Component;
public class ServerPing {
private final Version version;
private final Players players;
private final Component description;
private final String favicon;
public ServerPing(Version version, Players players, Component description, String favicon) {
this.version = version;
this.players = players;
this.description = description;
this.favicon = favicon;
}
public Version getVersion() {
return version;
}
public Players getPlayers() {
return players;
}
public Component getDescription() {
return description;
}
public String getFavicon() {
return favicon;
}
@Override
public String toString() {
return "ServerPing{" +
"version=" + version +
", players=" + players +
", description=" + description +
", favicon='" + favicon + '\'' +
'}';
}
public static class Version {
private final int protocol;
private final String name;
public Version(int protocol, String name) {
this.protocol = protocol;
this.name = name;
}
public int getProtocol() {
return protocol;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Version{" +
"protocol=" + protocol +
", name='" + name + '\'' +
'}';
}
}
public static class Players {
private final int online;
private final int max;
public Players(int online, int max) {
this.online = online;
this.max = max;
}
public int getOnline() {
return online;
}
public int getMax() {
return max;
}
@Override
public String toString() {
return "Players{" +
"online=" + online +
", max=" + max +
'}';
}
}
}

View File

@@ -0,0 +1,59 @@
package com.velocitypowered.proxy.data.scoreboard;
import net.kyori.text.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Objective {
private final String id;
private Component displayName;
private ObjectiveMode type;
private final List<Team> teams = new ArrayList<>();
private final Map<String, Score> scores = new HashMap<>();
public Objective(String id) {
this.id = id;
}
public String getId() {
return id;
}
public Component getDisplayName() {
return displayName;
}
public void setDisplayName(Component displayName) {
this.displayName = displayName;
}
public ObjectiveMode getType() {
return type;
}
public void setType(ObjectiveMode type) {
this.type = type;
}
public List<Team> getTeams() {
return teams;
}
public Map<String, Score> getScores() {
return scores;
}
@Override
public String toString() {
return "Objective{" +
"id='" + id + '\'' +
", displayName='" + displayName + '\'' +
", type='" + type + '\'' +
", teams=" + teams +
", scores=" + scores +
'}';
}
}

View File

@@ -0,0 +1,6 @@
package com.velocitypowered.proxy.data.scoreboard;
public enum ObjectiveMode {
INTEGER,
HEARTS
}

View File

@@ -0,0 +1,43 @@
package com.velocitypowered.proxy.data.scoreboard;
import java.util.Objects;
public class Score {
private final String target;
private final int value;
public Score(String target, int value) {
this.target = target;
this.value = value;
}
public String getTarget() {
return target;
}
public int getValue() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Score score = (Score) o;
return value == score.value &&
Objects.equals(target, score.target);
}
@Override
public int hashCode() {
return Objects.hash(target, value);
}
@Override
public String toString() {
return "Score{" +
"target='" + target + '\'' +
", value=" + value +
'}';
}
}

View File

@@ -0,0 +1,45 @@
package com.velocitypowered.proxy.data.scoreboard;
import java.util.HashMap;
import java.util.Map;
public class Scoreboard {
private String displayName;
private byte position;
private final Map<String, Objective> objectives = new HashMap<>();
private final Map<String, Team> teams = new HashMap<>();
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public byte getPosition() {
return position;
}
public void setPosition(byte position) {
this.position = position;
}
public Map<String, Objective> getObjectives() {
return objectives;
}
public Map<String, Team> getTeams() {
return teams;
}
@Override
public String toString() {
return "Scoreboard{" +
"displayName='" + displayName + '\'' +
", position=" + position +
", objectives=" + objectives +
", teams=" + teams +
'}';
}
}

View File

@@ -0,0 +1,89 @@
package com.velocitypowered.proxy.data.scoreboard;
import java.util.Collection;
import java.util.HashSet;
public class Team {
private final String id;
private String prefix;
private String suffix;
private byte flags;
private String nameTagVisibility;
private String collision;
private byte color;
private final Collection<String> players = new HashSet<>();
public Team(String id) {
this.id = id;
}
public String getId() {
return id;
}
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
public byte getFlags() {
return flags;
}
public void setFlags(byte flags) {
this.flags = flags;
}
public String getNameTagVisibility() {
return nameTagVisibility;
}
public void setNameTagVisibility(String nameTagVisibility) {
this.nameTagVisibility = nameTagVisibility;
}
public String getCollision() {
return collision;
}
public void setCollision(String collision) {
this.collision = collision;
}
public byte getColor() {
return color;
}
public void setColor(byte color) {
this.color = color;
}
public Collection<String> getPlayers() {
return players;
}
@Override
public String toString() {
return "Team{" +
"id='" + id + '\'' +
", prefix='" + prefix + '\'' +
", suffix='" + suffix + '\'' +
", flags=" + flags +
", nameTagVisibility='" + nameTagVisibility + '\'' +
", collision='" + collision + '\'' +
", color=" + color +
", players=" + players +
'}';
}
}

View File

@@ -0,0 +1,9 @@
package com.velocitypowered.proxy.protocol;
import io.netty.buffer.ByteBuf;
public interface MinecraftPacket {
void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion);
void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion);
}

View File

@@ -0,0 +1,30 @@
package com.velocitypowered.proxy.protocol;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
public class PacketWrapper {
private final MinecraftPacket packet;
private final ByteBuf buffer;
public PacketWrapper(MinecraftPacket packet, ByteBuf buffer) {
this.packet = packet;
this.buffer = buffer;
}
public MinecraftPacket getPacket() {
return packet;
}
public ByteBuf getBuffer() {
return buffer;
}
@Override
public String toString() {
return "PacketWrapper{" +
"packet=" + packet +
", buffer=" + ByteBufUtil.hexDump(buffer) +
'}';
}
}

View File

@@ -0,0 +1,42 @@
package com.velocitypowered.proxy.protocol;
import java.util.Arrays;
public enum ProtocolConstants { ;
public static final int MINECRAFT_1_9 = 107;
public static final int MINECRAFT_1_9_1 = 108;
public static final int MINECRAFT_1_9_2 = 109;
public static final int MINECRAFT_1_9_4 = 110;
public static final int MINECRAFT_1_10 = 210;
public static final int MINECRAFT_1_11 = 315;
public static final int MINECRAFT_1_11_1 = 316;
public static final int MINECRAFT_1_12 = 335;
public static final int MINECRAFT_1_12_1 = 338;
public static final int MINECRAFT_1_12_2 = 340;
public static final int MINECRAFT_1_13 = 393;
public static final int MINIMUM_GENERIC_VERSION = MINECRAFT_1_9;
public static final int[] SUPPORTED_VERSIONS = new int[] {
MINECRAFT_1_9,
MINECRAFT_1_9_1,
MINECRAFT_1_9_2,
MINECRAFT_1_9_4,
MINECRAFT_1_10,
MINECRAFT_1_11,
MINECRAFT_1_11_1,
MINECRAFT_1_12,
MINECRAFT_1_12_1,
MINECRAFT_1_12_2,
MINECRAFT_1_13
};
public static boolean isSupported(int version) {
return Arrays.binarySearch(SUPPORTED_VERSIONS, version) >= 0;
}
public enum Direction {
SERVERBOUND,
CLIENTBOUND
}
}

View File

@@ -0,0 +1,98 @@
package com.velocitypowered.proxy.protocol;
import com.google.common.base.Preconditions;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializer;
import net.kyori.text.serializer.ComponentSerializers;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
public enum ProtocolUtils { ;
private static final int DEFAULT_MAX_STRING_SIZE = 65536; // 64KiB
public static int readVarInt(ByteBuf buf) {
int i = 0;
int j = 0;
while (true) {
int k = buf.readByte();
i |= (k & 0x7F) << j++ * 7;
if (j > 5) throw new RuntimeException("VarInt too big");
if ((k & 0x80) != 128) break;
}
return i;
}
public static void writeVarInt(ByteBuf buf, int value) {
while (true) {
if ((value & 0xFFFFFF80) == 0) {
buf.writeByte(value);
return;
}
buf.writeByte(value & 0x7F | 0x80);
value >>>= 7;
}
}
public static String readString(ByteBuf buf) {
return readString(buf, DEFAULT_MAX_STRING_SIZE);
}
public static String readString(ByteBuf buf, int cap) {
int length = readVarInt(buf);
Preconditions.checkArgument(length <= cap, "Bad string size (got %s, maximum is %s)", length, cap);
String str = buf.toString(buf.readerIndex(), length, StandardCharsets.UTF_8);
buf.skipBytes(length);
return str;
}
public static void writeString(ByteBuf buf, String str) {
int size = ByteBufUtil.utf8Bytes(str);
writeVarInt(buf, size);
ByteBufUtil.writeUtf8(buf, str);
}
public static byte[] readByteArray(ByteBuf buf) {
return readByteArray(buf, DEFAULT_MAX_STRING_SIZE);
}
public static byte[] readByteArray(ByteBuf buf, int cap) {
int length = readVarInt(buf);
Preconditions.checkArgument(length <= cap, "Bad string size (got %s, maximum is %s)", length, cap);
byte[] array = new byte[length];
buf.readBytes(array);
return array;
}
public static void writeByteArray(ByteBuf buf, byte[] array) {
writeVarInt(buf, array.length);
buf.writeBytes(array);
}
public static UUID readUuid(ByteBuf buf) {
long msb = buf.readLong();
long lsb = buf.readLong();
return new UUID(msb, lsb);
}
public static void writeUuid(ByteBuf buf, UUID uuid) {
buf.writeLong(uuid.getMostSignificantBits());
buf.writeLong(uuid.getLeastSignificantBits());
}
public static Component readScoreboardTextComponent(ByteBuf buf, int protocolVersion) {
String toDeserialize = readString(buf);
ComponentSerializer<Component, ? extends Component, String> serializer =
protocolVersion >= ProtocolConstants.MINECRAFT_1_13 ? ComponentSerializers.JSON : ComponentSerializers.LEGACY;
return serializer.deserialize(toDeserialize);
}
public static void writeScoreboardTextComponent(ByteBuf buf, int protocolVersion, Component component) {
ComponentSerializer<Component, ? extends Component, String> serializer =
protocolVersion >= ProtocolConstants.MINECRAFT_1_13 ? ComponentSerializers.JSON : ComponentSerializers.LEGACY;
writeString(buf, serializer.serialize(component));
}
}

View File

@@ -0,0 +1,274 @@
package com.velocitypowered.proxy.protocol;
import com.velocitypowered.proxy.protocol.packets.*;
import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.collection.IntObjectMap;
import java.util.*;
import java.util.function.Supplier;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.*;
public enum StateRegistry {
HANDSHAKE {
{
SERVERBOUND.register(Handshake.class, Handshake::new,
genericMappings(0x00));
}
},
STATUS {
{
SERVERBOUND.register(StatusRequest.class, StatusRequest::new,
genericMappings(0x00));
SERVERBOUND.register(StatusPing.class, StatusPing::new,
genericMappings(0x01));
CLIENTBOUND.register(StatusResponse.class, StatusResponse::new,
genericMappings(0x00));
CLIENTBOUND.register(StatusPing.class, StatusPing::new,
genericMappings(0x01));
}
},
PLAY {
{
SERVERBOUND.register(Chat.class, Chat::new,
map(0x02, MINECRAFT_1_9),
map(0x03, MINECRAFT_1_12),
map(0x02, MINECRAFT_1_12_2),
map(0x02, MINECRAFT_1_13));
SERVERBOUND.register(ClientSettings.class, ClientSettings::new,
map(0x04, MINECRAFT_1_9),
map(0x05, MINECRAFT_1_12),
map(0x04, MINECRAFT_1_12_1),
map(0x04, MINECRAFT_1_13));
SERVERBOUND.register(PluginMessage.class, PluginMessage::new,
map(0x09, MINECRAFT_1_9),
map(0x0A, MINECRAFT_1_12),
map(0x09, MINECRAFT_1_12_1),
map(0x0A, MINECRAFT_1_13));
SERVERBOUND.register(KeepAlive.class, KeepAlive::new,
map(0x0B, MINECRAFT_1_9),
map(0x0C, MINECRAFT_1_12),
map(0x0B, MINECRAFT_1_12_1),
map(0x0E, MINECRAFT_1_13));
CLIENTBOUND.register(BossBar.class, BossBar::new,
map(0x0C, MINECRAFT_1_9),
map(0x0C, MINECRAFT_1_12));
CLIENTBOUND.register(Chat.class, Chat::new,
map(0x0F, MINECRAFT_1_9),
map(0x0F, MINECRAFT_1_12),
map(0x0E, MINECRAFT_1_13));
CLIENTBOUND.register(PluginMessage.class, PluginMessage::new,
map(0x18, MINECRAFT_1_9),
map(0x18, MINECRAFT_1_12),
map(0x19, MINECRAFT_1_13));
CLIENTBOUND.register(Disconnect.class, Disconnect::new,
map(0x1A, MINECRAFT_1_9),
map(0x1A, MINECRAFT_1_12),
map(0x1B, MINECRAFT_1_13));
CLIENTBOUND.register(KeepAlive.class, KeepAlive::new,
map(0x1F, MINECRAFT_1_9),
map(0x1F, MINECRAFT_1_12),
map(0x21, MINECRAFT_1_13));
CLIENTBOUND.register(JoinGame.class, JoinGame::new,
map(0x23, MINECRAFT_1_9),
map(0x23, MINECRAFT_1_12),
map(0x25, MINECRAFT_1_13));
CLIENTBOUND.register(Respawn.class, Respawn::new,
map(0x33, MINECRAFT_1_9),
map(0x34, MINECRAFT_1_12),
map(0x35, MINECRAFT_1_12_2),
map(0x38, MINECRAFT_1_13));
CLIENTBOUND.register(ScoreboardDisplay.class, ScoreboardDisplay::new,
map(0x38, MINECRAFT_1_9),
map(0x3A, MINECRAFT_1_12),
map(0x3B, MINECRAFT_1_12_1),
map(0x3E, MINECRAFT_1_13));
CLIENTBOUND.register(ScoreboardObjective.class, ScoreboardObjective::new,
map(0x3F, MINECRAFT_1_9),
map(0x41, MINECRAFT_1_12),
map(0x42, MINECRAFT_1_12_1),
map(0x45, MINECRAFT_1_13));
CLIENTBOUND.register(ScoreboardTeam.class, ScoreboardTeam::new,
map(0x41, MINECRAFT_1_9),
map(0x43, MINECRAFT_1_12),
map(0x44, MINECRAFT_1_12_1),
map(0x47, MINECRAFT_1_13));
CLIENTBOUND.register(ScoreboardSetScore.class, ScoreboardSetScore::new,
map(0x42, MINECRAFT_1_9),
map(0x44, MINECRAFT_1_12),
map(0x45, MINECRAFT_1_12_1),
map(0x48, MINECRAFT_1_13));
}
},
LOGIN {
{
SERVERBOUND.register(ServerLogin.class, ServerLogin::new,
genericMappings(0x00));
SERVERBOUND.register(EncryptionResponse.class, EncryptionResponse::new,
genericMappings(0x01));
SERVERBOUND.register(LoginPluginResponse.class, LoginPluginResponse::new,
map(0x02, MINECRAFT_1_13));
CLIENTBOUND.register(Disconnect.class, Disconnect::new,
genericMappings(0x00));
CLIENTBOUND.register(EncryptionRequest.class, EncryptionRequest::new,
genericMappings(0x01));
CLIENTBOUND.register(ServerLoginSuccess.class, ServerLoginSuccess::new,
genericMappings(0x02));
CLIENTBOUND.register(SetCompression.class, SetCompression::new,
genericMappings(0x03));
CLIENTBOUND.register(LoginPluginMessage.class, LoginPluginMessage::new,
map(0x04, MINECRAFT_1_13));
}
};
public static final int STATUS_ID = 1;
public static final int LOGIN_ID = 2;
public final PacketRegistry CLIENTBOUND = new PacketRegistry(ProtocolConstants.Direction.CLIENTBOUND, this);
public final PacketRegistry SERVERBOUND = new PacketRegistry(ProtocolConstants.Direction.SERVERBOUND, this);
public static class PacketRegistry {
private static final IntObjectMap<int[]> LINKED_PROTOCOL_VERSIONS = new IntObjectHashMap<>();
static {
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_9, new int[] { MINECRAFT_1_9_1, MINECRAFT_1_9_2, MINECRAFT_1_9_4 });
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_9_4, new int[] { MINECRAFT_1_10, MINECRAFT_1_11, MINECRAFT_1_11_1 });
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_12, new int[] { MINECRAFT_1_12_1 });
LINKED_PROTOCOL_VERSIONS.put(MINECRAFT_1_12_1, new int[] { MINECRAFT_1_12_2 });
}
private final ProtocolConstants.Direction direction;
private final StateRegistry state;
private final IntObjectMap<ProtocolVersion> versions = new IntObjectHashMap<>();
public PacketRegistry(Direction direction, StateRegistry state) {
this.direction = direction;
this.state = state;
for (int version : ProtocolConstants.SUPPORTED_VERSIONS) {
versions.put(version, new ProtocolVersion(version));
}
versions.put(MINIMUM_GENERIC_VERSION, new ProtocolVersion(MINIMUM_GENERIC_VERSION));
}
public ProtocolVersion getVersion(final int version) {
ProtocolVersion result = versions.get(version);
if (result == null) {
if (state != PLAY) {
return getVersion(MINIMUM_GENERIC_VERSION);
}
throw new IllegalArgumentException("Could not find data for protocol version " + version);
}
return result;
}
public <P extends MinecraftPacket> void register(Class<P> clazz, Supplier<P> packetSupplier, PacketMapping... mappings) {
if (mappings.length == 0) {
throw new IllegalArgumentException("At least one mapping must be provided.");
}
for (final PacketMapping mapping : mappings) {
ProtocolVersion version = this.versions.get(mapping.protocolVersion);
if (version == null) {
throw new IllegalArgumentException("Unknown protocol version " + mapping.protocolVersion);
}
version.packetIdToSupplier.put(mapping.id, packetSupplier);
version.packetClassToId.put(clazz, mapping.id);
int[] linked = LINKED_PROTOCOL_VERSIONS.get(mapping.protocolVersion);
if (linked != null) {
links: for (int i : linked) {
// Make sure that later mappings override this one.
for (PacketMapping m : mappings) {
if (i == m.protocolVersion) continue links;
}
register(clazz, packetSupplier, map(mapping.id, i));
}
}
}
}
public class ProtocolVersion {
public final int id;
final IntObjectMap<Supplier<? extends MinecraftPacket>> packetIdToSupplier = new IntObjectHashMap<>();
final Map<Class<? extends MinecraftPacket>, Integer> packetClassToId = new HashMap<>();
ProtocolVersion(final int id) {
this.id = id;
}
public MinecraftPacket createPacket(final int id) {
final Supplier<? extends MinecraftPacket> supplier = this.packetIdToSupplier.get(id);
if (supplier == null) {
return null;
}
return supplier.get();
}
public int getPacketId(final MinecraftPacket packet) {
final Integer id = this.packetClassToId.get(packet.getClass());
if (id == null) {
throw new IllegalArgumentException(String.format(
"Unable to find id for packet of type %s in %s protocol %s",
packet.getClass().getName(), PacketRegistry.this.direction, this.id
));
}
return id;
}
@Override
public String toString() {
return "ProtocolVersion{" +
"id=" + id +
", packetClassToId=" + packetClassToId +
'}';
}
}
}
public static class PacketMapping {
private final int id;
private final int protocolVersion;
public PacketMapping(int id, int protocolVersion) {
this.id = id;
this.protocolVersion = protocolVersion;
}
@Override
public String toString() {
return "PacketMapping{" +
"id=" + id +
", protocolVersion=" + protocolVersion +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PacketMapping that = (PacketMapping) o;
return id == that.id &&
protocolVersion == that.protocolVersion;
}
@Override
public int hashCode() {
return Objects.hash(id, protocolVersion);
}
}
private static PacketMapping map(int id, int version) {
return new PacketMapping(id, version);
}
private static PacketMapping[] genericMappings(int id) {
return new PacketMapping[]{
map(id, MINECRAFT_1_9),
map(id, MINECRAFT_1_12),
map(id, MINECRAFT_1_13)
};
}
}

View File

@@ -0,0 +1,60 @@
package com.velocitypowered.proxy.protocol.compression;
import com.google.common.base.Preconditions;
import io.netty.buffer.ByteBuf;
import java.util.zip.DataFormatException;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
public class JavaVelocityCompressor implements VelocityCompressor {
private final Deflater deflater;
private final Inflater inflater;
private final byte[] buf;
private boolean disposed = false;
public JavaVelocityCompressor() {
this.deflater = new Deflater();
this.inflater = new Inflater();
this.buf = new byte[8192];
}
@Override
public void inflate(ByteBuf source, ByteBuf destination) throws DataFormatException {
ensureNotDisposed();
byte[] inData = new byte[source.readableBytes()];
source.readBytes(inData);
inflater.setInput(inData);
while (!inflater.finished()) {
int read = inflater.inflate(buf);
destination.writeBytes(buf, 0, read);
}
inflater.reset();
}
@Override
public void deflate(ByteBuf source, ByteBuf destination) throws DataFormatException {
ensureNotDisposed();
byte[] inData = new byte[source.readableBytes()];
source.readBytes(inData);
deflater.setInput(inData);
deflater.finish();
while (!deflater.finished()) {
int bytes = deflater.deflate(buf);
destination.writeBytes(buf, 0, bytes);
}
deflater.reset();
}
@Override
public void dispose() {
ensureNotDisposed();
disposed = true;
}
private void ensureNotDisposed() {
Preconditions.checkState(!disposed, "Object already disposed");
}
}

View File

@@ -0,0 +1,12 @@
package com.velocitypowered.proxy.protocol.compression;
import com.velocitypowered.proxy.util.Disposable;
import io.netty.buffer.ByteBuf;
import java.util.zip.DataFormatException;
public interface VelocityCompressor extends Disposable {
void inflate(ByteBuf source, ByteBuf destination) throws DataFormatException;
void deflate(ByteBuf source, ByteBuf destination) throws DataFormatException;
}

View File

@@ -0,0 +1,43 @@
package com.velocitypowered.proxy.protocol.encryption;
import com.google.common.base.Preconditions;
import io.netty.buffer.ByteBuf;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import java.security.GeneralSecurityException;
public class JavaVelocityCipher implements VelocityCipher {
private final Cipher cipher;
private boolean disposed = false;
public JavaVelocityCipher(boolean encrypt, SecretKey key) throws GeneralSecurityException {
this.cipher = Cipher.getInstance("AES/CFB8/NoPadding");
this.cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key, new IvParameterSpec(key.getEncoded()));
}
@Override
public void process(ByteBuf source, ByteBuf destination) throws ShortBufferException {
ensureNotDisposed();
byte[] sourceAsBytes = new byte[source.readableBytes()];
source.readBytes(sourceAsBytes);
int outputSize = cipher.getOutputSize(sourceAsBytes.length);
byte[] destinationBytes = new byte[outputSize];
cipher.update(sourceAsBytes, 0, sourceAsBytes.length, destinationBytes);
destination.writeBytes(destinationBytes);
}
@Override
public void dispose() {
ensureNotDisposed();
disposed = true;
}
private void ensureNotDisposed() {
Preconditions.checkState(!disposed, "Object already disposed");
}
}

View File

@@ -0,0 +1,10 @@
package com.velocitypowered.proxy.protocol.encryption;
import com.velocitypowered.proxy.util.Disposable;
import io.netty.buffer.ByteBuf;
import javax.crypto.ShortBufferException;
public interface VelocityCipher extends Disposable {
void process(ByteBuf source, ByteBuf destination) throws ShortBufferException;
}

View File

@@ -0,0 +1,26 @@
package com.velocitypowered.proxy.protocol.netty;
import com.velocitypowered.proxy.protocol.packets.LegacyPing;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
public class LegacyPingDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() < 2) {
return;
}
short first = in.getUnsignedByte(in.readerIndex());
short second = in.getUnsignedByte(in.readerIndex() + 1);
if (first == 0xfe && second == 0x01) {
in.skipBytes(in.readableBytes());
out.add(new LegacyPing());
}
ctx.pipeline().remove(this);
}
}

View File

@@ -0,0 +1,40 @@
package com.velocitypowered.proxy.protocol.netty;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.proxy.protocol.packets.LegacyPingResponse;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
@ChannelHandler.Sharable
public class LegacyPingEncoder extends MessageToByteEncoder<LegacyPingResponse> {
public static final LegacyPingEncoder INSTANCE = new LegacyPingEncoder();
private LegacyPingEncoder() {}
@Override
protected void encode(ChannelHandlerContext ctx, LegacyPingResponse msg, ByteBuf out) throws Exception {
out.writeByte(0xff);
String serializedResponse = serialize(msg);
out.writeShort(serializedResponse.length());
out.writeBytes(serializedResponse.getBytes(StandardCharsets.UTF_16BE));
}
private String serialize(LegacyPingResponse response) {
List<String> parts = ImmutableList.of(
"§1",
Integer.toString(response.getProtocolVersion()),
response.getServerVersion(),
response.getMotd(),
Integer.toString(response.getPlayersOnline()),
Integer.toString(response.getPlayersMax())
);
return Joiner.on('\0').join(parts);
}
}

View File

@@ -0,0 +1,29 @@
package com.velocitypowered.proxy.protocol.netty;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.protocol.encryption.VelocityCipher;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
public class MinecraftCipherDecoder extends ByteToMessageDecoder {
private final VelocityCipher cipher;
public MinecraftCipherDecoder(VelocityCipher cipher) {
this.cipher = Preconditions.checkNotNull(cipher, "cipher");
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
ByteBuf decrypted = ctx.alloc().buffer();
try {
cipher.process(in, decrypted);
out.add(decrypted);
} catch (Exception e) {
decrypted.release();
throw e;
}
}
}

View File

@@ -0,0 +1,20 @@
package com.velocitypowered.proxy.protocol.netty;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.protocol.encryption.VelocityCipher;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MinecraftCipherEncoder extends MessageToByteEncoder<ByteBuf> {
private final VelocityCipher cipher;
public MinecraftCipherEncoder(VelocityCipher cipher) {
this.cipher = Preconditions.checkNotNull(cipher, "cipher");
}
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
cipher.process(msg, out);
}
}

View File

@@ -0,0 +1,45 @@
package com.velocitypowered.proxy.protocol.netty;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.compression.VelocityCompressor;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List;
public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
private static final int MAXIMUM_INITIAL_BUFFER_SIZE = 65536; // 64KiB
private final int threshold;
private final VelocityCompressor compressor;
public MinecraftCompressDecoder(int threshold, VelocityCompressor compressor) {
this.threshold = threshold;
this.compressor = compressor;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
int uncompressedSize = ProtocolUtils.readVarInt(msg);
if (uncompressedSize == 0) {
// Strip the now-useless uncompressed size, this message is already uncompressed.
out.add(msg.slice().retain());
msg.skipBytes(msg.readableBytes());
return;
}
Preconditions.checkState(uncompressedSize >= threshold, "Uncompressed size %s doesn't make sense with threshold %s", uncompressedSize, threshold);
ByteBuf uncompressed = ctx.alloc().buffer(Math.min(uncompressedSize, MAXIMUM_INITIAL_BUFFER_SIZE));
try {
compressor.inflate(msg, uncompressed);
Preconditions.checkState(uncompressedSize == uncompressed.readableBytes(), "Mismatched compression sizes");
out.add(uncompressed);
} catch (Exception e) {
// If something went wrong, rethrow the exception, but ensure we free our temporary buffer first.
uncompressed.release();
throw e;
}
}
}

View File

@@ -0,0 +1,37 @@
package com.velocitypowered.proxy.protocol.netty;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.compression.VelocityCompressor;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MinecraftCompressEncoder extends MessageToByteEncoder<ByteBuf> {
private final int threshold;
private final VelocityCompressor compressor;
public MinecraftCompressEncoder(int threshold, VelocityCompressor compressor) {
this.threshold = threshold;
this.compressor = compressor;
}
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
if (msg.readableBytes() <= threshold) {
// Under the threshold, there is nothing to do.
ProtocolUtils.writeVarInt(out, 0);
out.writeBytes(msg);
return;
}
ByteBuf compressedBuffer = ctx.alloc().buffer();
try {
int uncompressed = msg.readableBytes();
compressor.deflate(msg, compressedBuffer);
ProtocolUtils.writeVarInt(out, uncompressed);
out.writeBytes(compressedBuffer);
} finally {
compressedBuffer.release();
}
}
}

View File

@@ -0,0 +1,67 @@
package com.velocitypowered.proxy.protocol.netty;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.protocol.*;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List;
public class MinecraftDecoder extends MessageToMessageDecoder<ByteBuf> {
private StateRegistry state;
private final ProtocolConstants.Direction direction;
private StateRegistry.PacketRegistry.ProtocolVersion protocolVersion;
public MinecraftDecoder(ProtocolConstants.Direction direction) {
this.state = StateRegistry.HANDSHAKE;
this.direction = Preconditions.checkNotNull(direction, "direction");
this.setProtocolVersion(ProtocolConstants.MINIMUM_GENERIC_VERSION);
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
if (!msg.isReadable()) {
return;
}
ByteBuf slice = msg.slice().retain();
int packetId = ProtocolUtils.readVarInt(msg);
MinecraftPacket packet = this.protocolVersion.createPacket(packetId);
if (packet == null) {
msg.skipBytes(msg.readableBytes());
out.add(new PacketWrapper(null, slice));
} else {
try {
packet.decode(msg, direction, protocolVersion.id);
} catch (Exception e) {
throw new CorruptedFrameException("Error decoding " + packet.getClass() + " Direction " + direction
+ " Protocol " + protocolVersion + " State " + state + " ID " + Integer.toHexString(packetId), e);
}
out.add(new PacketWrapper(packet, slice));
}
}
public StateRegistry.PacketRegistry.ProtocolVersion getProtocolVersion() {
return protocolVersion;
}
public void setProtocolVersion(int protocolVersion) {
this.protocolVersion = (this.direction == ProtocolConstants.Direction.CLIENTBOUND ? this.state.CLIENTBOUND : this.state.SERVERBOUND).getVersion(protocolVersion);
}
public StateRegistry getState() {
return state;
}
public void setState(StateRegistry state) {
this.state = state;
this.setProtocolVersion(protocolVersion.id);
}
public ProtocolConstants.Direction getDirection() {
return direction;
}
}

View File

@@ -0,0 +1,50 @@
package com.velocitypowered.proxy.protocol.netty;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MinecraftEncoder extends MessageToByteEncoder<MinecraftPacket> {
private StateRegistry state;
private final ProtocolConstants.Direction direction;
private StateRegistry.PacketRegistry.ProtocolVersion protocolVersion;
public MinecraftEncoder(ProtocolConstants.Direction direction) {
this.state = StateRegistry.HANDSHAKE;
this.direction = Preconditions.checkNotNull(direction, "direction");
this.setProtocolVersion(ProtocolConstants.MINIMUM_GENERIC_VERSION);
}
@Override
protected void encode(ChannelHandlerContext ctx, MinecraftPacket msg, ByteBuf out) {
int packetId = this.protocolVersion.getPacketId(msg);
ProtocolUtils.writeVarInt(out, packetId);
msg.encode(out, direction, protocolVersion.id);
}
public StateRegistry.PacketRegistry.ProtocolVersion getProtocolVersion() {
return protocolVersion;
}
public void setProtocolVersion(final int protocolVersion) {
this.protocolVersion = (this.direction == ProtocolConstants.Direction.CLIENTBOUND ? this.state.CLIENTBOUND : this.state.SERVERBOUND).getVersion(protocolVersion);
}
public StateRegistry getState() {
return state;
}
public void setState(StateRegistry state) {
this.state = state;
this.setProtocolVersion(protocolVersion.id);
}
public ProtocolConstants.Direction getDirection() {
return direction;
}
}

View File

@@ -0,0 +1,27 @@
package com.velocitypowered.proxy.protocol.netty;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes() < 1) {
return;
}
in.markReaderIndex();
int packetLength = ProtocolUtils.readVarInt(in);
if (in.readableBytes() < packetLength) {
in.resetReaderIndex();
return;
}
out.add(in.slice(in.readerIndex(), packetLength).retain());
in.skipBytes(packetLength);
}
}

View File

@@ -0,0 +1,21 @@
package com.velocitypowered.proxy.protocol.netty;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
@ChannelHandler.Sharable
public class MinecraftVarintLengthEncoder extends MessageToByteEncoder<ByteBuf> {
public static final MinecraftVarintLengthEncoder INSTANCE = new MinecraftVarintLengthEncoder();
private MinecraftVarintLengthEncoder() { }
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
out.ensureWritable(msg.readableBytes() + 5);
ProtocolUtils.writeVarInt(out, msg.readableBytes());
out.writeBytes(msg);
}
}

View File

@@ -0,0 +1,153 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import java.util.UUID;
public class BossBar implements MinecraftPacket {
public static final int ADD = 0;
public static final int REMOVE = 1;
public static final int UPDATE_PERCENT = 2;
public static final int UPDATE_NAME = 3;
public static final int UPDATE_STYLE = 4;
public static final int UPDATE_PROPERTIES = 5;
private UUID uuid;
private int action;
private String name;
private float percent;
private int color;
private int overlay;
private short flags;
public UUID getUuid() {
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
public int getAction() {
return action;
}
public void setAction(int action) {
this.action = action;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getPercent() {
return percent;
}
public void setPercent(float percent) {
this.percent = percent;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public int getOverlay() {
return overlay;
}
public void setOverlay(int overlay) {
this.overlay = overlay;
}
public short getFlags() {
return flags;
}
public void setFlags(short flags) {
this.flags = flags;
}
@Override
public String toString() {
return "BossBar{" +
"uuid=" + uuid +
", action=" + action +
", name='" + name + '\'' +
", percent=" + percent +
", color=" + color +
", overlay=" + overlay +
", flags=" + flags +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.uuid = ProtocolUtils.readUuid(buf);
this.action = ProtocolUtils.readVarInt(buf);
switch (action) {
case ADD:
this.name = ProtocolUtils.readString(buf);
this.percent = buf.readFloat();
this.color = ProtocolUtils.readVarInt(buf);
this.overlay = ProtocolUtils.readVarInt(buf);
this.flags = buf.readUnsignedByte();
break;
case REMOVE:
break;
case UPDATE_PERCENT:
this.percent = buf.readFloat();
break;
case UPDATE_NAME:
this.name = ProtocolUtils.readString(buf);
break;
case UPDATE_STYLE:
this.color = ProtocolUtils.readVarInt(buf);
this.overlay = ProtocolUtils.readVarInt(buf);
break;
case UPDATE_PROPERTIES:
this.flags = buf.readUnsignedByte();
break;
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeUuid(buf, uuid);
ProtocolUtils.writeVarInt(buf, action);
switch (action) {
case ADD:
ProtocolUtils.writeString(buf, name);
buf.writeFloat(percent);
ProtocolUtils.writeVarInt(buf, color);
ProtocolUtils.writeVarInt(buf, overlay);
buf.writeByte(flags);
break;
case REMOVE:
break;
case UPDATE_PERCENT:
buf.writeFloat(percent);
break;
case UPDATE_NAME:
ProtocolUtils.writeString(buf, name);
break;
case UPDATE_STYLE:
ProtocolUtils.writeVarInt(buf, color);
ProtocolUtils.writeVarInt(buf, overlay);
break;
case UPDATE_PROPERTIES:
buf.writeByte(flags);
break;
}
}
}

View File

@@ -0,0 +1,72 @@
package com.velocitypowered.proxy.protocol.packets;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializers;
public class Chat implements MinecraftPacket {
public static final byte CHAT = (byte) 0;
private String message;
private byte type;
public Chat() {
}
public Chat(String message, byte type) {
this.message = message;
this.type = type;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public byte getType() {
return type;
}
public void setType(byte type) {
this.type = type;
}
@Override
public String toString() {
return "Chat{" +
"message='" + message + '\'' +
", type=" + type +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
message = ProtocolUtils.readString(buf);
if (direction == ProtocolConstants.Direction.CLIENTBOUND) {
type = buf.readByte();
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeString(buf, message);
if (direction == ProtocolConstants.Direction.CLIENTBOUND) {
buf.writeByte(type);
}
}
public static Chat create(Component component) {
return create(component, CHAT);
}
public static Chat create(Component component, byte type) {
Preconditions.checkNotNull(component, "component");
return new Chat(ComponentSerializers.JSON.serialize(component), type);
}
}

View File

@@ -0,0 +1,95 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
public class ClientSettings implements MinecraftPacket {
private String locale;
private byte viewDistance;
private int chatVisibility;
private boolean chatColors;
private short skinParts;
private int mainHand;
public String getLocale() {
return locale;
}
public void setLocale(String locale) {
this.locale = locale;
}
public byte getViewDistance() {
return viewDistance;
}
public void setViewDistance(byte viewDistance) {
this.viewDistance = viewDistance;
}
public int getChatVisibility() {
return chatVisibility;
}
public void setChatVisibility(int chatVisibility) {
this.chatVisibility = chatVisibility;
}
public boolean isChatColors() {
return chatColors;
}
public void setChatColors(boolean chatColors) {
this.chatColors = chatColors;
}
public short getSkinParts() {
return skinParts;
}
public void setSkinParts(short skinParts) {
this.skinParts = skinParts;
}
public int getMainHand() {
return mainHand;
}
public void setMainHand(int mainHand) {
this.mainHand = mainHand;
}
@Override
public String toString() {
return "ClientSettings{" +
"locale='" + locale + '\'' +
", viewDistance=" + viewDistance +
", chatVisibility=" + chatVisibility +
", chatColors=" + chatColors +
", skinParts=" + skinParts +
", mainHand=" + mainHand +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.locale = ProtocolUtils.readString(buf, 16);
this.viewDistance = buf.readByte();
this.chatVisibility = ProtocolUtils.readVarInt(buf);
this.chatColors = buf.readBoolean();
this.skinParts = buf.readUnsignedByte();
this.mainHand = ProtocolUtils.readVarInt(buf);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeString(buf, locale);
buf.writeByte(viewDistance);
ProtocolUtils.writeVarInt(buf, chatVisibility);
buf.writeBoolean(chatColors);
buf.writeByte(skinParts);
ProtocolUtils.writeVarInt(buf, mainHand);
}
}

View File

@@ -0,0 +1,50 @@
package com.velocitypowered.proxy.protocol.packets;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import net.kyori.text.Component;
import net.kyori.text.serializer.ComponentSerializers;
public class Disconnect implements MinecraftPacket {
private String reason;
public Disconnect() {
}
public Disconnect(String reason) {
this.reason = reason;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
@Override
public String toString() {
return "Disconnect{" +
"reason='" + reason + '\'' +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
reason = ProtocolUtils.readString(buf);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeString(buf, reason);
}
public static Disconnect create(Component component) {
Preconditions.checkNotNull(component, "component");
return new Disconnect(ComponentSerializers.JSON.serialize(component));
}
}

View File

@@ -0,0 +1,51 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import java.util.Arrays;
public class EncryptionRequest implements MinecraftPacket {
private byte[] publicKey;
private byte[] verifyToken;
public byte[] getPublicKey() {
return publicKey;
}
public void setPublicKey(byte[] publicKey) {
this.publicKey = publicKey;
}
public byte[] getVerifyToken() {
return verifyToken;
}
public void setVerifyToken(byte[] verifyToken) {
this.verifyToken = verifyToken;
}
@Override
public String toString() {
return "EncryptionRequest{" +
"publicKey=" + Arrays.toString(publicKey) +
", verifyToken=" + Arrays.toString(verifyToken) +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.readString(buf); // Server ID, can be ignored since it is an empty string
publicKey = ProtocolUtils.readByteArray(buf, 256);
verifyToken = ProtocolUtils.readByteArray(buf, 16);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeString(buf, ""); // Server ID
ProtocolUtils.writeByteArray(buf, publicKey);
ProtocolUtils.writeByteArray(buf, verifyToken);
}
}

View File

@@ -0,0 +1,49 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import java.util.Arrays;
public class EncryptionResponse implements MinecraftPacket {
private byte[] sharedSecret;
private byte[] verifyToken;
public byte[] getSharedSecret() {
return sharedSecret;
}
public void setSharedSecret(byte[] sharedSecret) {
this.sharedSecret = sharedSecret;
}
public byte[] getVerifyToken() {
return verifyToken;
}
public void setVerifyToken(byte[] verifyToken) {
this.verifyToken = verifyToken;
}
@Override
public String toString() {
return "EncryptionResponse{" +
"sharedSecret=" + Arrays.toString(sharedSecret) +
", verifyToken=" + Arrays.toString(verifyToken) +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.sharedSecret = ProtocolUtils.readByteArray(buf, 256);
this.verifyToken = ProtocolUtils.readByteArray(buf, 128);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeByteArray(buf, sharedSecret);
ProtocolUtils.writeByteArray(buf, verifyToken);
}
}

View File

@@ -0,0 +1,71 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
public class Handshake implements MinecraftPacket {
private int protocolVersion;
private String serverAddress;
private int port;
private int nextStatus;
public int getProtocolVersion() {
return protocolVersion;
}
public void setProtocolVersion(int protocolVersion) {
this.protocolVersion = protocolVersion;
}
public String getServerAddress() {
return serverAddress;
}
public void setServerAddress(String serverAddress) {
this.serverAddress = serverAddress;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public int getNextStatus() {
return nextStatus;
}
public void setNextStatus(int nextStatus) {
this.nextStatus = nextStatus;
}
@Override
public String toString() {
return "Handshake{" +
"protocolVersion=" + protocolVersion +
", serverAddress='" + serverAddress + '\'' +
", port=" + port +
", nextStatus=" + nextStatus +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.protocolVersion = ProtocolUtils.readVarInt(buf);
this.serverAddress = ProtocolUtils.readString(buf, 255);
this.port = buf.readUnsignedShort();
this.nextStatus = ProtocolUtils.readVarInt(buf);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, this.protocolVersion);
ProtocolUtils.writeString(buf, this.serverAddress);
buf.writeShort(this.port);
ProtocolUtils.writeVarInt(buf, this.nextStatus);
}
}

View File

@@ -0,0 +1,107 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
public class JoinGame implements MinecraftPacket {
private int entityId;
private short gamemode;
private int dimension;
private short difficulty;
private short maxPlayers;
private String levelType;
private boolean reducedDebugInfo;
public int getEntityId() {
return entityId;
}
public void setEntityId(int entityId) {
this.entityId = entityId;
}
public short getGamemode() {
return gamemode;
}
public void setGamemode(short gamemode) {
this.gamemode = gamemode;
}
public int getDimension() {
return dimension;
}
public void setDimension(int dimension) {
this.dimension = dimension;
}
public short getDifficulty() {
return difficulty;
}
public void setDifficulty(short difficulty) {
this.difficulty = difficulty;
}
public short getMaxPlayers() {
return maxPlayers;
}
public void setMaxPlayers(short maxPlayers) {
this.maxPlayers = maxPlayers;
}
public String getLevelType() {
return levelType;
}
public void setLevelType(String levelType) {
this.levelType = levelType;
}
public boolean isReducedDebugInfo() {
return reducedDebugInfo;
}
public void setReducedDebugInfo(boolean reducedDebugInfo) {
this.reducedDebugInfo = reducedDebugInfo;
}
@Override
public String toString() {
return "JoinGame{" +
"entityId=" + entityId +
", gamemode=" + gamemode +
", dimension=" + dimension +
", difficulty=" + difficulty +
", maxPlayers=" + maxPlayers +
", levelType='" + levelType + '\'' +
", reducedDebugInfo=" + reducedDebugInfo +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.entityId = buf.readInt();
this.gamemode = buf.readUnsignedByte();
this.dimension = buf.readInt();
this.difficulty = buf.readUnsignedByte();
this.maxPlayers = buf.readUnsignedByte();
this.levelType = ProtocolUtils.readString(buf, 16);
this.reducedDebugInfo = buf.readBoolean();
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
buf.writeInt(entityId);
buf.writeByte(gamemode);
buf.writeInt(dimension);
buf.writeByte(difficulty);
buf.writeByte(maxPlayers);
ProtocolUtils.writeString(buf, levelType);
buf.writeBoolean(reducedDebugInfo);
}
}

View File

@@ -0,0 +1,45 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_12_2;
public class KeepAlive implements MinecraftPacket {
private long randomId;
public long getRandomId() {
return randomId;
}
public void setRandomId(long randomId) {
this.randomId = randomId;
}
@Override
public String toString() {
return "KeepAlive{" +
"randomId=" + randomId +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (protocolVersion >= MINECRAFT_1_12_2) {
randomId = buf.readLong();
} else {
randomId = ProtocolUtils.readVarInt(buf);
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
if (protocolVersion >= MINECRAFT_1_12_2) {
buf.writeLong(randomId);
} else {
ProtocolUtils.writeVarInt(buf, (int) randomId);
}
}
}

View File

@@ -0,0 +1,4 @@
package com.velocitypowered.proxy.protocol.packets;
public class LegacyPing {
}

View File

@@ -0,0 +1,82 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.data.ServerPing;
import net.kyori.text.serializer.ComponentSerializers;
public class LegacyPingResponse {
private int protocolVersion;
private String serverVersion;
private String motd;
private int playersOnline;
private int playersMax;
public LegacyPingResponse() {
}
public LegacyPingResponse(int protocolVersion, String serverVersion, String motd, int playersOnline, int playersMax) {
this.protocolVersion = protocolVersion;
this.serverVersion = serverVersion;
this.motd = motd;
this.playersOnline = playersOnline;
this.playersMax = playersMax;
}
public int getProtocolVersion() {
return protocolVersion;
}
public void setProtocolVersion(int protocolVersion) {
this.protocolVersion = protocolVersion;
}
public String getServerVersion() {
return serverVersion;
}
public void setServerVersion(String serverVersion) {
this.serverVersion = serverVersion;
}
public String getMotd() {
return motd;
}
public void setMotd(String motd) {
this.motd = motd;
}
public int getPlayersOnline() {
return playersOnline;
}
public void setPlayersOnline(int playersOnline) {
this.playersOnline = playersOnline;
}
public int getPlayersMax() {
return playersMax;
}
public void setPlayersMax(int playersMax) {
this.playersMax = playersMax;
}
@Override
public String toString() {
return "LegacyPingResponse{" +
"protocolVersion=" + protocolVersion +
", serverVersion='" + serverVersion + '\'' +
", motd='" + motd + '\'' +
", playersOnline=" + playersOnline +
", playersMax=" + playersMax +
'}';
}
public static LegacyPingResponse from(ServerPing ping) {
return new LegacyPingResponse(ping.getVersion().getProtocol(),
ping.getVersion().getName(),
ComponentSerializers.LEGACY.serialize(ping.getDescription()),
ping.getPlayers().getOnline(),
ping.getPlayers().getMax());
}
}

View File

@@ -0,0 +1,64 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class LoginPluginMessage implements MinecraftPacket {
private int id;
private String channel;
private ByteBuf data;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getChannel() {
return channel;
}
public void setChannel(String channel) {
this.channel = channel;
}
public ByteBuf getData() {
return data;
}
public void setData(ByteBuf data) {
this.data = data;
}
@Override
public String toString() {
return "LoginPluginMessage{" +
"id=" + id +
", channel='" + channel + '\'' +
", data=" + data +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.id = ProtocolUtils.readVarInt(buf);
this.channel = ProtocolUtils.readString(buf);
if (buf.isReadable()) {
this.data = buf.readRetainedSlice(buf.readableBytes());
} else {
this.data = Unpooled.EMPTY_BUFFER;
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, id);
ProtocolUtils.writeString(buf, channel);
buf.writeBytes(data);
}
}

View File

@@ -0,0 +1,64 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class LoginPluginResponse implements MinecraftPacket {
private int id;
private boolean success;
private ByteBuf data;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public ByteBuf getData() {
return data;
}
public void setData(ByteBuf data) {
this.data = data;
}
@Override
public String toString() {
return "LoginPluginResponse{" +
"id=" + id +
", success=" + success +
", data=" + data +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.id = ProtocolUtils.readVarInt(buf);
this.success = buf.readBoolean();
if (buf.isReadable()) {
this.data = buf.readRetainedSlice(buf.readableBytes());
} else {
this.data = Unpooled.EMPTY_BUFFER;
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, id);
buf.writeBoolean(success);
buf.writeBytes(data);
}
}

View File

@@ -0,0 +1,48 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
public class PluginMessage implements MinecraftPacket {
private String channel;
private ByteBuf data;
public String getChannel() {
return channel;
}
public void setChannel(String channel) {
this.channel = channel;
}
public ByteBuf getData() {
return data;
}
public void setData(ByteBuf data) {
this.data = data;
}
@Override
public String toString() {
return "PluginMessage{" +
"channel='" + channel + '\'' +
", data=" + ByteBufUtil.hexDump(data) +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.channel = ProtocolUtils.readString(buf);
this.data = buf.readRetainedSlice(buf.readableBytes());
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeString(buf, channel);
buf.writeBytes(data);
}
}

View File

@@ -0,0 +1,81 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
public class Respawn implements MinecraftPacket {
private int dimension;
private short difficulty;
private short gamemode;
private String levelType;
public Respawn() {
}
public Respawn(int dimension, short difficulty, short gamemode, String levelType) {
this.dimension = dimension;
this.difficulty = difficulty;
this.gamemode = gamemode;
this.levelType = levelType;
}
public int getDimension() {
return dimension;
}
public void setDimension(int dimension) {
this.dimension = dimension;
}
public short getDifficulty() {
return difficulty;
}
public void setDifficulty(short difficulty) {
this.difficulty = difficulty;
}
public short getGamemode() {
return gamemode;
}
public void setGamemode(short gamemode) {
this.gamemode = gamemode;
}
public String getLevelType() {
return levelType;
}
public void setLevelType(String levelType) {
this.levelType = levelType;
}
@Override
public String toString() {
return "Respawn{" +
"dimension=" + dimension +
", difficulty=" + difficulty +
", gamemode=" + gamemode +
", levelType='" + levelType + '\'' +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.dimension = buf.readInt();
this.difficulty = buf.readUnsignedByte();
this.gamemode = buf.readUnsignedByte();
this.levelType = ProtocolUtils.readString(buf, 16);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
buf.writeInt(dimension);
buf.writeByte(difficulty);
buf.writeByte(gamemode);
ProtocolUtils.writeString(buf, levelType);
}
}

View File

@@ -0,0 +1,47 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
public class ScoreboardDisplay implements MinecraftPacket {
private byte position;
private String displayName;
public byte getPosition() {
return position;
}
public void setPosition(byte position) {
this.position = position;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
@Override
public String toString() {
return "ScoreboardDisplay{" +
"position=" + position +
", displayName='" + displayName + '\'' +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.position = buf.readByte();
this.displayName = ProtocolUtils.readString(buf, 16);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
buf.writeByte(position);
ProtocolUtils.writeString(buf, displayName);
}
}

View File

@@ -0,0 +1,89 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.data.scoreboard.ObjectiveMode;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.util.ScoreboardProtocolUtil;
import io.netty.buffer.ByteBuf;
import net.kyori.text.Component;
public class ScoreboardObjective implements MinecraftPacket {
public static final byte ADD = (byte) 0;
public static final byte REMOVE = (byte) 1;
public static final byte CHANGE = (byte) 2;
private String id;
private byte mode;
private Component displayName;
private ObjectiveMode type;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public byte getMode() {
return mode;
}
public void setMode(byte mode) {
this.mode = mode;
}
public Component getDisplayName() {
return displayName;
}
public void setDisplayName(Component displayName) {
this.displayName = displayName;
}
public ObjectiveMode getType() {
return type;
}
public void setType(ObjectiveMode type) {
this.type = type;
}
@Override
public String toString() {
return "ScoreboardObjective{" +
"id='" + id + '\'' +
", mode=" + mode +
", displayName='" + displayName + '\'' +
", type='" + type + '\'' +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.id = ProtocolUtils.readString(buf, 16);
this.mode = buf.readByte();
if (this.mode != REMOVE) {
this.displayName = ProtocolUtils.readScoreboardTextComponent(buf, protocolVersion);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_13) {
this.type = ScoreboardProtocolUtil.getMode(ProtocolUtils.readVarInt(buf));
} else {
this.type = ScoreboardProtocolUtil.getMode(ProtocolUtils.readString(buf));
}
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeString(buf, id);
buf.writeByte(mode);
if (this.mode != REMOVE) {
ProtocolUtils.writeScoreboardTextComponent(buf, protocolVersion, displayName);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_13) {
ProtocolUtils.writeVarInt(buf, type.ordinal());
} else {
ProtocolUtils.writeString(buf, type.name().toLowerCase());
}
}
}
}

View File

@@ -0,0 +1,77 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
public class ScoreboardSetScore implements MinecraftPacket {
public static final byte CHANGE = (byte) 0;
public static final byte REMOVE = (byte) 1;
private String entity;
private byte action;
private String objective;
private int value;
public String getEntity() {
return entity;
}
public void setEntity(String entity) {
this.entity = entity;
}
public byte getAction() {
return action;
}
public void setAction(byte action) {
this.action = action;
}
public String getObjective() {
return objective;
}
public void setObjective(String objective) {
this.objective = objective;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
@Override
public String toString() {
return "ScoreboardSetScore{" +
"entity='" + entity + '\'' +
", action=" + action +
", objective='" + objective + '\'' +
", value=" + value +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.entity = ProtocolUtils.readString(buf, 40);
this.action = buf.readByte();
this.objective = ProtocolUtils.readString(buf, 16);
if (this.action != REMOVE) {
value = ProtocolUtils.readVarInt(buf);
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeString(buf, entity);
buf.writeByte(action);
ProtocolUtils.writeString(buf, objective);
if (this.action != REMOVE) {
ProtocolUtils.writeVarInt(buf, value);
}
}
}

View File

@@ -0,0 +1,211 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import net.kyori.text.Component;
import java.util.ArrayList;
import java.util.List;
public class ScoreboardTeam implements MinecraftPacket {
public static final byte ADD = (byte) 0;
public static final byte REMOVE = (byte) 1;
public static final byte UPDATE = (byte) 2;
public static final byte ADD_PLAYER = (byte) 3;
public static final byte REMOVE_PLAYER = (byte) 4;
private String id;
private byte mode;
private Component displayName;
private Component prefix;
private Component suffix;
private byte flags;
private String nameTagVisibility;
private String collisionRule;
private int color;
private List<String> entities;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public byte getMode() {
return mode;
}
public void setMode(byte mode) {
this.mode = mode;
}
public Component getDisplayName() {
return displayName;
}
public void setDisplayName(Component displayName) {
this.displayName = displayName;
}
public Component getPrefix() {
return prefix;
}
public void setPrefix(Component prefix) {
this.prefix = prefix;
}
public Component getSuffix() {
return suffix;
}
public void setSuffix(Component suffix) {
this.suffix = suffix;
}
public byte getFlags() {
return flags;
}
public void setFlags(byte flags) {
this.flags = flags;
}
public String getNameTagVisibility() {
return nameTagVisibility;
}
public void setNameTagVisibility(String nameTagVisibility) {
this.nameTagVisibility = nameTagVisibility;
}
public String getCollisionRule() {
return collisionRule;
}
public void setCollisionRule(String collisionRule) {
this.collisionRule = collisionRule;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public List<String> getEntities() {
return entities;
}
public void setEntities(List<String> entities) {
this.entities = entities;
}
@Override
public String toString() {
return "ScoreboardTeam{" +
"id='" + id + '\'' +
", mode=" + mode +
", displayName='" + displayName + '\'' +
", prefix='" + prefix + '\'' +
", suffix='" + suffix + '\'' +
", flags=" + flags +
", nameTagVisibility='" + nameTagVisibility + '\'' +
", collisionRule='" + collisionRule + '\'' +
", color=" + color +
", entities=" + entities +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.id = ProtocolUtils.readString(buf, 16);
this.mode = buf.readByte();
switch (mode) {
case ADD:
case UPDATE:
this.displayName = ProtocolUtils.readScoreboardTextComponent(buf, protocolVersion);
if (protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2) {
this.prefix = ProtocolUtils.readScoreboardTextComponent(buf, protocolVersion);
this.suffix = ProtocolUtils.readScoreboardTextComponent(buf, protocolVersion);
}
this.flags = buf.readByte();
this.nameTagVisibility = ProtocolUtils.readString(buf, 32);
this.collisionRule = ProtocolUtils.readString(buf, 32);
this.color = protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2 ? buf.readByte() :
ProtocolUtils.readVarInt(buf);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_13) {
this.prefix = ProtocolUtils.readScoreboardTextComponent(buf, protocolVersion);
this.suffix = ProtocolUtils.readScoreboardTextComponent(buf, protocolVersion);
}
if (mode == ADD) {
this.entities = readEntities(buf);
}
break;
case REMOVE: // remove
break;
case ADD_PLAYER: // add player
case REMOVE_PLAYER: // remove player
this.entities = readEntities(buf);
break;
}
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeString(buf, id);
buf.writeByte(mode);
switch (mode) {
case ADD:
case UPDATE:
ProtocolUtils.writeScoreboardTextComponent(buf, protocolVersion, displayName);
if (protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2) {
ProtocolUtils.writeScoreboardTextComponent(buf, protocolVersion, prefix);
ProtocolUtils.writeScoreboardTextComponent(buf, protocolVersion, suffix);
}
buf.writeByte(flags);
ProtocolUtils.writeString(buf, nameTagVisibility);
ProtocolUtils.writeString(buf, collisionRule);
if (protocolVersion >= ProtocolConstants.MINECRAFT_1_13) {
ProtocolUtils.writeVarInt(buf, color);
ProtocolUtils.writeScoreboardTextComponent(buf, protocolVersion, prefix);
ProtocolUtils.writeScoreboardTextComponent(buf, protocolVersion, suffix);
} else {
buf.writeByte(color);
}
if (mode == ADD) {
writeEntities(buf, entities);
}
break;
case REMOVE:
break;
case ADD_PLAYER:
case REMOVE_PLAYER:
writeEntities(buf, entities);
break;
}
}
private static List<String> readEntities(ByteBuf buf) {
List<String> entities = new ArrayList<>();
int size = ProtocolUtils.readVarInt(buf);
for (int i = 0; i < size; i++) {
entities.add(ProtocolUtils.readString(buf, 40));
}
return entities;
}
private static void writeEntities(ByteBuf buf, List<String> entities) {
ProtocolUtils.writeVarInt(buf, entities.size());
for (String entity : entities) {
ProtocolUtils.writeString(buf, entity);
}
}
}

View File

@@ -0,0 +1,35 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
public class ServerLogin implements MinecraftPacket {
private String username;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "ServerLogin{" +
"username='" + username + '\'' +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
username = ProtocolUtils.readString(buf, 16);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeString(buf, username);
}
}

View File

@@ -0,0 +1,49 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import java.util.UUID;
public class ServerLoginSuccess implements MinecraftPacket {
private UUID uuid;
private String username;
public UUID getUuid() {
return uuid;
}
public void setUuid(UUID uuid) {
this.uuid = uuid;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "ServerLoginSuccess{" +
"uuid=" + uuid +
", username='" + username + '\'' +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
uuid = UUID.fromString(ProtocolUtils.readString(buf, 36));
username = ProtocolUtils.readString(buf, 16);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeString(buf, uuid.toString());
ProtocolUtils.writeString(buf, username);
}
}

View File

@@ -0,0 +1,41 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
public class SetCompression implements MinecraftPacket {
private int threshold;
public SetCompression() {}
public SetCompression(int threshold) {
this.threshold = threshold;
}
public int getThreshold() {
return threshold;
}
public void setThreshold(int threshold) {
this.threshold = threshold;
}
@Override
public String toString() {
return "SetCompression{" +
"threshold=" + threshold +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.threshold = ProtocolUtils.readVarInt(buf);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeVarInt(buf, threshold);
}
}

View File

@@ -0,0 +1,19 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import io.netty.buffer.ByteBuf;
public class StatusPing implements MinecraftPacket {
private long randomId;
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
randomId = buf.readLong();
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
buf.writeLong(randomId);
}
}

View File

@@ -0,0 +1,17 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import io.netty.buffer.ByteBuf;
public class StatusRequest implements MinecraftPacket {
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
}
}

View File

@@ -0,0 +1,35 @@
package com.velocitypowered.proxy.protocol.packets;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
public class StatusResponse implements MinecraftPacket {
private String status;
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
@Override
public String toString() {
return "StatusResponse{" +
"status='" + status + '\'' +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
status = ProtocolUtils.readString(buf, Short.MAX_VALUE);
}
@Override
public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
ProtocolUtils.writeString(buf, status);
}
}

View File

@@ -0,0 +1,29 @@
package com.velocitypowered.proxy.protocol.remap;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import io.netty.buffer.ByteBuf;
/**
* Represents a protocol-specific entity ID remapper for certain Minecraft packets. This is mostly required to support
* old versions of Minecraft. For Minecraft 1.9 clients and above, Velocity can use a more efficient method based on
* sending JoinGame packets multiple times.
*/
public interface EntityIdRemapper {
/**
* Remaps the entity IDs in this packet so that they apply to the player.
* @param original the packet to remap
* @param direction the direction of the packet
* @return a remapped packet, which may either be a retained version of the original buffer or an entirely new buffer
*/
ByteBuf remap(ByteBuf original, ProtocolConstants.Direction direction);
int getClientEntityId();
int getServerEntityId();
void setServerEntityId(int id);
static EntityIdRemapper getMapper(int eid, int protocolVersion) {
return NoopEntityIdRemapper.INSTANCE;
}
}

View File

@@ -0,0 +1,40 @@
package com.velocitypowered.proxy.protocol.remap;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import io.netty.buffer.ByteBuf;
public class Minecraft18EntityIdRemapper implements EntityIdRemapper {
private final int clientId;
private int serverId;
public Minecraft18EntityIdRemapper(int clientId, int serverId) {
this.clientId = clientId;
this.serverId = serverId;
}
@Override
public ByteBuf remap(ByteBuf original, ProtocolConstants.Direction direction) {
if (clientId == serverId) {
// If these are equal (i.e. first connection), no remapping is required.
return original.retain();
}
// TODO: Implement.
throw new UnsupportedOperationException("1.8 doesn't allow switching servers.");
}
@Override
public int getClientEntityId() {
return clientId;
}
@Override
public int getServerEntityId() {
return serverId;
}
@Override
public void setServerEntityId(int id) {
this.serverId = id;
}
}

View File

@@ -0,0 +1,32 @@
package com.velocitypowered.proxy.protocol.remap;
import com.velocitypowered.proxy.protocol.ProtocolConstants;
import io.netty.buffer.ByteBuf;
public class NoopEntityIdRemapper implements EntityIdRemapper {
public static final NoopEntityIdRemapper INSTANCE = new NoopEntityIdRemapper();
private NoopEntityIdRemapper() {
}
@Override
public ByteBuf remap(ByteBuf original, ProtocolConstants.Direction direction) {
return original.retain();
}
@Override
public int getClientEntityId() {
return 0;
}
@Override
public int getServerEntityId() {
return 0;
}
@Override
public void setServerEntityId(int id) {
}
}

View File

@@ -0,0 +1,53 @@
package com.velocitypowered.proxy.protocol.util;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packets.PluginMessage;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
public enum PluginMessageUtil {
;
public static List<String> getChannels(PluginMessage message) {
Preconditions.checkArgument(message.getChannel().equals("REGISTER") ||
message.getChannel().equals("UNREGISTER") ||
message.getChannel().equals("minecraft:register") ||
message.getChannel().equals("minecraft:unregister"),
"Unknown channel type " + message.getChannel());
String channels = message.getData().toString(StandardCharsets.UTF_8);
return ImmutableList.copyOf(channels.split("\0"));
}
public static PluginMessage constructChannelsPacket(String channel, Collection<String> channels) {
PluginMessage message = new PluginMessage();
message.setChannel(channel);
ByteBuf data = Unpooled.buffer();
for (String s : channels) {
ByteBufUtil.writeUtf8(data, s);
data.writeByte(0);
}
data.writerIndex(data.writerIndex() - 1);
message.setData(data);
return message;
}
public static PluginMessage rewriteMCBrand(PluginMessage message) {
ByteBuf rewrittenBuf = Unpooled.buffer();
String currentBrand = ProtocolUtils.readString(message.getData());
ProtocolUtils.writeString(rewrittenBuf, currentBrand + " (Velocity)");
PluginMessage newMsg = new PluginMessage();
newMsg.setChannel(message.getChannel());
newMsg.setData(rewrittenBuf);
return newMsg;
}
}

View File

@@ -0,0 +1,24 @@
package com.velocitypowered.proxy.protocol.util;
import com.velocitypowered.proxy.data.scoreboard.ObjectiveMode;
public class ScoreboardProtocolUtil {
private ScoreboardProtocolUtil() {
throw new AssertionError();
}
public static ObjectiveMode getMode(String mode) {
return ObjectiveMode.valueOf(mode.toUpperCase());
}
public static ObjectiveMode getMode(int enumVal) {
switch (enumVal) {
case 0:
return ObjectiveMode.INTEGER;
case 1:
return ObjectiveMode.HEARTS;
default:
throw new IllegalStateException("Unknown mode " + enumVal);
}
}
}

View File

@@ -0,0 +1,16 @@
package com.velocitypowered.proxy.util;
import com.google.common.base.Preconditions;
import java.net.InetSocketAddress;
import java.net.URI;
public enum AddressUtil {
;
public static InetSocketAddress parseAddress(String ip) {
Preconditions.checkNotNull(ip, "ip");
URI uri = URI.create("tcp://" + ip);
return new InetSocketAddress(uri.getHost(), uri.getPort());
}
}

View File

@@ -0,0 +1,9 @@
package com.velocitypowered.proxy.util;
/**
* This marker interface indicates that this object should be explicitly disposed before the object can no longer be used.
* Not disposing these objects will likely leak native resources and eventually lead to resource exhaustion.
*/
public interface Disposable {
void dispose();
}

View File

@@ -0,0 +1,40 @@
package com.velocitypowered.proxy.util;
import javax.crypto.Cipher;
import java.math.BigInteger;
import java.security.*;
public enum EncryptionUtils {
;
public static KeyPair createRsaKeyPair(final int keysize) {
try {
final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(keysize);
return generator.generateKeyPair();
} catch (final NoSuchAlgorithmException e) {
throw new RuntimeException("Unable to generate RSA keypair", e);
}
}
public static String twosComplementHexdigest(byte[] digest) {
return new BigInteger(digest).toString(16);
}
public static byte[] decryptRsa(KeyPair keyPair, byte[] bytes) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
return cipher.doFinal(bytes);
}
public static String generateServerId(byte[] sharedSecret, PublicKey key) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(sharedSecret);
digest.update(key.getEncoded());
return twosComplementHexdigest(digest.digest());
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,28 @@
package com.velocitypowered.proxy.util;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.servers.ServerInfo;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class ServerMap {
private final Map<String, ServerInfo> servers = new HashMap<>();
public Optional<ServerInfo> getServer(String name) {
Preconditions.checkNotNull(name, "name");
return Optional.ofNullable(servers.get(name.toLowerCase()));
}
public Collection<ServerInfo> getAllServers() {
return ImmutableList.copyOf(servers.values());
}
public void register(ServerInfo info) {
Preconditions.checkNotNull(info, "info");
servers.put(info.getName(), info);
}
}

View File

@@ -0,0 +1,9 @@
package com.velocitypowered.proxy.util;
public enum ThrowableUtils {
;
public static String briefDescription(Throwable throwable) {
return throwable.getClass().getName() + ": " + throwable.getMessage();
}
}

View File

@@ -0,0 +1,29 @@
package com.velocitypowered.proxy.util;
import com.google.common.base.Preconditions;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.UUID;
public enum UuidUtils {
;
public static UUID fromUndashed(final String string) {
Objects.requireNonNull(string, "string");
Preconditions.checkArgument(string.length() == 32, "Length is incorrect");
return new UUID(
Long.parseUnsignedLong(string.substring(0, 16), 16),
Long.parseUnsignedLong(string.substring(16), 16)
);
}
public static String toUndashed(final UUID uuid) {
Preconditions.checkNotNull(uuid, "uuid");
return Long.toUnsignedString(uuid.getMostSignificantBits(), 16) + Long.toUnsignedString(uuid.getLeastSignificantBits(), 16);
}
public static UUID generateOfflinePlayerUuid(String username) {
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8));
}
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n"/>
</Console>
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz"
immediateFlush="false">
<PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<OnStartupTriggeringPolicy/>
</Policies>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>

View File

@@ -0,0 +1,53 @@
package com.velocitypowered.proxy.protocol;
import com.velocitypowered.proxy.protocol.packets.Handshake;
import org.junit.jupiter.api.Test;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_12;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_12_1;
import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_12_2;
import static org.junit.jupiter.api.Assertions.*;
class PacketRegistryTest {
private StateRegistry.PacketRegistry setupRegistry() {
StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry(ProtocolConstants.Direction.CLIENTBOUND, StateRegistry.HANDSHAKE);
registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x00, MINECRAFT_1_12));
return registry;
}
@Test
void packetRegistryWorks() {
StateRegistry.PacketRegistry registry = setupRegistry();
MinecraftPacket packet = registry.getVersion(MINECRAFT_1_12).createPacket(0);
assertNotNull(packet, "Packet was not found in registry");
assertEquals(Handshake.class, packet.getClass(), "Registry returned wrong class");
assertEquals(0, registry.getVersion(MINECRAFT_1_12).getPacketId(packet), "Registry did not return the correct packet ID");
}
@Test
void packetRegistryLinkingWorks() {
StateRegistry.PacketRegistry registry = setupRegistry();
MinecraftPacket packet = registry.getVersion(MINECRAFT_1_12_1).createPacket(0);
assertNotNull(packet, "Packet was not found in registry");
assertEquals(Handshake.class, packet.getClass(), "Registry returned wrong class");
assertEquals(0, registry.getVersion(MINECRAFT_1_12_1).getPacketId(packet), "Registry did not return the correct packet ID");
}
@Test
void failOnNoMappings() {
StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry(ProtocolConstants.Direction.CLIENTBOUND, StateRegistry.HANDSHAKE);
assertThrows(IllegalArgumentException.class, () -> registry.register(Handshake.class, Handshake::new));
assertThrows(IllegalArgumentException.class, () -> registry.getVersion(0).getPacketId(new Handshake()));
}
@Test
void registrySuppliesCorrectPacketsByProtocol() {
StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry(ProtocolConstants.Direction.CLIENTBOUND, StateRegistry.HANDSHAKE);
registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x00, MINECRAFT_1_12),
new StateRegistry.PacketMapping(0x01, MINECRAFT_1_12_1));
assertEquals(Handshake.class, registry.getVersion(MINECRAFT_1_12).createPacket(0x00).getClass());
assertEquals(Handshake.class, registry.getVersion(MINECRAFT_1_12_1).createPacket(0x01).getClass());
assertEquals(Handshake.class, registry.getVersion(MINECRAFT_1_12_2).createPacket(0x01).getClass());
}
}

View File

@@ -0,0 +1,26 @@
package com.velocitypowered.proxy.util;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import static org.junit.jupiter.api.Assertions.assertEquals;
class EncryptionUtilsTest {
@Test
void twosComplementHexdigest() throws Exception {
String notchHash = mojangLoginSha1("Notch");
assertEquals("4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48", notchHash);
String jebHash = mojangLoginSha1("jeb_");
assertEquals("-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1", jebHash);
}
private String mojangLoginSha1(String str) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(str.getBytes(StandardCharsets.UTF_8));
byte[] digested = digest.digest();
return EncryptionUtils.twosComplementHexdigest(digested);
}
}

View File

@@ -0,0 +1,30 @@
package com.velocitypowered.proxy.util;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
class UuidUtilsTest {
private static final UUID EXPECTED_DASHED_UUID = UUID.fromString("6b501978-d3be-4f33-bcf6-6e7808f37a0d");
private static final String ACTUAL_UNDASHED_UUID = EXPECTED_DASHED_UUID.toString().replace("-", "");
private static final UUID TEST_OFFLINE_PLAYER_UUID = UUID.fromString("708f6260-183d-3912-bbde-5e279a5e739a");
private static final String TEST_OFFLINE_PLAYER = "tuxed";
@Test
void generateOfflinePlayerUuid() {
assertEquals(TEST_OFFLINE_PLAYER_UUID, UuidUtils.generateOfflinePlayerUuid(TEST_OFFLINE_PLAYER), "UUIDs do not match");
}
@Test
void fromUndashed() {
assertEquals(EXPECTED_DASHED_UUID, UuidUtils.fromUndashed(ACTUAL_UNDASHED_UUID), "UUIDs do not match");
}
@Test
void toUndashed() {
assertEquals(ACTUAL_UNDASHED_UUID, UuidUtils.toUndashed(EXPECTED_DASHED_UUID), "UUIDs do not match");
}
}