using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.IO; using System.Net.Sockets; using System.Threading; using TorControlLibrary.Responses; using TorControlLibrary.Exceptions; namespace TorControlLibrary { public class ControlConnection { public ControlConnection() { Host = new IPEndPoint(IPAddress.Loopback, 9051); Timeout = new TimeSpan(0, 0, 90); HashedControlPassword = String.Empty; } public void Connect(IPEndPoint host) { Host = host; Connect(); } public void Connect(String authenticationPassword) { HashedControlPassword = authenticationPassword; Connect(); } public void Connect(IPEndPoint host, String hashedControlPassword) { Host = host; HashedControlPassword = hashedControlPassword; Connect(); } public void Connect() { sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); sock.Connect(Host); socketStream = new NetworkStream(sock); socketReader = new StreamReader(socketStream); socketWriter = new StreamWriter(socketStream); socketWriter.AutoFlush = true; recvThread = new Thread(new ParameterizedThreadStart(receiveLoop)); recvThread.IsBackground = true; recvThread.Name = "Tor Control Library Receive Loop"; recvThread.Start(); Authenticate(); } public void Close() { sock.Close(); } protected void Authenticate() { CommandResponse resp = SendCommand(String.Format("AUTHENTICATE \"{0}\"",HashedControlPassword)); if (resp.StatusCode != 250) throw new TorAuthenticationException() { ErrorDescription = "There was a problem authenticating with the Tor control port.", HashedPassword = _hashedControlPassword, ServerResponse = resp.Response, StatusCode = resp.StatusCode }; } public List GetCircuitStatuses() { List circuitResponses = new List(); CommandResponse resp = SendCommand("GETINFO circuit-status"); StringReader sr = new StringReader(resp.Response); String line = sr.ReadLine(); if (!line.Contains("circuit-status")) throw TorException.Parse(resp.Response); if (line[3] == '+' || line.EndsWith("=")) line = sr.ReadLine(); while (line != null) { if (line.Equals(".") || line.Equals("250 OK")) break; if (line.Contains("circuit-status")) line = line.Substring(line.IndexOf('=') + 1); circuitResponses.Add(CircuitStatusResponse.Parse(line)); line = sr.ReadLine(); } return circuitResponses; } public List GetStreamStatuses() { List streamResponses = new List(); CommandResponse resp = SendCommand("GETINFO stream-status"); StringReader sr = new StringReader(resp.Response); String line = sr.ReadLine(); if (!line.Contains("stream-status")) throw TorException.Parse(resp.Response); if (line[3] == '+' || line.EndsWith("=")) line = sr.ReadLine(); while (line != null) { if (line.Equals(".") || line.Equals("250 OK")) break; if (line.Contains("stream-status")) line = line.Substring(line.IndexOf('=') + 1); streamResponses.Add(StreamStatusResponse.Parse(line)); line = sr.ReadLine(); } return streamResponses; } public List GetAllRouterStatusInfo() { List routerResponses = new List(); CommandResponse resp = SendCommand("GETINFO ns/all"); StringReader sr = new StringReader(resp.Response); String line = sr.ReadLine(), routerLine = String.Empty, flagLine = String.Empty; if (!line.Contains("ns/all")) throw TorException.Parse(resp.Response); while (line != null) { line = sr.ReadLine(); if (line.Equals(".")) break; switch (line.Substring(0, 1)) { case "r": routerLine = line; continue; case "s": flagLine = line; routerResponses.Add(RouterStatusResponse.Parse(routerLine, flagLine)); break; default: //I'm not currently parsing "w" or "p" lines. p doesn't describe a complete exit policy, and "w" doesn't describe all the available bandwidth metrics. That's retrieved via GETINFO desc continue; } } sr.Close(); return routerResponses; } public RouterStatusResponse GetRouterStatusInfoByNickname(String nickname) { RouterStatusResponse response = null; CommandResponse resp = SendCommand(String.Format("GETINFO ns/name/{0}", nickname)); StringReader sr = new StringReader(resp.Response); String line = sr.ReadLine(), routerLine = String.Empty, flagLine = String.Empty; if (!line.Contains("ns/name")) throw TorException.Parse(resp.Response); while (line != null) { line = sr.ReadLine(); if (line == null || line.Equals(".")) break; switch (line.Substring(0, 1)) { case "r": routerLine = line; continue; case "s": flagLine = line; response = RouterStatusResponse.Parse(routerLine, flagLine); break; default: //I'm not currently parsing "w" or "p" lines. p doesn't describe a complete exit policy, and "w" doesn't describe all the available bandwidth metrics. That's retrieved via GETINFO desc continue; } } sr.Close(); return response; } public RouterDescription GetRouterDescriptionByNickname(String nickname) { RouterDescription routerDescription = new RouterDescription(nickname); CommandResponse resp = SendCommand(String.Format("GETINFO desc/name/{0}", nickname)); if (resp.StatusCode != 250) throw TorException.Parse(resp.Response); using (StringReader reader = new StringReader(resp.Response)) { string line; String policy = String.Empty; while ((line = reader.ReadLine()) != null) { if (!line.Contains(' ')) continue; Int32 firstSpace = line.IndexOf(' '); string key = line.Substring(0, firstSpace).Trim(); string value = line.Substring(firstSpace, line.Length - firstSpace).Trim(); switch (key) { case "uptime": routerDescription.Uptime = new TimeSpan(0, 0, Int32.Parse(value)); break; case "bandwidth": String[] values = value.Split(' '); routerDescription.BandwidthAverage = Int32.Parse(values[0]); routerDescription.BandwidthBurst = Int32.Parse(values[1]); routerDescription.BandwidthObserved = Int32.Parse(values[2]); break; case "reject": case "accept": policy += line + Environment.NewLine; break; default: break; } } routerDescription.ExitPolicy = new ExitPolicy(policy); } return routerDescription; } public RouterStatusResponse GetRouterStatusInfoByID(String identity) { CommandResponse resp = SendCommand(String.Format("GETINFO ns/id/{0}", identity)); StringReader sr = new StringReader(resp.Response); String line = sr.ReadLine(); if (!line.Contains("ns/id")) throw TorException.Parse(resp.Response); line = sr.ReadLine(); String flagLine = sr.ReadLine(); RouterStatusResponse routerResponse = RouterStatusResponse.Parse(line, flagLine); sr.Close(); return routerResponse; } public void SetConfigurationEntry(String key, String value) { SendCommand(String.Format("SETCONF {0}={1}",key,value)); } /// /// Starts the process of creating a new circuit /// NOTE: The circuit is not usable until the BUILT event occurs /// /// The new circuit ID /// public Int32 CreateCircuit(String circuit) { CommandResponse resp = SendCommand(String.Format("EXTENDCIRCUIT 0 {0}", circuit)); if (resp.StatusCode == 250) { string[] parts = resp.Response.Split(' '); Int32 circuitID; if(Int32.TryParse(parts[parts.Length - 1],out circuitID)) return circuitID; } return 0; } public void AttachStream(Int32 streamId, Int32 circuitId) { SendCommand(String.Format("ATTACHSTREAM {0} {1}", streamId, circuitId)); } public void CloseCircuit(Int32 circuitID) { SendCommand(String.Format("CLOSECIRCUIT {0}", circuitID)); } public void CloseStream(Int32 streamID) { SendCommand(String.Format("CLOSESTREAM {0} 1", streamID)); } public CommandResponse SendRawCommand(String command) { return SendCommand(command); } public void RequestEvents(List events) { SendRawCommand(String.Format("SETEVENTS {0}", String.Join(" ", events))); } private CommandResponse SendCommand(String command) { CommandResponse resp; lock (requestLock) { socketWriter.WriteLine(command); resp = GetLastCommandResponse(); } return resp; } private CommandResponse GetLastCommandResponse() { if (responseEvent.WaitOne(Timeout)) { CommandResponse resp = responseQueue.Dequeue(); if (!resp.StatusCode.ToString().StartsWith("2")) throw TorException.Parse(resp.Response); else return resp; } else throw new TimeoutException(); } private void receiveLoop(object param) { String line; while (sock.Connected) { try { line = socketReader.ReadLine(); } catch (IOException e) { if (e.InnerException is SocketException && ((SocketException)e.InnerException).SocketErrorCode == SocketError.ConnectionAborted) break; else throw new ApplicationException(); } if (line == null) break; if (line.Equals("") || line.Length < 5) continue; switch (line.Substring(0, 3)) { //Command response case "250": case "246": case "249": CommandResponse resp = new CommandResponse(Int32.Parse(line.Substring(0,3)),line); while (!line.Equals("250 OK") && !line.StartsWith("250 ")) { line = socketReader.ReadLine(); resp.AppendResponse(line); } responseQueue.Enqueue(resp); responseEvent.Set(); break; //server-generated events case "650": EventResponse eventResp = EventResponse.Parse(line); if(ServerEvent != null) foreach (Action serverEventResponse in ServerEvent.GetInvocationList()) { serverEventResponse.BeginInvoke(eventResp, null, null); } break; default: if (line.StartsWith("5")) { responseQueue.Enqueue(new ErrorResponse(Int32.Parse(line.Substring(0, 3)), line)); responseEvent.Set(); } else throw new ApplicationException(); break; } } } public IPEndPoint Host { get; set; } public TimeSpan Timeout { get; set; } public event Action ServerEvent; public Boolean Connected { get { return sock.Connected; } } public String HashedControlPassword { get { return _hashedControlPassword; } set { if (!String.IsNullOrEmpty(value) && !value.StartsWith("16:")) throw new TorException() { ErrorDescription = "You must pass the already hashed version of the tor control port's password. You can obtain this by running the following command: tor.exe --hash-password mypass" }; _hashedControlPassword = value; } } private String _hashedControlPassword; private StreamReader socketReader; private StreamWriter socketWriter; private NetworkStream socketStream; private Socket sock; private Thread recvThread; private readonly object requestLock = new object(); private AutoResetEvent responseEvent = new AutoResetEvent(false); private Queue responseQueue = new Queue(); } }