Initial project commit

This commit is contained in:
2022-06-11 16:42:18 -04:00
commit 14565a17e2
60 changed files with 3739 additions and 0 deletions

View File

@@ -0,0 +1,382 @@
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<CircuitStatusResponse> GetCircuitStatuses()
{
List<CircuitStatusResponse> circuitResponses = new List<CircuitStatusResponse>();
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<StreamStatusResponse> GetStreamStatuses()
{
List<StreamStatusResponse> streamResponses = new List<StreamStatusResponse>();
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<RouterStatusResponse> GetAllRouterStatusInfo()
{
List<RouterStatusResponse> routerResponses = new List<RouterStatusResponse>();
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));
}
/// <summary>
/// Starts the process of creating a new circuit
/// NOTE: The circuit is not usable until the BUILT event occurs
/// </summary>
/// <param name="circuit">The new circuit ID</param>
/// <returns></returns>
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<String> 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<EventResponse> 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<EventResponse> 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<CommandResponse> responseQueue = new Queue<CommandResponse>();
}
}