Merge remote-tracking branch 'origin/main'

This commit is contained in:
Lenni0451 2023-04-08 15:08:58 +02:00
commit 52b257e206
11 changed files with 592 additions and 37 deletions

View File

@ -16,6 +16,8 @@ For a full user guide go to the [Usage for Players](#usage-for-players-gui) sect
## Supported Client versions ## Supported Client versions
- Release (1.7.2 - 1.19.4) - Release (1.7.2 - 1.19.4)
- Classic, Alpha, Beta, Release 1.0 - 1.6.4 (Only passthrough)
- Eaglercraft (1.5.2 / 1.8)
ViaProxy supports joining to any of the listed server version from any of the listed client versions. ViaProxy supports joining to any of the listed server version from any of the listed client versions.

View File

@ -59,6 +59,7 @@ public class Options {
public static String RESOURCE_PACK_URL; // Example: http://example.com/resourcepack.zip public static String RESOURCE_PACK_URL; // Example: http://example.com/resourcepack.zip
public static boolean SERVER_HAPROXY_PROTOCOL; public static boolean SERVER_HAPROXY_PROTOCOL;
public static boolean LEGACY_CLIENT_PASSTHROUGH; public static boolean LEGACY_CLIENT_PASSTHROUGH;
public static boolean ALLOW_EAGLERCRAFT_CLIENTS;
public static void parse(final String[] args) throws IOException { public static void parse(final String[] args) throws IOException {
final OptionParser parser = new OptionParser(); final OptionParser parser = new OptionParser();
@ -79,7 +80,8 @@ public class Options {
final OptionSpec<String> resourcePackUrl = parser.acceptsAll(asList("resource_pack_url", "resource_pack", "rpu", "rp"), "URL of a resource pack which clients can optionally download").withRequiredArg().ofType(String.class); final OptionSpec<String> resourcePackUrl = parser.acceptsAll(asList("resource_pack_url", "resource_pack", "rpu", "rp"), "URL of a resource pack which clients can optionally download").withRequiredArg().ofType(String.class);
final OptionSpec<String> proxyUrl = parser.acceptsAll(asList("proxy_url", "proxy"), "URL of a SOCKS(4/5)/HTTP(S) proxy which will be used for backend TCP connections").withRequiredArg().ofType(String.class); final OptionSpec<String> proxyUrl = parser.acceptsAll(asList("proxy_url", "proxy"), "URL of a SOCKS(4/5)/HTTP(S) proxy which will be used for backend TCP connections").withRequiredArg().ofType(String.class);
final OptionSpec<Void> serverHaProxyProtocol = parser.acceptsAll(asList("server-haproxy-protocol", "server-haproxy"), "Send HAProxy protocol messages to the backend server"); final OptionSpec<Void> serverHaProxyProtocol = parser.acceptsAll(asList("server-haproxy-protocol", "server-haproxy"), "Send HAProxy protocol messages to the backend server");
final OptionSpec<Void> legacyClientPassthrough = parser.acceptsAll(asList("legacy_client_passthrough", "legacy_passthrough"), "Allow <= 1.6.4 clients to connect to the backend server"); final OptionSpec<Void> legacyClientPassthrough = parser.acceptsAll(asList("legacy_client_passthrough", "legacy_passthrough"), "Allow <= 1.6.4 clients to connect to the backend server (No protocol translation)");
final OptionSpec<Void> allowEaglerCraftClients = parser.acceptsAll(asList("allow_eaglercraft_clients", "allow_eaglercraft"), "Allow Eaglercraft clients to connect to ViaProxy");
PluginManager.EVENT_MANAGER.call(new PreOptionsParseEvent(parser)); PluginManager.EVENT_MANAGER.call(new PreOptionsParseEvent(parser));
final OptionSet options = parser.parse(args); final OptionSet options = parser.parse(args);
@ -119,6 +121,7 @@ public class Options {
} }
SERVER_HAPROXY_PROTOCOL = options.has(serverHaProxyProtocol); SERVER_HAPROXY_PROTOCOL = options.has(serverHaProxyProtocol);
LEGACY_CLIENT_PASSTHROUGH = options.has(legacyClientPassthrough); LEGACY_CLIENT_PASSTHROUGH = options.has(legacyClientPassthrough);
ALLOW_EAGLERCRAFT_CLIENTS = options.has(allowEaglerCraftClients);
PluginManager.EVENT_MANAGER.call(new PostOptionsParseEvent(options)); PluginManager.EVENT_MANAGER.call(new PostOptionsParseEvent(options));
} }

View File

@ -26,11 +26,24 @@ import net.raphimc.viaproxy.cli.options.Options;
import net.raphimc.viaproxy.plugins.PluginManager; import net.raphimc.viaproxy.plugins.PluginManager;
import net.raphimc.viaproxy.plugins.events.Client2ProxyChannelInitializeEvent; import net.raphimc.viaproxy.plugins.events.Client2ProxyChannelInitializeEvent;
import net.raphimc.viaproxy.plugins.events.types.ITyped; import net.raphimc.viaproxy.plugins.events.types.ITyped;
import net.raphimc.viaproxy.proxy.client2proxy.eaglercraft.EaglercraftInitialHandler;
import net.raphimc.viaproxy.proxy.client2proxy.passthrough.LegacyClientPassthroughHandler;
import java.util.function.Supplier; import java.util.function.Supplier;
public class Client2ProxyChannelInitializer extends MinecraftChannelInitializer { public class Client2ProxyChannelInitializer extends MinecraftChannelInitializer {
public static final String EAGLERCRAFT_INITIAL_HANDLER_NAME = "eaglercraft-initial-handler";
public static final String WEBSOCKET_SSL_HANDLER_NAME = "ws-ssl-handler";
public static final String WEBSOCKET_HTTP_CODEC_NAME = "ws-http-codec";
public static final String WEBSOCKET_HTTP_AGGREGATOR_NAME = "ws-http-aggregator";
public static final String WEBSOCKET_COMPRESSION_NAME = "ws-compression";
public static final String WEBSOCKET_HANDLER_NAME = "ws-handler";
public static final String WEBSOCKET_ACTIVE_NOTIFIER_NAME = "ws-active-notifier";
public static final String EAGLERCRAFT_HANDLER_NAME = "eaglercraft-handler";
public static final String LEGACY_PASSTHROUGH_HANDLER_NAME = "legacy-passthrough-handler";
public Client2ProxyChannelInitializer(final Supplier<ChannelHandler> handlerSupplier) { public Client2ProxyChannelInitializer(final Supplier<ChannelHandler> handlerSupplier) {
super(handlerSupplier); super(handlerSupplier);
} }
@ -42,9 +55,13 @@ public class Client2ProxyChannelInitializer extends MinecraftChannelInitializer
return; return;
} }
if (Options.LEGACY_CLIENT_PASSTHROUGH) { if (Options.ALLOW_EAGLERCRAFT_CLIENTS) {
channel.pipeline().addLast("legacy-passthrough-handler", new LegacyClientPassthroughHandler()); channel.pipeline().addLast(EAGLERCRAFT_INITIAL_HANDLER_NAME, new EaglercraftInitialHandler());
} }
if (Options.LEGACY_CLIENT_PASSTHROUGH) {
channel.pipeline().addLast(LEGACY_PASSTHROUGH_HANDLER_NAME, new LegacyClientPassthroughHandler());
}
super.initChannel(channel); super.initChannel(channel);
channel.attr(MCPipeline.PACKET_REGISTRY_ATTRIBUTE_KEY).set(PacketRegistryUtil.getHandshakeRegistry(false)); channel.attr(MCPipeline.PACKET_REGISTRY_ATTRIBUTE_KEY).set(PacketRegistryUtil.getHandshakeRegistry(false));

View File

@ -0,0 +1,385 @@
/*
* This file is part of ViaProxy - https://github.com/RaphiMC/ViaProxy
* Copyright (C) 2023 RK_01/RaphiMC and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.raphimc.viaproxy.proxy.client2proxy.eaglercraft;
import com.google.common.net.HostAndPort;
import com.viaversion.viaversion.libs.gson.JsonArray;
import com.viaversion.viaversion.libs.gson.JsonObject;
import com.viaversion.viaversion.libs.gson.JsonParser;
import com.viaversion.viaversion.protocols.base.ClientboundStatusPackets;
import com.viaversion.viaversion.protocols.base.ServerboundHandshakePackets;
import com.viaversion.viaversion.protocols.base.ServerboundLoginPackets;
import com.viaversion.viaversion.protocols.base.ServerboundStatusPackets;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageCodec;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import net.lenni0451.mcstructs.text.serializer.TextComponentSerializer;
import net.raphimc.netminecraft.constants.ConnectionState;
import net.raphimc.netminecraft.constants.MCPackets;
import net.raphimc.netminecraft.constants.MCPipeline;
import net.raphimc.netminecraft.packet.PacketTypes;
import net.raphimc.vialegacy.protocols.release.protocol1_6_1to1_5_2.ServerboundPackets1_5_2;
import net.raphimc.vialegacy.protocols.release.protocol1_7_2_5to1_6_4.types.Types1_6_4;
import net.raphimc.viaprotocolhack.util.VersionEnum;
import net.raphimc.viaproxy.ViaProxy;
import net.raphimc.viaproxy.proxy.client2proxy.Client2ProxyChannelInitializer;
import net.raphimc.viaproxy.util.logging.Logger;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.UUID;
// Thanks to ayunami2000 for helping with the eaglercraft protocol
public class EaglercraftHandler extends MessageToMessageCodec<WebSocketFrame, ByteBuf> {
private static final int CLIENT_VERSION = 0x01;
private static final int SERVER_VERSION = 0x02;
private static final int VERSION_MISMATCH = 0x03;
private static final int CLIENT_REQUEST_LOGIN = 0x04;
private static final int SERVER_ALLOW_LOGIN = 0x05;
private static final int SERVER_DENY_LOGIN = 0x06;
private static final int CLIENT_PROFILE_DATA = 0x07;
private static final int CLIENT_FINISH_LOGIN = 0x08;
private static final int SERVER_FINISH_LOGIN = 0x09;
private static final int SERVER_ERROR = 0xFF;
private HostAndPort host;
private State state = State.PRE_HANDSHAKE;
private VersionEnum version;
private int pluginMessageId;
private String username;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.attr(MCPipeline.COMPRESSION_THRESHOLD_ATTRIBUTE_KEY).set(-2); // Disable automatic compression in Proxy2ServerHandler
ctx.pipeline().remove(MCPipeline.SIZER_HANDLER_NAME);
super.channelActive(ctx);
}
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws IOException {
if (this.state == State.STATUS) {
final int packetId = PacketTypes.readVarInt(in);
if (packetId != ClientboundStatusPackets.STATUS_RESPONSE.getId()) {
throw new IllegalStateException("Unexpected packet id " + packetId);
}
final JsonObject root = JsonParser.parseString(PacketTypes.readString(in, Short.MAX_VALUE)).getAsJsonObject();
final JsonObject response = new JsonObject();
response.addProperty("name", "ViaProxy");
response.addProperty("brand", "ViaProxy");
if (root.has("version")) {
response.add("vers", root.getAsJsonObject("version").get("name"));
} else {
response.addProperty("vers", "Unknown");
}
response.addProperty("cracked", true);
response.addProperty("secure", false);
response.addProperty("time", System.currentTimeMillis());
response.addProperty("uuid", UUID.randomUUID().toString());
response.addProperty("type", "motd");
final JsonObject data = new JsonObject();
data.addProperty("cache", false);
final JsonArray motd = new JsonArray();
if (root.has("description")) {
final String[] motdLines = TextComponentSerializer.V1_8.deserialize(root.get("description").toString()).asLegacyFormatString().split("\n");
for (String motdLine : motdLines) {
motd.add(motdLine);
}
}
data.add("motd", motd);
data.addProperty("icon", root.has("favicon"));
if (root.has("players")) {
final JsonObject javaPlayers = root.getAsJsonObject("players");
data.add("online", javaPlayers.get("online"));
data.add("max", javaPlayers.get("max"));
final JsonArray players = new JsonArray();
if (javaPlayers.has("sample")) {
javaPlayers.getAsJsonArray("sample").forEach(player -> players.add(TextComponentSerializer.V1_8.deserialize(player.getAsJsonObject().get("name").getAsString()).asLegacyFormatString()));
}
data.add("players", players);
}
response.add("data", data);
out.add(new TextWebSocketFrame(response.toString()));
if (root.has("favicon")) {
final BufferedImage icon = ImageIO.read(new ByteArrayInputStream(Base64.getDecoder().decode(root.get("favicon").getAsString().substring(22).replace("\n", "").getBytes(StandardCharsets.UTF_8))));
final int[] pixels = icon.getRGB(0, 0, 64, 64, null, 0, 64);
final byte[] iconPixels = new byte[64 * 64 * 4];
for (int i = 0; i < 64 * 64; ++i) {
iconPixels[i * 4] = (byte) ((pixels[i] >> 16) & 0xFF);
iconPixels[i * 4 + 1] = (byte) ((pixels[i] >> 8) & 0xFF);
iconPixels[i * 4 + 2] = (byte) (pixels[i] & 0xFF);
iconPixels[i * 4 + 3] = (byte) ((pixels[i] >> 24) & 0xFF);
}
out.add(new BinaryWebSocketFrame(ctx.alloc().buffer().writeBytes(iconPixels)));
}
} else if (this.state == State.LOGIN_COMPLETE) {
out.add(new BinaryWebSocketFrame(in.retain()));
} else {
throw new IllegalStateException("Cannot send packets before login is completed");
}
}
@Override
protected void decode(ChannelHandlerContext ctx, WebSocketFrame in, List<Object> out) throws Exception {
if (in instanceof BinaryWebSocketFrame) {
final ByteBuf data = in.content();
switch (this.state) {
case PRE_HANDSHAKE:
if (data.readableBytes() >= 2) { // Check for legacy client
if (data.getByte(0) == (byte) 2 && data.getByte(1) == (byte) 69) {
data.setByte(1, 61); // 1.5.2 protocol id
this.state = State.LOGIN_COMPLETE;
this.version = VersionEnum.r1_5_2;
out.add(data.retain());
break;
}
}
this.state = State.HANDSHAKE;
case HANDSHAKE:
int packetId = data.readUnsignedByte(); // packet id
if (packetId == CLIENT_VERSION) {
int eaglercraftVersion = data.readUnsignedByte(); // eaglercraft version
final int minecraftVersion;
if (eaglercraftVersion == 1) {
minecraftVersion = data.readUnsignedByte(); // minecraft version
} else if (eaglercraftVersion == 2) {
int count = data.readUnsignedShort(); // eaglercraft versions
final List<Integer> eaglercraftVersions = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
eaglercraftVersions.add(data.readUnsignedShort()); // eaglercraft version id
}
if (!eaglercraftVersions.contains(2) && !eaglercraftVersions.contains(3)) {
Logger.LOGGER.error("No supported eaglercraft versions found");
ctx.close();
return;
}
if (eaglercraftVersions.contains(3)) {
eaglercraftVersion = 3;
}
count = data.readUnsignedShort(); // minecraft versions
final List<Integer> minecraftVersions = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
minecraftVersions.add(data.readUnsignedShort()); // minecraft version id
}
if (minecraftVersions.size() != 1) {
Logger.LOGGER.error("No supported minecraft versions found");
ctx.close();
}
minecraftVersion = minecraftVersions.get(0);
} else {
throw new IllegalArgumentException("Unknown Eaglercraft version: " + eaglercraftVersion);
}
final String clientBrand = data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII).toString(); // client brand
final String clientVersionString = data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII).toString(); // client version
if (eaglercraftVersion >= 2) {
data.skipBytes(1); // auth enabled
data.skipBytes(data.readUnsignedByte()); // auth username
}
if (data.isReadable()) {
throw new IllegalArgumentException("Too much data in packet: " + data.readableBytes() + " bytes");
}
Logger.LOGGER.info("Eaglercraft client connected: " + clientBrand + " " + clientVersionString);
final String serverBrand = "ViaProxy";
final String serverVersionString = ViaProxy.VERSION;
this.state = State.HANDSHAKE_COMPLETE;
this.version = VersionEnum.fromProtocolId(minecraftVersion);
if (this.version.equals(VersionEnum.UNKNOWN)) {
Logger.LOGGER.error("Unsupported protocol version: " + minecraftVersion);
ctx.close();
return;
}
final ByteBuf response = ctx.alloc().buffer();
response.writeByte(SERVER_VERSION); // packet id
if (eaglercraftVersion == 1) {
response.writeByte(1); // eaglercraft version
} else {
response.writeShort(eaglercraftVersion); // eaglercraft version
response.writeShort(minecraftVersion); // minecraft version
}
response.writeByte(serverBrand.length()).writeCharSequence(serverBrand, StandardCharsets.US_ASCII); // server brand
response.writeByte(serverVersionString.length()).writeCharSequence(serverVersionString, StandardCharsets.US_ASCII); // server version
response.writeByte(0); // auth method
response.writeShort(0); // salt length
ctx.writeAndFlush(new BinaryWebSocketFrame(response));
} else {
throw new IllegalStateException("Unexpected packet id " + packetId + " in state " + this.state);
}
break;
case HANDSHAKE_COMPLETE:
packetId = data.readUnsignedByte(); // packet id
if (packetId == CLIENT_REQUEST_LOGIN) {
final String username = data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII).toString(); // username
data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII).toString(); // requested server
data.skipBytes(data.readUnsignedByte()); // auth password
if (data.isReadable()) {
throw new IllegalArgumentException("Too much data in packet: " + data.readableBytes() + " bytes");
}
this.state = State.LOGIN;
this.username = username;
final UUID uuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8));
final ByteBuf response = ctx.alloc().buffer();
response.writeByte(SERVER_ALLOW_LOGIN); // packet id
response.writeByte(username.length()).writeCharSequence(username, StandardCharsets.US_ASCII); // username
response.writeLong(uuid.getMostSignificantBits()).writeLong(uuid.getLeastSignificantBits()); // uuid
ctx.writeAndFlush(new BinaryWebSocketFrame(response));
} else {
throw new IllegalStateException("Unexpected packet id " + packetId + " in state " + this.state);
}
break;
case LOGIN:
packetId = data.readUnsignedByte(); // packet id
if (packetId == CLIENT_PROFILE_DATA) {
final String type = data.readCharSequence(data.readUnsignedByte(), StandardCharsets.US_ASCII).toString();
final byte[] dataBytes = new byte[data.readUnsignedShort()];
data.readBytes(dataBytes);
if (data.isReadable()) {
throw new IllegalArgumentException("Too much data in packet: " + data.readableBytes() + " bytes");
}
} else if (packetId == CLIENT_FINISH_LOGIN) {
if (data.isReadable()) {
throw new IllegalArgumentException("Too much data in packet: " + data.readableBytes() + " bytes");
}
this.state = State.LOGIN_COMPLETE;
this.pluginMessageId = MCPackets.C2S_PLUGIN_MESSAGE.getId(this.version.getVersion());
if (this.pluginMessageId == -1) {
Logger.LOGGER.error("Unsupported protocol version: " + this.version.getVersion());
ctx.close();
return;
}
if (ctx.pipeline().get(Client2ProxyChannelInitializer.LEGACY_PASSTHROUGH_HANDLER_NAME) != null) {
ctx.pipeline().remove(Client2ProxyChannelInitializer.LEGACY_PASSTHROUGH_HANDLER_NAME);
}
out.add(this.writeHandshake(ctx.alloc().buffer(), ConnectionState.LOGIN));
final ByteBuf loginHello = ctx.alloc().buffer();
PacketTypes.writeVarInt(loginHello, ServerboundLoginPackets.HELLO.getId()); // packet id
PacketTypes.writeString(loginHello, this.username); // username
out.add(loginHello);
final ByteBuf response = ctx.alloc().buffer();
response.writeByte(SERVER_FINISH_LOGIN); // packet id
ctx.writeAndFlush(new BinaryWebSocketFrame(response));
} else {
throw new IllegalStateException("Unexpected packet id " + packetId + " in state " + this.state);
}
break;
case LOGIN_COMPLETE:
if (this.version.equals(VersionEnum.r1_5_2)) {
packetId = data.readUnsignedByte();
if (packetId == ServerboundPackets1_5_2.SHARED_KEY.getId()) {
ctx.channel().writeAndFlush(new BinaryWebSocketFrame(data.readerIndex(0).retain()));
break;
} else if (packetId == ServerboundPackets1_5_2.PLUGIN_MESSAGE.getId()) {
if (Types1_6_4.STRING.read(data).startsWith("EAG|")) {
break;
}
}
} else if (this.version.isNewerThanOrEqualTo(VersionEnum.r1_7_2tor1_7_5)) {
packetId = PacketTypes.readVarInt(data);
if (packetId == this.pluginMessageId) {
if (PacketTypes.readString(data, Short.MAX_VALUE).startsWith("EAG|")) {
break;
}
}
}
out.add(data.readerIndex(0).retain());
break;
default:
throw new IllegalStateException("Unexpected binary frame in state " + this.state);
}
} else if (in instanceof TextWebSocketFrame) {
final String text = ((TextWebSocketFrame) in).text();
if (this.state != State.PRE_HANDSHAKE) {
throw new IllegalStateException("Unexpected text frame in state " + this.state);
}
if (!text.equalsIgnoreCase("accept: motd")) {
ctx.close();
return;
}
this.state = State.STATUS;
this.version = VersionEnum.r1_8;
out.add(this.writeHandshake(ctx.alloc().buffer(), ConnectionState.STATUS));
final ByteBuf statusRequest = ctx.alloc().buffer();
PacketTypes.writeVarInt(statusRequest, ServerboundStatusPackets.STATUS_REQUEST.getId()); // packet id
out.add(statusRequest);
} else {
throw new UnsupportedOperationException("Unsupported frame type: " + in.getClass().getName());
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
final WebSocketServerProtocolHandler.HandshakeComplete handshake = (WebSocketServerProtocolHandler.HandshakeComplete) evt;
if (!handshake.requestHeaders().contains("Host")) {
ctx.close();
return;
}
this.host = HostAndPort.fromString(handshake.requestHeaders().get("Host")).withDefaultPort(80);
}
super.userEventTriggered(ctx, evt);
}
private ByteBuf writeHandshake(final ByteBuf byteBuf, final ConnectionState state) {
PacketTypes.writeVarInt(byteBuf, ServerboundHandshakePackets.CLIENT_INTENTION.getId()); // packet id
PacketTypes.writeVarInt(byteBuf, this.version.getVersion()); // protocol version
PacketTypes.writeString(byteBuf, this.host.getHost()); // address
byteBuf.writeShort(this.host.getPort()); // port
PacketTypes.writeVarInt(byteBuf, state.getId()); // next state
return byteBuf;
}
public enum State {
STATUS,
PRE_HANDSHAKE,
HANDSHAKE,
HANDSHAKE_COMPLETE,
LOGIN,
LOGIN_COMPLETE,
}
}

View File

@ -0,0 +1,80 @@
/*
* This file is part of ViaProxy - https://github.com/RaphiMC/ViaProxy
* Copyright (C) 2023 RK_01/RaphiMC and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.raphimc.viaproxy.proxy.client2proxy.eaglercraft;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import net.raphimc.viaproxy.proxy.client2proxy.Client2ProxyChannelInitializer;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class EaglercraftInitialHandler extends ByteToMessageDecoder {
private static SslContext sslContext;
static {
final File certFolder = new File("certs");
if (certFolder.exists()) {
try {
sslContext = SslContextBuilder.forServer(new File(certFolder, "fullchain.pem"), new File(certFolder, "privkey.pem")).build();
} catch (Throwable e) {
throw new RuntimeException("Failed to load SSL context", e);
}
}
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (!ctx.channel().isOpen()) return;
if (!in.isReadable()) return;
if (in.readableBytes() >= 3) {
final byte[] data = new byte[3];
in.getBytes(0, data);
final String method = new String(data, StandardCharsets.UTF_8);
if (method.equals("GET")) { // Websocket request
if (sslContext != null) {
ctx.pipeline().addBefore(Client2ProxyChannelInitializer.EAGLERCRAFT_INITIAL_HANDLER_NAME, Client2ProxyChannelInitializer.WEBSOCKET_SSL_HANDLER_NAME, sslContext.newHandler(ctx.alloc()));
}
ctx.pipeline().addBefore(Client2ProxyChannelInitializer.EAGLERCRAFT_INITIAL_HANDLER_NAME, Client2ProxyChannelInitializer.WEBSOCKET_HTTP_CODEC_NAME, new HttpServerCodec());
ctx.pipeline().addBefore(Client2ProxyChannelInitializer.EAGLERCRAFT_INITIAL_HANDLER_NAME, Client2ProxyChannelInitializer.WEBSOCKET_HTTP_AGGREGATOR_NAME, new HttpObjectAggregator(65535, true));
ctx.pipeline().addBefore(Client2ProxyChannelInitializer.EAGLERCRAFT_INITIAL_HANDLER_NAME, Client2ProxyChannelInitializer.WEBSOCKET_COMPRESSION_NAME, new WebSocketServerCompressionHandler());
ctx.pipeline().addBefore(Client2ProxyChannelInitializer.EAGLERCRAFT_INITIAL_HANDLER_NAME, Client2ProxyChannelInitializer.WEBSOCKET_HANDLER_NAME, new WebSocketServerProtocolHandler("/", null, true));
ctx.pipeline().addBefore(Client2ProxyChannelInitializer.EAGLERCRAFT_INITIAL_HANDLER_NAME, Client2ProxyChannelInitializer.WEBSOCKET_ACTIVE_NOTIFIER_NAME, new WebSocketActiveNotifier());
ctx.pipeline().addBefore(Client2ProxyChannelInitializer.EAGLERCRAFT_INITIAL_HANDLER_NAME, Client2ProxyChannelInitializer.EAGLERCRAFT_HANDLER_NAME, new EaglercraftHandler());
ctx.pipeline().fireChannelRead(in.readBytes(in.readableBytes()));
} else {
out.add(in.readBytes(in.readableBytes()));
}
ctx.pipeline().remove(this);
}
}
}

View File

@ -0,0 +1,40 @@
/*
* This file is part of ViaProxy - https://github.com/RaphiMC/ViaProxy
* Copyright (C) 2023 RK_01/RaphiMC and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.raphimc.viaproxy.proxy.client2proxy.eaglercraft;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
public class WebSocketActiveNotifier extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) {
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof WebSocketServerProtocolHandler.HandshakeComplete) {
ctx.fireChannelActive();
ctx.pipeline().remove(this);
}
super.userEventTriggered(ctx, evt);
}
}

View File

@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package net.raphimc.viaproxy.proxy.client2proxy; package net.raphimc.viaproxy.proxy.client2proxy.passthrough;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
@ -26,10 +26,13 @@ import net.raphimc.viaproxy.cli.options.Options;
import net.raphimc.viaproxy.proxy.util.ExceptionUtil; import net.raphimc.viaproxy.proxy.util.ExceptionUtil;
import net.raphimc.viaproxy.util.logging.Logger; import net.raphimc.viaproxy.util.logging.Logger;
import java.util.function.Function;
import java.util.function.Supplier;
public class LegacyClientPassthroughHandler extends SimpleChannelInboundHandler<ByteBuf> { public class LegacyClientPassthroughHandler extends SimpleChannelInboundHandler<ByteBuf> {
private Channel c2pChannel; protected Channel c2pChannel;
private NetClient p2sConnection; protected NetClient p2sConnection;
@Override @Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { public void channelActive(ChannelHandlerContext ctx) throws Exception {
@ -56,8 +59,8 @@ public class LegacyClientPassthroughHandler extends SimpleChannelInboundHandler<
if (!msg.isReadable()) return; if (!msg.isReadable()) return;
if (this.p2sConnection == null) { if (this.p2sConnection == null) {
final int length = msg.getUnsignedByte(0); final int lengthOrPacketId = msg.getUnsignedByte(0);
if (length == 0/*classic*/ || length == 1/*a1.0.15*/ || length == 2/*<= 1.6.4*/ || length == 254/*<= 1.6.4 (ping)*/) { if (lengthOrPacketId == 0/*classic*/ || lengthOrPacketId == 1/*a1.0.15*/ || lengthOrPacketId == 2/*<= 1.6.4*/ || lengthOrPacketId == 254/*<= 1.6.4 (ping)*/) {
while (ctx.pipeline().last() != this) { while (ctx.pipeline().last() != this) {
ctx.pipeline().removeLast(); ctx.pipeline().removeLast();
} }
@ -73,8 +76,44 @@ public class LegacyClientPassthroughHandler extends SimpleChannelInboundHandler<
this.p2sConnection.getChannel().writeAndFlush(msg.retain()).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE); this.p2sConnection.getChannel().writeAndFlush(msg.retain()).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
} }
private void connectToServer() { @Override
this.p2sConnection = new NetClient(() -> new SimpleChannelInboundHandler<ByteBuf>() { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ExceptionUtil.handleNettyException(ctx, cause, null);
}
protected void connectToServer() {
this.p2sConnection = new NetClient(this.getHandlerSupplier(), this.getChannelInitializerSupplier()) {
@Override
public void initialize(Bootstrap bootstrap) {
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4_000);
super.initialize(bootstrap);
}
};
try {
this.p2sConnection.connect(this.getServerAddress());
} catch (Throwable e) {
Logger.LOGGER.error("Failed to connect to target server", e);
this.p2sConnection = null;
this.c2pChannel.close();
}
}
protected ServerAddress getServerAddress() {
return new ServerAddress(Options.CONNECT_ADDRESS, Options.CONNECT_PORT);
}
protected Function<Supplier<ChannelHandler>, ChannelInitializer<Channel>> getChannelInitializerSupplier() {
return s -> new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) {
channel.pipeline().addLast(s.get());
}
};
}
protected Supplier<ChannelHandler> getHandlerSupplier() {
return () -> new SimpleChannelInboundHandler<ByteBuf>() {
@Override @Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception { public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx); super.channelInactive(ctx);
@ -94,31 +133,7 @@ public class LegacyClientPassthroughHandler extends SimpleChannelInboundHandler<
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ExceptionUtil.handleNettyException(ctx, cause, null); ExceptionUtil.handleNettyException(ctx, cause, null);
} }
}, s -> new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) {
channel.pipeline().addLast(s.get());
}
}) {
@Override
public void initialize(Bootstrap bootstrap) {
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 4_000);
super.initialize(bootstrap);
}
}; };
try {
this.p2sConnection.connect(new ServerAddress(Options.CONNECT_ADDRESS, Options.CONNECT_PORT));
} catch (Throwable e) {
Logger.LOGGER.error("Failed to connect to target server", e);
this.p2sConnection = null;
this.c2pChannel.close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ExceptionUtil.handleNettyException(ctx, cause, null);
} }
} }

