/*
* Copyright (c) 2015 "JackWhite20"
*
* This file is part of Comix.
*
* Comix 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 de.jackwhite20.comix;
import com.google.gson.GsonBuilder;
import de.jackwhite20.comix.command.CommandManager;
import de.jackwhite20.comix.command.commands.*;
import de.jackwhite20.comix.config.ComixConfig;
import de.jackwhite20.comix.config.ConfigLoader;
import de.jackwhite20.comix.config.ip.IPRange;
import de.jackwhite20.comix.config.response.StatusResponse;
import de.jackwhite20.comix.handler.ComixChannelInitializer;
import de.jackwhite20.comix.logging.ComixLogger;
import de.jackwhite20.comix.network.ComixClient;
import de.jackwhite20.comix.strategy.BalancingStrategy;
import de.jackwhite20.comix.strategy.RoundRobinBalancingStrategy;
import de.jackwhite20.comix.tasks.CheckTargets;
import de.jackwhite20.comix.util.TargetData;
import de.jackwhite20.comix.whitelist.Whitelist;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.ResourceLeakDetector;
import jline.console.ConsoleReader;
import jline.console.completer.StringsCompleter;
import org.fusesource.jansi.AnsiConsole;
import sun.misc.BASE64Encoder;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
/**
* Created by JackWhite20 on 13.07.2015.
*/
public class Comix implements Runnable {
private static Comix instance;
private static Logger logger;
private static ConsoleReader consoleReader;
private boolean running;
private String balancerHost;
private int balancerPort;
private List<TargetData> targets = new ArrayList<>();
private BalancingStrategy balancingStrategy;
private ComixConfig comixConfig;
private StatusResponse statusResponse;
private String statusResponseString;
private NioEventLoopGroup bossGroup;
private NioEventLoopGroup workerGroup;
private List<String> ipBlacklist = new ArrayList<>();
private List<IPRange> ipRangeBlacklist = new ArrayList<>();
private Whitelist whitelist;
private List<ComixClient> clients = new ArrayList<>();
private CommandManager commandManager = new CommandManager();
public Comix() {
instance = this;
running = true;
try {
consoleReader = new ConsoleReader();
consoleReader.setExpandEvents(false);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
System.setProperty("java.net.preferIPv4Stack", "true");
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.DISABLED);
AnsiConsole.systemInstall();
LogManager.getLogManager().reset();
logger = new ComixLogger(consoleReader);
logger.log(Level.INFO, "Comix", "------ Comix v.0.1 ------");
loadConfig();
logger.log(Level.INFO, "Load-Balancer", (targets.size() > 0) ? "Targets:" : "No Target Servers found!");
targets.forEach(t -> logger.log(Level.INFO, "Load-Balancer", t.getName() + " - " + t.getHost() + ":" + t.getPort()));
logger.log(Level.INFO, "Commands", "Registering commands...");
registerCommands();
logger.log(Level.INFO, "Comix", "Starting Comix on " + balancerHost + ":" + balancerPort + "...");
balancingStrategy = new RoundRobinBalancingStrategy(targets);
new Timer("CheckTargets").scheduleAtFixedRate(new CheckTargets(balancingStrategy), 0, TimeUnit.SECONDS.toMillis(comixConfig.getCheckTime()));
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup(comixConfig.getThreads());
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.SO_BACKLOG, comixConfig.getBacklog())
.option(ChannelOption.SO_REUSEADDR, true)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.AUTO_READ, false)
.childOption(ChannelOption.SO_TIMEOUT, 4000)
.childHandler(new ComixChannelInitializer());
ChannelFuture f = bootstrap.bind(comixConfig.getPort()).sync();
reload();
logger.log(Level.INFO, "Comix", "Comix is started!");
f.channel().closeFuture().sync();
running = false;
} catch (Exception e) {
e.printStackTrace();
} finally {
shutdown();
}
}
private void registerCommands() {
commandManager.addCommand(new HelpCommand("help", new String[]{"h", "?"}, "List of commands"));
commandManager.addCommand(new ReloadCommand("reload", new String[]{"r"}, "Reloads 'ip-blacklist.comix', 'status.comix' and 'whitelist.comix'"));
commandManager.addCommand(new MaintenanceCommand("maintenance", new String[]{"m"}, "Switches between Maintenance"));
commandManager.addCommand(new KickallCommand("kickall", new String[]{"ka"}, "Kicks all players from Comix"));
commandManager.addCommand(new ClearCommand("clear", new String[]{"c"}, "Clears the screen"));
commandManager.addCommand(new StopCommand("stop", new String[] {"end"}, "Stops Comix"));
commandManager.addCommand(new StatsCommand("stats", new String[]{}, "Stats about total traffic in and out from currently conencted clients"));
List<String> cmds = new ArrayList<>();
commandManager.getCommands().forEach(c -> cmds.add(c.getName()));
consoleReader.addCompleter(new StringsCompleter(cmds));
}
public void kickAll() {
clients.forEach(c -> c.getUpstreamHandler().getUpstreamChannel().close());
}
public void reload() {
loadIpBlacklist();
loadWhitelist();
loadStatusResponse();
}
public boolean maintainMode() {
if(!comixConfig.isMaintenance()) {
comixConfig.setMaintenance(true);
saveConfig();
loadStatusResponse();
return true;
}else {
comixConfig.setMaintenance(false);
saveConfig();
loadStatusResponse();
return false;
}
}
public void saveConfig() {
String status = new GsonBuilder().setPrettyPrinting().create().toJson(comixConfig, ComixConfig.class);
try {
BufferedWriter writer = new BufferedWriter(new FileWriter(new File("config.comix")));
writer.write(status);
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
start();
}
private void loadConfig() {
try {
this.comixConfig = ConfigLoader.loadConfig("config.comix", ComixConfig.class);
this.balancerHost = comixConfig.getHost();
this.balancerPort = comixConfig.getPort();
this.targets = comixConfig.getTargets();
logger.log(Level.INFO, "Config", "Config loaded...");
} catch (Exception e) {
logger.log(Level.SEVERE, "Unable to load Comix Config file!");
System.exit(1);
}
}
private void loadWhitelist() {
try {
new File("whitelist.comix").createNewFile();
whitelist = ConfigLoader.loadConfig("whitelist.comix", Whitelist.class);
if(!whitelist.isEnabled())
logger.log(Level.INFO, "Whitelist", "Whitelist loaded...");
else
logger.info("Whitelisted: " + String.join(", ", whitelist.getNames()));
} catch (Exception e) {
logger.log(Level.SEVERE, "Error while loading 'whitelist.comix'!");
}
}
public boolean isIpRangeBanned(InetSocketAddress ip) {
for (IPRange range : ipRangeBlacklist) {
if(!range.isAllowed(ip))
return true;
}
return false;
}
public boolean isIpBanned(String ip) {
return ipBlacklist.contains(ip);
}
public boolean isWhitelisted(String name) {
return whitelist.getNames().contains(name);
}
public boolean isWhitelistEnabled() {
return whitelist.isEnabled();
}
public String getWhitelistKickMessage() {
return whitelist.getMessage();
}
public void shutdown() {
running = false;
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
//TODO: Shutdown more "nicely"
System.exit(0);
}
private void loadIpBlacklist() {
try {
new File("ip-blacklist.comix").createNewFile();
ConfigLoader.loadIpBlacklist(ipBlacklist, ipRangeBlacklist);
logger.log(Level.INFO, "IP-Blacklist", "File loaded...");
if(ipBlacklist.size() > 0)
logger.log(Level.INFO, "IP-Blacklist", ipBlacklist.size() + " IPs loaded!");
if(ipRangeBlacklist.size() > 0)
logger.log(Level.INFO, "IP-Blacklist", ipRangeBlacklist.size() + " IP-Ranges loaded!");
} catch (Exception e) {
logger.log(Level.WARNING, "Error while loading ip-blacklist.comix: " + e.getMessage());
}
}
public static String encodeToString(BufferedImage image, String type) {
String imageString;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
ImageIO.write(image, type, bos);
byte[] imageBytes = bos.toByteArray();
BASE64Encoder encoder = new BASE64Encoder();
imageString = encoder.encode(imageBytes);
bos.close();
} catch (IOException e) {
logger.log(Level.WARNING, "Error while loading favicon: " + e.getMessage());
return "";
}
logger.log(Level.INFO, "Status", "Favicon loaded...");
return imageString;
}
public void loadStatusResponse() {
try {
String faviconString = encodeToString(ImageIO.read(new File("favicon.png")), "png");
statusResponse = ConfigLoader.loadConfig("status.comix", StatusResponse.class);
if(!comixConfig.isMaintenance())
statusResponseString = "{\"version\":{\"name\":\"" + statusResponse.getVersion().getName() + "\",\"protocol\":" + statusResponse.getVersion().getProtocol() + "},\"players\":{\"max\":" + statusResponse.getPlayers().getMax() + ",\"online\":" + statusResponse.getPlayers().getOnline() + ", \"sample\":[{\"name\":\"" + statusResponse.getPlayers().getSample() + "\",\"id\":\"00000000-0000-0000-0000-000000000000\"}]},\"description\":\"" + statusResponse.getDescription() + "\",\"favicon\":\"data:image/png;base64," + faviconString + "\",\"modinfo\":{\"type\":\"FML\",\"modList\":[]}}";
else {
statusResponseString = "{\"version\":{\"name\":\"" + comixConfig.getMaintenancePingMessage() + "\",\"protocol\":0},\"players\":{\"max\":" + statusResponse.getPlayers().getMax() + ",\"online\":" + statusResponse.getPlayers().getOnline() + "},\"description\":\"" + comixConfig.getMaintenanceDescription() + "\",\"favicon\":\"data:image/png;base64," + faviconString + "\",\"modinfo\":{\"type\":\"FML\",\"modList\":[]}}";
}
logger.log(Level.INFO, "Status", "Status Response loaded...");
} catch (Exception e) {
logger.log(Level.SEVERE, "Error while loading status.comix");
e.printStackTrace();
}
}
public synchronized void addClient(ComixClient comixClient) {
clients.add(comixClient);
}
public synchronized void removeClient(ComixClient comixClient) {
clients.remove(comixClient);
}
public int getClientsOnline() {
return clients.size();
}
public List<ComixClient> getClients() {
return clients;
}
public boolean isRunning() {
return running;
}
public ComixConfig getComixConfig() {
return comixConfig;
}
public String getStatusResponseString() {
return statusResponseString;
}
public BalancingStrategy getBalancingStrategy() {
return balancingStrategy;
}
public static ConsoleReader getConsoleReader() {
return consoleReader;
}
public CommandManager getCommandManager() {
return commandManager;
}
public static Logger getLogger() {
return logger;
}
public static Comix getInstance() {
return instance;
}
}