Skip to content

Commit d4f2fd1

Browse files
committed
Add support for registering hidden services
Usage: ``` add Adds a service to register on the Tor network Usage: tor [options] add <name> <service> Arguments: <name> Name of the service to register <service> Address and port of the local service being registered, such as 127.0.0.1:8080 Options: -p, --port <port> Optional port on the Tor network to listen on, if different than the service port -?, -h, --help Show help and usage information ``` Service directory is set to `~/tor/[name]` since the service keys and address should be reused across tool reinstalls/updates (especially updates of the tor binaries themselves). The configuration is saved to `~/tor/.netconfig` so it's also preserved across reinstalls. Example configuration: ``` [tor] proxy = 1337 socks = 1338 control = 1339 [tor "echo"] port = 8080 service = 127.0.0.1:8080 ``` The `torrc` file is updated accordingly after configuration and before starting, so that it picks up our changes. > NOTE: this is possible because we depend on our fork of TorSharp which implements the fix proposed in joelverhagen/TorSharp#70. Fixes #1
1 parent cb98dd9 commit d4f2fd1

File tree

11 files changed

+164
-50
lines changed

11 files changed

+164
-50
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ artifacts
44
pack
55
.vs
66
.vscode
7+
/tor
78

89
*.suo
910
*.sdf

dotnet-tor.sln

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-tor", "src\dotnet-to
77
EndProject
88
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{95AB91E3-8D17-486B-A99D-1E074A054655}"
99
ProjectSection(SolutionItems) = preProject
10+
.editorconfig = .editorconfig
1011
readme.md = readme.md
12+
tor.cmd = tor.cmd
1113
EndProjectSection
1214
EndProject
1315
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TorSharp", "external\TorSharp\src\TorSharp\TorSharp.csproj", "{350FC483-DA36-4273-90C6-9808D87F5115}"

src/dotnet-tor/AddCommand.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.CommandLine;
3+
using System.CommandLine.Invocation;
4+
using System.Threading;
5+
6+
class AddCommand : Command
7+
{
8+
public AddCommand() : base("add", "Adds a service to register on the Tor network")
9+
{
10+
Add(new Argument<string>("name", "Name of the service to register"));
11+
Add(new Argument<string>("service", "Address and port of the local service being registered, such as 127.0.0.1:8080"));
12+
Add(new Option<int?>(new[] { "--port", "-p" }, "Optional port on the Tor network to listen on, if different than the service port"));
13+
14+
Handler = CommandHandler.Create<string, int?, string, CancellationToken>(Run);
15+
}
16+
17+
static void Run(string name, int? port, string service, CancellationToken cancellation)
18+
{
19+
if (!Uri.TryCreate("http://" + service, UriKind.Absolute, out var uri))
20+
throw new ArgumentException("Service specified is not valid. It should be an IP:PORT combination: " + service);
21+
22+
Tor.Config.GetSection("tor", name)
23+
.SetNumber("port", port ?? uri.Port)
24+
.SetString("service", service);
25+
}
26+
}

src/dotnet-tor/ConfigureCommand.cs renamed to src/dotnet-tor/ConfigCommand.cs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,19 @@
1010
using Knapcode.TorSharp;
1111
using Spectre.Console;
1212

13-
class ConfigureCommand : Command
13+
class ConfigCommand : Command
1414
{
15-
readonly string torPath;
16-
17-
public ConfigureCommand(string torPath) : base("config", "Edits the full torrc configuration file.")
15+
public ConfigCommand() : base("config", "Edits the full torrc configuration file.")
1816
{
19-
this.torPath = torPath;
2017
Handler = CommandHandler.Create(RunAsync);
2118
}
2219

2320
async Task RunAsync()
2421
{
2522
var settings = new TorSharpSettings
2623
{
27-
28-
ZippedToolsDirectory = Path.Combine(torPath, "zip"),
29-
ExtractedToolsDirectory = Path.Combine(torPath, "bin"),
24+
ZippedToolsDirectory = Path.Combine(Tor.AppPath, "zip"),
25+
ExtractedToolsDirectory = Path.Combine(Tor.AppPath, "bin"),
3026
};
3127

3228
var zipPath = await AnsiConsole.Status().StartAsync("Fetching Tor tools", async _ =>
@@ -49,7 +45,6 @@ async Task RunAsync()
4945
var torProxy = new TorSharpProxy(settings);
5046
await torProxy.ConfigureAsync();
5147

52-
// TODO: won't exist yet because the tools aren't unzipped yet.
5348
if (File.Exists(configPath))
5449
Process.Start(editor, configPath);
5550
}

src/dotnet-tor/Program.cs

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,11 @@
3232
AnsiConsole.MarkupLine($"[yellow]New version v{update.Identity.Version} from {(DateTimeOffset.Now - (update.Published ?? DateTimeOffset.Now)).Humanize()} ago is available.[/] Update with: [lime]dotnet tool update -g dotnet-echo[/]");
3333
}
3434

35-
var appPath = GetApplicationPath();
3635
#if !CI
37-
AnsiConsole.MarkupLine($"[yellow]AppPath: {appPath}[/]");
36+
AnsiConsole.MarkupLine($"[yellow]AppPath: {Tor.AppPath}[/]");
3837
#endif
3938

40-
await new TorCommand(appPath).WithConfigurableDefaults("tor").InvokeAsync(args);
41-
42-
// See GCM's Program.cs
43-
static string GetApplicationPath()
44-
{
45-
// Assembly::Location always returns an empty string if the application was published as a single file
46-
#pragma warning disable IL3000
47-
bool isSingleFile = string.IsNullOrEmpty(Assembly.GetEntryAssembly()?.Location);
48-
#pragma warning restore IL3000
49-
50-
// Use "argv[0]" to get the full path to the entry executable - this is consistent across
51-
// .NET Framework and .NET >= 5 when published as a single file.
52-
string[] args = Environment.GetCommandLineArgs();
53-
string candidatePath = args[0];
54-
55-
// If we have not been published as a single file on .NET 5 then we must strip the ".dll" file extension
56-
// to get the default AppHost/SuperHost name.
57-
if (!isSingleFile && Path.HasExtension(candidatePath))
58-
{
59-
return Path.ChangeExtension(candidatePath, null);
60-
}
61-
62-
return candidatePath;
63-
}
39+
await new TorCommand().InvokeAsync(args);
6440

6541
static Task<IPackageSearchMetadata> GetUpdateAsync() => AnsiConsole.Status().StartAsync("Checking for updates", async context =>
6642
{

src/dotnet-tor/Properties/launchSettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"profiles": {
33
"dotnet-tor": {
44
"commandName": "Project",
5-
"commandLineArgs": "-?"
5+
"commandLineArgs": "add -?"
66
}
77
}
88
}

src/dotnet-tor/Tor.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.IO;
3+
using System.Reflection;
4+
using DotNetConfig;
5+
6+
static class Tor
7+
{
8+
public static string AppPath { get; } = GetApplicationPath();
9+
10+
public static string DataDir { get; } = Path.Combine(
11+
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
12+
"tor");
13+
14+
// We rebuild on every request since other code can mutate the
15+
// immutable structure and cause this property to become out of
16+
// sync with the persisted state.
17+
public static Config Config => Config.Build(Path.Combine(DataDir, Config.FileName));
18+
19+
// See GCM's Program.cs
20+
static string GetApplicationPath()
21+
{
22+
// Assembly::Location always returns an empty string if the application was published as a single file
23+
#pragma warning disable IL3000
24+
bool isSingleFile = string.IsNullOrEmpty(Assembly.GetEntryAssembly()?.Location);
25+
#pragma warning restore IL3000
26+
27+
// Use "argv[0]" to get the full path to the entry executable - this is consistent across
28+
// .NET Framework and .NET >= 5 when published as a single file.
29+
string[] args = Environment.GetCommandLineArgs();
30+
string candidatePath = args[0];
31+
32+
// If we have not been published as a single file on .NET 5 then we must strip the ".dll" file extension
33+
// to get the default AppHost/SuperHost name.
34+
if (!isSingleFile && Path.HasExtension(candidatePath))
35+
{
36+
return Path.ChangeExtension(candidatePath, null);
37+
}
38+
39+
return candidatePath;
40+
}
41+
}
42+

src/dotnet-tor/TorCommand.cs

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
using System.CommandLine;
33
using System.CommandLine.Invocation;
44
using System.IO;
5+
using System.Linq;
56
using System.Net.Http;
6-
using System.Security.Cryptography;
77
using System.Threading;
88
using System.Threading.Tasks;
99
using DotNetConfig;
@@ -13,19 +13,19 @@
1313
class TorCommand : RootCommand
1414
{
1515
const string ControlPassword = "7B002936-978D-46D3-9C96-7579F97F333E";
16-
static readonly ConfigSection config = Config.Build().GetSection("tor");
16+
const string ConfigDelimiter = "#! dotnet-tor";
1717

18-
string torPath;
1918

20-
public TorCommand(string torPath) : base("Tor proxy service")
19+
public TorCommand() : base("Tor proxy service")
2120
{
22-
this.torPath = torPath;
21+
var config = Tor.Config.GetSection("tor");
2322

2423
Add(new Option<int>(new[] { "--proxy", "-p" }, () => (int?)config.GetNumber("proxy") ?? 1337, "Proxy port"));
2524
Add(new Option<int>(new[] { "--socks", "-s" }, () => (int?)config.GetNumber("socks") ?? 1338, "Socks port"));
2625
Add(new Option<int>(new[] { "--control", "-c" }, () => (int?)config.GetNumber("control") ?? 1339, "Control port"));
2726

28-
Add(new ConfigureCommand(torPath));
27+
Add(new AddCommand());
28+
Add(new ConfigCommand());
2929

3030
Handler = CommandHandler.Create<int, int, int, CancellationToken>(RunAsync);
3131
}
@@ -34,8 +34,8 @@ async Task RunAsync(int proxy, int socks, int control, CancellationToken cancell
3434
{
3535
var settings = new TorSharpSettings
3636
{
37-
ZippedToolsDirectory = Path.Combine(torPath, "zip"),
38-
ExtractedToolsDirectory = Path.Combine(torPath, "bin"),
37+
ZippedToolsDirectory = Path.Combine(Tor.AppPath, "zip"),
38+
ExtractedToolsDirectory = Path.Combine(Tor.AppPath, "bin"),
3939
PrivoxySettings =
4040
{
4141
Port = proxy,
@@ -45,18 +45,86 @@ async Task RunAsync(int proxy, int socks, int control, CancellationToken cancell
4545
ControlPassword = ControlPassword,
4646
SocksPort = socks,
4747
ControlPort = control,
48+
//DataDirectory = Tor.DataDir,
4849
},
4950
};
5051

51-
await AnsiConsole.Status().StartAsync("Fetching Tor tools",
52-
async _ => await new TorSharpToolFetcher(settings, new HttpClient()).FetchAsync());
52+
var zipPath = await AnsiConsole.Status().StartAsync("Fetching Tor tools", async _ =>
53+
{
54+
var fetcher = new TorSharpToolFetcher(settings, new HttpClient());
55+
var updates = await fetcher.CheckForUpdatesAsync();
56+
await fetcher.FetchAsync(updates);
57+
return updates.Tor.DestinationPath;
58+
});
5359

5460
cancellation.ThrowIfCancellationRequested();
5561
var tor = new TorSharpProxy(settings);
5662

57-
await tor.ConfigureAndStartAsync();
63+
await tor.ConfigureAsync();
5864
cancellation.ThrowIfCancellationRequested();
5965

66+
var configPath = Path.Combine(
67+
settings.ExtractedToolsDirectory,
68+
Path.GetFileNameWithoutExtension(zipPath),
69+
"Data", "Tor", "torrc");
70+
71+
if (!File.Exists(configPath))
72+
throw new ArgumentException($"Tor configuration file not found at expected location {configPath}");
73+
74+
var torConfig = Tor.Config;
75+
76+
// Clear dotnet-tor configuration from previous runs.
77+
var allLines = (await File.ReadAllLinesAsync(configPath, cancellation)).ToList();
78+
var begin = allLines.IndexOf(ConfigDelimiter);
79+
80+
if (begin != -1)
81+
{
82+
var end = allLines.LastIndexOf(ConfigDelimiter);
83+
allLines.RemoveRange(begin, end != -1 && end != begin ? end - begin + 1 : allLines.Count - begin);
84+
await File.WriteAllLinesAsync(configPath, allLines.Where(line => !string.IsNullOrEmpty(line)), cancellation);
85+
}
86+
87+
var services = torConfig
88+
.Where(x => x.Section == "tor" && x.Subsection != null)
89+
.Select(x => x.Subsection!)
90+
.Distinct().ToList();
91+
92+
if (services.Count > 0)
93+
{
94+
await File.AppendAllLinesAsync(configPath, new[] { ConfigDelimiter }, cancellation);
95+
foreach (var service in services)
96+
{
97+
var config = torConfig.GetSection("tor", service);
98+
var serviceDir = Path.Combine(Tor.DataDir, service);
99+
await File.AppendAllLinesAsync(configPath, new[]
100+
{
101+
"HiddenServiceDir " + serviceDir,
102+
$"HiddenServicePort {config.GetNumber("port")} {config.GetString("service")}"
103+
}, cancellation);
104+
}
105+
await File.AppendAllLinesAsync(configPath, new[] { ConfigDelimiter }, cancellation);
106+
}
107+
108+
await tor.StartAsync();
109+
cancellation.ThrowIfCancellationRequested();
110+
111+
// Save successfully run args as settings
112+
Tor.Config.GetSection("tor")
113+
.SetNumber("proxy", proxy)
114+
.SetNumber("socks", socks)
115+
.SetNumber("control", control);
116+
117+
foreach (var service in services)
118+
{
119+
var hostName = Path.Combine(Tor.DataDir, service, "hostname");
120+
if (File.Exists(hostName))
121+
{
122+
var config = torConfig.GetSection("tor", service);
123+
var onion = await File.ReadAllTextAsync(hostName, cancellation);
124+
AnsiConsole.MarkupLine($"[yellow]Service {service} ({config.GetString("service")}):[/] [lime]{onion.Trim()}[/]");
125+
}
126+
}
127+
60128
while (!cancellation.IsCancellationRequested)
61129
Thread.Sleep(100);
62130
}

src/dotnet-tor/dotnet-tor.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
Usage:
1717

18-
> dotnet tor -?
18+
&gt; dotnet tor -?
1919
tor
2020
Tor proxy service
2121

@@ -36,9 +36,9 @@ Commands:
3636
</PropertyGroup>
3737

3838
<ItemGroup>
39-
<PackageReference Include="DotNetConfig.CommandLine" Version="1.0.3" />
40-
<!--<PackageReference Include="Knapcode.TorSharp" Version="2.6.0" />-->
39+
<PackageReference Include="DotNetConfig" Version="1.0.3" />
4140
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
41+
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.21216.1" />
4242
<PackageReference Include="ThisAssembly" Version="1.0.8" />
4343
<PackageReference Include="NuGet.Protocol" Version="5.9.1" />
4444
<PackageReference Include="Spectre.Console" Version="0.39.0" />

src/kzu.snk

-596 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)