/*
 * Copyright (C) 2009-2017 Alistair Neil <info@dazzleships.net>
 *
 * This program 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 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package client;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import lib.SimpleFile;

/**
 *
 * @author Alistair Neil <info@dazzleships.net>
 */
public class TorController extends TorProcess {

    // Event constants, which supplement the ones provide by TorProcess
    public static final int EVENT_CIRCUITS_BUILT = 15;
    public static final int EVENT_CIRCUITS_FAILED = 16;
    public static final int EVENT_CIRCUIT_BUILT = 17;
    public static final int EVENT_CIRCUIT_FAILED = 18;
    public static final int EVENT_LATENCY_DONE = 19;
    public static final int EVENT_TESTING_DONE = 20;
    public static final int EVENT_CIRCUIT_CHANGED = 21;
    public static final int EVENT_ABORTED = 22;
    private static final String LOCALHOST = "127.0.0.1";
    private static final String[] EVENTMESSAGES = new String[]{
        "EVENT_CIRCUITS_BUILT", "EVENT_CIRCUITS_FAILED", "EVENT_CIRCUIT_BUILT",
        "EVENT_CIRCUIT_FAILED", "EVENT_LATENCY_DONE", "EVENT_TESTING_DONE",
        "EVENT_CIRCUIT_CHANGED", "EVENT_ABORTED"
    };

    // Status constants
    public static final int STATUS_DEAD = 0;
    public static final int STATUS_BOOTING = 1;
    public static final int STATUS_IDLE = 2;
    public static final int STATUS_CIRCUIT_CREATION = 3;
    public static final int STATUS_LATENCY_CHECKING = 4;

    // Other constants
    public static final long LATENCY_FAIL = 9999;
    public static final int STREAM_IP = 5;
    public static final int NODE_GUARD = 0;
    public static final int NODE_MIDDLE = 1;
    public static final int NODE_EXIT = 2;
    private static final int DEFBUILDTIME = 20;

    private volatile Socket sockControl;
    private volatile BufferedReader brSocket;
    private volatile PrintWriter pwSocket;
    private String strLatencyURL;
    private final ConcurrentHashMap<String, TorCircuit> chmUseableCircuits;
    private ArrayList<String> alActiveStreams;
    private int intStatus = STATUS_DEAD;
    private volatile Socket sockProxy;
    private long lngLatency;
    private String strBestHops;
    private long lngBestLatency;
    private String entrynodes = "";
    private Thread tActive;
    private boolean haveEntryNode;

    /**
     * Constructor
     *
     * @param clientpath Path to Tor client
     * @param configfolder Location of configuration file torrc
     */
    public TorController(String clientpath, String configfolder) {
        super(clientpath, configfolder);
        this.chmUseableCircuits = new ConcurrentHashMap<>();
        this.alActiveStreams = new ArrayList<>();
    }

    /**
     * Tor process event
     *
     * @param event
     * @param data
     */
    @Override
    public final void torProcessEventFired(int event, String data) {

        Logger.getGlobal().logp(Level.FINE, TorProcess.class.getName(),
                "torProcessEventFired() on Port=" + getListenPort(), getEventMessage(event) + ", Data=" + data);

        switch (event) {
            case TOR_BRIDGE:
                haveEntryNode = true;
                break;
            case TOR_STOPPED:
                setStatus(STATUS_DEAD);
                break;
            case TOR_BOOTED:
            case TOR_RESTARTED:
                openControlSocket();
                // Authenticate our control
                authenticateTor(getSecret());
                takeOwnership();
                waitForBridgeNodes(20);
                setStatus(STATUS_IDLE);
                break;
        }
        controllerEventFired(event, data);
    }

    @Override
    public String getEventMessage(int event) {
        if (event < EVENT_CIRCUITS_BUILT) {
            return super.getEventMessage(event);
        } else {
            return EVENTMESSAGES[event - EVENT_CIRCUITS_BUILT];
        }
    }

