/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package gcb; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.SecureRandom; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Random; import java.util.Set; import org.apache.commons.configuration.ConversionException; /** * * @author wizardus */ public class WC3Interface { public static int BROADCAST_PORT = 6112; int broadcast_port; DatagramSocket socket; byte[] buf; Map<Integer, GarenaInterface> garenaConnections; int[] rebroadcastPorts; //gcb_tcp_host list; only set if broadcastfilter is true Set<Integer> tcpPorts; Set<InetAddress> tcpHosts; //games that we have detected for gcb_broadcastfilter_key //use this to generate unique entry keys for Garena so that people can't spoof regular LAN joining //that is, LAN entry key is hidden from Garena users HashMap<WC3GameIdentifier, Integer> entryKeys; HashMap<Integer, WC3GameIdentifier> games; //this random is used to generate entry keys for Garena Random random; //whether we're exiting nicely boolean exitingNicely = false; public WC3Interface(Map<Integer, GarenaInterface> garenaConnections) { this.garenaConnections = garenaConnections; buf = new byte[65536]; if(GCBConfig.configuration.getBoolean("gcb_broadcastfilter_key", true)) { random = new SecureRandom(); entryKeys = new HashMap<WC3GameIdentifier, Integer>(); games = new HashMap<Integer, WC3GameIdentifier>(); } else if(GCBConfig.configuration.getBoolean("gcb_broadcastfilter_cache", true)) { //for some reason, caching the game packets is enabled even though // entry key rewriting is disabled //this combination does not work, so we print a warning message Main.println(11, "Warning: gcb_broadcastfilter_cache has been disabled; gcb_broadcastfilter_key must be on for caching to work properly!"); } //config try { String[] array = GCBConfig.configuration.getStringArray("gcb_rebroadcast"); rebroadcastPorts = new int[array.length]; for(int i = 0; i < array.length; i++) { rebroadcastPorts[i] = Integer.parseInt(array[i]); } } catch(ConversionException ce) { Main.println(1, "[WC3Interface] Conversion exception while processing gcb_rebroadcast; ignoring rebroadcast"); rebroadcastPorts = new int[] {}; } catch(NumberFormatException nfe) { Main.println(1, "[WC3Interface] Number format exception while processing gcb_rebroadcast; ignoring rebroadcast"); rebroadcastPorts = new int[] {}; } Main.println(11, "[WC3Interface] Detected " + rebroadcastPorts.length + " rebroadcast ports"); tcpPorts = new HashSet<Integer>(); tcpHosts = new HashSet<InetAddress>(); if(GCBConfig.configuration.getBoolean("gcb_broadcastfilter", true) || GCBConfig.configuration.getBoolean("gcb_broadcastfilter_ip", false)) { try { String[] array = GCBConfig.configuration.getStringArray("gcb_tcp_host"); for(int i = 0; i < array.length; i++) { String[] parts = array[i].split(":"); int port = 6112; if(parts.length >= 2) { try { port = Integer.parseInt(parts[1]); } catch(NumberFormatException e) { Main.println(1, "[WC3Interface] Configuration warning: unable to parse " + parts[1] + " as port"); continue; } } else { Main.println(1, "[WC3Interface] Warning: missing port for gcb_tcp_host [" + array[i] + "]; assuming 6112"); } if(GCBConfig.configuration.getBoolean("gcb_broadcastfilter", true)) { tcpPorts.add(port); } if(GCBConfig.configuration.getBoolean("gcb_broadcastfilter_ip", false)) { try { tcpHosts.add(InetAddress.getByName(parts[0])); } catch(IOException ioe) { Main.println(1, "[WC3Interface] Failed to resolve gcb_tcp_host; ignoring IP filter"); } } } } catch(ConversionException ce) { Main.println(1, "[WC3Interface] Conversion exception while processing gcb_tcp_host; ignoring port/host filters"); } } } public boolean init() { try { broadcast_port = GCBConfig.configuration.getInt("gcb_broadcastport", BROADCAST_PORT); socket = new DatagramSocket(broadcast_port); return true; } catch(IOException ioe) { if(Main.DEBUG) { ioe.printStackTrace(); } Main.println(1, "[WC3Interface] Error: cannot bind to broadcast port"); return false; } } public void exitNicely() { exitingNicely = true; socket.close(); } //returns true if port is in tcpPorts array and tcpPorts array is not empty public boolean isValidPort(int port) { if(tcpPorts.isEmpty()) return true; for(int x : tcpPorts) { if(x == port) return true; } return false; } public void receivedUDP(GarenaInterface garena, ByteBuffer lbuf, InetAddress address, int port, int senderId) { Main.println(11, "[WC3Interface] Received UDP packet (Garena) from " + address); if(GarenaEncrypt.unsignedByte(lbuf.get()) == 247 //247 is W3GS header constant && GarenaEncrypt.unsignedByte(lbuf.get()) == 47 //if packet is W3GS_SEARCHGAME; 47 is packet id && GCBConfig.configuration.getBoolean("gcb_broadcastfilter_key", true) && GCBConfig.configuration.getBoolean("gcb_broadcastfilter_cache", true)) { Main.println(11, "[WC3Interface] Sending games to " + address); removeOldGames(); //ok, then I guess we should send all cached packets to the client synchronized(games) { Iterator<WC3GameIdentifier> it = games.values().iterator(); while(it.hasNext()) { WC3GameIdentifier game = it.next(); byte[] data = game.rawPacket; //Warcraft clients always listen on BROADCAST_PORT garena.sendUDPEncap(address, port, game.gameport, BROADCAST_PORT, data, 0, data.length); } } } } public void readBroadcast() { //we want to process a broadcast here, specifically looking for GAMEINFO // then we do various things with it depending on our configuration //this is called in a loop from GarenaThread //first make sure we weren't exiting nicely if(exitingNicely) { try { Thread.sleep(60000); } catch(InterruptedException ie) {} return; } try { //receive the packet DatagramPacket packet = new DatagramPacket(buf, buf.length); socket.receive(packet); byte[] data = packet.getData(); int offset = packet.getOffset(); int length = packet.getLength(); //LAN FIX: rename game so you can differentiate if(GCBConfig.configuration.getBoolean("gcb_lanfix", false)) { data[22] = 119; } Main.println(11, "[WC3Interface] Received UDP packet from " + packet.getAddress()); //this will be false if we want to filter the packet boolean filterSuccess = true; //with default configuration, we will wait until a user specifically requests a // game through SEARCHGAME packet until sending the GAMEINFO packet //however, with certain configuration this is not possible (either broadcast // filter is disabled completely, or caching is specifically disabled) //we also always broadcast immediately when a new game is hosted! boolean broadcastImmediately = !GCBConfig.configuration.getBoolean("gcb_broadcastfilter", true) || !GCBConfig.configuration.getBoolean("gcb_broadcastfilter_cache", true); //if gcb_broadcastfilter is disabled, filterSuccess will already be set to true //so if filter succeeds, ignore; only if it fails, set filtersuccess to false if(GCBConfig.configuration.getBoolean("gcb_broadcastfilter", true)) { //first check IP address if(tcpHosts.isEmpty() || tcpHosts.contains(packet.getAddress()) || packet.getAddress().isAnyLocalAddress()) { try { ByteBuffer buf = ByteBuffer.wrap(data, offset, length); buf.position(0); buf.order(ByteOrder.LITTLE_ENDIAN); ByteBuffer newPacket = ByteBuffer.allocate(1024); newPacket.order(ByteOrder.LITTLE_ENDIAN); //check header constant if(GarenaEncrypt.unsignedByte(buf.get()) == Constants.W3GS_HEADER_CONSTANT) { newPacket.put((byte) Constants.W3GS_HEADER_CONSTANT); //check packet type if(GarenaEncrypt.unsignedByte(buf.get()) == Constants.W3GS_GAMEINFO) { newPacket.put((byte) Constants.W3GS_GAMEINFO); System.arraycopy(buf.array(), buf.position(), newPacket.array(), newPacket.position(), 18); buf.position(buf.position() + 18); //skip to gamename newPacket.position(newPacket.position() + 18); String gamename = GarenaEncrypt.getTerminatedString(buf); //get/skip gamename String gamenameSkeleton = GCBConfig.configuration.getString("gcb_broadcastfilter_gamename"); if(gamenameSkeleton != null && !gamenameSkeleton.trim().isEmpty()) { gamenameSkeleton = gamenameSkeleton.replace("%g", gamename); if(gamenameSkeleton.length() >= 31) { gamenameSkeleton = gamenameSkeleton.substring(0, 30); } newPacket.put(gamenameSkeleton.getBytes()); } else { newPacket.put(gamename.getBytes()); } newPacket.put((byte) 0); //null terminator for gamename newPacket.put(buf.get()); //skip game password newPacket.put(GarenaEncrypt.getTerminatedArray(buf)); //skip statstring newPacket.put((byte) 0); //null terminator for stats string System.arraycopy(buf.array(), buf.position(), newPacket.array(), newPacket.position(), 12); buf.position(buf.position() + 12); //skip to slots available newPacket.position(newPacket.position() + 12); //read slots available for the REFRESHGAME packet int slotsAvailable = buf.getInt(); newPacket.putInt(slotsAvailable); newPacket.putInt(buf.getInt()); //skip to port //read port in _little_ endian int port = GarenaEncrypt.unsignedShort(buf.getShort()); newPacket.putShort((short) port); //copy any remaining bytes after the port int remainingBytes = buf.limit() - buf.position(); if(remainingBytes > 0) { System.arraycopy(buf.array(), buf.position(), newPacket.array(), newPacket.position(), remainingBytes); newPacket.position(newPacket.position() + remainingBytes); } //check port if(!isValidPort(port)) { Main.println(11, "[WC3Interface] Filter fail: invalid port " + port); filterSuccess = false; } else { //if we let Garena users know the LAN entry key, they can spoof joining through LAN directly (without gcb) //if they do this, then they can spoof owner names and other bad stuff, avoiding gcb filter //so, we broadcast a different entry key to Garena so that they can only connect through gcb //note that gcb_broadcastfilter_cache will not work if gcb_broadcastfilter_key is // disabled because we use the same classes to store information // this is the reason for the sanity check in constructor if(GCBConfig.configuration.getBoolean("gcb_broadcastfilter_key", true)) { if(!GCBConfig.configuration.getBoolean("gcb_tcp_buffer", true)) { Main.println(1, "[WC3Interface] Warning: gcb_tcp_buffer must be enabled if gcb_broadcastfilter_key is!"); } //return to entry key buf.position(12); int ghostHostCounter = buf.getInt(); int ghostEntryKey = buf.getInt(); //check if we have received this game already Integer garenaEntryKey = getGameExists(gamename, port, ghostEntryKey); WC3GameIdentifier game; if(garenaEntryKey == null) { //generate a new entry key and put into hashmap garenaEntryKey = random.nextInt(); game = new WC3GameIdentifier(gamename, port, ghostEntryKey, ghostHostCounter, garenaEntryKey); Main.println(4, "[WC3Interface] Detected new game with name " + gamename + "; generated entry key: " + garenaEntryKey + " (original: " + ghostEntryKey + ")"); synchronized(games) { games.put(garenaEntryKey, game); } synchronized(entryKeys) { entryKeys.put(game, garenaEntryKey); } //always broadcast game immediately if it was just hosted broadcastImmediately = true; } else { game = games.get(garenaEntryKey); } //replace packet's war3version with the configured one, if any if(GCBConfig.configuration.getInt("gcb_broadcastfilter_war3version", 0) != 0) { newPacket.putInt(8, GCBConfig.configuration.getInt("gcb_broadcastfilter_war3version", 0)); } //replace packet's entry key from GHost with our generated one newPacket.putInt(16, garenaEntryKey); //update the data packet, which gets broadcasted data = new byte[newPacket.position()]; System.arraycopy(newPacket.array(), 0, data, 0, newPacket.position()); offset = 0; length = data.length; //update the existing WC3GameIdentifier so it doesn't get deleted //we must do this after rewriting the packet (above) or else we will // cache the unrewritten packet! game.update(data, offset, length); removeOldGames(); //create and broadcast the W3GS_REFRESHGAME packet //this is currently disabled pending investigation into // whether or not it actually works and why it results in // so many packets being sent /*ByteBuffer refreshPacket = ByteBuffer.allocate(16); refreshPacket.order(ByteOrder.LITTLE_ENDIAN); refreshPacket.put((byte) Constants.W3GS_HEADER_CONSTANT); refreshPacket.put((byte) Constants.W3GS_REFRESHGAME); refreshPacket.putShort((short) 16); refreshPacket.putInt(game.hostCounter); refreshPacket.putInt(game.garenaEntryKey); refreshPacket.putInt(slotsAvailable); synchronized(garenaConnections) { Iterator<GarenaInterface> it = garenaConnections.values().iterator(); while(it.hasNext()) { //use BROADCAST_PORT instead of broadcast_port in case the latter is customized with rebroadcast it.next().broadcastUDPEncap(BROADCAST_PORT, BROADCAST_PORT, refreshPacket.array(), 0, 8); } }*/ } else { //we must broadcast immediately if we didn't cache the packet broadcastImmediately = true; } } } else { Main.println(11, "[WC3Interface] Filter fail: not W3GS_GAMEINFO or bad length"); filterSuccess = false; } } else { Main.println(11, "[WC3Interface] Filter fail: invalid header constant"); filterSuccess = false; } } catch(BufferUnderflowException bue) { if(Main.DEBUG) { bue.printStackTrace(); } Main.println(11, "[WC3Interface] Filter fail: invalid packet format"); filterSuccess = false; } } else { Main.println(11, "[WC3Interface] Filter fail: wrong IP address: " + packet.getAddress()); filterSuccess = false; } } if(filterSuccess) { //if broadcast filter is disabled, we have to forward the packet to client immediately //otherwise, we can cache packet and send to client when we receive SEARCHGAME // from them (we cached packet already in code above) //no matter what, though, we send the W3GS_REFRESHGAME packet (this is done earlier in the method) if(broadcastImmediately) { synchronized(garenaConnections) { Iterator<GarenaInterface> it = garenaConnections.values().iterator(); while(it.hasNext()) { //use BROADCAST_PORT instead of broadcast_port in case the latter is customized with rebroadcast it.next().broadcastUDPEncap(BROADCAST_PORT, BROADCAST_PORT, data, offset, length); } } } } else { //let user know why packet was filtered, in case they didn't want this functionality Main.println(11, "[WC3Interface] Warning: not broadcasting packet to Garena (filtered by gcb_broadcastfilter)"); } //always rebroadcast packets: other gcb instances may be using different TCP ports for(int port : rebroadcastPorts) { Main.println(11, "[WC3Interface] Retransmitting packet to port " + port); String broadcastTarget = GCBConfig.getString("udp_broadcasttarget"); if(broadcastTarget == null) { broadcastTarget = "both"; } if(broadcastTarget.equals("both") || broadcastTarget.equals("localhost")) { DatagramPacket retransmitPacket = new DatagramPacket(data, offset, length, InetAddress.getLocalHost(), port); socket.send(retransmitPacket); if(broadcastTarget.equals("both")) { retransmitPacket = new DatagramPacket(data, offset, length, InetAddress.getByName("255.255.255.255"), port); socket.send(retransmitPacket); } } else { DatagramPacket retransmitPacket = new DatagramPacket(data, offset, length, InetAddress.getByName(broadcastTarget), port); socket.send(retransmitPacket); } } } catch(IOException ioe) { if(!exitingNicely) { if(Main.DEBUG) { ioe.printStackTrace(); } Main.println(1, "[WC3Interface] Error: " + ioe.getLocalizedMessage()); } } } public WC3GameIdentifier getGameIdentifier(int key) { synchronized(games) { if(games.containsKey(key)) { return games.get(key); } else { return null; } } } private Integer getGameExists(String gamename, int gameport, int ghostEntryKey) { synchronized(entryKeys) { Iterator<WC3GameIdentifier> i = entryKeys.keySet().iterator(); while(i.hasNext()) { WC3GameIdentifier game = i.next(); if(game.check(gamename, gameport, ghostEntryKey)) { return entryKeys.get(game); } } } return null; } private void removeOldGames() { synchronized(entryKeys) { Object[] game_identifiers = entryKeys.keySet().toArray(); for(Object o : game_identifiers) { WC3GameIdentifier game = (WC3GameIdentifier) o; if(System.currentTimeMillis() - game.timeReceived > 1000 * 15) { entryKeys.remove(game); //broadcast a UDP packet (W3GS_DECREATEGAME) to destroy the game in the LAN gamelist ByteBuffer decreatePacket = ByteBuffer.allocate(8); decreatePacket.order(ByteOrder.LITTLE_ENDIAN); decreatePacket.put((byte) Constants.W3GS_HEADER_CONSTANT); decreatePacket.put((byte) Constants.W3GS_DECREATEGAME); decreatePacket.putShort((short) 8); decreatePacket.putInt(game.hostCounter); synchronized(garenaConnections) { Iterator<GarenaInterface> it = garenaConnections.values().iterator(); while(it.hasNext()) { //use BROADCAST_PORT instead of broadcast_port in case the latter is customized with rebroadcast it.next().broadcastUDPEncap(BROADCAST_PORT, BROADCAST_PORT, decreatePacket.array(), 0, 8); } } synchronized(games) { games.remove(game.garenaEntryKey); } Main.println(4, "[WC3Interface] Removed old game with name: " + game.gamename); } } } } }