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 NIO_EVENT_LOOP = new EventLoopLoader() { protected NioEventLoopGroup createEventLoop() { return new NioEventLoopGroup(0, (new ThreadFactoryBuilder()).setNameFormat("Netty Server IO #%d").setDaemon(true).build()); } }; public static final EventLoopLoader EPOLL_EVENT_LOOP = new EventLoopLoader() { 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 endpoints = Collections.synchronizedList(Lists.newArrayList()); private final List networkManagers = Collections.synchronizedList(Lists.newArrayList()); private final Queue> futureTaskQueue = Queues.>newArrayDeque(); private final Map users = Maps.newTreeMap(); private final Map players = Maps.newTreeMap(); private final Map 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 oclass; EventLoopLoader 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() { 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 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>() { public void operationComplete(Future p_operationComplete_1_) throws Exception { networkmanager.closeChannel(reason); } }); networkmanager.disableAutoRead(); } } } } } } private ListenableFuture callFromMainThread(Callable callable) { if(!this.isMainThread()) { ListenableFutureTask task = ListenableFutureTask.create(callable); synchronized(this.futureTaskQueue) { this.futureTaskQueue.add(task); return task; } } else { try { return Futures.immediateFuture(callable.call()); } catch(Exception e) { return Futures.immediateFailedCheckedFuture(e); } } } public void schedule(Runnable task) { this.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 getCommands() { return this.commands; } public Collection getPlayers() { return this.players.values(); } public List getPlayerNames() { List list = Lists.newArrayList(); for(ProxyHandler player : this.players.values()) { list.add(player.getUsername()); } return list; } public Collection getUsers() { return this.users.values(); } public List getUserNames() { List 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(" [ ...]").toString(); } Log.info("%s executed: %s", player.getUsername(), line); } return true; } }