    /**
     * Handle for controllerEventFired should be overridden by parent class
     *
     * @param event
     * @param data
     */
    public void controllerEventFired(int event, Object data) {
    }

    /**
     * Set status flag
     *
     * @param status
     */
    public synchronized void setStatus(int status) {
        intStatus = status;
    }

    /**
     * Get current status
     *
     * @return status integer constant, see defined status constants
     */
    public synchronized int getStatus() {
        return intStatus;
    }

    /**
     * Convenience test for idle status
     *
     * @return true if idle
     */
    public boolean isIdle() {
        return (getStatus() <= STATUS_IDLE);
    }

    /**
     * Stop the tor controller process completely
     *
     */
    public final void stop() {
        haveEntryNode = false;
        abortActions();
        Logger.getGlobal().logp(Level.INFO, TorController.class.getName(),
                "stop() on Port=" + getListenPort(), "Stop requested");
        sendCommand("QUIT");
        closeControlSocket();
        stopProcess();
        setStatus(STATUS_DEAD);
    }

    /**
     * Start tor controller process and issue the TOR_BOOTED event
     *
     */
    public final void start() {
        createDefaultConfig();
        start(TOR_BOOTED);
    }

    /**
     * Start tor controller process and issue the given event
     *
     * @param bootevent
     */
    public final void start(int bootevent) {
        if (getStatus() == STATUS_DEAD) {
            setInitialBootEvent(bootevent);
            setStatus(STATUS_BOOTING);
            startProcess();
            return;
        }
        if (getStatus() > STATUS_BOOTING) {
            setStatus(STATUS_IDLE);
            controllerEventFired(bootevent, null);
        }
    }

    /**
     * Abort all current actions
     */
    public final void abortActions() {
        if (getStatus() < STATUS_IDLE) {
            return;
        }
        setStatus(STATUS_IDLE);
        // Abort latency checking
        abortLatencyCheck();
        // Interrupt sleep thread if active
        Thread t = getActiveThread();
        if (t != null) {
            t.interrupt();
        }
    }

    /**
     * Abort latency checking
     */
    public final void abortLatencyCheck() {
        // Kill off latency checking proxy socket
        if (sockProxy != null) {
            try {
                sockProxy.close();
            } catch (IOException ex) {
            }
        }
        sockProxy = null;
    }

    /**
     * Save configuration file
     */
    public void saveConf() {
        SimpleFile sf = new SimpleFile(getConfigFilePath());
        sf.delete();
        sendCommand("SAVECONF");
    }

    /**
     * Load configuration file
     */
    public void loadConf() {
        SimpleFile sf = new SimpleFile(getConfigFilePath());
        sf.openBufferedRead();
        String text = sf.readEntireFile();
        sf.closeFile();
        sendCommand("+loadconf\r\n" + text + "\r\n.");
    }

    /**
     * Attempts to return the Country associated with an ip address
     *
     * @param ip
     * @return Country or null if not found
     */
    public String getCountryFromIP(String ip) {
        // Get country from ip address
        String cmd = "ip-to-country/" + ip;
        ArrayList<String> infoList = getInfo(cmd);
        try {
            if (infoList != null) {
                return infoList.get(0).toUpperCase();
            }
        } catch (Exception ex) {
        }
        return null;
    }

    /**
     * Set the url used for latency checking
     *
     * @param url
     */
    public void setTestingURL(String url) {
        strLatencyURL = url;
    }

    /**
     * Take ownership of the Tor client process so that it shuts down if the
     * process is destroyed, particular useful for linux desktops that dont
     * issue proper terminations.
     */
    private void takeOwnership() {
        sendCommand("TAKEOWNERSHIP");
        sendCommand("RESETCONF __OwningControllerProcess");
    }

    /**
     * Authenticate a password protected tor control socket
     *
     * @param password
     */
    private void authenticateTor(String password) {
        if (password == null) {
            sendCommand("AUTHENTICATE");
        } else {
            sendCommand("AUTHENTICATE " + "\"" + password + "\"");
        }
    }

