Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proxy,Tests: add TorProxy #11

Merged
merged 1 commit into from
Mar 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading