vproxy/proxy/src/main/java/proxy/Proxy.java
2025-06-05 12:04:27 +02:00

521 lines
16 KiB
Java
Executable file

package proxy;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelException;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
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.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.base64.Base64;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.IDN;
import java.net.InetAddress;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import javax.imageio.ImageIO;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Queues;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import proxy.network.Direction;
import proxy.network.Decoder;
import proxy.network.Splitter;
import proxy.network.Encoder;
import proxy.network.EventLoopLoader;
import proxy.network.Prepender;
import proxy.command.*;
import proxy.handler.HandshakeHandler;
import proxy.handler.ProxyHandler;
import proxy.handler.Handler.ThreadQuickExitException;
import proxy.network.Connection;
import proxy.packet.S40PacketDisconnect;
import proxy.packet.ServerInfo;
import proxy.util.Formatter;
import proxy.util.User;
import proxy.util.Log;
public class Proxy {
public static final EventLoopLoader<NioEventLoopGroup> NIO_EVENT_LOOP = new EventLoopLoader<NioEventLoopGroup>() {
protected NioEventLoopGroup createEventLoop() {
return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Server IO #%d").setDaemon(true).build());
}
};
public static final EventLoopLoader<EpollEventLoopGroup> EPOLL_EVENT_LOOP = new EventLoopLoader<EpollEventLoopGroup>() {
protected EpollEventLoopGroup createEventLoop() {
return new EpollEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Epoll Server IO #%d").setDaemon(true).build());
}
};
private volatile boolean isAlive;
private volatile boolean running = true;
private final List<ChannelFuture> endpoints = Collections.<ChannelFuture>synchronizedList(Lists.newArrayList());
private final List<Connection> networkManagers = Collections.<Connection>synchronizedList(Lists.newArrayList());
private final Queue<FutureTask<?>> futureTaskQueue = Queues.<FutureTask<?>>newArrayDeque();
private final Map<String, User> users = Maps.newTreeMap();
private final Map<String, ProxyHandler> players = Maps.newTreeMap();
private final Map<String, Command> commands = Maps.newTreeMap();
private final Thread serverThread;
private int compression = -1;
private boolean epoll = true;
private boolean register = true;
private boolean checkCase = true;
private boolean kickOnConnect = true;
private String forwardHost = "127.0.0.1";
private int forwardPort = 25563;
private String proxyHost = "";
private int proxyPort = 25564;
private int minPassLength = 8;
private int maxAttempts = 5;
private int maxPlayers = 64;
private ServerInfo status;
private static String getIcon(File file) {
if(file.isFile()) {
ByteBuf buf = Unpooled.buffer();
try {
BufferedImage img = ImageIO.read(file);
if(img.getWidth() != 64 || img.getHeight() != 64)
throw new IllegalArgumentException("Icon must be 64x64 pixels");
ImageIO.write(img, "PNG", new ByteBufOutputStream(buf));
return "data:image/png;base64," + Base64.encode(buf).toString(Charsets.UTF_8);
}
catch(Exception e) {
Log.error(e, "Couldn't load server icon");
}
finally {
buf.release();
}
}
return null;
}
public static UUID getOfflineUUID(String name) {
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + name).getBytes(Charsets.UTF_8));
}
public static void main(String[] args) {
Thread.currentThread().setName("Proxy thread");
Proxy proxy = new Proxy();
proxy.run();
}
public Proxy() {
this.serverThread = Thread.currentThread();
this.isAlive = true;
this.registerCommands();
}
public void run() {
this.status = new ServerInfo("VLoginProxy 1.8.9", 47);
this.status.setCapacity(this.maxPlayers);
Collections.addAll(this.status.getMotds(), Formatter.DARK_RED + "Miau\n" + Formatter.YELLOW + "Test??", Formatter.AQUA + "Server\n" + Formatter.GREEN + "Test!!");
String icon = getIcon(new File("icon1.png"));
if(icon != null)
this.status.getIcons().add(icon);
icon = getIcon(new File("icon2.png"));
if(icon != null)
this.status.getIcons().add(icon);
Collections.addAll(this.status.getList(), Formatter.DARK_GREEN + "TESTTTT", "Test 2!", Formatter.BLUE + "Test numbah 3!!!!!");
Log.info("Starting login proxy on %s:%d", this.proxyHost.isEmpty() ? "0.0.0.0" : this.proxyHost, this.proxyPort);
try {
this.addLanEndpoint(this.proxyHost.isEmpty() ? null : InetAddress.getByName(IDN.toASCII(this.proxyHost)), this.proxyPort);
}
catch(IOException e) {
Log.error(e, "Could not bind to port");
return;
}
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
Proxy.this.terminateEndpoints();
}
}, "Proxy shutdown thread"));
Thread con = new Thread(new Runnable() {
private final BufferedReader reader = new BufferedReader(new InputStreamReader(new BufferedInputStream(System.in)));
public void run() {
while(true) {
String line;
try {
line = this.reader.readLine();
}
catch(IOException e) {
line = null;
}
if(line == null)
break;
final String cmd = line;
Proxy.this.schedule(new Runnable() {
public void run() {
String msg = Formatter.filterSpaces(cmd);
Proxy.this.runCommand(null, msg.isEmpty() ? new String[] {"phelp"} : msg.split(" "), msg);
}
});
}
}
}, "Proxy console handler");
con.setDaemon(true);
con.start();
while(this.running) {
synchronized (this.futureTaskQueue)
{
while (!this.futureTaskQueue.isEmpty())
{
FutureTask<?> task = this.futureTaskQueue.poll();
try {
task.run();
task.get();
}
catch(ExecutionException e1) {
if(!(e1.getCause() instanceof ThreadQuickExitException))
Log.error(e1, "Error executing task " + task);
}
catch(InterruptedException e2) {
Log.error(e2, "Error executing task " + task);
}
}
}
this.networkTick();
try {
Thread.sleep(50L);
}
catch(InterruptedException e) {
}
}
this.terminateEndpoints();
Log.info("Proxy stopped");
}
public void addLanEndpoint(InetAddress address, int port) throws IOException {
synchronized(this.endpoints) {
Class<? extends ServerSocketChannel> oclass;
EventLoopLoader<? extends EventLoopGroup> lazyloadbase;
if(Epoll.isAvailable() && this.epoll) {
oclass = EpollServerSocketChannel.class;
lazyloadbase = EPOLL_EVENT_LOOP;
Log.info("Using epoll channel type");
}
else {
oclass = NioServerSocketChannel.class;
lazyloadbase = NIO_EVENT_LOOP;
Log.info("Using default channel type");
}
this.endpoints
.add(((ServerBootstrap)((ServerBootstrap)(new ServerBootstrap()).channel(oclass)).childHandler(new ChannelInitializer<Channel>() {
protected void initChannel(Channel p_initChannel_1_) throws Exception {
try {
p_initChannel_1_.config().setOption(ChannelOption.TCP_NODELAY, Boolean.valueOf(true));
}
catch(ChannelException var3) {
;
}
p_initChannel_1_.pipeline().addLast((String)"timeout", (ChannelHandler)(new ReadTimeoutHandler(30)))
.addLast((String)"splitter", (ChannelHandler)(new Splitter()))
.addLast((String)"decoder", (ChannelHandler)(new Decoder(Direction.SERVER)))
.addLast((String)"prepender", (ChannelHandler)(new Prepender()))
.addLast((String)"encoder", (ChannelHandler)(new Encoder(Direction.CLIENT)));
Connection networkmanager = new Connection(Direction.SERVER);
Proxy.this.networkManagers.add(networkmanager);
p_initChannel_1_.pipeline().addLast((String)"packet_handler", (ChannelHandler)networkmanager);
networkmanager.setNetHandler(new HandshakeHandler(Proxy.this, networkmanager));
}
}).group(lazyloadbase.get()).localAddress(address, port)).bind().syncUninterruptibly());
}
}
public void terminateEndpoints() {
this.isAlive = false;
for(ChannelFuture channelfuture : this.endpoints) {
try {
channelfuture.channel().close().sync();
}
catch(InterruptedException var4) {
Log.error("Interrupted whilst closing channel");
}
}
}
public void networkTick() {
synchronized(this.networkManagers) {
Iterator<Connection> iterator = this.networkManagers.iterator();
while(iterator.hasNext()) {
final Connection networkmanager = (Connection)iterator.next();
if(!networkmanager.hasNoChannel()) {
if(!networkmanager.isChannelOpen()) {
iterator.remove();
networkmanager.checkDisconnected();
}
else {
try {
networkmanager.processReceivedPackets();
}
catch(Exception exception) {
Log.error(exception, "Failed to handle packet for " + networkmanager.getRemoteAddress());
final String reason = "Internal server error";
networkmanager.sendPacket(new S40PacketDisconnect(reason), new GenericFutureListener<Future<? super Void>>() {
public void operationComplete(Future<? super Void> p_operationComplete_1_) throws Exception {
networkmanager.closeChannel(reason);
}
});
networkmanager.disableAutoRead();
}
}
}
}
}
}
private <V> ListenableFuture<V> callFromMainThread(Callable<V> callable) {
if(!this.isMainThread()) {
ListenableFutureTask<V> task = ListenableFutureTask.<V>create(callable);
synchronized(this.futureTaskQueue) {
this.futureTaskQueue.add(task);
return task;
}
}
else {
try {
return Futures.<V>immediateFuture(callable.call());
}
catch(Exception e) {
return Futures.immediateFailedCheckedFuture(e);
}
}
}
public void schedule(Runnable task) {
this.<Object>callFromMainThread(Executors.callable(task));
}
public boolean isMainThread() {
return Thread.currentThread() == this.serverThread;
}
public int getCompression() {
return this.compression;
}
public ServerInfo getStatus() {
return this.status;
}
public String getForwardHost() {
return this.forwardHost;
}
public int getForwardPort() {
return this.forwardPort;
}
public boolean isUsingEPoll() {
return this.epoll;
}
public boolean canRegister() {
return this.register;
}
public boolean isCheckingCase() {
return this.checkCase;
}
public boolean isKickingOnConnect() {
return this.kickOnConnect;
}
public int getMinimumPasswordLength() {
return this.minPassLength;
}
public int getMaximumPasswordAttempts() {
return this.maxAttempts;
}
public int getMaximumPlayers() {
return this.maxPlayers;
}
public int getOnlinePlayers() {
return this.players.size();
}
public User getUser(String user) {
return this.users.get(user.toLowerCase(Locale.US));
}
public void setUser(User usr) {
this.users.put(usr.getUsername().toLowerCase(Locale.US), usr);
}
public void deleteUser(String user) {
user = user.toLowerCase(Locale.US);
this.users.remove(user);
this.players.remove(user);
this.status.setOnline(this.players.size());
}
public void setLoggedIn(String user, ProxyHandler handler) {
user = user.toLowerCase(Locale.US);
User usr = this.users.remove(user);
if(usr != null)
handler.copyFrom(usr);
this.users.put(user, handler);
this.players.put(user, handler);
this.status.setOnline(this.players.size());
}
public void setLoggedOut(String user) {
user = user.toLowerCase(Locale.US);
ProxyHandler handler = this.players.remove(user);
if(handler != null) {
User usr = new User(handler.getUsername());
usr.copyFrom(handler);
this.users.put(user, usr);
}
this.status.setOnline(this.players.size());
}
public boolean isLoggedIn(String user) {
return this.players.containsKey(user.toLowerCase(Locale.US));
}
public ProxyHandler getPlayer(String user) {
return this.players.get(user.toLowerCase(Locale.US));
}
private void register(Command cmd) {
if(this.commands.containsKey(cmd.getName()))
throw new IllegalArgumentException("Command '" + cmd.getName() + "' ist already registered");
this.commands.put(cmd.getName(), cmd);
}
private void registerCommands() {
this.register(new CommandHelp());
this.register(new CommandExit());
this.register(new CommandAdmin());
this.register(new CommandRevoke());
this.register(new CommandRegister());
this.register(new CommandDelete());
}
public Map<String, Command> getCommands() {
return this.commands;
}
public Collection<ProxyHandler> getPlayers() {
return this.players.values();
}
public List<String> getPlayerNames() {
List<String> list = Lists.newArrayList();
for(ProxyHandler player : this.players.values()) {
list.add(player.getUsername());
}
return list;
}
public Collection<User> getUsers() {
return this.users.values();
}
public List<String> getUserNames() {
List<String> list = Lists.newArrayList();
for(User user : this.users.values()) {
list.add(user.getUsername());
}
return list;
}
public void shutdown() {
this.running = false;
}
public boolean runCommand(ProxyHandler player, String[] args, String line) {
if(args.length == 0)
return false;
Command cmd = this.commands.get(args[0].toLowerCase(Locale.US));
if(cmd == null) {
if(player == null)
Log.error("Command '%s' not found", args[0]);
return false;
}
if(player != null && !player.isAdmin()) {
player.sendMessage(Formatter.DARK_RED + "You are not allowed to execute this command");
return true;
}
String[] argv = new String[args.length - 1];
System.arraycopy(args, 1, argv, 0, argv.length);
try {
cmd.run(this, player, argv);
}
catch(RunException e) {
if(player != null)
player.sendMessage(Formatter.DARK_RED + e.getMessage());
else
Log.error(e.getMessage());
}
catch(Throwable t) {
if(player != null)
player.sendMessage(Formatter.DARK_RED + "Internal error trying to execute command");
Log.error(t, "Could not execute '%s'" + (player != null ? " as %s" : ""), line, player != null ? player.getUsername() : null);
}
if(player != null) {
int redacted = cmd.getRedactedLogArg(argv.length);
if(redacted >= 0 && redacted < argv.length) {
StringBuilder sb = new StringBuilder(cmd.getName());
for(int z = 0; z < argv.length && z < redacted; z++) {
sb.append(' ').append(argv[z]);
}
line = sb.append(" [<redacted> ...]").toString();
}
Log.info("%s executed: %s", player.getUsername(), line);
}
return true;
}
}