Skip to content

Commit b585eb2

Browse files
committed
Proxy,Tests: add TorProxy
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.
1 parent f110b6f commit b585eb2

File tree

5 files changed

+392
-17
lines changed

5 files changed

+392
-17
lines changed

NOnion.Tests/TorProxyTests.cs

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
using System.Net;
2+
using System.Net.Http;
3+
using System.Threading.Tasks;
4+
5+
using Newtonsoft.Json;
6+
using NUnit.Framework;
7+
8+
using NOnion.Proxy;
9+
10+
namespace NOnion.Tests
11+
{
12+
internal class TorProxyTests
13+
{
14+
private const int MaximumRetry = 3;
15+
16+
private class TorProjectCheckResult
17+
{
18+
[JsonProperty("IsTor")]
19+
internal bool IsTor { get; set; }
20+
21+
[JsonProperty("IP")]
22+
internal string IP { get; set; }
23+
}
24+
25+
[Test]
26+
[Retry(MaximumRetry)]
27+
public void CanProxyTorProjectExitNodeCheck()
28+
{
29+
Assert.DoesNotThrowAsync(ProxyTorProjectExitNodeCheck);
30+
}
31+
32+
private async Task ProxyTorProjectExitNodeCheck()
33+
{
34+
using (await TorProxy.StartAsync(IPAddress.Loopback, 20000))
35+
{
36+
var handler = new HttpClientHandler
37+
{
38+
Proxy = new WebProxy("http://localhost:20000")
39+
};
40+
41+
var client = new HttpClient(handler);
42+
var resultStr = await client.GetStringAsync("https://check.torproject.org/api/ip");
43+
var result = JsonConvert.DeserializeObject<TorProjectCheckResult>(resultStr);
44+
Assert.IsTrue(result.IsTor);
45+
}
46+
}
47+
48+
[Test]
49+
[Retry(MaximumRetry)]
50+
public void CanProxyHttps()
51+
{
52+
Assert.DoesNotThrowAsync(ProxyHttps);
53+
}
54+
55+
private async Task ProxyHttps()
56+
{
57+
using (await TorProxy.StartAsync(IPAddress.Loopback, 20000))
58+
{
59+
var handler = new HttpClientHandler
60+
{
61+
Proxy = new WebProxy("http://localhost:20000")
62+
};
63+
64+
var client = new HttpClient(handler);
65+
var googleResponse = await client.GetAsync("https://google.com");
66+
Assert.That(googleResponse.StatusCode > 0);
67+
}
68+
}
69+
70+
[Test]
71+
[Retry(MaximumRetry)]
72+
public void CanProxyHttp()
73+
{
74+
Assert.DoesNotThrowAsync(ProxyHttp);
75+
}
76+
77+
private async Task ProxyHttp()
78+
{
79+
using (await TorProxy.StartAsync(IPAddress.Loopback, 20000))
80+
{
81+
var handler = new HttpClientHandler
82+
{
83+
Proxy = new WebProxy("http://localhost:20000")
84+
};
85+
86+
var client = new HttpClient(handler);
87+
var googleResponse = await client.GetAsync("http://google.com/search?q=Http+Test");
88+
Assert.That(googleResponse.StatusCode > 0);
89+
}
90+
}
91+
92+
[Test]
93+
[Retry(MaximumRetry)]
94+
public void CanProxyHiddenService()
95+
{
96+
Assert.DoesNotThrowAsync(ProxyHiddenService);
97+
}
98+
99+
private async Task ProxyHiddenService()
100+
{
101+
using (await TorProxy.StartAsync(IPAddress.Loopback, 20000))
102+
{
103+
var handler = new HttpClientHandler
104+
{
105+
Proxy = new WebProxy("http://localhost:20000"),
106+
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
107+
};
108+
109+
var client = new HttpClient(handler);
110+
var facebookResponse = await client.GetAsync("https://facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion");
111+
Assert.That(facebookResponse.StatusCode > 0);
112+
}
113+
}
114+
}
115+
}

NOnion/NOnion.fsproj

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
<Compile Include="Client\TorClient.fs" />
9090
<Compile Include="Services\TorServiceHost.fs" />
9191
<Compile Include="Services\TorServiceClient.fs" />
92+
<Compile Include="Proxy\TorProxy.fs" />
9293
</ItemGroup>
9394