    /**
     * Enable/Disable predictive circuit building
     *
     * @param enabled
     */
    public void enablePredictiveCircuits(boolean enabled) {
        if (enabled) {
            setConf("__DisablePredictedCircuits=0");
        } else {
            setConf("__DisablePredictedCircuits=1");
        }
    }

    /**
     * Get tor status information for a given property. See TOR control-spec
     * documentation for valid properties
     *
     * @param property
     * @return Arraylist containg the result of the command
     */
    public final ArrayList<String> getInfo(String property) {
        return sendCommand("GETINFO " + property);
    }

    /**
     * Close a circuit with ID
     *
     * @param id
     */
    public final void closeCircuit(String id) {
        sendCommand("CLOSECIRCUIT " + id);
    }

    /**
     * Set a tor configuration property
     *
     * @param property
     * @return Arraylist containg the result of the command
     */
    public final ArrayList<String> setConf(String property) {
        return sendCommand("SETCONF " + property);
    }

    /**
     * reset a tor configuration property
     *
     * @param property
     * @return Arraylist containing the result of the command
     */
    public final ArrayList<String> resetConf(String property) {
        return sendCommand("RESETCONF " + property);
    }

    /**
     * Send signal to tor
     *
     * @param cmd
     * @return Arraylist containing the result of the command
     */
    public final ArrayList<String> signal(String cmd) {
        return sendCommand("SIGNAL " + cmd);
    }

    /**
     * Get entry guards chosen by tor client
     *
     * @return entry guards as comma separated fingerprints
     */
    public String getEntryGuardsAsCSV() {
        ArrayList<String> al = getInfo("entry-guards");
        StringBuilder sbResult = new StringBuilder();
        if (!al.contains("250 OK")) {
            return sbResult.toString();
        }
        // Remove 250 OK entry
        al.remove("250 OK");
        String sep = "";
        for (String s : al) {
            if (s.contains("~")) {
                sbResult.append(sep);
                sbResult.append(s.substring(0, s.indexOf('~')));
                if (sep.isEmpty()) {
                    sep = ",";
                }
            }
        }
        return sbResult.toString();
    }

    /**
     * Triggers a socks latency check, EVENT_LATENCY_CHECK is fired on
     * completion
     *
     * @param timeout
     */
    public final void doLatencyCheck(final int timeout) {

        Thread t = new Thread(new java.lang.Runnable() {
            long latency;

            @Override
            public void run() {
                latency = getTorLatency(timeout);
                SwingUtilities.invokeLater(new java.lang.Runnable() {
                    @Override
                    public void run() {
                        lngLatency = latency;
                        controllerEventFired(EVENT_LATENCY_DONE, null);
                    }
                });
            }
        });
        if (getStatus() < STATUS_IDLE) {
            return;
        }
        alActiveStreams = getInfo("stream-status");
        alActiveStreams.remove("250 OK");
        t.start();
    }

    /**
     * Get current latency
     *
     * @return latency in ms as long
     */
    public final long getLatency() {
        return lngLatency;
    }

    /**
     * Returns measured latency for the active circuit without creating a
     * stream, this blocks so be careful
     *
     * @param timeout
     * @return latency
     */
    public long getTorLatency(int timeout) {
        long lngResult = LATENCY_FAIL;
        DataInputStream dis = null;
        try {
            sockProxy = createTorSocketToURL(strLatencyURL, true);
            if (sockProxy != null) {
                sockProxy.setSoTimeout(timeout);
                dis = new DataInputStream(sockProxy.getInputStream());
                long lngStart = System.currentTimeMillis();
                dis.skipBytes(1);
                lngResult = System.currentTimeMillis() - lngStart;
            }
        } catch (IOException ex) {
            Logger.getGlobal().logp(Level.INFO, TorController.class.getName(),
                    "getTorLatency Exception " + getListenPort(), ex.getMessage());
        }
        try {
            if (!sockProxy.isClosed()) {
                sockProxy.close();
            }
            sockProxy = null;
        } catch (Exception ex) {
        }
        try {
            if (dis != null) {
                dis.close();
            }
        } catch (Exception ex) {
        }
        return lngResult;
    }