View File

@ -45,6 +45,9 @@ import java.util.function.Supplier;
public class Proxy2ServerChannelInitializer extends MinecraftChannelInitializer { public class Proxy2ServerChannelInitializer extends MinecraftChannelInitializer {
public static final String VIAPROXY_PROXY_HANDLER_NAME = "viaproxy-proxy-handler";
public static final String VIAPROXY_HAPROXY_ENCODER_NAME = "viaproxy-haproxy-encoder";
public Proxy2ServerChannelInitializer(final Supplier<ChannelHandler> handlerSupplier) { public Proxy2ServerChannelInitializer(final Supplier<ChannelHandler> handlerSupplier) {
super(handlerSupplier); super(handlerSupplier);
} }
@ -62,10 +65,10 @@ public class Proxy2ServerChannelInitializer extends MinecraftChannelInitializer
proxyConnection.setUserConnection(user); proxyConnection.setUserConnection(user);
if (Options.PROXY_URL != null && !proxyConnection.getServerVersion().equals(VersionEnum.bedrockLatest)) { if (Options.PROXY_URL != null && !proxyConnection.getServerVersion().equals(VersionEnum.bedrockLatest)) {
channel.pipeline().addLast("viaproxy-proxy-handler", this.getProxyHandler()); channel.pipeline().addLast(VIAPROXY_PROXY_HANDLER_NAME, this.getProxyHandler());
} }
if (Options.SERVER_HAPROXY_PROTOCOL) { if (Options.SERVER_HAPROXY_PROTOCOL) {
channel.pipeline().addLast("viaproxy-haproxy-encoder", HAProxyMessageEncoder.INSTANCE); channel.pipeline().addLast(VIAPROXY_HAPROXY_ENCODER_NAME, HAProxyMessageEncoder.INSTANCE);
} }
super.initChannel(channel); super.initChannel(channel);

View File

@ -137,7 +137,7 @@ public class Proxy2ServerHandler extends SimpleChannelInboundHandler<IPacket> {
private void handleLoginSuccess(final S2CLoginSuccessPacket1_7 packet) throws Exception { private void handleLoginSuccess(final S2CLoginSuccessPacket1_7 packet) throws Exception {
if (this.proxyConnection.getClientVersion().isNewerThanOrEqualTo(VersionEnum.r1_8)) { if (this.proxyConnection.getClientVersion().isNewerThanOrEqualTo(VersionEnum.r1_8)) {
if (Options.COMPRESSION_THRESHOLD > -1) { if (Options.COMPRESSION_THRESHOLD > -1 && this.proxyConnection.getC2P().attr(MCPipeline.COMPRESSION_THRESHOLD_ATTRIBUTE_KEY).get() == -1) {
this.proxyConnection.getC2P().writeAndFlush(new S2CLoginCompressionPacket(Options.COMPRESSION_THRESHOLD)).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE).await(); this.proxyConnection.getC2P().writeAndFlush(new S2CLoginCompressionPacket(Options.COMPRESSION_THRESHOLD)).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE).await();
this.proxyConnection.getC2P().attr(MCPipeline.COMPRESSION_THRESHOLD_ATTRIBUTE_KEY).set(Options.COMPRESSION_THRESHOLD); this.proxyConnection.getC2P().attr(MCPipeline.COMPRESSION_THRESHOLD_ATTRIBUTE_KEY).set(Options.COMPRESSION_THRESHOLD);
} }

View File

@ -31,6 +31,7 @@ public class AdvancedTab extends AUITab {
JTextField proxy; JTextField proxy;
JCheckBox proxyOnlineMode; JCheckBox proxyOnlineMode;
JCheckBox legacySkinLoading; JCheckBox legacySkinLoading;
JCheckBox eaglercraftSupport;
public AdvancedTab(final ViaProxyUI frame) { public AdvancedTab(final ViaProxyUI frame) {
super(frame, "Advanced"); super(frame, "Advanced");
@ -84,6 +85,13 @@ public class AdvancedTab extends AUITab {
ViaProxy.saveManager.uiSave.loadCheckBox("legacy_skin_loading", this.legacySkinLoading); ViaProxy.saveManager.uiSave.loadCheckBox("legacy_skin_loading", this.legacySkinLoading);
contentPane.add(this.legacySkinLoading); contentPane.add(this.legacySkinLoading);
} }
{
this.eaglercraftSupport = new JCheckBox("Eaglercraft Support");
this.eaglercraftSupport.setBounds(10, 170, 465, 20);
this.eaglercraftSupport.setToolTipText("Enabling Eaglercraft Support allows Eaglercraft clients to connect to ViaProxy");
ViaProxy.saveManager.uiSave.loadCheckBox("eaglercraft_support", this.eaglercraftSupport);
contentPane.add(this.eaglercraftSupport);
}
} }
@Override @Override

View File

@ -203,6 +203,7 @@ public class GeneralTab extends AUITab {
final boolean betaCraftAuth = this.betaCraftAuth.isSelected(); final boolean betaCraftAuth = this.betaCraftAuth.isSelected();
final boolean proxyOnlineMode = ViaProxy.ui.advancedTab.proxyOnlineMode.isSelected(); final boolean proxyOnlineMode = ViaProxy.ui.advancedTab.proxyOnlineMode.isSelected();
final boolean legacySkinLoading = ViaProxy.ui.advancedTab.legacySkinLoading.isSelected(); final boolean legacySkinLoading = ViaProxy.ui.advancedTab.legacySkinLoading.isSelected();
final boolean eaglercraftSupport = ViaProxy.ui.advancedTab.eaglercraftSupport.isSelected();
final String proxyUrl = ViaProxy.ui.advancedTab.proxy.getText(); final String proxyUrl = ViaProxy.ui.advancedTab.proxy.getText();
try { try {
@ -240,6 +241,7 @@ public class GeneralTab extends AUITab {
Options.PROTOCOL_VERSION = serverVersion; Options.PROTOCOL_VERSION = serverVersion;
Options.BETACRAFT_AUTH = betaCraftAuth; Options.BETACRAFT_AUTH = betaCraftAuth;
Options.LEGACY_SKIN_LOADING = legacySkinLoading; Options.LEGACY_SKIN_LOADING = legacySkinLoading;
Options.ALLOW_EAGLERCRAFT_CLIENTS = eaglercraftSupport;
if (authMethod == 2) { if (authMethod == 2) {
Options.OPENAUTHMOD_AUTH = true; Options.OPENAUTHMOD_AUTH = true;