/* * Copyright 2016 Nathan Howard * * This file is part of OpenGrave * * OpenGrave 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. * * OpenGrave 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 OpenGrave. If not, see <http://www.gnu.org/licenses/>. */ package com.opengrave.server; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.net.*; import java.util.ArrayList; import java.util.Timer; import java.util.UUID; import java.util.Vector; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.bitlet.weupnp.GatewayDevice; import org.bitlet.weupnp.GatewayDiscover; import org.bitlet.weupnp.PortMappingEntry; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.SAXException; import com.opengrave.common.Connector; import com.opengrave.common.DebugExceptionHandler; import com.opengrave.common.ModSession; import com.opengrave.common.Readyable; import com.opengrave.common.config.BinaryNodeException; import com.opengrave.common.config.BinaryParent; import com.opengrave.common.event.*; import com.opengrave.common.packet.Packet; import com.opengrave.common.packet.fromclient.ClientAuthPacket; import com.opengrave.common.packet.fromclient.ClientSaveFilePacket; import com.opengrave.common.packet.fromclient.NewOrderPacket; import com.opengrave.common.packet.fromclient.PlayerCharacterChosen; import com.opengrave.common.packet.fromserver.*; import com.opengrave.common.world.*; import com.opengrave.common.xml.HGXMLThread; import com.opengrave.common.xml.XML; import com.opengrave.server.events.*; import com.opengrave.server.exptoken.Token; import com.opengrave.server.runnables.RunnableThread; public class Server extends Readyable implements Runnable, EventListener { private int port = 4242; public boolean running = false; private Vector<DataConnector> clients = new Vector<DataConnector>(); private String uPnPHost; private int uPnPPort = -1; private static Server server = null; private ArrayList<Token> tokenTypes = new ArrayList<Token>(); private GatewayDevice d; private Thread shutdownHook; PortMappingEntry portMapping; int externalport; RunnableThread rt; Thread connectionThread; Timer timer; ModSession session; public static ModSession getSession() { if (server.session == null) { server.session = new ModSession(); } return server.session; } public static void setModSession(ModSession session) { server.session = session; } public static Server getServer() { return server; } public void dumpConnections() { System.out.println("------START-----"); synchronized (clients) { for (DataConnector dc : clients) { System.out.println(dc.getIP() + " : " + dc.name + " " + dc.identified); } } System.out.println("------END-----"); } /** * Init with a port. * * @param port */ public Server(int port) { server = this; EventDispatcher.addHandler(this); this.port = port; running = true; connectionThread = new Thread(this, "Server Connections thread"); connectionThread.start(); initGateway(); ServerHeartbeat shb = new ServerHeartbeat(this); timer = new Timer(); timer.schedule(shb, 0, 250); rt = new RunnableThread(this); } public void addRunnable(Runnable r) { rt.addNewRunnable(r); } // public void run() { EventDispatcher.dispatchEvent(new ServerStartingEvent()); EventDispatcher.dispatchEvent(new PrepareSessionEvent()); ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(port); } catch (IOException e) { new DebugExceptionHandler(e); } HGXMLThread.requestServerRegister(port); synchronized (readyLock) { ready = true; readyLock.notifyAll(); } while (running) { Socket clientSocket = null; // System.out.println("SERVER : awaiting connection"); try { clientSocket = serverSocket.accept(); dumpConnections(); } catch (IOException e) { if (!running) { System.out.println("Server Stopped."); return; } new DebugExceptionHandler(e, "Error accepting client connection"); } EventDispatcher.dispatchEvent(new ServerConnectionAttempt(clientSocket)); DataConnector conn = new DataConnector(clientSocket, "Multithreaded Server"); synchronized (clients) { clients.add(conn); } } try { serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } timer.cancel(); rt.interupt(); synchronized (clients) { for (DataConnector dc : clients) { dc.finalDestroy(); } } closeGateway(); } public void sendToAllObject(UUID uuid, Packet orop) { ArrayList<Connector> copyList; synchronized (clients) { copyList = new ArrayList<Connector>(clients); } CommonObject co = getSession().getObjectStorage().getObject(uuid); CommonAreaLoc cloc = CommonWorld.getAreaLocFor(co.getLocation()); for (Connector c : copyList) { DataConnector dc = (DataConnector) c; ArrayList<CommonAreaLoc> list; if (dc.lastSeenAreas == null || dc.lastSeenAreas.size() == 0) { // New users won't have a last tick yet. Make one up for now, but don't store it so they still get the first tick full of objects list = new ArrayList<CommonAreaLoc>(); for (PlayerCharacter pc : dc.pcList) { CommonLocation l = pc.getLocation(); if (l == null) { continue; } CommonAreaLoc center = CommonWorld.getAreaLocFor(l); for (int x = -1; x < 2; x++) { for (int y = -1; y < 2; y++) { CommonAreaLoc loc = center.getNeighbour(x, y); if (!list.contains(loc)) { list.add(loc); } } } } } else { list = dc.lastSeenAreas; } if (list.contains(cloc)) { c.send(orop); } } } public void sendAll(DataConnector dConn, Packet p) { sendAll(dConn, p, false); } public void sendAll(DataConnector dConn, Packet p, boolean allowLoopback) { ArrayList<Connector> copyList; synchronized (clients) { copyList = new ArrayList<Connector>(clients); } // All connected devices for (Connector c : copyList) { if (allowLoopback == false && c.loopback == false && c == dConn) { continue; } c.send(p); } } public String getExternalIP() { if (uPnPHost != null) { return uPnPHost; } return "unknown"; } public int getExternalPort() { return uPnPPort; } public void setExternalIp(String externalAddress, int externalport) { uPnPHost = externalAddress; uPnPPort = externalport; } public ArrayList<DataConnector> getConnectionsWithID(String id) { ArrayList<DataConnector> newList = new ArrayList<DataConnector>(); synchronized (clients) { for (DataConnector client : clients) { if (client.name.equals(id)) { newList.add(client); } } } return newList; } public boolean hasConnection(Connector connector) { synchronized (clients) { for (DataConnector client : clients) { if (client.equals(connector)) { return true; } } } return false; } public ArrayList<DataConnector> getConnectionsCopy() { synchronized (clients) { ArrayList<DataConnector> dCList = new ArrayList<DataConnector>(clients); return dCList; } } public void dropConnection(DataConnector c) { synchronized (clients) { clients.remove(c); c.finalDestroy(); } } public boolean addTokens(File f) { if (f.isFile()) { try (FileInputStream fis = new FileInputStream(f)) { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder builder; builder = dbf.newDocumentBuilder(); Document d = builder.parse(fis); Element list = XML.getChild(d, "tokenlist"); for (Element e : XML.getChildren(list, "token")) { String id = e.getAttribute("id"); String desc = e.getAttribute("desc"); String klass = e.getAttribute("class"); String valueS = e.getAttribute("value"); int value = 0; try { value = Integer.parseInt(valueS); } catch (NumberFormatException nfe) { } Token t = new Token(id, desc, value, klass); synchronized (tokenTypes) { tokenTypes.add(t); } } } catch (FileNotFoundException e) { new DebugExceptionHandler(e, f.getAbsolutePath()); } catch (IOException e) { new DebugExceptionHandler(e, f.getAbsolutePath()); } catch (ParserConfigurationException e) { new DebugExceptionHandler(e); } catch (SAXException e) { new DebugExceptionHandler(e); } return true; } return false; } public void initGateway() { d = null; portMapping = null; externalport = 4242; // First we mess about with uPnP System.out.println("Discovering uPnP Gateway for internet access to local server"); try { GatewayDiscover discover = new GatewayDiscover(); discover.discover(); d = discover.getValidGateway(); if (d != null) { InetAddress localAddress = d.getLocalAddress(); String externalAddress = d.getExternalIPAddress(); portMapping = new PortMappingEntry(); while (d.getSpecificPortMappingEntry(externalport, "TCP", portMapping)) { externalport++; } // External port, Internal port, Internal Host if (d.addPortMapping(externalport, 4242, localAddress.getHostAddress(), "TCP", "HiddenGrave")) { System.out.println("Bound Local " + localAddress.getHostAddress() + ":" + 4242 + " to External " + externalAddress + ":" + externalport); setExternalIp(externalAddress, externalport); EventDispatcher.dispatchEvent(new ServerGotExternalHostDetails(externalAddress, externalport)); HGXMLThread.requestClientCheckIn(); shutdownHook = new Thread(new ServerShutdownThread(d, externalport)); Runtime.getRuntime().addShutdownHook(shutdownHook); // Force check in with new server data } else { System.out.println("uPnP Gateway port mapping failed. Cannot host an internet game"); } } else { System.out.println("We have no valid uPnP Gateway. Cannot host an internet game"); } } catch (SocketException e) { new DebugExceptionHandler(e); } catch (UnknownHostException e) { new DebugExceptionHandler(e); } catch (IOException e) { new DebugExceptionHandler(e); } catch (SAXException e) { new DebugExceptionHandler(e); } catch (ParserConfigurationException e) { new DebugExceptionHandler(e); } } public void closeGateway() { if (d != null && portMapping != null) { // Remove port mapping for future use. try { d.deletePortMapping(externalport, "TCP"); if (shutdownHook != null) { Runtime.getRuntime().removeShutdownHook(shutdownHook); } } catch (IOException e) { e.printStackTrace(); } catch (SAXException e) { e.printStackTrace(); } } } @EventHandler(priority = EventHandlerPriority.LATE) public void onClientAuthFailed(ServerAuthFailedEvent event) { for (DataConnector c : Server.getServer().getConnectionsWithID(event.getID())) { if (c.identified == false) { c.setDestroy(); } } } @EventHandler(priority = EventHandlerPriority.LATE) public void onClientAuth(ServerAuthEvent event) { for (DataConnector c : Server.getServer().getConnectionsWithID(event.getId())) { if (c.identified == false && c.token.equals(event.getToken())) { c.name = event.getUserName(); // Throw event to ensure newly identified user is not banned by Mods EventDispatcher.dispatchEvent(new PlayerJoinedEvent(c.name, event.getId(), c)); } } } @EventHandler(priority = EventHandlerPriority.LATE) public void onPlayerJoined(PlayerJoinedEvent event) { if (event.getBanReason() == null) { // This is it, they're joining the server officially now. Better // throw some data at them first and then tell them they logged in // fine event.getConnection().identified = true; event.getConnection().setName("Server to " + event.getConnection().name); // Send all loaded worlds for (CommonWorld world : getSession().getWorlds()) { Packet p = new LoadWorldPacket(world.getName()); event.getConnection().send(p); } // Send all ItemTypes // TODO Possibly just serialise the whole copy of Server Session? // Would make it seriously easy to just dump the whole modded session on the user and // not have to worry about anything but updating whatever changes Packet p = new PlayerJoinedPacket(event.getPlayerName(), event.getPlayerId()); Server.getServer().sendAll(event.getConnection(), p); } else { // TODO Send ban reason event.getConnection().setDestroy(); } } @EventHandler(priority = EventHandlerPriority.LATE) public void onClientSaveFilePacket(ClientSaveFilePacket packet) { DataConnector dc = (DataConnector) packet.getFrom(); if (dc.identified) { // TODO Read through save-file data, list back players characters that are able to be played with the mod-set of this server dc.pcList.add(new PlayerCharacter(dc.name + ":1", new BinaryParent())); PlayerAddCharacterOption paco = new PlayerAddCharacterOption(); paco.characterData = new BinaryParent(); try { paco.characterData.setString("name", "Mr ploppy pants the first"); paco.characterData.setString("class", "poshtwat"); paco.characterData.setLocation("location", new CommonLocation()); paco.characterData.setMaterialList("mat", new MaterialList()); paco.characterData.setString("model", "mod/craig.dae:Cylinder001"); } catch (BinaryNodeException e) { new DebugExceptionHandler(e); } dc.send(paco); } } @EventHandler(priority = EventHandlerPriority.LATE) public void onClientChosenCharacter(PlayerCharacterChosen packet) { DataConnector dc = (DataConnector) packet.getFrom(); if (dc.identified) { if (packet.choice < dc.pcList.size()) { PlayerCharacterSpawn pcs = new PlayerCharacterSpawn(); PlayerCharacter obj = dc.pcList.get(packet.choice); // Since the obj currently isn't from a save file, let's drop sensible defaults in. obj.setType(CommonObject.Type.Anim); obj.getUUID(); // Pre-fill UUID CommonLocation loc = new CommonLocation(); loc.setScale(0.15f, 0.15f, 0.15f); obj.setLocation(loc); obj.setModelLabel("mod/craig.dae:Cylinder001"); getSession().getObjectStorage().addObject(obj); pcs.objectLinked = obj.getUUID(); pcs.playerSpot = 0; pcs.data = obj.getData(); obj.replaceOptions(); dc.send(pcs); } } } @EventHandler(priority = EventHandlerPriority.LATE) public void onClientServerConnect(ClientAuthPacket packet) { System.out.println("Connection packet : " + packet.userID + " " + packet.passKey); ErrorPacket ep = new ErrorPacket(); DataConnector conn = (DataConnector) packet.getFrom(); ep.error = "Identity data is wrong. Please download a new copy of your launcher"; if (packet.userID == null || packet.passKey == null) { conn.send(ep); return; } conn.identified = false; conn.name = packet.userID; conn.token = packet.passKey; // Send XML request to confirm user identity HGXMLThread.requestAuthClientFromServer(packet.userID, packet.passKey); } @EventHandler(priority = EventHandlerPriority.LATE) public void onConnectionLost(ConnectionLostEvent event) { if (Server.getServer().hasConnection(event.getConnector())) { DataConnector dc = (DataConnector) event.getConnector(); dc.identified = false; dc.setDestroy(); } } @EventHandler(priority = EventHandlerPriority.LATE) public void onNewOrderGiven(NewOrderPacket packet) { if (packet.getFrom() instanceof DataConnector) { DataConnector dc = (DataConnector) packet.getFrom(); dc.addOrder(packet.order); } } @EventHandler(priority = EventHandlerPriority.LATE) public void onPathSet(ServerObjectSetPathEvent event) { if (event.isConsumed()) { return; } if (event.getPathFinder() == null) { return; } event.setConsumed(); event.getObject().setPath(event.getPathFinder().getPath()); ObjectPathSetPacket packet = new ObjectPathSetPacket(event.getPathFinder().getPath(), event.getObject().getUUID()); getServer().sendAll(null, packet); } @EventHandler(priority = EventHandlerPriority.LATE) public void onOptionsReplace(OptionReplaceEvent event) { ObjectReplaceOptionsPacket orop = new ObjectReplaceOptionsPacket(); orop.uuid = event.getObjectId(); orop.mi = event.getMenu(); getServer().sendToAllObject(orop.uuid, orop); } public void stop() { running = false; connectionThread.interrupt(); } public void replaceOptionsAll(UUID id) { synchronized (clients) { for (DataConnector dC : clients) { dC.replaceOptions(id); } } } }