    /**
     * Close open circuits except circuit specified by id
     *
     * @param id
     * @param filtered
     */
    public void closeCircuitsExcept(String id, boolean filtered) {
        Logger.getGlobal().logp(Level.FINEST, TorController.class.getName(),
                "closeCircuitsExcept() Port=" + getListenPort(), "");
        Set keys = getBuiltCircuits(filtered).keySet();
        Iterator i = keys.iterator();
        String hopid;
        while (i.hasNext()) {
            hopid = (String) i.next();
            if (!hopid.contentEquals(id)) {
                closeCircuit(hopid);
            }
        }
    }

    private String getCircuitIdFromStream(String stream) {
        String data[];
        Pattern pat = Pattern.compile(" ");
        data = pat.split(stream);
        return data[2];
    }

    /**
     * Get list of active streams
     *
     * @return active streams as a list
     */
    public final ArrayList<String> getActiveStreams() {
        return alActiveStreams;
    }

    /**
     * Set the csv list of exit node fingers to be used by tor, a single exit
     * node may also be specified, this does not block
     *
     * @param fingers
     * @param nocircs
     */
    public final void activateNodes(final String fingers, final int nocircs) {

        Thread t = new Thread(new java.lang.Runnable() {

            @Override
            public void run() {
                activateNodesBlocking(fingers, nocircs);
                SwingUtilities.invokeLater(new java.lang.Runnable() {
                    @Override
                    public void run() {
                        if (isIdle()) {
                            controllerEventFired(EVENT_ABORTED, null);
                            return;
                        }
                        setStatus(STATUS_IDLE);
                        if (chmUseableCircuits.isEmpty()) {
                            controllerEventFired(EVENT_CIRCUITS_FAILED, 0);
                        } else {
                            controllerEventFired(EVENT_CIRCUITS_BUILT, chmUseableCircuits.size());
                        }
                    }
                });

            }
        });
        if (getStatus() < STATUS_IDLE) {
            return;
        }
        abortActions();
        setStatus(STATUS_CIRCUIT_CREATION);
        t.start();
    }

    /**
     * Activate given circuit, does not block
     *
     * @param hops
     */
    public final void activateCircuit(final String hops) {

        Thread t = new Thread(new java.lang.Runnable() {

            @Override
            public void run() {
                activateCircuitBlocking(hops);
                SwingUtilities.invokeLater(new java.lang.Runnable() {
                    @Override
                    public void run() {
                        if (isIdle()) {
                            controllerEventFired(EVENT_ABORTED, null);
                            return;
                        }
                        setStatus(STATUS_IDLE);
                        if (chmUseableCircuits.isEmpty()) {
                            controllerEventFired(EVENT_CIRCUIT_FAILED, null);
                        } else {
                            controllerEventFired(EVENT_CIRCUIT_BUILT, null);
                        }
                    }
                });

            }
        });
        if (getStatus() < STATUS_IDLE) {
            return;
        }
        setStatus(STATUS_CIRCUIT_CREATION);
        t.start();
    }

    /**
     * Set entry nodes
     *
     * @param fingers
     */
    public void setEntryNodes(String fingers) {
        entrynodes = "";
        if (fingers == null || !getBridges().isEmpty()) {
            return;
        }
        entrynodes = fingers;
    }

    /**
     * Get configured entry nodes
     *
     * @return entry nodes in CSV format
     */
    public String getEntryNodes() {
        return entrynodes;
    }

    /**
     * Set the csv list of exit node fingers to be used by tor, a single exit
     * node may also be specified, this blocks
     *
     * @param fingers
     * @param nocircs
     */
    public void activateNodesBlocking(String fingers, int nocircs) {
        chmUseableCircuits.clear();
        enablePredictiveCircuits(true);
        setConf("EntryNodes=" + entrynodes);
        setConf("ExitNodes=" + fingers);
        if (fingers.isEmpty() || fingers.contains("{")) {
            fingers = null;
        }
        waitForCircuits(DEFBUILDTIME, nocircs, fingers);
        enablePredictiveCircuits(false);
    }

