/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package gcb; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.PriorityQueue; import org.apache.commons.configuration.ConversionException; /** * * @author wizardus */ public class GarenaTCP extends Thread { boolean terminated; //termination flag int conn_id; //this virtual TCP connection identifier long last_time; //last time in milliseconds that a packet was sent long last_received; //last time in milliseconds that a packet was received from GHost++ //ports on local server that we can connect to // maps from port to hostname Map<Integer, String> local_ports; int remote_id; //remote user ID String remote_username; InetAddress remote_address; int remote_port; //not thread safe objects List<GarenaTCPPacket> packets; //to transmit to Garena PriorityQueue<GarenaTCPPacket> packetRetransmitQueue; //standard retransmission queue HashMap<Integer, GarenaTCPPacket> out_packets; //sequence number -> packet; to transmit to GHost++ ByteBuffer out_buffer; //if buffered output is use, only full packets will be sent and packets will be dissected to correct information String[] reservedNames; GarenaInterface garena; TCPWorker worker; Socket socket; DataOutputStream out; DataInputStream in; ByteBuffer buf; boolean localBuffered; //dynamic connection properties Integer seq; //our current sequence number Integer ack; //our current acknowledgement number boolean rttMade; //whether we have made a round trip time measurement double smoothedRTT; //smoothed round trip time double rttVariation; //round-trip time variation int retransmissionTimeout; //current retransmission timeout; at first set to standardDelay //static connection properties int maximumBufferedPackets; //max number of packets to buffer before stopping transmission int standardDelay; //delay until packets are retransmitted int soTimeout; //timeout before doing standard retransmission instead of reading double srttAlpha; //alpha value, see rfc2988 double srttBeta; //beta value, see rfc2988 double srttLower; //minimum round trip time double srttUpper; //maximum round trip time double srttClockGranularity; //clock granularity, default to 20 ms int srttK; //no idea, but RFC2988 says it is 4 int maxUDPSize; //limits the size of UDP packets int maxTCPSize; //limits the size of TCP packets public GarenaTCP(GarenaInterface garena, TCPWorker worker) { this.garena = garena; this.worker = worker; packets = new ArrayList<GarenaTCPPacket>(); packetRetransmitQueue = new PriorityQueue<GarenaTCPPacket>(); out_packets = new HashMap<Integer, GarenaTCPPacket>(); terminated = false; last_time = System.currentTimeMillis(); last_received = System.currentTimeMillis(); seq = 0; ack = 0; //configuration local_ports = new HashMap<Integer, String>(); try { String[] local_ports_str = GCBConfig.configuration.getStringArray("gcb_tcp_host"); for(int i = 0; i < local_ports_str.length; i++) { String[] parts = local_ports_str[i].split(":"); int port = 6112; if(parts.length >= 2) { try { port = Integer.parseInt(parts[1]); } catch(NumberFormatException e) { Main.println(2, "[GarenaTCP " + conn_id + "] Configuration warning: unable to parse " + parts[1] + " as port"); continue; } } local_ports.put(port, parts[0]); } } catch(ConversionException e) { Main.println(2, "[GarenaTCP " + conn_id + "] Configuration error: while parsing host as string array"); } try { reservedNames = GCBConfig.configuration.getStringArray("gcb_tcp_reservednames"); } catch(ConversionException e) { Main.println(2, "[GarenaTCP " + conn_id + "] Configuration error: while parsing gcb_tcp_reservednames as string array"); reservedNames = new String[] {}; } boolean useBufferedOutput = GCBConfig.configuration.getBoolean("gcb_tcp_buffer", true); localBuffered = GCBConfig.configuration.getBoolean("gcb_tcp_localbuffer", true); //connection properties maximumBufferedPackets = GCBConfig.configuration.getInt("gcb_tcp_maxbufferedpackets", 20); standardDelay = GCBConfig.configuration.getInt("gcb_tcp_standarddelay", 3000); soTimeout = GCBConfig.configuration.getInt("gcb_tcp_sotimeout", 1000); srttAlpha = GCBConfig.configuration.getDouble("gcb_tcp_srttalpha", 0.125); srttBeta = GCBConfig.configuration.getDouble("gcb_tcp_srttbeta", 0.25); srttLower = GCBConfig.configuration.getDouble("gcb_tcp_srttlower", 10); srttUpper = GCBConfig.configuration.getDouble("gcb_tcp_srttupper", 60000); srttClockGranularity = GCBConfig.configuration.getDouble("gcb_tcp_srttg", 20); srttK = GCBConfig.configuration.getInt("gcb_tcp_srttk", 4); maxUDPSize = GCBConfig.configuration.getInt("gcb_tcp_maxpacketsize", 512); maxTCPSize = GCBConfig.configuration.getInt("gcb_tcp_maxtcpsize", 2000); if(maxUDPSize == 0) { maxUDPSize = 512; } buf = ByteBuffer.allocate(maxTCPSize); if(useBufferedOutput) { out_buffer = ByteBuffer.allocate(maxTCPSize * 2); out_buffer.order(ByteOrder.LITTLE_ENDIAN); } rttMade = false; retransmissionTimeout = standardDelay; } public void setWorker(TCPWorker worker) { this.worker = worker; } public String getPortHost(int port) { return local_ports.get(port); //returns null on failure } public boolean init(InetAddress remote_address, int remote_port, int remote_id, int conn_id, int destination_port, MemberInfo member) { this.remote_address = remote_address; this.remote_port = remote_port; this.remote_id = remote_id; this.conn_id = conn_id; if(member != null) { this.remote_username = member.username; } else { this.remote_username = remote_id + ""; } Main.println(5, "[GarenaTCP " + conn_id + "] Starting new virtual TCP connection " + conn_id + " with user " + remote_username + " at " + remote_address + " to " + destination_port); //make sure their username is not reserved if(isReservedName(remote_username)) { Main.println(6, "[GarenaTCP " + conn_id + "] User " + remote_username + " at " + remote_address + " in connection " + conn_id + " tried to use a reserved name"); end(true); return false; } String hostname = getPortHost(destination_port); if(hostname == null) { //means this port is not allowed Main.println(6, "[GarenaTCP " + conn_id + "] User " + remote_username + " tried to connect on port " + destination_port + "; terminating"); end(true); return false; } else { //establish real TCP connection with GHost (hopefully) Main.println(7, "[GarenaTCP " + conn_id + "] Connecting to GAMEHOST at " + hostname + " on port " + destination_port + " for connection " + conn_id); try { InetAddress local_address = InetAddress.getByName(hostname); socket = new Socket(local_address, destination_port); out = new DataOutputStream(socket.getOutputStream()); in = new DataInputStream(socket.getInputStream()); } catch(IOException ioe) { end(true); if(Main.DEBUG) { ioe.printStackTrace(); } return false; } //lastly, send the GCBI packet with information about the remote user if(GCBConfig.configuration.getBoolean("gcb_enablegcbi", false)) { buf.clear(); buf.order(ByteOrder.LITTLE_ENDIAN); buf.put((byte) Constants.GCBI_HEADER_CONSTANT); buf.put((byte) Constants.GCBI_INIT); buf.putShort((short) 22); buf.order(ByteOrder.BIG_ENDIAN); buf.put(remote_address.getAddress()); buf.putInt(remote_id); buf.putInt(garena.room_id); if(member != null && member.country.length() >= 2) { buf.putInt(member.experience); buf.put(member.country.substring(0, 2).getBytes()); } else { buf.putInt(-1); buf.put("??".getBytes()); } writeOutData(buf.array(), 0, buf.position(), true); } start(); return true; } } public void initReverse(InetAddress remote_address, int remote_port, int remote_id, int conn_id, Socket socket) { this.remote_address = remote_address; this.remote_port = remote_port; this.remote_id = remote_id; this.conn_id = conn_id; this.socket = socket; try { out = new DataOutputStream(socket.getOutputStream()); in = new DataInputStream(socket.getInputStream()); } catch(IOException ioe) { end(true); if(Main.DEBUG) { ioe.printStackTrace(); } } Main.println(5, "[GarenaTCP " + conn_id + "] Starting new reverse virtual TCP " + conn_id + " with " + remote_address + " on port " + remote_port); start(); } //called on acknowledgement from remote Garena user public void connAck(int seq, int ack) { if(terminated) return; Main.println(12, "[GarenaTCP " + conn_id + "] debug@connack@" + System.currentTimeMillis() + ": received acknowledge for " + seq + ", remote ack=" + ack + " in connection " + conn_id); //acknowledge packets =seq or <ack synchronized(packets) { for(int i = 0; i < packets.size(); i++) { GarenaTCPPacket curr = packets.get(i); if(curr.seq < ack || curr.seq == seq) { acknowledgePacket(i); } } } //fast retransmission: resend packets from ack to seq-1 if(ack < seq) { synchronized(packets) { for(GarenaTCPPacket curr : packets) { if(!curr.fastRetransmitted && curr.seq >= ack && curr.seq <= seq - 1) { curr.send_time = System.currentTimeMillis(); curr.fastRetransmitted = true; curr.timesSent++; garena.sendTCPData(remote_address, remote_port, conn_id, lastTime(), curr.seq, this.ack, curr.data, curr.data.length, buf); Main.println(12, "[GarenaTCP " + conn_id + "] debug@connack@" + System.currentTimeMillis() + ": fast retransmitting seq=" + curr.seq + " in connection " + conn_id); if(worker != null) { worker.pool.incrementStatistics(GarenaTCPPool.STATISTIC_RETRANSMISSION_COUNT); worker.pool.incrementStatistics(GarenaTCPPool.STATISTIC_TRANSMIT_PACKETS); worker.pool.incrementStatistics(GarenaTCPPool.STATISTIC_TRANSMIT_BYTES, curr.data.length); } } } } } standardRetransmission(); } //called when data is received from remote Garena user public void data(int seq, int ack, byte[] data, int offset, int length) { if(terminated) return; if(length > maxTCPSize) { Main.println(6, "[GarenaTCP " + conn_id + "] Remote invalid packet length (len=" + length + "), terminating"); end(true); return; } Main.println(12, "[GarenaTCP " + conn_id + "] debug@data@" + System.currentTimeMillis() + ": received SEQ=" + seq + "; remote ACK=" + ack + "; len=" + length + " in connection " + conn_id); if(worker != null) { worker.pool.incrementStatistics(GarenaTCPPool.STATISTIC_RECEIVE_PACKETS); worker.pool.incrementStatistics(GarenaTCPPool.STATISTIC_RECEIVE_BYTES, length); } //acknowledge packets synchronized(packets) { for(int i = 0; i < packets.size(); i++) { GarenaTCPPacket curr = packets.get(i); if(curr.seq < ack) { acknowledgePacket(i); } } } standardRetransmission(); //pass data on to local server if(seq == this.ack) { synchronized(this) { this.ack++; } writeOutData(data, offset, length, false); //if buffered output, extract packets from out_buffer and process if(out_buffer != null) { processOutData(out_buffer); } //send any other packets that we have stored synchronized(out_packets) { while(out_packets.containsKey(this.ack)) { GarenaTCPPacket packet = out_packets.remove(this.ack); synchronized(this) { this.ack++; } Main.println(12, "[GarenaTCP " + conn_id + "] debug@data@" + System.currentTimeMillis() + ": sending stored packet to GHost++, SEQ=" + packet.seq + " in connection " + conn_id); writeOutData(packet.data, false); //if buffered output, extract packets from out_buffer and process if(out_buffer != null) { processOutData(out_buffer); } packet = out_packets.get(this.ack); } } } else if(seq > this.ack) { //store the packet, we'll send it later byte[] copy = new byte[length]; System.arraycopy(data, offset, copy, 0, length); GarenaTCPPacket packet = new GarenaTCPPacket(); packet.seq = seq; packet.data = copy; Main.println(12, "[GarenaTCP " + conn_id + "] debug@data@" + System.currentTimeMillis() + ": storing remote packet, SEQ=" + packet.seq + "; our ACK=" + this.ack + " in connection " + conn_id); synchronized(out_packets) { out_packets.put(seq, packet); } } else { //ignore packet if seq is less than our ack Main.println(12, "[GarenaTCP " + conn_id + "] debug@data@" + System.currentTimeMillis() + ": ignoring remote packet, SEQ=" + seq + "; our ACK=" + this.ack + " in connection " + conn_id); } //send conn ack Main.println(12, "[GarenaTCP " + conn_id + "] debug@data@" + System.currentTimeMillis() + ": acknowledging " + seq + "; our ACK=" + this.ack + " in connection " + conn_id); garena.sendTCPAck(remote_address, remote_port, conn_id, lastTime(), seq, this.ack, buf); } public void processOutData(ByteBuffer buf) { while(out_buffer.position() >= 4) { int header = GarenaEncrypt.unsignedByte(out_buffer.get(0)); //validate header if(header == Constants.W3GS_HEADER_CONSTANT) { int oLength = GarenaEncrypt.unsignedShort(out_buffer.getShort(2)); Main.println(12, "[GarenaTCP " + conn_id + "] debug@" + System.currentTimeMillis() + ": " + conn_id + " out buffered header=" + header + ", length=" + oLength); //validate length; minimum packet legnth is 4 if(oLength >= 4) { if(out_buffer.position() >= oLength) { //write the data if process returns true, else disconnect processOutDataSingle(out_buffer, oLength); //reset buffer: move everything so buffer starts at zero int remainingBytes = out_buffer.position() - oLength; System.arraycopy(out_buffer.array(), oLength, out_buffer.array(), 0, remainingBytes); out_buffer.position(remainingBytes); } else { //not enough bytes yet return; } } else { Main.println(6, "[GarenaTCP " + conn_id + "] Received invalid length in connection " + conn_id + ", disconnecting"); end(true); return; } } else { Main.println(6, "[GarenaTCP " + conn_id + "] Received invalid header " + header + " in connection " + conn_id + ", disconnecting"); end(true); return; } } } //processes one packet from the output buffer to local WC3 host public void processOutDataSingle(ByteBuffer buf, int length) { int old_position = buf.position(); if(GarenaEncrypt.unsignedByte(buf.get(1)) == Constants.W3GS_REQJOIN) { if(length > 20) { buf.position(4); int hostCounter = buf.getInt(); int entryKey = buf.getInt(); byte unknown = buf.get(); short listenPort = buf.getShort(); int peerKey = buf.getInt(); String name = GarenaEncrypt.getTerminatedString(buf); int remainderLength = length - buf.position(); if(!name.equalsIgnoreCase(remote_username)) { Main.println(6, "[GarenaTCP " + conn_id + "] User " + remote_username + " in connection " + conn_id + " attempted to use an invalid name: " + name); } //add part after username + part before username + name + null terminator byte[] remote_username_bytes = remote_username.getBytes(); int rewrittenLength = remainderLength + 19 + remote_username_bytes.length + 1; //rewrite data to use their actual Garena username ByteBuffer rewrittenData = ByteBuffer.allocate(rewrittenLength); rewrittenData.order(ByteOrder.LITTLE_ENDIAN); rewrittenData.put((byte) Constants.W3GS_HEADER_CONSTANT); rewrittenData.put((byte) Constants.W3GS_REQJOIN); rewrittenData.putShort((short) rewrittenLength); rewrittenData.putInt(hostCounter); //if we're using gcb_broadcastfilter_key, then we've been broadcasting a fake entry key to Garena //replace with the actual entry key here if(GCBConfig.configuration.getBoolean("gcb_broadcastfilter_key")) { WC3GameIdentifier identifier = garena.getWC3Interface().getGameIdentifier(entryKey); if(identifier != null) { rewrittenData.putInt(identifier.ghostEntryKey); Main.println(10, "[GarenaTCP] Rewrote entry key (" + entryKey + " -> " + identifier.ghostEntryKey + ")"); } else { rewrittenData.putInt(entryKey); } } else { rewrittenData.putInt(entryKey); } rewrittenData.put(unknown); rewrittenData.putShort(listenPort); rewrittenData.putInt(peerKey); rewrittenData.put(remote_username_bytes); rewrittenData.put((byte) 0); rewrittenData.put(buf.array(), buf.position(), remainderLength); //force so that it doesn't go straight back into the output buffer writeOutData(rewrittenData.array(), true); } else { Main.println(6, "[GarenaTCP " + conn_id + "] Invalid length in join request in connection " + conn_id); end(true); return; } } else { writeOutData(buf.array(), 0, length, true); } buf.position(old_position); } public void writeOutData(byte[] data, boolean force) { writeOutData(data, 0, data.length, force); } public void writeOutData(byte[] data, int offset, int length, boolean force) { if(out_buffer == null || force) { try { out.write(data, offset, length); } catch(IOException ioe) { if(Main.DEBUG) { ioe.printStackTrace(); } } } else { //write to our output buffer to later extract packets out_buffer.put(data, offset, length); } } public void standardRetransmission() { //standard retransmission: resend old packets synchronized(packets) { GarenaTCPPacket curr; while((curr = packetRetransmitQueue.peek()) != null) { if(curr.send_time < System.currentTimeMillis() - retransmissionTimeout) { curr.send_time = System.currentTimeMillis(); curr.timesSent++; //double the retransmission timeout retransmissionTimeout *= 2; smoothedRTT *= 2; //don't fast retransmit this packet since we standard retranmsitted it curr.fastRetransmitted = true; garena.sendTCPData(remote_address, remote_port, conn_id, lastTime(), curr.seq, this.ack, curr.data, curr.data.length, buf); Main.println(12, "[GarenaTCP " + conn_id + "] debug@" + System.currentTimeMillis() + ": standard retransmitting in connection " + conn_id); if(worker != null) { worker.pool.incrementStatistics(GarenaTCPPool.STATISTIC_TRANSMIT_PACKETS); worker.pool.incrementStatistics(GarenaTCPPool.STATISTIC_TRANSMIT_BYTES, curr.data.length); worker.pool.incrementStatistics(GarenaTCPPool.STATISTIC_RETRANSMISSION_COUNT); } //update the retransmission queue, since the time of this element changed packetRetransmitQueue.poll(); packetRetransmitQueue.add(curr); } else { //since our minimum doesn't need to be retransmitted, neither does any other element break; } } } } public void acknowledgePacket(int x) { //acknowledge a single packet synchronized(packets) { GarenaTCPPacket packet = packets.remove(x); packets.notifyAll(); packetRetransmitQueue.remove(packet); if(packet.timesSent == 1) { //update standard retransmission timeout double roundTripTime = System.currentTimeMillis() - packet.send_time; //impose limitations on round trip time if(roundTripTime < srttLower) { roundTripTime = srttLower; } else if(roundTripTime > srttUpper) { roundTripTime = srttUpper; } if(!rttMade) { smoothedRTT = roundTripTime; rttVariation = roundTripTime / 2; rttMade = true; } else { rttVariation = (1 - srttBeta) * rttVariation + srttBeta * Math.abs(smoothedRTT - roundTripTime); smoothedRTT = (1 - srttAlpha) * smoothedRTT + srttAlpha * roundTripTime; } retransmissionTimeout = (int) Math.ceil(smoothedRTT + Math.max(srttClockGranularity, srttK * rttVariation)); Main.println(12, "[GarenaTCP " + conn_id + "] debug@" + System.currentTimeMillis() + ": " + conn_id + " setting retransmission timeout to " + retransmissionTimeout + " (last rtt=" + roundTripTime + ", srtt = " + smoothedRTT + ", rttvar = " + rttVariation + ")"); } } } public void run() { byte[] rbuf = new byte[maxTCPSize]; ByteBuffer lbuf = ByteBuffer.allocate(maxTCPSize); while(!terminated) { try { int len; if(localBuffered) { //read packet header, which includes packet length in.readFully(rbuf, 0, 4); len = GarenaEncrypt.unsignedByte(rbuf[2]) + GarenaEncrypt.unsignedByte(rbuf[3]) * 256; if(len >= 4 && len <= maxTCPSize - 4) { in.readFully(rbuf, 4, len - 4); } else { Main.println(6, "[GarenaTCP " + conn_id + "] Read invalid packet length (len=" + len + "), terminating"); end(true); break; } } else { //read as many bytes as we can and relay them onwards to remote len = in.read(rbuf); //definitely _don't_ want to readfully here! if(len == -1) { Main.println(6, "[GarenaTCP " + conn_id + "] Local host for connection " + conn_id + " disconnected"); end(true); break; } } last_received = System.currentTimeMillis(); byte[] data = new byte[len]; System.arraycopy(rbuf, 0, data, 0, len); Main.println(12, "[GarenaTCP " + conn_id + "] debug@" + System.currentTimeMillis() + ": " + conn_id + " new packet from local: " + seq + " (len=" + len + ")"); handleLocalPacket(data, lbuf); } catch(IOException ioe) { end(true); if(Main.DEBUG) { ioe.printStackTrace(); } break; } } } private void handleLocalPacket(byte[] data, ByteBuffer lbuf) { for(int i = 0; i < data.length; i += maxUDPSize) { int currentLength = Math.min(maxUDPSize, data.length - i); byte[] currentData = new byte[currentLength]; System.arraycopy(data, i, currentData, 0, currentLength); //save packet in case it doesn't go through GarenaTCPPacket packet = new GarenaTCPPacket(); packet.seq = seq; packet.data = currentData; synchronized(packets) { while(maximumBufferedPackets != 0 && packets.size() > maximumBufferedPackets) { //let's wait a while before sending more Main.println(12, "[GarenaTCP " + conn_id + "] debug@" + System.currentTimeMillis() + ": " + conn_id + " waiting because of " + packets.size() + " packets"); if(terminated) { //oh, we terminated, don't loop here forever! return; } try { packets.wait(100); } catch(InterruptedException e) {} //continue to standard retransmit packets standardRetransmission(); } packets.add(packet); //also update the packet retransmit queue here packetRetransmitQueue.add(packet); } //don't use buf here so there isn't thread problems garena.sendTCPData(remote_address, remote_port, conn_id, lastTime(), seq, ack, currentData, currentLength, lbuf); if(worker != null) { worker.pool.incrementStatistics(GarenaTCPPool.STATISTIC_TRANSMIT_PACKETS); worker.pool.incrementStatistics(GarenaTCPPool.STATISTIC_TRANSMIT_BYTES, currentLength); } //increment sequence number synchronized(this) { seq++; } } } private synchronized long lastTime() { // //return current last_time and set last_time to current time // if(last_time == -1) { // last_time = System.nanoTime(); // return last_time; // } else { // long old_time = last_time; // last_time = System.nanoTime(); // return old_time; // } //this method is synchronized and only one that edits last_time last_time = System.currentTimeMillis(); return -1; //todo: implement timestamp correctly in GarenaInterface } public void end(boolean removeWorker) { Main.println(6, "[GarenaTCP " + conn_id + "] Terminating connection " + conn_id + " with " + remote_address + " (" + remote_username + ")"); terminated = true; //allocate a new buffer so we don't do anything thread-unsafe ByteBuffer tbuf = ByteBuffer.allocate(maxTCPSize); if(socket != null) { try { socket.close(); } catch(IOException ioe) { ioe.printStackTrace(); } } //send four times because that's what the standard client does garena.sendTCPFin(remote_address, remote_port, conn_id, last_time, tbuf); garena.sendTCPFin(remote_address, remote_port, conn_id, last_time, tbuf); garena.sendTCPFin(remote_address, remote_port, conn_id, last_time, tbuf); garena.sendTCPFin(remote_address, remote_port, conn_id, last_time, tbuf); if(removeWorker && worker != null) { //remove connection from GarenaInterface map worker.removeTCPConnection(conn_id); } } //check if name is in the list of reserved names public boolean isReservedName(String test) { for(String name : reservedNames) { if(name.equalsIgnoreCase(test)) { return true; } } return false; } public boolean isTimeout() { long maxTime = Math.max(System.currentTimeMillis() - last_time, System.currentTimeMillis() - last_received); if(maxTime > 300000) { return true; } else { return false; } } } class GarenaTCPPacket implements Comparable<GarenaTCPPacket> { int seq; //this packet's sequence number long send_time; //time that this packet was last sent (including both retransmission) byte[] data; boolean fastRetransmitted; //only fast retransmit packets once int timesSent; //how many times this packet was sent, including both retransmission public GarenaTCPPacket() { fastRetransmitted = false; timesSent = 1; send_time = System.currentTimeMillis(); } public int compareTo(GarenaTCPPacket packet) { long this_time = send_time + (fastRetransmitted ? 100000 : 0); long that_time = packet.send_time + (packet.fastRetransmitted ? 100000 : 0); return (int) (this_time - that_time); } }