/* Tor Research Framework - easy to use tor client library/framework Copyright (C) 2014 Dr Gareth Owen <drgowen@gmail.com> www.ghowen.me / github.com/drgowen/tor-research-framework 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 3 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, see <http://www.gnu.org/licenses/>. */ package tor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bouncycastle.jce.provider.BouncyCastleProvider; import sun.misc.IOUtils; import tor.util.TrustAllManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.net.InetAddress; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.Security; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.TreeMap; public class TorSocket { final static Logger log = LogManager.getLogger(); final static String certTypes[] = {"", "LINK", "IDENTITY", "AUTH"}; private static Consensus consensus; public int PROTOCOL_VERSION = 3; // auto negotiated later - this is minimum value supported. public Class defaultTorCircuitClass = TorCircuit.class; protected int PROTOCOL_VERSION_MAX = 4; // max protocol version supported SSLSocket sslsocket; OutputStream out; InputStream in; OnionRouter firstHop; // e.g. hop connected to // circuits for this socket TreeMap<Long, TorCircuit> circuits = new TreeMap<>(); ; HashMap<String, X509Certificate> remoteCerts = new HashMap<>(); private STATES state = STATES.INITIALISING; private Object stateNotify = new Object(); public TorSocket() { } /** * Main constructor. Connects and does connection setup. * * @param fh OnionRouter for first hop (used for Hostname/IP string and Port) */ public TorSocket(OnionRouter fh) throws IOException { firstHop = fh; if (firstHop == null) log.exit("Invalid first-hop"); connSetup(firstHop.ip, firstHop.orport); } public TorSocket(InetAddress ip, int port) throws IOException { firstHop = null; connSetup(ip, port); } public void connSetup(InetAddress ip, int port) throws IOException { if (consensus == null) consensus = Consensus.getConsensus(); Security.addProvider(new BouncyCastleProvider()); SSLContext sc; try { sc = SSLContext.getInstance("SSL"); sc.init(null, new TrustManager[]{new TrustAllManager()}, new java.security.SecureRandom()); } catch (KeyManagementException | NoSuchAlgorithmException e) { throw new RuntimeException(e); } // connect sslsocket = (SSLSocket) sc.getSocketFactory().createSocket(ip, port); out = sslsocket.getOutputStream(); in = sslsocket.getInputStream(); // versions cell log.trace("Sending VERSIONS"); sendCell(0, Cell.VERSIONS, new byte[]{00, 03, 00, 04}); Cell versionReply = recvCell(); ByteBuffer verBuf = ByteBuffer.wrap(versionReply.payload); for (int i = 0; i < versionReply.payload.length; i += 2) { int offeredVer = verBuf.getShort(); if (offeredVer <= PROTOCOL_VERSION_MAX && offeredVer > PROTOCOL_VERSION) PROTOCOL_VERSION = offeredVer; } log.info("Negotiated protocol vesrsion: " + PROTOCOL_VERSION); new Thread(new Runnable() { @Override public void run() { receiveHandlerLoop(); } }).start(); while (state != STATES.READY) { synchronized (stateNotify) { try { stateNotify.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } log.info("Tor connection established - socket ready"); } /** * Send a cell given payload * * @param circid Circuit ID * @param cmd Cell Command. See Cell.* * @param payload Cell Payload */ public void sendCell(long circid, int cmd, byte[] payload) throws IOException { sendCell(new Cell(circid, cmd, payload)); } public void sendCell(Cell c) throws IOException { log.trace("Sending {}", c); out.write(c.getBytes(PROTOCOL_VERSION)); } private byte[] blockingRead(int length) throws IOException { return IOUtils.readFully(in, length, true); } public Cell recvCell() throws IOException { byte hdr[] = blockingRead(PROTOCOL_VERSION == 3 ? 3 : 5); ByteBuffer buf = ByteBuffer.wrap(hdr); buf.order(ByteOrder.BIG_ENDIAN); long circid = 0; if (PROTOCOL_VERSION < 4) circid = buf.getShort() & 0xFFFF; else circid = buf.getInt() & 0xFFFFFFFF; int cmdId = buf.get() & 0xff; int pllength = 509; if (cmdId == 7 || cmdId >= 128) { pllength = ByteBuffer.wrap(blockingRead(2)).getShort(); } byte payload[] = blockingRead(pllength); log.trace("Cell received: circId {} cmdId {}", circid, cmdId); return new Cell(circid, cmdId, payload); } /** * Sends a NETINFO cell (used in connection init) */ public void sendNetInfo() throws IOException { byte nibuf[] = new byte[4 + 2 + 4 + 3 + 4]; byte[] remote = sslsocket.getInetAddress().getAddress(); byte[] local = sslsocket.getLocalAddress().getAddress(); int epoch = (int) (System.currentTimeMillis() / 1000L); ByteBuffer buf = ByteBuffer.wrap(nibuf); buf.putInt(epoch); buf.put(new byte[]{04, 04}); // remote's address buf.put(remote); buf.put(new byte[]{01, 04, 04}); // our address buf.put(local); sendCell(0, Cell.NETINFO, nibuf); } /** * Receive CERTS cell from remote host and put in remoteCerts hash-map. * Type 1 = LINK, 2 = ID, 3 = AUTH * * @param certsCell */ public void recvCerts(Cell certsCell) { ByteBuffer buf = ByteBuffer.wrap(certsCell.payload); int numCerts = buf.get() & 0xff; for (int i = 0; i < numCerts; i++) { int type=buf.get() & 0xff; int len = buf.getShort(); byte[] cert = new byte[len]; buf.get(cert); CertificateFactory cf = null; try { cf = CertificateFactory.getInstance("X.509"); X509Certificate xCert = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(cert)); //String ident = Hex.encodeHexString(TorCrypto.getSHA1().digest(TorCrypto.publicKeyToASN1((java.security.interfaces.RSAPublicKey) xCert.getPublicKey()))); log.debug("Got certificate of type " + certTypes[type]); remoteCerts.put(certTypes[type], xCert); } catch (CertificateException e) { e.printStackTrace(); } } } public void setState(STATES newState) { log.trace("New State {} (oldState {})", newState, this.state); synchronized (stateNotify) { this.state = newState; this.stateNotify.notify(); } } /** * Main loop. Handles incoming cells and sends any data waiting to be send down circuits/streams */ public void receiveHandlerLoop() { while (true) { // receive a cell Cell c = null; try { c = recvCell(); switch (c.cmdId) { case Cell.NETINFO: log.trace("Got NETINFO Sending NETINFO"); sendNetInfo(); setState(STATES.READY); continue; case Cell.CERTS: recvCerts(c); continue; } TorCircuit circ = circuits.get(new Long(c.circId)); if (circ == null || !circ.handleCell(c)) log.info("Received unhandled cell {}", c); } catch (IOException e) { e.printStackTrace(); return; } } } /** * Creates a circuit * * @return TorCircuit object */ public TorCircuit createCircuit(boolean blocking) { return createCircuit(defaultTorCircuitClass, blocking); } /** * Creates a circuit using a custom TorCircuit class * * @return TorCircuit object */ public <T extends TorCircuit> T createCircuit(Class<T> torCircClass, boolean blocking) { T circ; try { circ = torCircClass.getDeclaredConstructor(TorSocket.class).newInstance(this); } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { throw new RuntimeException(e); } circ.setBlocking(blocking); circuits.put((long) circ.circId, circ); return circ; } //LinkedBlockingQueue<Cell> sendQueue = new LinkedBlockingQueue<Cell>(); enum STATES { INITIALISING, READY } }