Skip to content

Commit

Permalink
Proxy,Tests: add TorProxy
Browse files Browse the repository at this point in the history
This commit adds a helper class that provides an HTTP proxy for
easy usage of NOnion alongside things like HttpClient that don't
support communicating over custom streams.
  • Loading branch information
aarani committed Mar 31, 2024
1 parent f110b6f commit b585eb2
Show file tree
Hide file tree
Showing 5 changed files with 392 additions and 17 deletions.
115 changes: 115 additions & 0 deletions NOnion.Tests/TorProxyTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;

using Newtonsoft.Json;
using NUnit.Framework;

using NOnion.Proxy;

namespace NOnion.Tests
{
internal class TorProxyTests
{
private const int MaximumRetry = 3;

private class TorProjectCheckResult
{
[JsonProperty("IsTor")]
internal bool IsTor { get; set; }

[JsonProperty("IP")]
internal string IP { get; set; }
}

[Test]
[Retry(MaximumRetry)]
public void CanProxyTorProjectExitNodeCheck()
{
Assert.DoesNotThrowAsync(ProxyTorProjectExitNodeCheck);
}

private async Task ProxyTorProjectExitNodeCheck()
{
using (await TorProxy.StartAsync(IPAddress.Loopback, 20000))
{
var handler = new HttpClientHandler
{
Proxy = new WebProxy("http://localhost:20000")
};

var client = new HttpClient(handler);
var resultStr = await client.GetStringAsync("https://check.torproject.org/api/ip");
var result = JsonConvert.DeserializeObject<TorProjectCheckResult>(resultStr);
Assert.IsTrue(result.IsTor);
}
}

[Test]
[Retry(MaximumRetry)]
public void CanProxyHttps()
{
Assert.DoesNotThrowAsync(ProxyHttps);
}

private async Task ProxyHttps()
{
using (await TorProxy.StartAsync(IPAddress.Loopback, 20000))
{
var handler = new HttpClientHandler
{
Proxy = new WebProxy("http://localhost:20000")
};

var client = new HttpClient(handler);
var googleResponse = await client.GetAsync("https://google.com");
Assert.That(googleResponse.StatusCode > 0);
}
}

[Test]
[Retry(MaximumRetry)]
public void CanProxyHttp()
{
Assert.DoesNotThrowAsync(ProxyHttp);
}

private async Task ProxyHttp()
{
using (await TorProxy.StartAsync(IPAddress.Loopback, 20000))
{
var handler = new HttpClientHandler
{
Proxy = new WebProxy("http://localhost:20000")
};

var client = new HttpClient(handler);
var googleResponse = await client.GetAsync("http://google.com/search?q=Http+Test");
Assert.That(googleResponse.StatusCode > 0);
}
}

[Test]
[Retry(MaximumRetry)]
public void CanProxyHiddenService()
{
Assert.DoesNotThrowAsync(ProxyHiddenService);
}

private async Task ProxyHiddenService()
{
using (await TorProxy.StartAsync(IPAddress.Loopback, 20000))
{
var handler = new HttpClientHandler
{
Proxy = new WebProxy("http://localhost:20000"),
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
};

var client = new HttpClient(handler);
var facebookResponse = await client.GetAsync("https://facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion");
Assert.That(facebookResponse.StatusCode > 0);
}
}
}
}
1 change: 1 addition & 0 deletions NOnion/NOnion.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
<Compile Include="Client\TorClient.fs" />
<Compile Include="Services\TorServiceHost.fs" />
<Compile Include="Services\TorServiceClient.fs" />
<Compile Include="Proxy\TorProxy.fs" />
</ItemGroup>

<ItemGroup>
Expand Down
7 changes: 7 additions & 0 deletions NOnion/Network/TorCircuit.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,13 @@ and TorCircuit
failwith
"Should not happen: can't get circuitId for non-initialized circuit."

member __.IsActive =
match circuitState with
| Ready _
| ReadyAsIntroductionPoint _
| ReadyAsRendezvousPoint _ -> true
| _ -> false

