Public Access
Keep downloads inside the folder they were sent to, never run a file opened for editing, and let a bucket upload be retried
- A remote name went straight into Path.Combine. A POSIX name may hold backslashes, a colon or a drive letter, and an S3 key may hold "../", so a name chosen by whoever could write to the host decided where on this machine a download landed. LocalDirectory.ChildPath cleans the name and requires the resolved parent to be the target folder; every download path and EDIT use it, and a name that cannot be a file there is left on the host and reported. - EDIT handed the copy to the shell's default action, which for .bat, .hta, .js and friends is to run it. LocalEditor opens such a file with its own "edit" verb or Notepad on Windows and `open -t` on macOS, and refuses a .desktop launcher on Linux. - A failed bucket upload leaves its part object, and a retry asked for an offset no bucket accepts, so the row could never finish. IRemoteFileStore.CanWriteFromAnOffset is false for buckets and the queue starts those over. - The SFTP host picker re-found its selection from the empty moment of every vault reload and landed on the first host once a minute; the chosen host is now remembered, and a typed password is only sent to the host it was typed for.
This commit is contained in:
1 parent
4c7f47c026
commit
03c8e8589a
12 files changed
+586
-83
No files matched your search
@@ -0,0 +1,126 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace DodoSSH.Client.App.Platform;
|
||||
|
||||
/// <summary>
|
||||
/// Opens a local copy of a remote file for editing, without running it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// One of the two halves of editing a remote file in place, and both are this platform's rather than the
|
||||
/// shell's: what opens a <c>.conf</c> is a registry association, a desktop entry or a Launch Services binding
|
||||
/// depending on which desktop this is. The shell decides <em>when</em>; see
|
||||
/// <c>TransfersViewModel.EditRemoteAsync</c>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// ◆ <b>"Open" is not "edit", and for a file somebody else named the difference is code execution.</b> The
|
||||
/// file comes from a host, and whoever can write to that host chooses its name. Handing <c>deploy.bat</c>,
|
||||
/// <c>notes.hta</c> or <c>run.command</c> to the desktop's default action runs it, on this machine, as this
|
||||
/// user — so a file whose default action is to run is opened with an editor instead. On Windows that is the
|
||||
/// type's own <c>edit</c> verb where it has one (Notepad, for <c>.bat</c>, <c>.cmd</c>, <c>.js</c>,
|
||||
/// <c>.vbs</c>, <c>.ps1</c> and <c>.reg</c>) and Notepad where it has not; on macOS it is <c>open -t</c>,
|
||||
/// which is the default text editor whatever the type. Everything else still goes to its association, which
|
||||
/// is the point of the feature — an <c>nginx.conf</c> should open in whatever this person edits configs with.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Linux is the exception: there is no portable "the text editor", so a <c>.desktop</c> entry — the one type
|
||||
/// <c>xdg-open</c> may launch rather than open — is refused with a sentence instead. A shell script is opened
|
||||
/// by every mainstream desktop's file associations in an editor, not run.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The process is not waited on and is deliberately not kept: an editor may hand off to an already-open
|
||||
/// instance and exit at once, so its lifetime says nothing about whether the file is still being edited.
|
||||
/// What ends an edit session is the user saying so — see the file browser's STOP EDITING.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal static class LocalEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Extensions Windows runs rather than opens, beyond whatever <c>PATHEXT</c> lists on this machine.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>PATHEXT</c> covers the console's notion of executable (<c>.exe</c>, <c>.bat</c>, <c>.js</c>,
|
||||
/// <c>.vbs</c>…) and is read as well, because an interpreter's installer may add its own. These are the
|
||||
/// ones Explorer acts on that are not in it: shortcuts, installers, HTML applications, control panel
|
||||
/// items, and the script hosts that installers do not register there.
|
||||
/// </remarks>
|
||||
private static readonly string[] WindowsRunsThese =
|
||||
[
|
||||
".application", ".appref-ms", ".bat", ".chm", ".cmd", ".com", ".cpl", ".diagcab", ".exe", ".hta",
|
||||
".inf", ".jar", ".js", ".jse", ".library-ms", ".lnk", ".msc", ".msi", ".msp", ".pif", ".ps1",
|
||||
".psm1", ".py", ".pyw", ".reg", ".scf", ".scr", ".search-ms", ".settingcontent-ms", ".url",
|
||||
".vbe", ".vbs", ".website", ".ws", ".wsc", ".wsf", ".wsh", ".xll",
|
||||
];
|
||||
|
||||
/// <summary>Extensions Launch Services runs, or installs, rather than opens.</summary>
|
||||
private static readonly string[] MacRunsThese =
|
||||
[
|
||||
".app", ".command", ".dmg", ".fileloc", ".inetloc", ".jar", ".mpkg", ".pkg", ".scpt", ".terminal",
|
||||
".tool", ".webloc", ".workflow",
|
||||
];
|
||||
|
||||
/// <summary>Opens <paramref name="path"/> for editing.</summary>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// This desktop would run the file and has no editor to offer in its place.
|
||||
/// </exception>
|
||||
internal static Task OpenAsync(string path)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(path);
|
||||
|
||||
using var opened = Process.Start(StartInfoFor(path));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>What to start so that <paramref name="path"/> opens in an editor.</summary>
|
||||
internal static ProcessStartInfo StartInfoFor(string path)
|
||||
{
|
||||
if (OperatingSystem.IsWindows() && WouldRunOnWindows(path))
|
||||
{
|
||||
var edit = new ProcessStartInfo(path) { UseShellExecute = true, Verb = "edit" };
|
||||
|
||||
if (edit.Verbs.Contains("edit", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return edit;
|
||||
}
|
||||
|
||||
return new ProcessStartInfo("notepad.exe") { UseShellExecute = false, ArgumentList = { path } };
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsMacOS() && HasExtension(path, MacRunsThese))
|
||||
{
|
||||
return new ProcessStartInfo("open") { UseShellExecute = false, ArgumentList = { "-t", path } };
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux() && HasExtension(path, [".desktop"]))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{Path.GetFileName(path)} is a launcher, which this desktop would run rather than open. "
|
||||
+ "Download it and open it in an editor instead.");
|
||||
}
|
||||
|
||||
return new ProcessStartInfo(path) { UseShellExecute = true };
|
||||
}
|
||||
|
||||
/// <summary>Whether Windows' default action for this file is to run it.</summary>
|
||||
internal static bool WouldRunOnWindows(string path, string? pathExt = null)
|
||||
{
|
||||
var listed = (pathExt ?? Environment.GetEnvironmentVariable("PATHEXT") ?? string.Empty)
|
||||
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
return HasExtension(path, WindowsRunsThese) || HasExtension(path, listed);
|
||||
}
|
||||
|
||||
/// <summary>Whether the file's last extension is one of these, ignoring case.</summary>
|
||||
/// <remarks>
|
||||
/// Trailing dots and spaces are dropped first, because Windows drops them too: <c>run.bat.</c> is
|
||||
/// <c>run.bat</c> by the time anything acts on it.
|
||||
/// </remarks>
|
||||
private static bool HasExtension(string path, IEnumerable<string> extensions)
|
||||
{
|
||||
var extension = Path.GetExtension(Path.GetFileName(path).TrimEnd('.', ' '));
|
||||
|
||||
return extension.Length > 0
|
||||
&& extensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user