    /**
     * Activate given circuit, blocks
     *
     * @param hops
     */
    private void activateCircuitBlocking(String hops) {
        chmUseableCircuits.clear();
        sendCommand("EXTENDCIRCUIT 0 " + hops + " PURPOSE=GENERAL");
        waitForCircuit(DEFBUILDTIME, hops);
    }

    /**
     * Wait so many seconds for valid bridges to appear if bridges are set
     *
     * @param secs
     */
    private void waitForBridgeNodes(int secs) {

        long timeout = System.currentTimeMillis() + (secs * 1000);
        while (!haveValidEntryNode()) {
            try {
                Thread.sleep(250);
                if (System.currentTimeMillis() > timeout) {
                    break;
                }
            } catch (InterruptedException ex) {
                break;
            }
        }
    }

    public boolean haveValidEntryNode() {
        if (getBridges().isEmpty()) {
            return true;
        }
        return haveEntryNode;
    }

    private void waitForCircuits(long secs, int nocircs, String reqFinger) {

        long timeout = System.currentTimeMillis() + (secs * nocircs * 1000);
        setActiveThread(Thread.currentThread());
        while (chmUseableCircuits.size() < nocircs) {
            try {
                Thread.sleep(250);
            } catch (InterruptedException ex) {
                break;
            }
            if (System.currentTimeMillis() > timeout) {
                Logger.getGlobal().logp(Level.INFO, TorController.class
                        .getName(),
                        "waitForCircuits() on Port=" + getListenPort(), "Timed Out");
                break;
            }
            if (isIdle()) {
                Logger.getGlobal().logp(Level.INFO, TorController.class
                        .getName(),
                        "waitForCircuits() on Port=" + getListenPort(), "Aborting");
                chmUseableCircuits.clear();
                break;
            }
            TorCircuit tc = getLatestCircuit();
            if (tc != null) {
                // If we have a node request then ensure that the circuit contains the requested node fingerprint
                if (reqFinger != null && !reqFinger.contains(tc.getExit(TorCircuit.FINGER))) {
                    continue;
                }
                // If we have custom guards, then ensure circuit contains guard
                if (entrynodes.isEmpty()) {
                    chmUseableCircuits.put(tc.getID(), tc);
                } else if (entrynodes.contains(tc.getGuard(TorCircuit.FINGER))) {
                    chmUseableCircuits.put(tc.getID(), tc);
                }
            }

        }
    }

    private void waitForCircuit(long secs, String reqHops) {

        long timeout = System.currentTimeMillis() + (secs * 1000);
        setActiveThread(Thread.currentThread());
        while (chmUseableCircuits.size() < 1) {
            try {
                Thread.sleep(250);
            } catch (InterruptedException ex) {
                break;
            }
            if (System.currentTimeMillis() > timeout) {
                Logger.getGlobal().logp(Level.INFO, TorController.class
                        .getName(),
                        "waitForCircuit() on Port=" + getListenPort(), "Timed Out");
                break;
            }
            if (isIdle()) {
                Logger.getGlobal().logp(Level.INFO, TorController.class
                        .getName(),
                        "waitForCircuit() on Port=" + getListenPort(), "Aborting");
                chmUseableCircuits.clear();
                break;
            }
            TorCircuit tcActive = getLatestCircuit();
            if (tcActive != null) {
                String hops = tcActive.getHops();
                if (hops.contains(reqHops)) {
                    chmUseableCircuits.put(tcActive.getID(), tcActive);
                }
            }
        }
    }

    private synchronized void setActiveThread(Thread t) {
        tActive = t;
    }

    private synchronized Thread getActiveThread() {
        return tActive;
    }