member __.GetLastNode() =
async {
let! lastNodeResult =
Expand Down
246 changes: 246 additions & 0 deletions NOnion/Proxy/TorProxy.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
namespace NOnion.Proxy

open FSharpx.Collections
open System
open System.IO
open System.Net
open System.Net.Sockets
open System.Text
open System.Threading

open NOnion
open NOnion.Client
open NOnion.Network
open NOnion.Services

type TorProxy private (listener: TcpListener, torClient: TorClient) =
let mutable lastActiveCircuitOpt: Option<TorCircuit> = None

let handleConnection(client: TcpClient) =
async {
let! cancelToken = Async.CancellationToken
cancelToken.ThrowIfCancellationRequested()

let stream = client.GetStream()

let readHeaders() =
async {
let stringBuilder = StringBuilder()
// minimum request 16 bytes: GET / HTTP/1.1\r\n\r\n
let preReadLen = 18
let! buffer = stream.AsyncRead preReadLen

buffer
|> Encoding.ASCII.GetString
|> stringBuilder.Append
|> ignore<StringBuilder>

let rec innerReadRest() =
async {
if stringBuilder.ToString().EndsWith("\r\n\r\n") then
return ()
else
let! newByte = stream.AsyncRead 1

newByte
|> Encoding.ASCII.GetString
|> stringBuilder.Append
|> ignore<StringBuilder>

return! innerReadRest()
}

do! innerReadRest()

return stringBuilder.ToString()
}

let! headers = readHeaders()

let headerLines =
headers.Split(
[| "\r\n" |],
StringSplitOptions.RemoveEmptyEntries
)

match Seq.tryHeadTail headerLines with
| Some(firstLine, restOfHeaders) ->
let firstLineParts = firstLine.Split(' ')

let method = firstLineParts.[0]
let url = firstLineParts.[1]
let protocolVersion = firstLineParts.[2]

if protocolVersion <> "HTTP/1.1" then
return failwith "TorProxy: protocol version mismatch"

let rec copySourceToDestination
(source: Stream)
(dest: Stream)
=
async {
do! source.CopyToAsync dest |> Async.AwaitTask

// CopyToAsync returns when source is closed so we can close dest
dest.Close()
}

let createStreamToDestination(parsedUrl: Uri) =
async {
if parsedUrl.DnsSafeHost.EndsWith(".onion") then
let! client =
TorServiceClient.Connect
torClient
(sprintf
"%s:%i"
parsedUrl.DnsSafeHost
parsedUrl.Port)

return! client.GetStream()
else
let! circuit =
match lastActiveCircuitOpt with
| Some lastActiveCircuit when
lastActiveCircuit.IsActive
->
async {
TorLogger.Log
"TorProxy: we had active circuit, no need to recreate"

return lastActiveCircuit
}
| _ ->
async {
TorLogger.Log
"TorProxy: we didn't have an active circuit, recreating..."

let! circuit =
torClient.AsyncCreateCircuit
3
CircuitPurpose.Exit
None

lastActiveCircuitOpt <- Some circuit
return circuit
}

let torStream = new TorStream(circuit)

do!
torStream.ConnectToOutside
parsedUrl.DnsSafeHost
parsedUrl.Port
|> Async.Ignore

return torStream
}

if method <> "CONNECT" then
let parsedUrl = Uri url

use! torStream = createStreamToDestination parsedUrl

let firstLineToRetransmit =
sprintf
"%s %s HTTP/1.1\r\n"
method
parsedUrl.PathAndQuery

let headersToForwardLines =
restOfHeaders
|> Seq.filter(fun header ->
not(header.StartsWith "Proxy-")
)
|> Seq.map(fun header -> sprintf "%s\r\n" header)

let headersToForward =
String.Join(String.Empty, headersToForwardLines)

do!
Encoding.ASCII.GetBytes firstLineToRetransmit
|> torStream.AsyncWrite

do!
Encoding.ASCII.GetBytes headersToForward
|> torStream.AsyncWrite

do! Encoding.ASCII.GetBytes "\r\n" |> torStream.AsyncWrite

return!
[
copySourceToDestination torStream stream
copySourceToDestination stream torStream
]
|> Async.Parallel
|> Async.Ignore
else
let parsedUrl = Uri <| sprintf "http://%s" url

use! torStream = createStreamToDestination parsedUrl

let connectResponse =
"HTTP/1.1 200 Connection Established\r\nConnection: close\r\n\r\n"

do!
Encoding.ASCII.GetBytes connectResponse
|> stream.AsyncWrite

return!
[
copySourceToDestination torStream stream
copySourceToDestination stream torStream
]
|> Async.Parallel
|> Async.Ignore
| None ->
return failwith "TorProxy: incomplete http header detected"

}

let rec acceptConnections() =
async {
let! cancelToken = Async.CancellationToken
cancelToken.ThrowIfCancellationRequested()

let! client = listener.AcceptTcpClientAsync() |> Async.AwaitTask

Async.Start(handleConnection client, cancelToken)

return! acceptConnections()
}

let shutdownToken = new CancellationTokenSource()

static member Start (localAddress: IPAddress) (port: int) =
async {
let! client = TorClient.AsyncBootstrapWithEmbeddedList None
let listener = TcpListener(localAddress, port)
let proxy = new TorProxy(listener, client)
proxy.StartListening()
return proxy
}

static member StartAsync(localAddress: IPAddress, port: int) =
TorProxy.Start localAddress port |> Async.StartAsTask

member private self.StartListening() =
listener.Start()

Async.Start(acceptConnections(), shutdownToken.Token)

member __.GetNewIdentity() =
async {
let! newCircuit =
torClient.AsyncCreateCircuit 3 CircuitPurpose.Exit None

lastActiveCircuitOpt <- Some newCircuit
}

member self.GetNewIdentityAsync() =
self.GetNewIdentity() |> Async.StartAsTask

interface IDisposable with
member __.Dispose() =
shutdownToken.Cancel()
listener.Stop()
(torClient :> IDisposable).Dispose()
Loading

0 comments on commit b585eb2

Please sign in to comment.