Skip to content

Commit fef536d

Browse files
Report the rclone engine in storage requests
1 parent 0228c09 commit fef536d

16 files changed

Lines changed: 138 additions & 44 deletions

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
## [0.3.12] - 2026-09-21
6+
7+
- Include the verified bundled rclone version in ResoDrive's storage User-Agent so administrators can distinguish the client and storage engine versions.
8+
- Use the same client identity for mounts, sync jobs, setup checks and Nextcloud remote-wipe requests.
9+
510
## [0.3.11] - 2026-09-21
611

712
- Enable remote wipe for existing Nextcloud app-password connections after updating, without asking users to reconnect.
@@ -110,6 +115,7 @@ First public preview of ResoDrive.
110115
- Install through a small setup bundle that downloads the .NET Desktop Runtime
111116
only when it is missing.
112117

118+
[0.3.12]: https://github.com/alphasixtyfive/ResoDrive/releases/tag/v0.3.12
113119
[0.3.11]: https://github.com/alphasixtyfive/ResoDrive/releases/tag/v0.3.11
114120
[0.3.10]: https://github.com/alphasixtyfive/ResoDrive/tree/v0.3.10
115121
[0.3.9]: https://github.com/alphasixtyfive/ResoDrive/releases/tag/v0.3.9

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
1010
<Deterministic>true</Deterministic>
1111
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
12-
<VersionPrefix>0.3.11</VersionPrefix>
12+
<VersionPrefix>0.3.12</VersionPrefix>
1313
<ProductDisplayName>ResoDrive</ProductDisplayName>
1414
<ExecutableBaseName>resodrive</ExecutableBaseName>
1515
<ProductDescription>Mount Nextcloud, other WebDAV storage, and SFTP servers as drives in Windows</ProductDescription>

RELEASE_NOTES.md

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
1-
ResoDrive 0.3.11 adds remote wipe to existing Nextcloud app-password connections after updating. Users do not need to sign in again or re-add their drives.
1+
ResoDrive 0.3.12 reports the bundled rclone version alongside the ResoDrive and Windows versions in storage requests. Nextcloud administrators can now see which storage engine a connected ResoDrive client is using without installing a separate reporting service.
22

3-
- Reuses the URL, username and app password already saved in ResoDrive.
4-
- Checks for a pending wipe before automatic drives start.
5-
- Shows whether remote wipe is configured or needs attention beside each drive.
3+
The same client identity is used by mounted drives, sync jobs, setup checks and remote-wipe traffic. ResoDrive reads the version from its verified private rclone installation once when the host starts; it does not run another version check for every request.
64