    /**
     * Test specified node, non blocking
     *
     * @param finger
     */
    public void testNode(final String finger) {

        SwingWorker<Void, Integer> sw = new SwingWorker<Void, Integer>() {
            private long bestLatency = LATENCY_FAIL;
            private String bestHops = null;

            @Override
            protected Void doInBackground() {
                closeCircuitsExcept("", true);
                activateNodesBlocking(finger, 1);
                if (isIdle()) {
                    return null;
                }
                if (chmUseableCircuits.size() > 0) {
                    String id = chmUseableCircuits.keys().nextElement();
                    TorCircuit tc = chmUseableCircuits.get(id);
                    closeCircuitsExcept("", true);
                    activateCircuitBlocking(tc.getHops());
                    publish(EVENT_CIRCUIT_BUILT);
                    long latency = getTorLatency(5000);
                    if (latency < bestLatency) {
                        bestLatency = latency;
                        bestHops = tc.getHops();
                    }
                }
                return null;
            }

            @Override
            protected void process(List<Integer> chunks) {
                for (Integer i : chunks) {
                    controllerEventFired(i, chmUseableCircuits.size());
                }
            }

            @Override
            protected void done() {
                if (isIdle()) {
                    lngBestLatency = LATENCY_FAIL;
                    strBestHops = null;
                    controllerEventFired(EVENT_ABORTED, null);
                } else {
                    setStatus(STATUS_IDLE);
                    lngBestLatency = bestLatency;
                    strBestHops = bestHops;
                    controllerEventFired(EVENT_TESTING_DONE, chmUseableCircuits.size());
                }
            }

        };
        setStatus(STATUS_CIRCUIT_CREATION);
        strBestHops = null;
        lngBestLatency = LATENCY_FAIL;
        sw.execute();
    }

    /**
     * Get best latency
     *
     * @return best latency value in ms as long
     */
    public final long getBestLatency() {
        return lngBestLatency;
    }

    /**
     * Get best hops
     *
     * @return hops info as string
     */
    public final String getBestHops() {
        return strBestHops;
    }

    /**
     * Get built circuits
     *
     * @param filtered Filter out unwanted circuits
     * @return HashMap of built circuits keyed to their circuit id
     */
    public final HashMap<String, TorCircuit> getBuiltCircuits(boolean filtered) {
        HashMap<String, TorCircuit> hm = new HashMap<>();
        ArrayList<String> circuits = getInfo("circuit-status");
        circuits.remove("250 OK");
        for (String circuit : circuits) {
            if (circuit.contains("BUILT")) {
                if (filtered) {
                    if (circuit.contains("ONEHOP_TUNNEL")) {
                        continue;
                    }
                    if (circuit.contains("IS_INTERNAL")) {
                        continue;
                    }
                    if (!circuit.contains("PURPOSE=GENERAL")) {
                        continue;
                    }
                }
                TorCircuit tc = new TorCircuit(circuit);
                hm.put(tc.getID(), tc);
                // Debug purposes
                //               if (getListenPort() == 9054) {
                //                   System.out.println("Circuit=" + circuit);
                //              }
            }
        }
        return hm;
    }

    /**
     * Verify we have comms on the control socket
     *
     * @return true if its good
     */
    public final boolean verifyControlComms() {
        ArrayList<String> circuits = getInfo("circuit-status");
        return !circuits.isEmpty();
    }

    /**
     * Get the latest circuit
     *
     * @return circuit
     */
    public final TorCircuit getLatestCircuit() {

        HashMap<String, TorCircuit> hm = getBuiltCircuits(true);
        Iterator i = getBuiltCircuits(true).keySet().iterator();
        String strId;
        TorCircuit tcRecent = null;
        int hid = 0;
        int id;
        while (i.hasNext()) {
            strId = (String) i.next();
            id = Integer.parseInt(strId);
            if (id > hid) {
                hid = id;
                tcRecent = hm.get(strId);
            }
        }
        return tcRecent;
    }

