1package eu.siacs.conversations.utils;
  2
  3import com.google.common.io.ByteStreams;
  4import com.google.common.net.InetAddresses;
  5import eu.siacs.conversations.Config;
  6import java.io.IOException;
  7import java.io.InputStream;
  8import java.io.OutputStream;
  9import java.net.Inet4Address;
 10import java.net.Inet6Address;
 11import java.net.InetAddress;
 12import java.net.InetSocketAddress;
 13import java.net.Socket;
 14import java.nio.ByteBuffer;
 15
 16public class SocksSocketFactory {
 17
 18    private static final byte[] LOCALHOST = new byte[] {127, 0, 0, 1};
 19
 20    public static void createSocksConnection(
 21            final Socket socket, final String destination, final int port) throws IOException {
 22        final InputStream proxyIs = socket.getInputStream();
 23        final OutputStream proxyOs = socket.getOutputStream();
 24        proxyOs.write(new byte[] {0x05, 0x01, 0x00});
 25        proxyOs.flush();
 26        final byte[] handshake = new byte[2];
 27        ByteStreams.readFully(proxyIs, handshake);
 28        if (handshake[0] != 0x05 || handshake[1] != 0x00) {
 29            throw new SocksConnectionException("Socks 5 handshake failed");
 30        }
 31        final byte type;
 32        final ByteBuffer request;
 33        if (InetAddresses.isInetAddress(destination)) {
 34            final var ip = InetAddresses.forString(destination);
 35            final var dest = ip.getAddress();
 36            request = ByteBuffer.allocate(6 + dest.length);
 37            if (ip instanceof Inet4Address) {
 38                type = 0x01;
 39            } else if (ip instanceof Inet6Address) {
 40                type = 0x04;
 41            } else {
 42                throw new IOException("IP address is of unknown subtype");
 43            }
 44            request.put(new byte[] {0x05, 0x01, 0x00, type});
 45            request.put(dest);
 46        } else {
 47            final byte[] dest = destination.getBytes();
 48            type = 0x03;
 49            request = ByteBuffer.allocate(7 + dest.length);
 50            request.put(new byte[] {0x05, 0x01, 0x00, type});
 51            request.put((byte) dest.length);
 52            request.put(dest);
 53        }
 54        request.putShort((short) port);
 55        proxyOs.write(request.array());
 56        proxyOs.flush();
 57        final byte[] response = new byte[4];
 58        ByteStreams.readFully(proxyIs, response);
 59        final byte ver = response[0];
 60        if (ver != 0x05) {
 61            throw new IOException(String.format("Unknown Socks version %02X ", ver));
 62        }
 63        final byte status = response[1];
 64        final byte bndAddressType = response[3];
 65        final byte[] bndDestination = readDestination(bndAddressType, proxyIs);
 66        final byte[] bndPort = new byte[2];
 67        if (bndAddressType == 0x03) {
 68            final String receivedDestination = new String(bndDestination);
 69            if (!receivedDestination.equalsIgnoreCase(destination)) {
 70                throw new IOException(
 71                        String.format(
 72                                "Destination mismatch. Received %s Expected %s",
 73                                receivedDestination, destination));
 74            }
 75        }
 76        ByteStreams.readFully(proxyIs, bndPort);
 77        if (status != 0x00) {
 78            if (status == 0x04) {
 79                throw new HostNotFoundException("Host unreachable");
 80            }
 81            if (status == 0x05) {
 82                throw new HostNotFoundException("Connection refused");
 83            }
 84            throw new IOException(String.format("Unknown status code %02X ", status));
 85        }
 86    }
 87
 88    private static byte[] readDestination(final byte type, final InputStream inputStream)
 89            throws IOException {
 90        final byte[] bndDestination;
 91        if (type == 0x01) {
 92            bndDestination = new byte[4];
 93        } else if (type == 0x03) {
 94            final int length = inputStream.read();
 95            bndDestination = new byte[length];
 96        } else if (type == 0x04) {
 97            bndDestination = new byte[16];
 98        } else {
 99            throw new IOException(String.format("Unknown Socks address type %02X ", type));
100        }
101        ByteStreams.readFully(inputStream, bndDestination);
102        return bndDestination;
103    }
104
105    public static boolean contains(byte needle, byte[] haystack) {
106        for (byte hay : haystack) {
107            if (hay == needle) {
108                return true;
109            }
110        }
111        return false;
112    }
113
114    private static Socket createSocket(InetSocketAddress address, String destination, int port)
115            throws IOException {
116        Socket socket = new Socket();
117        try {
118            socket.connect(address, Config.CONNECT_TIMEOUT * 1000);
119        } catch (IOException e) {
120            throw new SocksProxyNotFoundException();
121        }
122        createSocksConnection(socket, destination, port);
123        return socket;
124    }
125
126    public static Socket createSocketOverTor(String destination, int port) throws IOException {
127        return createSocket(
128                new InetSocketAddress(InetAddress.getByAddress(LOCALHOST), 9050),
129                destination,
130                port);
131    }
132
133    private static class SocksConnectionException extends IOException {
134        SocksConnectionException(String message) {
135            super(message);
136        }
137    }
138
139    public static class SocksProxyNotFoundException extends IOException {}
140
141    public static class HostNotFoundException extends SocksConnectionException {
142        HostNotFoundException(String message) {
143            super(message);
144        }
145    }
146}