521 lines
16 KiB
Java
Executable file
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;
|
|
}
|
|
}
|