public class ForwarderClientConfig {

    private InetSocketAddress remoteAddress;

    private int localPort;

    private String password;

    private int downMbps, upMbps;

    private final DragoniteSocketParameters dragoniteSocketParameters = new DragoniteSocketParameters();

    public ForwarderClientConfig(final InetSocketAddress remoteAddress, final int localPort, final int downMbps, final int upMbps) {
        setRemoteAddress(remoteAddress);
        setLocalPort(localPort);
        setDownMbps(downMbps);
        setUpMbps(upMbps);
    }

    public InetSocketAddress getRemoteAddress() {
        return remoteAddress;
    }

    public void setRemoteAddress(final InetSocketAddress remoteAddress) {
        checkArgument(remoteAddress != null, "Invalid remote address");
        this.remoteAddress = remoteAddress;
    }

    public int getLocalPort() {
        return localPort;
    }

    public void setLocalPort(final int localPort) {
        checkArgument(inPortRange(localPort), "Invalid port");
        this.localPort = localPort;
    }

    public int getDownMbps() {
        return downMbps;
    }

    public void setDownMbps(final int downMbps) {
        checkArgument(downMbps > 0 && downMbps <= 65535, "Invalid Mbps");
        this.downMbps = downMbps;
    }

    public int getUpMbps() {
        return upMbps;
    }

