/* * Copyright (c) 2010 SimpleServer authors (see CONTRIBUTORS) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package simpleserver; import java.io.File; import static simpleserver.lang.Translations.t; import static simpleserver.util.Util.*; import java.io.IOException; import java.net.BindException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.UnknownHostException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Semaphore; import simpleserver.bot.BotController; import simpleserver.command.ExternalCommand; import simpleserver.command.PlayerCommand; import simpleserver.command.RollbackCommand; import simpleserver.config.GiveAliasList; import simpleserver.config.HelpText; import simpleserver.config.IPBanList; import simpleserver.config.KitList; import simpleserver.config.MOTD; import simpleserver.config.MuteList; import simpleserver.config.ReadFiles; import simpleserver.config.RobotList; import simpleserver.config.Rules; import simpleserver.config.WhiteList; import simpleserver.config.data.GlobalData; import simpleserver.config.xml.CommandConfig; import simpleserver.config.xml.Config; import simpleserver.config.xml.GlobalConfig; import simpleserver.config.xml.Group; import simpleserver.events.EventHost; import simpleserver.export.CustAuthExport; import simpleserver.lang.Translations; import simpleserver.log.AdminLog; import simpleserver.log.ConnectionLog; import simpleserver.log.ErrorLog; import simpleserver.log.MessageLog; import simpleserver.log.EventsLog; import simpleserver.message.Chat; import simpleserver.message.Messager; import simpleserver.minecraft.MinecraftWrapper; import simpleserver.nbt.WorldFile; import simpleserver.options.Options; import simpleserver.rcon.RconServer; import simpleserver.stream.Encryption.ClientEncryption; import simpleserver.telnet.TelnetServer; import simpleserver.thread.AutoBackup; import simpleserver.thread.AutoFreeSpaceChecker; import simpleserver.thread.AutoRestart; import simpleserver.thread.AutoRun; import simpleserver.thread.AutoSave; import simpleserver.thread.RequestTracker; import simpleserver.thread.SystemInputQueue; public class Server { private final Listener listener; private ServerSocket socket; private List<String> outputLog = new LinkedList<String>(); public static final LocalAddressFactory addressFactory = new LocalAddressFactory(); public Options options; public MOTD motd; public KitList kits; public Rules rules; public HelpText helptext; public IPBanList ipBans; public WhiteList whitelist; public MuteList mutelist; public GiveAliasList giveAliasList; public GlobalData data; private RobotList robots; public ReadFiles docs; public Config config; private GlobalConfig globalConfig; private SecureRandom random = new SecureRandom(); public PlayerList playerList; public Authenticator authenticator; private List<Resource> resources; private CommandParser commandParser; private Messager messager; private AdminLog adminLog; private ErrorLog errorLog; private ConnectionLog connectionLog; private MessageLog messageLog; private EventsLog eventsLog; private SystemInputQueue systemInput; public CustAuthExport custAuthExport; private MinecraftWrapper minecraft; private RconServer rconServer; private TelnetServer telnetServer; private AutoRun c10t; public AutoFreeSpaceChecker autoSpaceCheck; private AutoBackup autoBackup; private AutoSave autosave; private AutoRestart autoRestart; public RequestTracker requestTracker; public EventHost eventhost; public long mapSeed; private boolean run = true; private boolean waitStartup = false; // do not start until set private boolean restart = false; private boolean save = false; public Semaphore saveLock = new Semaphore(1); public Time time; public BotController bots; public WorldFile world; private boolean warnFirstStart = false; // set to true when critical files // were created to enable restart // warning public Server() { listener = new Listener(); listener.start(); listener.setName("SimpleServerListener"); } /** * Checks if this server runs bukkit. Determined by existence of "bukkit.yml". */ public boolean isBukkitServer() { return new File("bukkit.yml").exists(); } /** * Returns the main "world" directory, containing "level.dat", "players" etc. */ public File getWorldDirectory() { File file = new File(options.get("levelName")); if (isBukkitServer()) { // with bukkit: one level deeper (world-container) return new File(file, options.get("levelName")); } return file; } /** * Returns the whole directory containing all world files. */ public File getMapDirectory() { return new File(options.get("levelName")); } /** * Returns the minecraft world dat-file. */ public File getWorldFile() { return new File(getWorldDirectory(), "level.dat"); } /** * Returns the dat-file of player 'name'. */ public File getPlayerFile(String name) { return new File(new File(getWorldDirectory(), "players"), name + ".dat"); } /** * Initiate a restart. Shut down server but do not start again until * 'manualRestart()' is called. */ public void manualRestart() { waitStartup = true; restart(); } /** * Continue a restart initiated by 'manualRestart()'. Startup the waiting * server. */ public void continueRestart() { waitStartup = false; listener.interrupt(); } /** * Do an immediate restart. */ public void restart() { restart = true; stop(); } public void stop() { run = restart; try { socket.close(); } catch (Exception e) { } listener.interrupt(); } public void addRobot(Player p) { robots.addRobot(p.getIPAddress()); } public boolean isRobot(String ipAddress) { return robots.isRobot(ipAddress); } public void addRobotPort(int port) { robots.addRobotPort(port); } public void removeRobotPort(int port) { robots.removeRobotPort(port); } public List<Resource> getResources() { return resources; } public Integer[] getRobotPorts() { if (robots != null) { return robots.getRobotPorts(); } return null; } public String nextHash() { return Long.toHexString(random.nextLong() & 0x7fffffff); } public int numPlayers() { return playerList.size(); } public boolean isIPBanned(String ipAddress) { return ipBans.isBanned(ipAddress); } public void banKickIP(String ipAddress, String reason) { if (!isIPBanned(ipAddress)) { ipBans.addBan(ipAddress); } adminLog("IP Address " + ipAddress + " was banned:\t " + reason); for (Player player : playerList.getArray()) { if (player.getIPAddress().equals(ipAddress)) { player.kick(reason); adminLog("Player " + player.getName() + " was ip-banned:\t " + reason); } } } public void banKickIP(String ipAddress) { banKickIP(ipAddress, t("Banned!")); } public void banKick(String name, String msg) { if (name != null) { runCommand("ban", name); Player p = playerList.findPlayer(name); if (p != null) { adminLog("Player " + p.getName() + " was banned:\t " + msg); p.kick(msg); } } } public void banKick(String name) { banKick(name, t("Banned!")); } public void kick(String name, String reason) { Player player = playerList.findPlayer(name); if (player != null) { player.kick(reason); } } public boolean loadResources() { for (Resource resource : resources) { resource.load(); } if (playerList != null) { playerList.updatePlayerGroups(); } if (globalConfig.loadsuccess) { config = globalConfig.config; } else { println("Syntax error in config.xml! Config was not reloaded."); return false; } if (!Translations.getInstance().setLanguage(config.properties.get("serverLanguage"))) { options.set("serverLanguage", "en"); options.save(); } addressFactory.toggle(!config.properties.getBoolean("disableAddressFactory")); // reload events from config if (eventhost != null) { eventhost.loadEvents(); } saveResources(); return globalConfig.loadsuccess; } public void saveResources() { if (eventhost != null) { eventhost.saveGlobalVars(); } for (Resource resource : resources) { resource.save(); } } public void saveConfig() { globalConfig.save(); } public void setCriticalFileWarning() { warnFirstStart = true; } public String findName(String prefix) { Player i = playerList.findPlayer(prefix); if (i != null) { return i.getName(); } return null; } public Player findPlayer(String prefix) { return playerList.findPlayer(prefix); } public Player findPlayerExact(String exact) { return playerList.findPlayerExact(exact); } public void updateGroup(String name) { Player p = playerList.findPlayer(name); if (p != null) { p.updateGroup(); } } public void updateGroups() { playerList.updatePlayerGroups(); } public int localChat(Player player, String msg) { int localPlayers = 0; int radius = config.properties.getInt("localChatRadius"); for (Player friend : playerList.getArray()) { if (friend.distanceTo(player) < radius) { friend.addCaptionedMessage(t("%s says", player.getName()), msg); if (player != friend) { localPlayers++; } } } return localPlayers; } public void addOutputLine(String s) { synchronized (outputLog) { int size = outputLog.size(); for (int c = 0; c <= size - 30; ++c) { outputLog.remove(0); } outputLog.add(s); } } public String[] getOutputLog() { synchronized (outputLog) { return outputLog.toArray(new String[outputLog.size()]); } } public CommandParser getCommandParser() { return commandParser; } public PlayerCommand resolvePlayerCommand(String commandName, Group groupObject) { CommandConfig cmdconfig = config.commands.getTopConfig(commandName); String originalName = cmdconfig == null ? commandName : cmdconfig.originalName; PlayerCommand command; if (cmdconfig == null) { command = commandParser.getPlayerCommand(commandName); if (command != null && !command.hidden()) { command = null; } } else { command = commandParser.getPlayerCommand(originalName); } if (command == null) { if ((groupObject != null && groupObject.forwardUnknownCommands) || cmdconfig != null) { command = new ExternalCommand(commandName); } else { command = commandParser.getPlayerCommand((String) null); } } return command; } public PlayerCommand resolvePlayerCommand(String commandName) { return resolvePlayerCommand(commandName, null); } public void runCommand(String command, String arguments) { minecraft.execute(command, arguments); } public Messager getMessager() { return messager; } public void adminLog(String message) { adminLog.addMessage(message); } public void errorLog(Exception exception, String message) { errorLog.addMessage(exception, message); } public void eventsLog(String event, String message) { eventsLog.addMessage(event,message); } public void connectionLog(String type, Socket socket, String comments) { connectionLog.addMessage(type, socket, comments); } public void messageLog(Chat chat, String message) { messageLog.addMessage(chat, message); } public boolean isRestarting() { return restart; } public boolean isStopping() { return !run; } public boolean isSaving() { return save; } public void setSaving(boolean save) { this.save = save; } public void forceBackup() { autoBackup.forceBackup(); } public void forceBackup(String tag) { autoBackup.forceBackup(tag); } /** * Rollback to n-th last auto backup. */ public void rollback(RollbackCommand.ExecCom com, int n) throws Exception { autoBackup.rollback(com, n); } /** * Rollback to backup with tag 'tag'. */ public void rollback(RollbackCommand.ExecCom com, String tag) throws Exception { autoBackup.rollback(com, tag); } private void kickAllPlayers() { String message = t("Server shutting down!"); if (restart) { message = t("Server restarting!"); } for (Player player : playerList.getArray()) { player.kick(message); } } private void initialize() { resources = new LinkedList<Resource>(); resources.add(options = new Options()); resources.add(globalConfig = new GlobalConfig(options)); resources.add(robots = new RobotList()); resources.add(motd = new MOTD()); resources.add(rules = new Rules()); resources.add(helptext = new HelpText()); resources.add(kits = new KitList()); resources.add(ipBans = new IPBanList()); resources.add(whitelist = new WhiteList()); resources.add(mutelist = new MuteList()); resources.add(giveAliasList = new GiveAliasList()); resources.add(data = new GlobalData()); resources.add(docs = new ReadFiles()); time = new Time(this); bots = new BotController(this); systemInput = new SystemInputQueue(); adminLog = new AdminLog(); errorLog = new ErrorLog(); connectionLog = new ConnectionLog(); eventsLog = new EventsLog(); commandParser = new CommandParser(options); } private void cleanup() { systemInput.stop(); adminLog.stop(); errorLog.stop(); connectionLog.stop(); eventsLog.stop(); messageLog.stop(); time.unfreeze(); bots.cleanup(); } private void startup() { restart = false; loadResources(); if (!globalConfig.loadsuccess) { println("Syntax error in config.xml! Emergency shutdown..."); System.exit(1); } authenticator = new Authenticator(this); playerList = new PlayerList(this); requestTracker = new RequestTracker(this); messager = new Messager(this); if (options.getBoolean("enableCustAuthExport")) { resources.add(custAuthExport = new CustAuthExport(this)); custAuthExport.load(); } messageLog = new MessageLog(config.properties.get("logMessageFormat"), config.properties.getBoolean("logMessages")); minecraft = new MinecraftWrapper(this, options, systemInput); if (!minecraft.prepareServerJar()) { println("Please download minecraft_server.jar to the folder with SimpleServer.jar."); System.exit(1); } try { minecraft.start(); } catch (InterruptedException e) { // Severe error happened while starting up. // Already on track to stop/restart. } try { ClientEncryption.generateKeyPair(); } catch (NoSuchAlgorithmException e) { println("Error while generating RSA key pair"); e.printStackTrace(); System.exit(1); } if (options.getBoolean("enableTelnet")) { telnetServer = new TelnetServer(this); } if (options.getBoolean("enableRcon")) { rconServer = new RconServer(this); } try { world = new WorldFile(this); } catch (Exception ex) { setCriticalFileWarning(); } autoSpaceCheck = new AutoFreeSpaceChecker(this); autoBackup = new AutoBackup(this); autosave = new AutoSave(this); autoRestart = new AutoRestart(this); c10t = new AutoRun(this, options.get("c10tArgs")); if (data.freezeTime() >= 0) { time.freeze(data.freezeTime()); } if (options.getBoolean("enableEvents")) { eventhost = new EventHost(this); } bots.ready(); if (warnFirstStart) { println("Critical files were not found or were regenerated!\n" + "This may occur when starting the server for the first time or deleting files.\n" + "RESTARTING THE SERVER BEFORE USAGE IS RECOMMENDED!"); warnFirstStart = false; } } private void shutdown() { println("Stopping Server..."); save = false; bots.stop(); if (!saveLock.tryAcquire()) { println("Server is currently Backing Up/Saving..."); while (true) { try { saveLock.acquire(); break; } catch (InterruptedException e) { e.printStackTrace(); } } } kickAllPlayers(); authenticator.finalize(); if (telnetServer != null) { telnetServer.stop(); } if (rconServer != null) { rconServer.stop(); } autoSpaceCheck.cleanup(); autoBackup.stop(); autosave.stop(); autoRestart.stop(); requestTracker.stop(); c10t.stop(); saveResources(); playerList.waitUntilEmpty(); minecraft.stop(); println("Server stopped successfully!"); saveLock.release(); } private final class Listener extends Thread { @Override public void run() { initialize(); while (run) { while (waitStartup) { try { Thread.sleep(1000); } catch (InterruptedException ex) { } } startup(); String ip = options.get("ipAddress"); int port = options.getInt("port"); InetAddress address; if (ip.equals("0.0.0.0")) { address = null; } else { try { address = InetAddress.getByName(ip); } catch (UnknownHostException e) { println(e); println("Invalid listening address " + ip); break; } } try { socket = new ServerSocket(port, 0, address); } catch (IOException e) { println(e); println("Could not listen on port " + port + "!\nIs it already in use? Exiting application..."); break; } println("Wrapper listening on " + socket.getInetAddress().getHostAddress() + ":" + socket.getLocalPort() + " (connect here)"); if (socket.getInetAddress().getHostAddress().equals("0.0.0.0")) { println("Note: 0.0.0.0 means all" + " IP addresses; you want this."); } try { while (run) { Socket client; try { client = socket.accept(); } catch (IOException e) { if (run && !restart) { println(e); println("Accept failed on port " + port + "!"); } break; } new Player(client, Server.this); } } finally { try { socket.close(); } catch (IOException e) { } } shutdown(); } cleanup(); } } public void setTime(long time) { this.time.is(time); } public long time() { return time.get(); } public void setMapSeed(long seed) { if (mapSeed != seed) { mapSeed = seed; // System.out.println("[MAP SEED] " + mapSeed); } } public long getMapSeed() { return mapSeed; } public static final class LocalAddressFactory { private static final int[] octets = { 0, 0, 1 }; private static Boolean canCycle = null; private static boolean enabled = true; private void toggle(boolean enabled) { LocalAddressFactory.enabled = enabled; } public synchronized String getNextAddress() { if (!enabled || !canCycle()) { return "127.0.0.1"; } if (octets[2] >= 255) { if (octets[1] >= 255) { if (octets[0] >= 255) { octets[0] = 0; } else { ++octets[0]; } octets[1] = 0; } else { ++octets[1]; } octets[2] = 2; } else { ++octets[2]; } return "127." + octets[0] + "." + octets[1] + "." + octets[2]; } private boolean canCycle() { if (canCycle == null) { InetAddress testDestination; InetAddress testSource; try { testDestination = InetAddress.getByName(null); testSource = InetAddress.getByName("127.0.1.2"); } catch (UnknownHostException e) { canCycle = false; return false; } try { Socket testSocket = new Socket(testDestination, 80, testSource, 0); testSocket.close(); } catch (BindException e) { canCycle = false; return false; } catch (IOException e) { // Probably nothing listening on port 80 } canCycle = true; } return canCycle; } } }