7-
This replaces the 0.3.10 downloads, which did not automatically include older connections.
8-
9-
Nextcloud must explicitly request a wipe. It removes local ResoDrive accounts, cache and managed download folders; external folders, upload originals and server files are left alone. ResoDrive must be running and able to reach Nextcloud. This is not a whole-PC reset.
10-
11-
See the [remote-wipe guide](https://github.com/alphasixtyfive/ResoDrive/blob/v0.3.11/docs/REMOTE-WIPE.md) for setup, scope and the live-server acceptance steps to complete before relying on it.
12-
13-
Known limitation: the full desktop update from the previous public version, including cancellation and blocked shutdown, still needs checking on a disposable desktop.
5+
No hostname, account name, device identifier, file path or software inventory is added. Servers only see the header when the client makes an authenticated storage or wipe request.

docs/REMOTE-WIPE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Nextcloud remote wipe: administrator guide
22

3-
This guide describes ResoDrive 0.3.11. Earlier packages do not include all recovery
3+
This guide describes ResoDrive 0.3.12. Earlier packages do not include all recovery
44
fixes listed in the [changelog](../CHANGELOG.md). Server commands and UI below
55
were checked against Nextcloud's official documentation
66
and `stable32` source on 21 September 2026; live server acceptance is still required.
@@ -134,8 +134,8 @@ until it runs and can reach Nextcloud again. Each account uses its own app token
134134
without shared session cookies. HTTPS origin and port must match the registered
135135
server, redirects are not followed, and installation subdirectories are preserved.
136136

137-
These requests include the running ResoDrive version and available Windows
138-
edition/release/build details in their HTTP User-Agent. See
137+
These requests include the running ResoDrive version, verified private rclone version and available
138+
Windows edition/release/build details in their HTTP User-Agent. See
139139
[storage client identification](SECURITY.md#storage-client-identification) for
140140
the fields and server-log visibility. This does not add a separate reporting API.
141141

@@ -267,7 +267,7 @@ distinguishes HTTP 200 from 404.
267267
junction and an outside sentinel; cleanup must refuse the redirected path
268268
and preserve the outside file.
269269
11. Repeat with an older ResoDrive connection whose app password is saved but has
270-
no local wipe-check entry. Update to 0.3.11 and confirm **Remote wipe configured**
270+
no local wipe-check entry. Update to 0.3.12 and confirm **Remote wipe configured**
271271
appears without another login. Also queue a wipe before starting the updated
272272
host: cleanup must begin before automatic drives mount. Existing external
273273
sync folders must remain outside the wipe scope.

docs/SECURITY.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,21 @@ WebDAV setup, Nextcloud access/wipe checks and managed rclone storage requests
4949
identify the client with a `User-Agent` such as:
5050

5151
```text
52-
ResoDrive/0.3.11 (Windows 11; Edition: Professional; Release: 24H2; Build: 10.0.26100.9457; ClientArchitecture: x64; OSArchitecture: x64)
52+
ResoDrive/0.3.12 (Windows 11; Edition: Professional; Release: 25H2; Build: 10.0.26200.9457; ClientArchitecture: x64; OSArchitecture: x64; Rclone: v1.75.1)
5353
```
5454

5555
This is an example, not a minimum supported build. The product version comes from
56-
the running build. Windows details are read locally without elevation and cached
56+
the running build. The rclone version comes from ResoDrive's verified private
57+
runtime and is inspected once when the background host starts or setup begins,
58+
not once per request. Windows details are read locally without elevation and cached
5759
for the process lifetime; restart ResoDrive to refresh them after OS changes.
5860
Missing or unreadable optional registry details are omitted. Edition identifiers
5961
are preserved, including LTSC variants; Windows Server is distinguished from
6062
Windows 10/11 when its installation type is available. No hostname, account name,
6163
device identifier, file path or installed-software inventory is added to the
6264
header. Existing authenticated requests still carry the credentials they require.
6365

64-
The additional OS details are sent to configured storage services. GitHub update
66+
The additional OS and rclone details are sent to configured storage services. GitHub update
6567
checks and component downloads do not use this enriched header. An inherited
6668
`RCLONE_USER_AGENT` override is respected by managed rclone processes. SFTP does
6769
not use HTTP User-Agent headers.

src/ResoDrive.Host/Worker.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,20 @@ public sealed partial class Worker : BackgroundService
4141
internal bool CanResumeRemoteWipe { get; private set; } = true;
4242
internal bool RestartForRemoteWipe { get; private set; }
4343
private bool _recoveringRemoteWipe;
44+
private string _clientUserAgent = ClientUserAgent.Value;
45+
private readonly bool _inspectRuntimeUserAgent;
4446

4547
public Worker(
4648
ApplicationPaths paths,
4749
ILogger<Worker> logger,
4850
IHostApplicationLifetime applicationLifetime)
49-
: this(paths, logger, applicationLifetime, new RemoteWipeClient())
51+
: this(paths, logger, applicationLifetime, new RemoteWipeClient(), inspectRuntimeUserAgent: true)
5052
{
5153
}
5254

5355
internal Worker(ApplicationPaths paths, ILogger<Worker> logger,
5456
IHostApplicationLifetime applicationLifetime, RemoteWipeClient remoteWipeClient,
55-
RemoteWipeEnrollmentService? wipeEnrollment = null)
57+
RemoteWipeEnrollmentService? wipeEnrollment = null, bool inspectRuntimeUserAgent = false)
5658
{
5759
_paths = paths;
5860
_logger = logger;
@@ -61,10 +63,18 @@ internal Worker(ApplicationPaths paths, ILogger<Worker> logger,
6163
_remoteWipe = new RemoteWipeStore(paths);
6264
_wipeCoordinator = new(paths, _remoteWipeClient);
6365
_wipeEnrollment = wipeEnrollment ?? new(paths);
66+
_inspectRuntimeUserAgent = inspectRuntimeUserAgent;
6467
}
6568

6669
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
6770
{
71+
if (_inspectRuntimeUserAgent)
72+
{
73+
var runtime = await new RcloneRuntimeLocator(_paths).InspectAsync(stoppingToken).ConfigureAwait(false);
74+
if (runtime.Succeeded)
75+
_clientUserAgent = ClientUserAgent.WithRcloneVersion(runtime.Value?.Version);
76+
}
77+
_remoteWipeClient.SetClientUserAgent(_clientUserAgent);
6878
// Resume accepted commands before loading settings or starting any automatic work.
6979
if (new RemoteWipeStateStore(_paths).Read() is { Phase: not RemoteWipePhase.Completed })
7080
{
@@ -691,7 +701,7 @@ await CheckForRemoteWipeAsync(token).ConfigureAwait(false))
691701
_syncs?.Dispose();
692702
_syncs = null;
693703
}
694-
_mounts ??= new(rclone, config, _paths, new MountTargetInventory());
704+
_mounts ??= new(rclone, config, _paths, new MountTargetInventory(), _clientUserAgent);
695705
var mountCoordinator = _mounts;
696706
var reconciled = await mountCoordinator.ReconcileAsync(definitions, token).ConfigureAwait(false);
697707
if (!reconciled.Succeeded)
@@ -723,7 +733,7 @@ await CheckForRemoteWipeAsync(token).ConfigureAwait(false))
723733
{
724734
_lastRuns.TryRemove(obsoleteId, out _);
725735
}
726-
_syncs ??= new(rclone, config, _paths, () => _definitions);
736+
_syncs ??= new(rclone, config, _paths, () => _definitions, _clientUserAgent);
727737
_rclonePath = rclone;
728738
_configPath = config;
729739
var isFirstLoad = _settings is null;

src/ResoDrive.Windows/Mounting/RcloneMountCoordinator.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public sealed class RcloneMountCoordinator : IAsyncDisposable
1818
private readonly string _rclonePath;
1919
private readonly string _configPath;
2020
private readonly ApplicationPaths _paths;
21+
private readonly string _clientUserAgent;
2122
private readonly IMountTargetInventory _inventory;
2223
private readonly MountOwnershipStore _ownership;
2324
private readonly ConcurrentDictionary<MountId, MountDefinition> _definitions = new();
@@ -30,11 +31,20 @@ public sealed class RcloneMountCoordinator : IAsyncDisposable
3031
private bool _recovered;
3132

3233
public RcloneMountCoordinator(string rclonePath, string configPath, ApplicationPaths paths, IMountTargetInventory targetInventory)
34+
: this(rclonePath, configPath, paths, targetInventory, ClientUserAgent.Value)
35+
{
36+
}
37+
38+
public RcloneMountCoordinator(string rclonePath, string configPath, ApplicationPaths paths,
39+
IMountTargetInventory targetInventory, string clientUserAgent)
3340
{
3441
_rclonePath = Path.GetFullPath(rclonePath);
3542
_configPath = Path.GetFullPath(configPath);
3643
_paths = paths ?? throw new ArgumentNullException(nameof(paths));
3744
_inventory = targetInventory ?? throw new ArgumentNullException(nameof(targetInventory));
45+
_clientUserAgent = string.IsNullOrWhiteSpace(clientUserAgent)
46+
? throw new ArgumentException("A client User-Agent is required.", nameof(clientUserAgent))
47+
: clientUserAgent;
3848
_paths.EnsureCreated();
3949
_ownership = new(paths);
4050
}
@@ -570,7 +580,8 @@ private IEnumerable<string> Arguments(MountDefinition definition, RcloneControl
570580
yield return "--config";
571581
yield return _configPath;
572582
yield return "--ask-password=false";
573-
foreach (var argument in RcloneUserAgentArguments.Create(definition.Arguments))
583+
foreach (var argument in RcloneUserAgentArguments.Create(
584+
definition.Arguments, Environment.GetEnvironmentVariable("RCLONE_USER_AGENT"), _clientUserAgent))
574585
yield return argument;
575586
if (File.Exists(_paths.ConfigSecretFile))
576587
{

src/ResoDrive.Windows/Profiles/ProfileProvisioningService.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,13 @@ public async Task<OperationResult<ProfileProvisioningResult>> ProvisionAsync(
147147
installation.Error?.Code ?? "rclone.bundled_invalid",
148148
installation.Error?.Message ?? "The managed rclone component is not installed.",
149149
installation.Error?.IsTransient ?? false);
150+
var clientUserAgent = ClientUserAgent.WithRcloneVersion(installation.Value.Version);
150151

151152
if (profile.Connection is WebDavConnectionDefinition)
152153
{
153154
progress?.Report("Checking account");
154155
var credentialCheck = await CheckWebDavCredentialsAsync(
155-
endpoint, request.Username, normalizedPassword, cancellationToken).ConfigureAwait(false);
156+
endpoint, request.Username, normalizedPassword, clientUserAgent, cancellationToken).ConfigureAwait(false);
156157
if (!credentialCheck.Succeeded)
157158
return Failure(
158159
credentialCheck.Error?.Code ?? "setup.credentials",
@@ -265,7 +266,9 @@ await _mutation.AppendSftpKeyFileRemoteAsync(
265266
var verification = await RunCheckedAsync(
266267
installation.Value.ExecutablePath,
267268
["--config", stagedConfig, "--ask-password=false", "--password-command", configPasswordCommand,
268-
"lsf", source, "--max-depth", "1", .. RcloneUserAgentArguments.Create([])],
269+
"lsf", source, "--max-depth", "1",
270+
.. RcloneUserAgentArguments.Create([], Environment.GetEnvironmentVariable("RCLONE_USER_AGENT"),
271+
clientUserAgent)],
269272
null, null, cancellationToken).ConfigureAwait(false);
270273
if (!verification.Succeeded)
271274
return FailureFrom(verification, "setup.remote_check", "The configured remote could not be verified.");
@@ -384,11 +387,12 @@ internal static string ValidateSftpKeyFile(string path)
384387
}
385388

386389
private async Task<OperationResult> CheckWebDavCredentialsAsync(
387-
Uri endpoint, string username, string password, CancellationToken cancellationToken)
390+
Uri endpoint, string username, string password, string clientUserAgent,
391+
CancellationToken cancellationToken)
388392
{
389393
using var request = new HttpRequestMessage(new HttpMethod("PROPFIND"), endpoint);
390394
request.Headers.Add("Depth", "0");
391-
request.Headers.UserAgent.ParseAdd(ClientUserAgent.Value);
395+
request.Headers.UserAgent.ParseAdd(clientUserAgent);
392396
request.Headers.Authorization = new AuthenticationHeaderValue(
393397
"Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")));
394398
try

src/ResoDrive.Windows/Rclone/RcloneUserAgentArguments.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ namespace ResoDrive.Windows;
33
internal static class RcloneUserAgentArguments
44
{
55
public static IReadOnlyList<string> Create(IEnumerable<string> arguments) =>
6-
Create(arguments, Environment.GetEnvironmentVariable("RCLONE_USER_AGENT"));
6+
Create(arguments, Environment.GetEnvironmentVariable("RCLONE_USER_AGENT"), ClientUserAgent.Value);
77

8-
internal static IReadOnlyList<string> Create(IEnumerable<string> arguments, string? inheritedUserAgent)
8+
internal static IReadOnlyList<string> Create(
9+
IEnumerable<string> arguments,
10+
string? inheritedUserAgent,
11+
string? defaultUserAgent = null)
912
{
1013
// Keep explicit rclone overrides. The manager's default is only for
1114
// requests that would otherwise use rclone's generic identification.
@@ -14,6 +17,6 @@ internal static IReadOnlyList<string> Create(IEnumerable<string> arguments, stri
1417
argument.StartsWith("--user-agent=", StringComparison.OrdinalIgnoreCase)))
1518
return [];
1619

17-
return ["--user-agent", ClientUserAgent.Value];
20+
return ["--user-agent", defaultUserAgent ?? ClientUserAgent.Value];
1821
}
1922
}