    private void openControlSocket() {
        try {
            sockControl = new Socket(LOCALHOST, getControlPort());
            sockControl.setKeepAlive(true);
            sockControl.setSoTimeout(2000);
            pwSocket = new PrintWriter(sockControl.getOutputStream());
            brSocket = new BufferedReader(new InputStreamReader(sockControl.getInputStream()), 1024);
        } catch (IOException ex) {
            Logger.getGlobal().throwing(TorController.class
                    .getName(),
                    "openControlSocket() on Port=" + getListenPort(), ex);
        }
    }

    private void closeControlSocket() {
        try {
            if (sockControl != null) {
                sockControl.setKeepAlive(false);
                sockControl.close();
                sockControl = null;
                pwSocket.close();
                brSocket.close();
                pwSocket = null;
                brSocket = null;

            }
        } catch (IOException ex) {
            Logger.getGlobal().throwing(TorController.class
                    .getName(),
                    "closeControlSocket() on Port=" + getListenPort(), ex);
        }
    }

    /**
     * Send a command to tor client
     *
     * @param command
     * @return Arraylist containing the result of the command
     */
    public final synchronized ArrayList<String> sendCommand(String command) {

        ArrayList<String> result = new ArrayList<>();

        if (sockControl == null) {
            Logger.getGlobal().logp(Level.WARNING, TorController.class
                    .getName(),
                    "sendCommand() Port=" + getListenPort(), "Cmd=" + command
                    + ", Non-existent socket");
            return result;
        }

        if (sockControl.isClosed()) {
            Logger.getGlobal().logp(Level.WARNING, TorController.class
                    .getName(),
                    "sendCommand() Port=" + getListenPort(), "Cmd=" + command
                    + ", Socket is closed.");
            return result;
        }

        flushReadBuffer();

        // Write out the command       
        if (pwSocket.checkError()) {
            Logger.getGlobal().logp(Level.FINEST, TorController.class
                    .getName(),
                    "sendCommand() Port=" + getListenPort(), "Cmd=" + command
                    + ", Write Socket Error");
            return result;
        }

        pwSocket.write(command + "\r\n");
        pwSocket.flush();

        // Dont wait for command response if quitting
        if (command.contentEquals("QUIT")) {
            return result;
        }

        result = getCommandResponse();

        if (!result.isEmpty()) {
            Logger.getGlobal().logp(Level.FINEST, TorController.class
                    .getName(),
                    "sendCommand() Port=" + getListenPort(), "Cmd=" + command
                    + ", Response=" + result.toString());
        } else {
            Logger.getGlobal().logp(Level.WARNING, TorController.class
                    .getName(),
                    "sendCommand() Port=" + getListenPort(), "Cmd=" + command
                    + ", Response=Timed out");
        }
        return result;
    }

    private ArrayList<String> getCommandResponse() {

        boolean boolMultiReply = false;
        String response;
        ArrayList<String> result = new ArrayList<>();
        try {
            while (true) {
                response = brSocket.readLine();
                if (response == null) {
                    break;
                }
                // Check to see if we are in the middle of a multi line reply
                if (boolMultiReply) {
                    // A multiline extended reply is part terminated by a fullstop
                    if (response.startsWith(".")) {
                        boolMultiReply = false;
                    } else {
                        result.add(response);
                    }
                    continue;
                }
                // Check for the start of a multi line reply
                if (response.startsWith("250+")) {
                    boolMultiReply = true;
                    response = response.substring(response.indexOf('=') + 1).trim();
                    if (!response.isEmpty()) {
                        result.add(response);
                    }
                    continue;
                }
                // Check for single line reply
                if (response.startsWith("250-")) {
                    response = response.substring(response.indexOf('=') + 1).trim();
                    if (!response.isEmpty()) {
                        result.add(response);
                    }
                    continue;
                }
                result.add(response);
                if (response.startsWith("250 ") || response.startsWith("251 ")
                        || response.startsWith("4") || response.startsWith("5")) {
                    break;
                }
            }
        } catch (IOException ex) {
            result.clear();
        }
        return result;
    }

