TorProxySpawner.java

package eu.javaexperience.proxy;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.Socket;

import eu.javaexperience.asserts.AssertArgument;
import eu.javaexperience.io.IOTools;
import eu.javaexperience.reflect.Mirror;
import eu.javaexperience.text.StringTools;

public class TorProxySpawner
{
	public final String root;
	
	/**
	 * @throws IOException 
	 * 
	 * */
	public TorProxySpawner(String root) throws IOException
	{
		AssertArgument.assertNotNull(this.root = new File(root).getCanonicalFile().toString(), "root_working_directory_path");
	}
	
	private static String proto =
	"# This file was generated by Tor; if you edit it, comments will not be preserved\n"+
	"# The old torrc file was renamed to torrc.orig.1 or similar, and Tor will ignore it\n"+
	""+
	"ControlSocket $root$/$off$/control\n"+
	"DataDirectory $root$/$off$/data\n"+
	"\n"+
	"#9030+offset\n"+
	"DirPort $dp$\n"+
	"\n"+
	"DirReqStatistics 0\n"+
	"ExitPolicy reject *:*\n"+
	"Log notice stdout\n"+
	"Nickname Unnamed\n"+
	"\n"+
	"#9001+offset\n"+
	"ORPort $op$\n"+

	"#9050+offset\n"+
	"SOCKSPort $sp$\n"+
	
	"\n"+
	//disable publishing on pub lic tor list
	"PublishServerDescriptor 0\n"+
	//disable exit traffic
	//"ExitRelay 0\n"+
	//disable UPnP
	"PortForwarding 0\n"+

	"RelayBandwidthBurst 524288000\n"+
	"RelayBandwidthRate 524288000\n";
	
	public TorProxy spawnWithOffset(int off) throws InterruptedException, IOException
	{
		TorProxy tp = new TorProxy();
		tp.offset = off;
		tp.setup();
		tp.start();
		
		if(tp.waitPortOpen(9050+off, 30))
		{
			tp.updateAccessTimeNow();
			return tp;
		}
		else
		{
			return null;
		}
	}
	
	public static final Process exec(boolean wait, String... args) throws IOException, InterruptedException
	{
		ProcessBuilder pb = new ProcessBuilder(args);
		Process p = pb.start();
		if(wait)
		{
			p.waitFor();
		}
		
		return p;
	}
	
	public static Proxy proxyForLocalPort(int port, boolean http)
	{
		return new Proxy(http?Proxy.Type.HTTP:Proxy.Type.SOCKS, InetSocketAddress.createUnresolved("127.0.0.1", port));
	}
	
	public static interface ProxySource
	{
		public String getAddress();
		public int getPort();
		public Proxy.Type getType();
		public Proxy getProxy();
	}
	
	public static abstract class AbstractProxySource implements ProxySource
	{
		@Override
		public Proxy getProxy()
		{
			return new Proxy(getType(), InetSocketAddress.createUnresolved(getAddress(), getPort()));
		}
		
		public static ProxySource createLocalSocksProxy(final int port)
		{
			return new AbstractProxySource()
			{
				@Override
				public Type getType()
				{
					return Type.SOCKS;
				}
				
				@Override
				public int getPort()
				{
					return port;
				}
				
				@Override
				public String getAddress()
				{
					return "127.0.0.1";
				}
			};
		}
		
	}
	
	public class TorProxy extends AbstractProxySource
	{
		protected int offset;
		protected long lastAccess = 0;
		
		public long getLastAccessTime()
		{
			return lastAccess;
		}
		
		public void updateAccessTimeNow()
		{
			lastAccess = System.currentTimeMillis();
		}
		
		public void setup() throws IOException, InterruptedException
		{
			String conf = StringTools.replaceAllStrings(proto, "$off$", String.valueOf(offset));
			/*conf = Strings.replaceAllStrings(conf, "$dp$", String.valueOf(9030+offset));
			conf = Strings.replaceAllStrings(conf, "$op$", String.valueOf(9001+offset));
			conf = Strings.replaceAllStrings(conf, "$sp$", String.valueOf(9050+offset));*/

			
			conf = StringTools.replaceAllStrings(conf, "$dp$", String.valueOf(10001+offset));
			conf = StringTools.replaceAllStrings(conf, "$op$", String.valueOf(12001+offset));
			conf = StringTools.replaceAllStrings(conf, "$sp$", String.valueOf(9050+offset));
			
			conf = StringTools.replaceAllStrings(conf, "$root$", root);
			IOTools.putFileContent(root+"/cfg"+offset, false, conf.getBytes());
			
			File r = new File(root+"/"+offset);
			r.mkdirs();
			exec(true, "chmod", "700", r.toString());
			new File(root+"/"+offset+"/data").mkdir();
		}
		
		protected Process process = null;
		
		public void start() throws IOException, InterruptedException
		{
			if(null != process)
			{
				throw new RuntimeException("Tor process alredy started");
			}
			process = exec(false, "tor", "-f", root+"/cfg"+offset);
		}
		
		public void stop() throws InterruptedException
		{
			process.destroy();
			process.waitFor();
			process = null;
		}
		
		public boolean waitPortOpen(int port, int max_try) throws InterruptedException
		{
			boolean conn = false;
			int i=0;
			while(!conn && i++ < max_try)
			{
				try
				{
					Socket s = new Socket("127.0.0.1", port);
					conn = true;
					s.close();
					System.out.println("proxy ready");
					break;
				}
				catch(Exception e)
				{
					System.out.println("wait");
				}
				Thread.sleep(500);
			}
			
			System.out.println(port+". Tor proxy started: "+conn);
			return conn;
		}
		
		public void withinTimeOrRestart(long milisecs) throws IOException, InterruptedException
		{
			if(lastAccess + milisecs < System.currentTimeMillis())
			{
				//reboot
				stop();
				start();
				waitPortOpen(9050+offset, 30);
			}
		}
		
		@Override
		public Proxy getProxy()
		{
			return proxyForLocalPort(9050+offset, false);
		}

		@Override
		public int getPort()
		{
			return 9050+offset;
		}

		@Override
		public String getAddress()
		{
			return "127.0.0.1";
		}

		@Override
		public Type getType()
		{
			return Type.SOCKS;
		}
	}

	public static TorProxySpawner runtimeThrowCreate(String path)
	{
		try
		{
			return new TorProxySpawner(path);
		}
		catch (IOException e)
		{
			Mirror.throwSoftOrHardButAnyway(e);
			return null;
		}
	}
}