    public void setUpMbps(final int upMbps) {
        checkArgument(upMbps > 0 && upMbps <= 65535, "Invalid Mbps");
        this.upMbps = upMbps;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(final String password) throws EncryptionException {
        checkArgument(password != null && password.length() >= ForwarderGlobalConstants.PASSWORD_MIN_LENGTH, "Invalid password");
        dragoniteSocketParameters.setPacketCryptor(new AESCryptor(password));
        this.password = password;
    }

    public int getMTU() {
        return dragoniteSocketParameters.getPacketSize();
    }

    public void setMTU(final int mtu) {
        dragoniteSocketParameters.setPacketSize(mtu);
    }

    public int getWindowMultiplier() {
        return dragoniteSocketParameters.getWindowMultiplier();
    }

    public void setWindowMultiplier(final int mult) {
        dragoniteSocketParameters.setWindowMultiplier(mult);
    }

    public int getTrafficClass() {
        return dragoniteSocketParameters.getTrafficClass();
    }

    public void setTrafficClass(final int tc) {
        dragoniteSocketParameters.setTrafficClass(tc);
    }

    public boolean getWebPanelEnabled() {
        return dragoniteSocketParameters.isEnableWebPanel();
    }

    public void setWebPanelEnabled(final boolean enabled) {
        dragoniteSocketParameters.setEnableWebPanel(enabled);
    }

    public InetSocketAddress getWebPanelBind() {
        return dragoniteSocketParameters.getWebPanelBindAddress();
    }

    public void setWebPanelBind(final InetSocketAddress address) {
        dragoniteSocketParameters.setWebPanelBindAddress(address);
    }

    public DragoniteSocketParameters getDragoniteSocketParameters() {
        return dragoniteSocketParameters;
    }
} public class IncorrectHeaderException extends Exception {

    public IncorrectHeaderException(final String msg) {
        super(msg);
    }

} public class ServerRejectedException extends Exception {

    public ServerRejectedException(final String msg) {
        super(msg);
    }

} public class ClientInfoHeader {

    private static final byte VERSION = ForwarderGlobalConstants.PROTOCOL_VERSION;

    public static final int FIXED_LENGTH = 8;

    private int downMbps, upMbps;

    private String name, appVer, osName;

    public ClientInfoHeader(final int downMbps, final int upMbps, final String name, final String appVer, final String osName) {
        this.downMbps = downMbps;
        this.upMbps = upMbps;
        this.name = name;
        this.appVer = appVer;
        this.osName = osName;
    }

    public ClientInfoHeader(final byte[] header) throws IncorrectHeaderException {
        final BinaryReader reader = new BinaryReader(header);

        try {

            final byte remoteVersion = reader.getSignedByte();

            if (remoteVersion != VERSION) {
                throw new IncorrectHeaderException("Incorrect version (" + remoteVersion + ", should be " + VERSION + ")");
            }

            downMbps = reader.getUnsignedShort();
            upMbps = reader.getUnsignedShort();

            name = new String(reader.getBytesGroupWithByteLength(), ForwarderGlobalConstants.STRING_CHARSET);

            appVer = new String(reader.getBytesGroupWithByteLength(), ForwarderGlobalConstants.STRING_CHARSET);

            osName = new String(reader.getBytesGroupWithByteLength(), ForwarderGlobalConstants.STRING_CHARSET);

        } catch (final BufferUnderflowException e) {
            throw new IncorrectHeaderException("Incorrect header length");
        }
    }

    public int getDownMbps() {
        return downMbps;
    }

    public void setDownMbps(final int downMbps) {
        this.downMbps = downMbps;
    }

    public int getUpMbps() {
        return upMbps;
    }

    public void setUpMbps(final int upMbps) {
        this.upMbps = upMbps;
    }

    public String getName() {
        return name;
    }

    public void setName(final String name) {
        this.name = name;
    }

    public String getAppVer() {
        return appVer;
    }

    public void setAppVer(final String appVer) {
        this.appVer = appVer;
    }

    public String getOsName() {
        return osName;
    }

    public void setOsName(final String osName) {
        this.osName = osName;
    }

    public static byte getVersion() {
        return VERSION;
    }

    public static int getFixedLength() {
        return FIXED_LENGTH;
    }

    public byte[] toBytes() {
        final byte[] nameBytes = name.getBytes(ForwarderGlobalConstants.STRING_CHARSET);
        final byte[] appVerBytes = appVer.getBytes(ForwarderGlobalConstants.STRING_CHARSET);
        final byte[] osNameBytes = osName.getBytes(ForwarderGlobalConstants.STRING_CHARSET);

        final BinaryWriter writer = new BinaryWriter(getFixedLength() + nameBytes.length + appVerBytes.length + osNameBytes.length);

        writer.putSignedByte(VERSION)
                .putUnsignedShort(downMbps)
                .putUnsignedShort(upMbps)
                .putBytesGroupWithByteLength(nameBytes)
                .putBytesGroupWithByteLength(appVerBytes)
                .putBytesGroupWithByteLength(osNameBytes);

        return writer.toBytes();
    }
} public class ServerResponseHeader {

    private static final byte VERSION = ForwarderGlobalConstants.PROTOCOL_VERSION;

    public static final int FIXED_LENGTH = 4;

    private byte status;

    private String msg;

    public ServerResponseHeader(final byte status, final String msg) {
        this.status = status;
        this.msg = msg;
    }

    public ServerResponseHeader(final byte[] header) throws IncorrectHeaderException {
        final BinaryReader reader = new BinaryReader(header);

        try {

            final byte remoteVersion = reader.getSignedByte();

            if (remoteVersion != VERSION) {
                throw new IncorrectHeaderException("Incorrect version (" + remoteVersion + ", should be " + VERSION + ")");
            }

            status = reader.getSignedByte();

            msg = new String(reader.getBytesGroupWithShortLength(), ForwarderGlobalConstants.STRING_CHARSET);

        } catch (final BufferUnderflowException e) {
            throw new IncorrectHeaderException("Incorrect header length");
        }
    }

    public byte getStatus() {
        return status;
    }

    public void setStatus(final byte status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(final String msg) {
        this.msg = msg;
    }

    public static byte getVersion() {
        return VERSION;
    }

    public static int getFixedLength() {
        return FIXED_LENGTH;
    }

    public byte[] toBytes() {
        final byte[] msgBytes = msg.getBytes(ForwarderGlobalConstants.STRING_CHARSET);

        final BinaryWriter writer = new BinaryWriter(getFixedLength() + msgBytes.length);

        writer.putSignedByte(VERSION)
                .putSignedByte(status)
                .putBytesGroupWithShortLength(msgBytes);

        return writer.toBytes();
    }
} public final class ForwarderGlobalConstants {

    public static final String APP_VERSION = ForwarderBuildConfig.VERSION;

    public static final byte PROTOCOL_VERSION = 3;

    public static final int DEFAULT_SERVER_PORT = 5233;

    public static final long INIT_SEND_SPEED = 100 * 1024; //100kb/s

    public static final Charset STRING_CHARSET = StandardCharsets.UTF_8;

    public static final short PIPE_BUFFER_SIZE = 10240;

    public static final short MAX_FRAME_SIZE = 20480;

    public static final int PASSWORD_MIN_LENGTH = 4;

    public static final String UPDATE_API_URL = "https://github.com/dragonite-network/dragonite-java/raw/master/VERSIONS";

    public static final String UPDATE_API_PRODUCT_NAME = "forwarder";

} public class Pipe {

    private final short bufferSize;

    public Pipe(final short bufferSize) {
        this.bufferSize = bufferSize;
    }

    public void pipe(final InputStream inputStream, final MultiplexedConnection connection) throws IOException, SenderClosedException, InterruptedException {
        int len;
        final byte[] buf = new byte[bufferSize];
        while ((len = inputStream.read(buf)) > 0) {
            connection.send(Arrays.copyOf(buf, len));
        }
    }

    public void pipe(final MultiplexedConnection connection, final OutputStream outputStream) throws ConnectionNotAliveException, InterruptedException, IOException {
        byte[] buf;
        while ((buf = connection.read()) != null) {
            outputStream.write(buf);
        }
    }

} public class MultiplexedConnection {

    private final Multiplexer multiplexer;

    private final short connectionID;

    private final PacketSender packetSender;

    private final Queue<Frame> frameQueue = new LinkedList<>();

    private volatile boolean alive = true;

    private volatile boolean stopSend = false;

    private final Object closeLock = new Object();

    private final Object frameLock = new Object();

    private volatile int bufSize = 0;

    private volatile boolean remotePauseSend = false; //Told remote to stop send

    private volatile boolean localPauseSend = false; //Remote told to stop send

    private final Object sendLock = new Object();

    public MultiplexedConnection(final Multiplexer multiplexer, final short connectionID, final PacketSender packetSender) {
        this.multiplexer = multiplexer;
        this.connectionID = connectionID;
        this.packetSender = packetSender;
    }

    protected void addFrame(final Frame frame) {
        if (alive) {
            synchronized (frameLock) {
                frameQueue.add(frame);

                if (frame instanceof DataFrame) {
                    bufSize += ((DataFrame) frame).getData().length;
                    if (bufSize > MuxGlobalConstants.CONNECTION_MAX_DATA_BUFFER_SIZE && !remotePauseSend) {
                        remotePauseSend = true;
                        packetSender.sendPacket(new PauseConnectionFrame(connectionID).toBytes());
                    }
                }

                frameLock.notifyAll();
            }
        }
    }

    private byte[] readRaw() throws InterruptedException, ConnectionNotAliveException {
        if (alive) {
            Frame frame;
            synchronized (frameLock) {
                while ((frame = frameQueue.poll()) == null && alive) {
                    frameLock.wait();
                }
            }

            if (frame == null) {
                throw new ConnectionNotAliveException();
            }

            if (frame instanceof DataFrame) {
                final byte[] data = ((DataFrame) frame).getData();

                synchronized (frameLock) {
                    bufSize -= data.length;
                    if (bufSize < MuxGlobalConstants.CONNECTION_MAX_DATA_BUFFER_SIZE && remotePauseSend) {
                        remotePauseSend = false;
                        packetSender.sendPacket(new ContinueConnectionFrame(connectionID).toBytes());
                    }
                }

                return data;
            } else if (frame instanceof CloseConnectionFrame) {
                close();
                return null;
            } else {
                return null;
            }
        } else {
            throw new ConnectionNotAliveException();
        }
    }

    public byte[] read() throws ConnectionNotAliveException, InterruptedException {
        byte[] tmp = null;
        while (tmp == null) {
            tmp = readRaw();
        }
        return tmp;
    }

    private boolean canSend() {
        return alive && !stopSend;
    }

    public void send(final byte[] bytes) throws SenderClosedException, InterruptedException {
        if (canSend()) {
            synchronized (sendLock) {
                while (localPauseSend && canSend()) {
                    sendLock.wait();
                }
            }
            packetSender.sendPacket(new DataFrame(connectionID, bytes).toBytes());
        } else {
            throw new SenderClosedException();
        }
    }

    protected void pauseSend() {
        localPauseSend = true;
    }

    protected void continueSend() {
        localPauseSend = false;
        synchronized (sendLock) {
            sendLock.notifyAll();
        }
    }

    public boolean isAlive() {
        return alive;
    }

    public short getID() {
        return connectionID;
    }

    protected void closeSender() {
        stopSend = true;
    }

    public void close() {
        close_impl(true);
    }

    protected void closeNoRemove() {
        close_impl(false);
    }

    private void close_impl(final boolean removeFromMap) {
        synchronized (closeLock) {
            if (alive) {

                packetSender.sendPacket(new CloseConnectionFrame(connectionID).toBytes());

                alive = false;

                continueSend();

                if (removeFromMap) multiplexer.removeConnectionFromMap(connectionID);

                synchronized (frameLock) {
                    frameQueue.clear();
                    frameLock.notifyAll();
                }

            }
        }
    }
} public class Multiplexer {

    private final PacketSender packetSender;

    private final FrameParser frameParser;

    private final Map<Short, MultiplexedConnection> connectionMap = new HashMap<>();

    private final Object connLock = new Object();

    private final Queue<MultiplexedConnection> acceptQueue = new LinkedList<>();

    private final Object acceptLock = new Object();

    private volatile boolean alive = true;

    public Multiplexer(final PacketSender packetSender, final int maxFrameSize) {
        this.packetSender = packetSender;
        frameParser = new FrameParser(maxFrameSize);
    }

    public MultiplexedConnection createConnection(final short connID) throws ConnectionAlreadyExistsException, MultiplexerClosedException {
        if (alive) {
            synchronized (connLock) {
                if (!connectionMap.containsKey(connID)) {
                    final MultiplexedConnection connection = new MultiplexedConnection(this, connID, packetSender);
                    connectionMap.put(connID, connection);
                    packetSender.sendPacket(new CreateConnectionFrame(connID).toBytes());
                    return connection;
                } else {
                    throw new ConnectionAlreadyExistsException();
                }
            }
        } else {
            throw new MultiplexerClosedException();
        }
    }

    public MultiplexedConnection acceptConnection() throws InterruptedException, MultiplexerClosedException {
        if (alive) {
            synchronized (acceptLock) {
                MultiplexedConnection connection;
                while ((connection = acceptQueue.poll()) == null && alive) {
                    acceptLock.wait();
                }
                if (connection != null)
                    return connection;
                else
                    throw new MultiplexerClosedException();
            }
        } else {
            throw new MultiplexerClosedException();
        }
    }

    protected void removeConnectionFromMap(final short connID) {
        synchronized (connLock) {
            connectionMap.remove(connID);
        }
    }

    public boolean isAlive() {
        return alive;
    }

    public void close() {
        if (alive) {
            alive = false;
            synchronized (connLock) {
                for (final Iterator<Map.Entry<Short, MultiplexedConnection>> it = connectionMap.entrySet().iterator(); it.hasNext(); ) {
                    final Map.Entry<Short, MultiplexedConnection> entry = it.next();
                    entry.getValue().closeNoRemove();
                    it.remove();
                }
            }
            synchronized (acceptLock) {
                acceptQueue.clear();
                acceptLock.notifyAll();
            }
        }
    }

    public void onReceiveBytes(final byte[] rawBytes) {
        final Frame frame = frameParser.feed(rawBytes);
        if (frame != null) {
            processFrame(frame);
        }
    }

    private void processFrame(final Frame frame) {
        if (frame instanceof DataFrame) {

            final short connID = ((DataFrame) frame).getConnectionID();
            final MultiplexedConnection connection = connectionMap.get(connID);

            if (connection != null) connection.addFrame(frame);

        } else if (frame instanceof CloseConnectionFrame) {

            final short connID = ((CloseConnectionFrame) frame).getConnectionID();
            final MultiplexedConnection connection = connectionMap.get(connID);

            if (connection != null) {
                connection.closeSender();
                connection.addFrame(frame);
            }

        } else if (frame instanceof CreateConnectionFrame) {

            final short connID = ((CreateConnectionFrame) frame).getConnectionID();
            final MultiplexedConnection connection;
            synchronized (connLock) {
                connection = new MultiplexedConnection(this, connID, packetSender);
                connectionMap.put(connID, connection);
            }

            synchronized (acceptLock) {
                acceptQueue.add(connection);
                acceptLock.notifyAll();
            }
        } else if (frame instanceof PauseConnectionFrame) {
            final short connID = ((PauseConnectionFrame) frame).getConnectionID();
            final MultiplexedConnection connection = connectionMap.get(connID);

            if (connection != null) {
                connection.pauseSend();
            }
        } else if (frame instanceof ContinueConnectionFrame) {
            final short connID = ((ContinueConnectionFrame) frame).getConnectionID();
            final MultiplexedConnection connection = connectionMap.get(connID);

            if (connection != null) {
                connection.continueSend();
            }
        }
    }

} public class ConnectionAlreadyExistsException extends MuxException {

    public ConnectionAlreadyExistsException() {
        super("Connection already exists");
    }

    public ConnectionAlreadyExistsException(final String msg) {
        super(msg);
    }

} public class MultiplexerClosedException extends MuxException {

    public MultiplexerClosedException() {
        super("Multiplexer closed");
    }

    public MultiplexerClosedException(final String msg) {
        super(msg);
    }

} public class DragoniteSocketParameters {

    private int packetSize = 1300;

    private boolean autoSplit = true;

    private int maxPacketBufferSize = 0;

    private int windowMultiplier = 4;

    private int resendMinDelayMS = 50;

    private int heartbeatIntervalSec = 5;

    private int receiveTimeoutSec = 10;

    private boolean enableWebPanel = false;

    private InetSocketAddress webPanelBindAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(), DragoniteGlobalConstants.WEB_PANEL_PORT);

    private PacketCryptor packetCryptor = null;

    private int trafficClass = 0;

    public int getPacketSize() {
        return packetSize;
    }

    public void setPacketSize(final int packetSize) {
        checkArgument(packetSize >= 500, "Packet size is too small");
        this.packetSize = packetSize;
    }

    public boolean isAutoSplit() {
        return autoSplit;
    }

    public void setAutoSplit(final boolean autoSplit) {
        this.autoSplit = autoSplit;
    }

    public int getMaxPacketBufferSize() {
        return maxPacketBufferSize;
    }

    public void setMaxPacketBufferSize(final int maxPacketBufferSize) {
        checkArgument(maxPacketBufferSize >= 1, "Receive window must be greater than zero");
        this.maxPacketBufferSize = maxPacketBufferSize;
    }

    public int getWindowMultiplier() {
        return windowMultiplier;
    }

    public void setWindowMultiplier(final int windowMultiplier) {
        checkArgument(windowMultiplier >= 1 && windowMultiplier <= 10, "Multiplier must be greater than zero (and no more than 10)");
        this.windowMultiplier = windowMultiplier;
    }

    public int getResendMinDelayMS() {
        return resendMinDelayMS;
    }

    public void setResendMinDelayMS(final int resendMinDelayMS) {
        checkArgument(resendMinDelayMS >= 1, "Resend delay must be greater than zero");
        this.resendMinDelayMS = resendMinDelayMS;
    }

    public int getHeartbeatIntervalSec() {
        return heartbeatIntervalSec;
    }

    public void setHeartbeatIntervalSec(final int heartbeatIntervalSec) {
        checkArgument(heartbeatIntervalSec >= 1, "Heartbeat interval must be greater than zero");
        this.heartbeatIntervalSec = heartbeatIntervalSec;
    }

    public int getReceiveTimeoutSec() {
        return receiveTimeoutSec;
    }

    public void setReceiveTimeoutSec(final int receiveTimeoutSec) {
        checkArgument(receiveTimeoutSec >= 1, "Receive timeout must be greater than zero");
        this.receiveTimeoutSec = receiveTimeoutSec;
    }

    public boolean isEnableWebPanel() {
        return enableWebPanel;
    }

    public void setEnableWebPanel(final boolean enableWebPanel) {
        this.enableWebPanel = enableWebPanel;
    }

    public InetSocketAddress getWebPanelBindAddress() {
        return webPanelBindAddress;
    }

    public void setWebPanelBindAddress(final InetSocketAddress webPanelBindAddress) {
        checkArgument(webPanelBindAddress != null, "Bind address cannot be null");
        this.webPanelBindAddress = webPanelBindAddress;
    }

    public PacketCryptor getPacketCryptor() {
        return packetCryptor;
    }

    public void setPacketCryptor(final PacketCryptor packetCryptor) {
        this.packetCryptor = packetCryptor;
    }

    public int getTrafficClass() {
        return trafficClass;
    }

    public void setTrafficClass(final int trafficClass) {
        checkArgument(inTrafficClassRange(trafficClass), "TC must be in the range 0 <= tc <= 255");
        this.trafficClass = trafficClass;
    }
} public class ConnectionNotAliveException extends DragoniteException {

    public ConnectionNotAliveException() {
        super("Connection is not alive");
    }

    public ConnectionNotAliveException(final String msg) {
        super(msg);
    }

} public class DragoniteException extends Exception {

    public DragoniteException() {
        super();
    }

    public DragoniteException(final String msg) {
        super(msg);
    }

} public class IncorrectSizeException extends DragoniteException {

    public IncorrectSizeException(final String msg) {
        super(msg);
    }

} public class SenderClosedException extends DragoniteException {

    public SenderClosedException() {
        super("Sender closed");
    }

    public SenderClosedException(final String msg) {
        super(msg);
    }

} public class DragoniteClientSocket extends DragoniteSocket {

    //From parameters
    private final int packetSize, maxPacketBufferSize;
    private final int windowMultiplier;
    private final int resendMinDelayMS;
    private final int heartbeatIntervalSec, receiveTimeoutSec;
    private final boolean autoSplit;
    private final boolean enableWebPanel;
    private final PacketCryptor packetCryptor;
    private final int cryptorOverhead;
    //end

    private final Thread receiveThread; //THREAD

    private final Thread handleThread; //THREAD

    private final Thread aliveDetectThread; //THREAD

    private volatile boolean doReceive = true, doHandle = true, doAliveDetect = true;

    private final DatagramSocket datagramSocket;

    private final SocketAddress remoteAddress;

    private final BlockingQueue<DatagramPacket> packetBuffer;

    private final BucketPacketSender bucketPacketSender;

    private final ReceiveHandler receiver;

    private final ResendHandler resender; //THREAD

    private final SendHandler sender;

    private final ACKSender ackSender; //THREAD

    private final ConnectionState state = new ConnectionState();

    private volatile boolean alive = true;

    private volatile long lastReceiveTime, lastSendTime;

    private final Object closeLock = new Object();

    private volatile String description;

    private final DevConsoleWebServer devConsoleWebServer;

    private final InetSocketAddress devConsoleBindAddress;

    public DragoniteClientSocket(final SocketAddress remoteAddress, final long sendSpeed, final DragoniteSocketParameters parameters) throws SocketException {
        this.remoteAddress = remoteAddress;
        datagramSocket = new DatagramSocket();

        //set from parameters
        packetSize = parameters.getPacketSize();
        maxPacketBufferSize = parameters.getMaxPacketBufferSize();
        windowMultiplier = parameters.getWindowMultiplier();
        resendMinDelayMS = parameters.getResendMinDelayMS();
        heartbeatIntervalSec = parameters.getHeartbeatIntervalSec();
        receiveTimeoutSec = parameters.getReceiveTimeoutSec();
        autoSplit = parameters.isAutoSplit();
        enableWebPanel = parameters.isEnableWebPanel();
        devConsoleBindAddress = parameters.getWebPanelBindAddress();
        packetCryptor = parameters.getPacketCryptor();
        cryptorOverhead = packetCryptor != null ? packetCryptor.getMaxAdditionalBytesLength() : 0;

        datagramSocket.setTrafficClass(parameters.getTrafficClass());
        //end

        if (maxPacketBufferSize == 0) {
            packetBuffer = new LinkedBlockingQueue<>();
        } else {
            packetBuffer = new LinkedBlockingQueue<>(maxPacketBufferSize);
        }

        updateLastReceiveTime();

        bucketPacketSender = new BucketPacketSender(bytes -> sendPacket(bytes, remoteAddress), sendSpeed);

        ackSender = new ACKSender(this, bucketPacketSender, DragoniteGlobalConstants.ACK_INTERVAL_MS, packetSize);

        resender = new ResendHandler(this, bucketPacketSender, state, resendMinDelayMS, DragoniteGlobalConstants.ACK_INTERVAL_MS);

        receiver = new ReceiveHandler(this, ackSender, state, windowMultiplier,
                resender, packetSize);

        sender = new SendHandler(this, bucketPacketSender, receiver, state, resender, packetSize);

        description = "DCSocket";

        receiveThread = new Thread(() -> {
            try {
                while (doReceive) {
                    final byte[] b = new byte[packetSize + cryptorOverhead];
                    final DatagramPacket packet = new DatagramPacket(b, b.length);
                    try {
                        datagramSocket.receive(packet);
                        packetBuffer.put(packet);
                    } catch (final IOException ignored) {
                    }
                }
            } catch (final InterruptedException ignored) {
                //okay
            }
        }, "DC-Receive");
        receiveThread.start();

        handleThread = new Thread(() -> {
            try {
                while (doHandle) {
                    final DatagramPacket packet = packetBuffer.take();
                    handlePacket(packet);
                }
            } catch (final InterruptedException ignored) {
                //okay
            }
        }, "DC-PacketHandle");
        handleThread.start();

        aliveDetectThread = new Thread(() -> {
            try {
                while (doAliveDetect) {
                    final long current = System.currentTimeMillis();
                    if (alive) {
                        if (current - getLastReceiveTime() > receiveTimeoutSec * 1000) {
                            destroy();
                        } else if (current - getLastSendTime() > heartbeatIntervalSec * 1000) {
                            try {
                                //TODO Fix blocking
                                sendHeartbeat();
                            } catch (IOException | SenderClosedException ignored) {
                            }
                        }
                    } else {
                        doAliveDetect = false;
                    }
                    Thread.sleep(1000);
                }
            } catch (final InterruptedException ignored) {
                //okay
            }
        }, "DC-AliveDetect");
        aliveDetectThread.start();

        DevConsoleWebServer tmpServer = null;
        if (enableWebPanel) {
            try {
                tmpServer = new DevConsoleWebServer(devConsoleBindAddress, () -> {
                    final ArrayList<DragoniteSocketStatistics> list = new ArrayList<>(1);
                    list.add(getStatistics());
                    return list;
                });
            } catch (final IOException ignored) {
            }
        }
        devConsoleWebServer = tmpServer;

    }

    private void handlePacket(final DatagramPacket packet) {

        Message message = null;
        try {
            final byte[] packetData = Arrays.copyOf(packet.getData(), packet.getLength());
            final byte[] data = packetCryptor != null ? packetCryptor.decrypt(packetData) : packetData;
            if (data != null) message = MessageParser.parseMessage(data);
        } catch (final IncorrectMessageException ignored) {
        }

        if (message != null) {
            receiver.onHandleMessage(message, packet.getLength());
        }

    }

    private void sendHeartbeat() throws InterruptedException, IOException, SenderClosedException {
        sender.sendHeartbeatMessage();
    }

    //SEND ALL PACKETS THROUGH THIS!!
    private void sendPacket(final byte[] bytes, final SocketAddress socketAddress) throws IOException {
        final byte[] data = packetCryptor != null ? packetCryptor.encrypt(bytes) : bytes;
        if (data != null) {
            final DatagramPacket packet = new DatagramPacket(data, data.length);
            packet.setSocketAddress(socketAddress);
            datagramSocket.send(packet);
            updateLastSendTime();
        }
    }

    @Override
    public byte[] read() throws InterruptedException, ConnectionNotAliveException {
        return receiver.read();
    }

    @Override
    public void send(final byte[] bytes) throws InterruptedException, IncorrectSizeException, IOException, SenderClosedException {
        if (autoSplit) {
            sender.sendDataMessage_autoSplit(bytes);
        } else {
            sender.sendDataMessage_noSplit(bytes);
        }
    }

    @Override
    public DragoniteSocketStatistics getStatistics() {
        return new DragoniteSocketStatistics(remoteAddress, description,
                sender.getSendLength(), bucketPacketSender.getSendRawLength(),
                receiver.getReadLength(), receiver.getReceivedRawLength(),
                state.getEstimatedRTT(), state.getDevRTT(),
                resender.getTotalMessageCount(), resender.getResendCount(),
                receiver.getReceivedPktCount(), receiver.getDupPktCount());
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public void setDescription(final String description) {
        this.description = description;
    }

    @Override
    public boolean isAlive() {
        return alive;
    }

    @Override
    protected void closeSender() {
        sender.stopSend();
    }

    @Override
    public void closeGracefully() throws InterruptedException, IOException, SenderClosedException {
        synchronized (closeLock) {
            if (alive) {
                sender.sendCloseMessage((short) 0, true, true);
                destroy();
            }
        }
    }

    @Override
    public void destroy() {
        synchronized (closeLock) {
            if (alive) {
                alive = false;

                doReceive = false;
                doHandle = false;
                doAliveDetect = false;

                sender.stopSend();
                receiver.close();
                resender.close();
                ackSender.close();

                receiveThread.interrupt();
                handleThread.interrupt();
                aliveDetectThread.interrupt();

                packetBuffer.clear();

                datagramSocket.close();

                if (devConsoleWebServer != null) {
                    devConsoleWebServer.stop();
                }
            }
        }
    }

    @Override
    protected void updateLastReceiveTime() {
        lastReceiveTime = System.currentTimeMillis();
    }

    @Override
    public long getLastReceiveTime() {
        return lastReceiveTime;
    }

    @Override
    protected void updateLastSendTime() {
        lastSendTime = System.currentTimeMillis();
    }

    @Override
    public long getLastSendTime() {
        return lastSendTime;
    }

    @Override
    public SocketAddress getRemoteSocketAddress() {
        return remoteAddress;
    }

    @Override
    public void setSendSpeed(final long sendSpeed) {
        bucketPacketSender.setSpeed(sendSpeed);
    }

    @Override
    public long getSendSpeed() {
        return bucketPacketSender.getSpeed();
    }

    public int getPacketSize() {
        return packetSize;
    }

    public int getMaxPacketBufferSize() {
        return maxPacketBufferSize;
    }

    public int getWindowMultiplier() {
        return windowMultiplier;
    }

    public int getResendMinDelayMS() {
        return resendMinDelayMS;
    }

    public int getHeartbeatIntervalSec() {
        return heartbeatIntervalSec;
    }

    public int getReceiveTimeoutSec() {
        return receiveTimeoutSec;
    }

    public boolean isAutoSplit() {
        return autoSplit;
    }

    public boolean isEnableWebPanel() {
        return enableWebPanel;
    }
} public final class SystemInfo {

    public static String getUsername() {
        final String name = System.getProperty("user.name");
        return name != null ? name : "Unknown";
    }

    public static String getHostname() {
        try {
            return InetAddress.getLocalHost().getHostName();
        } catch (final UnknownHostException e) {
            return "Unknown";
        }
    }

    public static String getOS() {
        final String os = System.getProperty("os.name");
        return os != null ? os : "Unknown";
    }

    public static int getProcessorsCount() {
        return Runtime.getRuntime().availableProcessors();
    }

} public final class UnitConverter {

    public static long mbpsToSpeed(final int mbps) {
        return mbps * 125000L;
    }

    public static int speedToMbps(final long speed) {
        return (int) (speed / 125000);
    }

    public static int DSCPtoTrafficClass(final int dscp) {
        return dscp << 2;
    }

}
import com.vecsight.dragonite.forwarder.config.ForwarderClientConfig;
import com.vecsight.dragonite.forwarder.exception.IncorrectHeaderException;
import com.vecsight.dragonite.forwarder.exception.ServerRejectedException;
import com.vecsight.dragonite.forwarder.header.ClientInfoHeader;
import com.vecsight.dragonite.forwarder.header.ServerResponseHeader;
import com.vecsight.dragonite.forwarder.misc.ForwarderGlobalConstants;
import com.vecsight.dragonite.forwarder.network.Pipe;
import com.vecsight.dragonite.mux.conn.MultiplexedConnection;
import com.vecsight.dragonite.mux.conn.Multiplexer;
import com.vecsight.dragonite.mux.exception.ConnectionAlreadyExistsException;
import com.vecsight.dragonite.mux.exception.MultiplexerClosedException;
import com.vecsight.dragonite.sdk.config.DragoniteSocketParameters;
import com.vecsight.dragonite.sdk.exception.ConnectionNotAliveException;
import com.vecsight.dragonite.sdk.exception.DragoniteException;
import com.vecsight.dragonite.sdk.exception.IncorrectSizeException;
import com.vecsight.dragonite.sdk.exception.SenderClosedException;
import com.vecsight.dragonite.sdk.socket.DragoniteClientSocket;
import com.vecsight.dragonite.utils.system.SystemInfo;
import com.vecsight.dragonite.utils.type.UnitConverter;
import org.pmw.tinylog.Logger;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
/*
 * The Dragonite Project
 * -------------------------
 * See the LICENSE file in the root directory for license information.
 */


package com.vecsight.dragonite.forwarder.network.client;



public class ForwarderClient {

    private final InetSocketAddress remoteAddress;

    private final int localPort;

    private final int downMbps, upMbps;

    private final DragoniteSocketParameters dragoniteSocketParameters;

    private volatile boolean doAccept = true;

    private final ServerSocket serverSocket;

    private volatile DragoniteClientSocket dragoniteClientSocket;

    private volatile Multiplexer multiplexer;

    private final Thread acceptThread;

    private volatile Thread muxReceiveThread;

    private short nextConnID = 0; //single-threaded internal

    private final Object connectLock = new Object();

    public ForwarderClient(final ForwarderClientConfig config) throws IOException, InterruptedException, DragoniteException, IncorrectHeaderException, ServerRejectedException {
        this.remoteAddress = config.getRemoteAddress();
        this.localPort = config.getLocalPort();
        this.downMbps = config.getDownMbps();
        this.upMbps = config.getUpMbps();
        this.dragoniteSocketParameters = config.getDragoniteSocketParameters();

        serverSocket = new ServerSocket(localPort);

        prepareUnderlyingConnection(dragoniteSocketParameters);

        acceptThread = new Thread(() -> {
            Socket socket;
            try {
                while (doAccept && (socket = serverSocket.accept()) != null) {
                    handleConnection(socket);
                }
            } catch (final IOException e) {
                Logger.error(e, "Unable to accept TCP connections");
            }
        }, "FC-Accept");
        acceptThread.start();
    }

    private void prepareUnderlyingConnection(final DragoniteSocketParameters dragoniteSocketParameters) throws IOException, InterruptedException, DragoniteException, IncorrectHeaderException, ServerRejectedException {
        dragoniteClientSocket = new DragoniteClientSocket(remoteAddress, UnitConverter.mbpsToSpeed(upMbps), dragoniteSocketParameters);

        dragoniteClientSocket.setDescription("Forwarder");

        try {
            dragoniteClientSocket.send(new ClientInfoHeader(downMbps, upMbps, SystemInfo.getUsername(), ForwarderGlobalConstants.APP_VERSION, SystemInfo.getOS()).toBytes());

            final byte[] response = dragoniteClientSocket.read();
            final ServerResponseHeader responseHeader = new ServerResponseHeader(response);

            if (responseHeader.getStatus() != 0) {
                Logger.error("The server has rejected this connection (Error code {}): {}", responseHeader.getStatus(), responseHeader.getMsg());
                throw new ServerRejectedException(responseHeader.getMsg());
            } else if (responseHeader.getMsg().length() > 0) {
                Logger.info("Server welcome message: {}", responseHeader.getMsg());
            }

        } catch (InterruptedException | IOException | DragoniteException | IncorrectHeaderException | ServerRejectedException e) {
            Logger.error(e, "Unable to connect to remote server");
            try {
                dragoniteClientSocket.closeGracefully();
            } catch (InterruptedException | SenderClosedException | IOException ignored) {
            }
            throw e;
        }

        multiplexer = new Multiplexer(bytes -> {
            try {
                dragoniteClientSocket.send(bytes);
            } catch (InterruptedException | IncorrectSizeException | IOException | SenderClosedException e) {