Skip to content

Commit a1bcf93

Browse files
committed
Avoid starting a dead WebDav server edge-case
1 parent ca625e7 commit a1bcf93

1 file changed

Lines changed: 39 additions & 6 deletions

File tree

src/Core/SecureFolderFS.Core.WebDav/WebDavFileSystem.cs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ namespace SecureFolderFS.Core.WebDav
2222
/// <inheritdoc cref="IFileSystemInfo"/>
2323
public abstract class WebDavFileSystem : IFileSystemInfo
2424
{
25+
private const int MAX_LISTENER_START_ATTEMPTS = 10;
26+
2527
/// <inheritdoc/>
2628
public string Id { get; } = Constants.FileSystem.FS_ID;
2729

@@ -49,13 +51,12 @@ public virtual async Task<IVfsRoot> MountAsync(IFolder folder, IDisposable unloc
4951
if (!PortHelpers.IsPortAvailable(webDavOptions.Port))
5052
webDavOptions.SetPortInternal(PortHelpers.GetNextAvailablePort(webDavOptions.Port));
5153

52-
var prefix = $"{webDavOptions.Protocol}://{webDavOptions.Domain}:{webDavOptions.Port}/";
53-
var httpListener = new HttpListener();
54-
55-
httpListener.Prefixes.Add(prefix);
56-
httpListener.AuthenticationSchemes = AuthenticationSchemes.Anonymous;
54+
// Start the listener up-front (self-healing onto another port if the bind fails) so a genuine
55+
// failure surfaces to the caller and fails the unlocking operation, instead of leaving a silently-dead server.
56+
// Starting here also fixes webDavOptions.Port to the actually bound port before the platform
57+
// implementation derives the mount URL from it.
58+
var httpListener = StartListener(webDavOptions);
5759

58-
//var store = new EncryptingDiskStore(specifics.ContentFolder.Id, specifics, !specifics.Options.IsReadOnly);
5960
var rootFolder = new CryptoFolder(specifics.ContentFolder.Id, specifics.ContentFolder, specifics);
6061
var store = new BackedDavStore(rootFolder, !specifics.Options.IsReadOnly);
6162
var dispatcher = new WebDavDispatcher(new RootDiskStore(specifics.Options.VolumeName, store), new RequestHandlerProvider(), null);
@@ -68,6 +69,38 @@ public virtual async Task<IVfsRoot> MountAsync(IFolder folder, IDisposable unloc
6869
cancellationToken);
6970
}
7071

72+
/// <summary>
73+
/// Creates and starts an <see cref="HttpListener"/> for the configured WebDAV endpoint, with a fallback to
74+
/// the next available port if the bind fails (e.g., the port was taken between the availability check and the bind).
75+
/// </summary>
76+
/// <param name="options">The WebDAV options. The <see cref="WebDavOptions.Port"/> is updated to the actually bound port.</param>
77+
/// <returns>A started <see cref="HttpListener"/> bound to the resolved port.</returns>
78+
private static HttpListener StartListener(WebDavOptions options)
79+
{
80+
HttpListenerException? lastException = null;
81+
for (var attempt = 0; attempt < MAX_LISTENER_START_ATTEMPTS; attempt++)
82+
{
83+
var listener = new HttpListener();
84+
listener.Prefixes.Add($"{options.Protocol}://{options.Domain}:{options.Port}/");
85+
listener.AuthenticationSchemes = AuthenticationSchemes.Anonymous;
86+
87+
try
88+
{
89+
listener.Start();
90+
return listener;
91+
}
92+
catch (HttpListenerException ex)
93+
{
94+
// The port became unavailable between the check and the bind, proceed with self-heal onto the next free port
95+
lastException = ex;
96+
listener.Close();
97+
options.SetPortInternal(PortHelpers.GetNextAvailablePort(options.Port + 1));
98+
}
99+
}
100+
101+
throw lastException ?? new HttpListenerException();
102+
}
103+
71104
/// <inheritdoc/>
72105
public abstract Task<string> GetVolumeNameAsync(string candidateName, CancellationToken cancellationToken = default);
73106

0 commit comments

Comments
 (0)