mirror of
https://github.com/ViaVersion/ViaProxy.git
synced 2024-11-21 11:45:16 +01:00
Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
52b257e206
@ -16,6 +16,8 @@ For a full user guide go to the [Usage for Players](#usage-for-players-gui) sect
|
||||
|
||||
## Supported Client versions
|
||||
- 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.
|
||||
|
||||
|
@ -59,6 +59,7 @@ public class Options {
|
||||
public static String RESOURCE_PACK_URL; // Example: http://example.com/resourcepack.zip
|
||||
public static boolean SERVER_HAPROXY_PROTOCOL;
|
||||
public static boolean LEGACY_CLIENT_PASSTHROUGH;
|
||||
public static boolean ALLOW_EAGLERCRAFT_CLIENTS;
|
||||
|
||||
public static void parse(final String[] args) throws IOException {
|
||||
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> 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> 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));
|
||||
|
||||
final OptionSet options = parser.parse(args);
|
||||
@ -119,6 +121,7 @@ public class Options {
|
||||
}
|
||||
SERVER_HAPROXY_PROTOCOL = options.has(serverHaProxyProtocol);
|
||||
LEGACY_CLIENT_PASSTHROUGH = options.has(legacyClientPassthrough);
|
||||
ALLOW_EAGLERCRAFT_CLIENTS = options.has(allowEaglerCraftClients);
|
||||
PluginManager.EVENT_MANAGER.call(new PostOptionsParseEvent(options));
|
||||
}
|
||||
|
||||
|
@ -26,11 +26,24 @@ import net.raphimc.viaproxy.cli.options.Options;
|
||||
import net.raphimc.viaproxy.plugins.PluginManager;
|
||||
import net.raphimc.viaproxy.plugins.events.Client2ProxyChannelInitializeEvent;
|
||||
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;
|
||||
|
||||
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) {
|
||||
super(handlerSupplier);
|
||||
}
|
||||
@ -42,9 +55,13 @@ public class Client2ProxyChannelInitializer extends MinecraftChannelInitializer
|
||||
return;
|
||||
}
|
||||
|
||||
if (Options.LEGACY_CLIENT_PASSTHROUGH) {
|
||||
channel.pipeline().addLast("legacy-passthrough-handler", new LegacyClientPassthroughHandler());
|
||||
if (Options.ALLOW_EAGLERCRAFT_CLIENTS) {
|
||||
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);
|
||||
channel.attr(MCPipeline.PACKET_REGISTRY_ATTRIBUTE_KEY).set(PacketRegistryUtil.getHandshakeRegistry(false));
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
* 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;
|
||||
package net.raphimc.viaproxy.proxy.client2proxy.passthrough;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
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.util.logging.Logger;
|
||||
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class LegacyClientPassthroughHandler extends SimpleChannelInboundHandler<ByteBuf> {
|
||||
|
||||
private Channel c2pChannel;
|
||||
private NetClient p2sConnection;
|
||||
protected Channel c2pChannel;
|
||||
protected NetClient p2sConnection;
|
||||
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
||||
@ -56,8 +59,8 @@ public class LegacyClientPassthroughHandler extends SimpleChannelInboundHandler<
|
||||
if (!msg.isReadable()) return;
|
||||
|
||||
if (this.p2sConnection == null) {
|
||||
final int length = msg.getUnsignedByte(0);
|
||||
if (length == 0/*classic*/ || length == 1/*a1.0.15*/ || length == 2/*<= 1.6.4*/ || length == 254/*<= 1.6.4 (ping)*/) {
|
||||
final int lengthOrPacketId = msg.getUnsignedByte(0);
|
||||
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) {
|
||||
ctx.pipeline().removeLast();
|
||||
}
|
||||
@ -73,8 +76,44 @@ public class LegacyClientPassthroughHandler extends SimpleChannelInboundHandler<
|
||||
this.p2sConnection.getChannel().writeAndFlush(msg.retain()).addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
|
||||
}
|
||||
|
||||
private void connectToServer() {
|
||||
this.p2sConnection = new NetClient(() -> new SimpleChannelInboundHandler<ByteBuf>() {
|
||||
@Override
|
||||
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
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
||||
super.channelInactive(ctx);
|
||||
@ -94,31 +133,7 @@ public class LegacyClientPassthroughHandler extends SimpleChannelInboundHandler<
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -45,6 +45,9 @@ import java.util.function.Supplier;
|
||||
|
||||
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) {
|
||||
super(handlerSupplier);
|
||||
}
|
||||
@ -62,10 +65,10 @@ public class Proxy2ServerChannelInitializer extends MinecraftChannelInitializer
|
||||
proxyConnection.setUserConnection(user);
|
||||
|
||||
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) {
|
||||
channel.pipeline().addLast("viaproxy-haproxy-encoder", HAProxyMessageEncoder.INSTANCE);
|
||||
channel.pipeline().addLast(VIAPROXY_HAPROXY_ENCODER_NAME, HAProxyMessageEncoder.INSTANCE);
|
||||
}
|
||||
|
||||
super.initChannel(channel);
|
||||
|
@ -137,7 +137,7 @@ public class Proxy2ServerHandler extends SimpleChannelInboundHandler<IPacket> {
|
||||
|
||||
private void handleLoginSuccess(final S2CLoginSuccessPacket1_7 packet) throws Exception {
|
||||
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().attr(MCPipeline.COMPRESSION_THRESHOLD_ATTRIBUTE_KEY).set(Options.COMPRESSION_THRESHOLD);
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ public class AdvancedTab extends AUITab {
|
||||
JTextField proxy;
|
||||
JCheckBox proxyOnlineMode;
|
||||
JCheckBox legacySkinLoading;
|
||||
JCheckBox eaglercraftSupport;
|
||||
|
||||
public AdvancedTab(final ViaProxyUI frame) {
|
||||
super(frame, "Advanced");
|
||||
@ -84,6 +85,13 @@ public class AdvancedTab extends AUITab {
|
||||
ViaProxy.saveManager.uiSave.loadCheckBox("legacy_skin_loading", 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
|
||||
|
@ -203,6 +203,7 @@ public class GeneralTab extends AUITab {
|
||||
final boolean betaCraftAuth = this.betaCraftAuth.isSelected();
|
||||
final boolean proxyOnlineMode = ViaProxy.ui.advancedTab.proxyOnlineMode.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();
|
||||
|
||||
try {
|
||||
@ -240,6 +241,7 @@ public class GeneralTab extends AUITab {
|
||||
Options.PROTOCOL_VERSION = serverVersion;
|
||||
Options.BETACRAFT_AUTH = betaCraftAuth;
|
||||
Options.LEGACY_SKIN_LOADING = legacySkinLoading;
|
||||
Options.ALLOW_EAGLERCRAFT_CLIENTS = eaglercraftSupport;
|
||||
|
||||
if (authMethod == 2) {
|
||||
Options.OPENAUTHMOD_AUTH = true;
|
||||
|
Loading…
Reference in New Issue
Block a user