    private void flushReadBuffer() {
        try {
            while (brSocket.ready()) {
                brSocket.readLine();
            }
        } catch (IOException ex) {
        }
    }

    /**
     * Create a socks4a socket to this URL on this tor connection, if nowww is
     * true then remove www. from domain
     *
     * @param url
     * @param nowww
     * @return socket
     */
    public Socket createTorSocketToURL(String url, boolean nowww) {

        try {
            URI uri = new URI(url);
            String host = uri.getHost().toLowerCase();
            if (nowww) {
                host = host.replace("www.", "");
            }
            String protocol = uri.getScheme();
            int port = uri.getPort();
            if (port == -1) {
                switch (protocol) {
                    default:
                        port = 80;
                        break;
                    case "https":
                        port = 443;
                        break;
                }
            }
            return createSocks4aSocket(LOCALHOST, getListenPort(), host, port);

        } catch (URISyntaxException ex) {
            Logger.getLogger(TorController.class
                    .getName()).log(Level.SEVERE, null, ex);
        }
        return null;
    }

    /**
     * Create a Socks4a socket Taken from Wikipedia SOCKS4a is a simple
     * extension to SOCKS4 protocol that allows a client that cannot resolve the
     * destination host's domain name to specify it.
     *
     * The client should set the first three bytes of DSTIP to NULL and the last
     * byte to a non-zero value. (This corresponds to IP address 0.0.0.x, with x
     * nonzero, an inadmissible destination address and thus should never occur
     * if the client can resolve the domain name.) Following the NULL byte
     * terminating USERID, the client must send the destination domain name and
     * terminate it with another NULL byte. This is used for both "connect" and
     * "bind" requests.
     *
     * Client to SOCKS server: field 1: SOCKS version number, 1 byte, must be
     * 0x04 for this version field 2: command code, 1 byte: 0x01 = establish a
     * TCP/IP stream connection 0x02 = establish a TCP/IP port binding field 3:
     * network byte order port number, 2 bytes field 4: deliberate invalid IP
     * address, 4 bytes, first three must be 0x00 and the last one must not be
     * 0x00 field 5: the user ID string, variable length, terminated with a null
     * (0x00) field 6: the domain name of the host we want to contact, variable
     * length, terminated with a null (0x00)
     *
     *
     * Server to SOCKS client: field 1: null byte field 2: status, 1 byte: 0x5a
     * = request granted 0x5b = request rejected or failed 0x5c = request failed
     * because client is not running identd (or not reachable from the server)
     * 0x5d = request failed because client's identd could not confirm the user
     * ID string in the request field 3: network byte order port number, 2 bytes
     * field 4: network byte order IP address, 4 bytes
     *
     * A server using protocol SOCKS4A must check the DSTIP in the request
     * packet. If it represents address 0.0.0.x with nonzero x, the server must
     * read in the domain name that the client sends in the packet. The server
     * should resolve the domain name and make connection to the destination
     * host if it can.
     *
     * @param socksaddr Socks ip address
     * @param socksport Socks port
     * @param remotehost Remote host
     * @param remoteport Remote port
     * @return Socket
     */
    public Socket createSocks4aSocket(String socksaddr, int socksport, String remotehost, int remoteport) {
        try {
            Socket s = new Socket(socksaddr, socksport);
            DataOutputStream dos = new DataOutputStream(s.getOutputStream());
            dos.writeByte(0x04);            // Version 4 Socks
            dos.writeByte(0x01);            // Connect command code
            dos.writeShort(remoteport);     // Remote Port number
            dos.writeInt(0x01);             // IP address of 0.0.0.1 means use Socks 4a
            dos.writeByte(0x00);            // Null terminator
            dos.writeBytes(remotehost);     // Remote host IP address
            dos.writeByte(0x00);            // Null terminator
            return s;
        } catch (IOException ex) {
            Logger.getGlobal().logp(Level.FINE, this.getClass().getName(), "createSocks4aSocket", "", ex);
        }
        return null;
    }

}