src/ResoDrive.Windows/Security/RemoteWipeClient.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ public sealed class RemoteWipeClient : IDisposable
1111
private readonly HttpClient _httpClient;
1212
private readonly bool _ownsClient;
1313
private readonly TimeSpan _requestTimeout;
14+
private string _clientUserAgent;
1415

1516
public RemoteWipeClient(HttpClient? httpClient = null, TimeSpan? requestTimeout = null)
1617
{
1718
_ownsClient = httpClient is null;
1819
_requestTimeout = requestTimeout ?? TimeSpan.FromSeconds(15);
20+
_clientUserAgent = ClientUserAgent.Value;
1921
// Injected clients must preserve the same redirect and cookie isolation.
2022
_httpClient = httpClient ?? CreateHttpClient();
2123
}
@@ -32,6 +34,12 @@ public RemoteWipeClient(HttpClient? httpClient = null, TimeSpan? requestTimeout
3234
Timeout = Timeout.InfiniteTimeSpan
3335
};
3436

37+
public void SetClientUserAgent(string value)
38+
{
39+
ArgumentException.ThrowIfNullOrWhiteSpace(value);
40+
Volatile.Write(ref _clientUserAgent, value);
41+
}
42+
3543
public async Task<bool> IsWipeRequestedAsync(
3644
RemoteWipeRegistration registration,
3745
CancellationToken cancellationToken = default)
@@ -45,7 +53,7 @@ public async Task<bool> IsWipeRequestedAsync(
4553

4654
using var probe = new HttpRequestMessage(new HttpMethod("PROPFIND"), probeEndpoint);
4755
probe.Headers.Add("Depth", "0");
48-
probe.Headers.UserAgent.ParseAdd(ClientUserAgent.Value);
56+
probe.Headers.UserAgent.ParseAdd(Volatile.Read(ref _clientUserAgent));
4957
AddBasicAuthentication(probe, registration.Username, registration.AppToken);
5058

5159
try
@@ -57,7 +65,8 @@ public async Task<bool> IsWipeRequestedAsync(
5765
return false;
5866
}
5967

60-
using var check = CreateTokenRequest(serverBase, "index.php/core/wipe/check", registration.AppToken);
68+
using var check = CreateTokenRequest(
69+
serverBase, "index.php/core/wipe/check", registration.AppToken, Volatile.Read(ref _clientUserAgent));
6170
using var checkResponse = await _httpClient.SendAsync(
6271
check, HttpCompletionOption.ResponseHeadersRead, requestToken).ConfigureAwait(false);
6372
if (checkResponse.StatusCode != HttpStatusCode.OK)
@@ -95,7 +104,8 @@ public async Task<bool> SignalSuccessAsync(
95104
throw new InvalidOperationException("The remote-wipe registration is not a valid HTTPS endpoint.");
96105
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
97106
deadline.CancelAfter(_requestTimeout);
98-
using var request = CreateTokenRequest(serverBase, "index.php/core/wipe/success", registration.AppToken);
107+
using var request = CreateTokenRequest(
108+
serverBase, "index.php/core/wipe/success", registration.AppToken, Volatile.Read(ref _clientUserAgent));
99109
using var response = await _httpClient.SendAsync(
100110
request, HttpCompletionOption.ResponseHeadersRead, deadline.Token).ConfigureAwait(false);
101111
// Nextcloud removes the token on acknowledgement. A retry after a lost response
@@ -109,13 +119,14 @@ public async Task<bool> SignalSuccessAsync(
109119
return true;
110120
}
111121

112-
private static HttpRequestMessage CreateTokenRequest(Uri serverBase, string path, string token)
122+
private static HttpRequestMessage CreateTokenRequest(
123+
Uri serverBase, string path, string token, string clientUserAgent)
113124
{
114125
var request = new HttpRequestMessage(HttpMethod.Post, new Uri(serverBase, path))
115126
{
116127
Content = new FormUrlEncodedContent([new KeyValuePair<string, string>("token", token)])
117128
};
118-
request.Headers.UserAgent.ParseAdd(ClientUserAgent.Value);
129+
request.Headers.UserAgent.ParseAdd(clientUserAgent);
119130
return request;
120131
}
121132

0 commit comments

Comments
 (0)