9495
<ItemGroup>

NOnion/Network/TorCircuit.fs

+7
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,13 @@ and TorCircuit
10721072
failwith
10731073
"Should not happen: can't get circuitId for non-initialized circuit."
10741074

1075+
member __.IsActive =
1076+
match circuitState with
1077+
| Ready _
1078+
| ReadyAsIntroductionPoint _
1079+
| ReadyAsRendezvousPoint _ -> true
1080+
| _ -> false
1081+
10751082
member __.GetLastNode() =
10761083
async {
10771084
let! lastNodeResult =

NOnion/Proxy/TorProxy.fs

+246
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
namespace NOnion.Proxy
2+
3+
open FSharpx.Collections
4+
open System
5+
open System.IO
6+
open System.Net
7+
open System.Net.Sockets
8+
open System.Text
9+
open System.Threading
10+
11+
open NOnion
12+
open NOnion.Client
13+
open NOnion.Network
14+
open NOnion.Services
15+
16+
type TorProxy private (listener: TcpListener, torClient: TorClient) =
17+
let mutable lastActiveCircuitOpt: Option<TorCircuit> = None
18+
19+
let handleConnection(client: TcpClient) =
20+
async {
21+
let! cancelToken = Async.CancellationToken
22+
cancelToken.ThrowIfCancellationRequested()
23+
24+
let stream = client.GetStream()
25+
26+
let readHeaders() =
27+
async {
28+
let stringBuilder = StringBuilder()
29+
// minimum request 16 bytes: GET / HTTP/1.1\r\n\r\n
30+
let preReadLen = 18
31+
let! buffer = stream.AsyncRead preReadLen
32+
33+
buffer
34+
|> Encoding.ASCII.GetString
35+
|> stringBuilder.Append
36+
|> ignore<StringBuilder>
37+
38+
let rec innerReadRest() =
39+
async {
40+
if stringBuilder.ToString().EndsWith("\r\n\r\n") then
41+
return ()
42+
else
43+
let! newByte = stream.AsyncRead 1
44+
45+
newByte
46+
|> Encoding.ASCII.GetString
47+
|> stringBuilder.Append
48+
|> ignore<StringBuilder>
49+
50+
return! innerReadRest()
51+
}
52+
53+
do! innerReadRest()
54+
55+
return stringBuilder.ToString()
56+
}
57+
58+
let! headers = readHeaders()
59+
60+
let headerLines =
61+
headers.Split(
62+
[| "\r\n" |],
63+
StringSplitOptions.RemoveEmptyEntries
64+
)
65+
66+
match Seq.tryHeadTail headerLines with
67+
| Some(firstLine, restOfHeaders) ->
68+
let firstLineParts = firstLine.Split(' ')
69+
70+
let method = firstLineParts.[0]
71+
let url = firstLineParts.[1]
72+
let protocolVersion = firstLineParts.[2]
73+
74+
if protocolVersion <> "HTTP/1.1" then
75+
return failwith "TorProxy: protocol version mismatch"
76+
77+
let rec copySourceToDestination
78+
(source: Stream)
79+
(dest: Stream)
80+
=
81+
async {
82+
do! source.CopyToAsync dest |> Async.AwaitTask
83+
84+
// CopyToAsync returns when source is closed so we can close dest
85+
dest.Close()
86+
}
87+
88+
let createStreamToDestination(parsedUrl: Uri) =
89+
async {
90+
if parsedUrl.DnsSafeHost.EndsWith(".onion") then
91+
let! client =
92+
TorServiceClient.Connect
93+
torClient
94+
(sprintf
95+
"%s:%i"
96+
parsedUrl.DnsSafeHost
97+
parsedUrl.Port)
98+
99+
return! client.GetStream()
100+
else
101+
let! circuit =
102+
match lastActiveCircuitOpt with
103+
| Some lastActiveCircuit when
104+
lastActiveCircuit.IsActive
105+
->
106+
async {
107+
TorLogger.Log
108+
"TorProxy: we had active circuit, no need to recreate"
109+
110+
return lastActiveCircuit
111+
}
112+
| _ ->
113+
async {
114+
TorLogger.Log
115+
"TorProxy: we didn't have an active circuit, recreating..."
116+
117+
let! circuit =
118+
torClient.AsyncCreateCircuit
119+
3
120+
CircuitPurpose.Exit
121+
None
122+
123+
lastActiveCircuitOpt <- Some circuit
124+
return circuit
125+
}
126+
127+
let torStream = new TorStream(circuit)
128+
129+
do!
130+
torStream.ConnectToOutside
131+
parsedUrl.DnsSafeHost
132+
parsedUrl.Port
133+
|> Async.Ignore
134+
135+
return torStream
136+
}
137+
138+
if method <> "CONNECT" then
139+
let parsedUrl = Uri url
140+
141+
use! torStream = createStreamToDestination parsedUrl
142+
143+
let firstLineToRetransmit =
144+
sprintf
145+
"%s %s HTTP/1.1\r\n"
146+
method
147+
parsedUrl.PathAndQuery
148+
149+
let headersToForwardLines =
150+
restOfHeaders
151+
|> Seq.filter(fun header ->
152+
not(header.StartsWith "Proxy-")
153+
)
154+
|> Seq.map(fun header -> sprintf "%s\r\n" header)
155+
156+
let headersToForward =
157+
String.Join(String.Empty, headersToForwardLines)
158+
159+
do!
160+
Encoding.ASCII.GetBytes firstLineToRetransmit
161+
|> torStream.AsyncWrite
162+
163+
do!
164+
Encoding.ASCII.GetBytes headersToForward
165+
|> torStream.AsyncWrite
166+
167+
do! Encoding.ASCII.GetBytes "\r\n" |> torStream.AsyncWrite
168+
169+
return!
170+
[
171+
copySourceToDestination torStream stream
172+
copySourceToDestination stream torStream
173+
]
174+
|> Async.Parallel
175+
|> Async.Ignore
176+
else
177+
let parsedUrl = Uri <| sprintf "http://%s" url
178+
179+
use! torStream = createStreamToDestination parsedUrl
180+
181+
let connectResponse =
182+
"HTTP/1.1 200 Connection Established\r\nConnection: close\r\n\r\n"
183+
184+
do!
185+
Encoding.ASCII.GetBytes connectResponse
186+
|> stream.AsyncWrite
187+
188+
return!
189+
[
190+
copySourceToDestination torStream stream
191+
copySourceToDestination stream torStream
192+
]
193+
|> Async.Parallel
194+
|> Async.Ignore
195+
| None ->
196+
return failwith "TorProxy: incomplete http header detected"
197+
198+
}
199+
200+
let rec acceptConnections() =
201+
async {
202+
let! cancelToken = Async.CancellationToken
203+
cancelToken.ThrowIfCancellationRequested()
204+
205+
let! client = listener.AcceptTcpClientAsync() |> Async.AwaitTask
206+
207+
Async.Start(handleConnection client, cancelToken)
208+
209+
return! acceptConnections()
210+
}
211+
212+
let shutdownToken = new CancellationTokenSource()
213+
214+
static member Start (localAddress: IPAddress) (port: int) =
215+
async {
216+
let! client = TorClient.AsyncBootstrapWithEmbeddedList None
217+
let listener = TcpListener(localAddress, port)
218+
let proxy = new TorProxy(listener, client)
219+
proxy.StartListening()
220+
return proxy
221+
}
222+
223+
static member StartAsync(localAddress: IPAddress, port: int) =
224+
TorProxy.Start localAddress port |> Async.StartAsTask
225+
226+
member private self.StartListening() =
227+
listener.Start()
228+
229+
Async.Start(acceptConnections(), shutdownToken.Token)
230+
231+
member __.GetNewIdentity() =
232+
async {
233+
let! newCircuit =
234+
torClient.AsyncCreateCircuit 3 CircuitPurpose.Exit None
235+
236+
lastActiveCircuitOpt <- Some newCircuit
237+
}
238+
239+
member self.GetNewIdentityAsync() =
240+
self.GetNewIdentity() |> Async.StartAsTask
241+
242+
interface IDisposable with
243+
member __.Dispose() =
244+
shutdownToken.Cancel()
245+
listener.Stop()
246+
(torClient :> IDisposable).Dispose()

0 commit comments

Comments
 (0)