This commit is contained in:
crschnick 2025-04-14 01:20:43 +00:00
parent 041fb45778
commit 466f5d4a75
10 changed files with 140 additions and 80 deletions

View file

@ -1,15 +1,18 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.ext.ConnectionFileSystem;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.FileBridge;
import io.xpipe.app.util.FileOpener;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ElevationFunction;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileInfo;
import io.xpipe.core.store.FilePath;
import java.io.FilterOutputStream;
import java.io.IOException;
@ -31,19 +34,12 @@ public class BrowserFileOpener {
}
var info = (FileInfo.Unix) file.getInfo();
if (info.getPermissions() == null) {
var requiresSudo = requiresSudo(model, info, file.getPath());
if (!requiresSudo) {
return fileSystem.openOutput(file.getPath(), totalBytes);
}
var zero = Integer.valueOf(0);
var otherWrite = info.getPermissions().charAt(7) == 'w';
var requiresRoot = zero.equals(info.getUid()) && zero.equals(info.getGid()) && !otherWrite;
if (!requiresRoot || model.getCache().isRoot()) {
return fileSystem.openOutput(file.getPath(), totalBytes);
}
var elevate = AppWindowHelper.showConfirmationAlert(
"app.fileWriteSudoTitle", "app.fileWriteSudoHeader", "app.fileWriteSudoContent");
var elevate = AppDialog.confirm("fileWriteSudo");
if (!elevate) {
return fileSystem.openOutput(file.getPath(), totalBytes);
}
@ -66,6 +62,29 @@ public class BrowserFileOpener {
}
}
private static boolean requiresSudo(BrowserFileSystemTabModel model, FileInfo.Unix info, FilePath filePath) throws Exception {
if (model.getCache().isRoot()) {
return false;
}
if (info != null) {
var otherWrite = info.getPermissions().charAt(7) == 'w';
if (otherWrite) {
return false;
}
var userOwned = info.getUid() != null && model.getCache().getUidForUser(model.getCache().getUsername()) == info.getUid() ||
info.getUser() != null && model.getCache().getUsername().equals(info.getUser());
var userWrite = info.getPermissions().charAt(1) == 'w';
if (userOwned && userWrite) {
return false;
}
}
var test = model.getFileSystem().getShell().orElseThrow().command(CommandBuilder.of().add("test", "-w").addFile(filePath)).executeAndCheck();
return !test;
}
private static int calculateKey(FileEntry entry) {
return Objects.hash(entry.getPath(), entry.getFileSystem(), entry.getKind(), entry.getInfo());
}

View file

@ -136,7 +136,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
.getAccentColor()
.desaturate()
.desaturate();
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(16, 1, 14, 2)));
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(17, 1, 15, 2)));
},
Platform.getPreferences().accentColorProperty());
var hoverBorder = Bindings.createObjectBinding(
@ -146,7 +146,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
.darker()
.desaturate()
.desaturate();
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(16, 1, 14, 2)));
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(17, 1, 15, 2)));
},
Platform.getPreferences().accentColorProperty());
var noneBorder = Bindings.createObjectBinding(

View file

@ -21,23 +21,6 @@ public class ContainerStoreState extends ShellStoreState {
String containerState;
Boolean shellMissing;
public boolean isExited() {
if (containerState == null) {
return false;
}
return containerState.toLowerCase().contains("exited");
}
public boolean isRunning() {
if (containerState == null) {
return false;
}
return containerState.toLowerCase().contains("running")
|| containerState.toLowerCase().contains("up");
}
@Override
public DataStoreState mergeCopy(DataStoreState newer) {
var n = (ContainerStoreState) newer;

View file

@ -121,7 +121,7 @@ public class TerminalLauncher {
TerminalInitFunction.none()),
true);
var config = new TerminalLaunchConfiguration(null, title, title, true, script, sc.getShellDialect());
type.launch(config);
launch(type, config, new CountDownLatch(1));
}
}
@ -164,18 +164,23 @@ public class TerminalLauncher {
var config = TerminalLaunchConfiguration.create(
request, entry, cleanTitle, adjustedTitle, preferTabs, promptRestart);
if (preferTabs && launchMultiplexerTabInExistingTerminal(request, terminalConfig, config)) {
latch.await();
return;
}
synchronized (TerminalLauncher.class) {
// There will be timing issues when launching multiple tabs in a short time span
TerminalMultiplexerManager.synchronizeMultiplexerLaunchTiming();
if (preferTabs) {
var multiplexerConfig = launchMultiplexerTabInNewTerminal(request, terminalConfig, config);
if (multiplexerConfig.isPresent()) {
TerminalMultiplexerManager.registerMultiplexerLaunch(request);
launch(type, multiplexerConfig.get(), latch);
if (preferTabs && launchMultiplexerTabInExistingTerminal(request, terminalConfig, config)) {
latch.await();
return;
}
if (preferTabs) {
var multiplexerConfig = launchMultiplexerTabInNewTerminal(request, terminalConfig, config);
if (multiplexerConfig.isPresent()) {
TerminalMultiplexerManager.registerMultiplexerLaunch(request);
launch(type, multiplexerConfig.get(), latch);
return;
}
}
}
var proxyConfig = launchProxy(request, config);
@ -194,6 +199,10 @@ public class TerminalLauncher {
private static void launch(ExternalTerminalType type, TerminalLaunchConfiguration config, CountDownLatch latch)
throws Exception {
if (type == null) {
return;
}
try {
type.launch(config);
latch.await();

View file

@ -3,11 +3,14 @@ package io.xpipe.app.terminal;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.ThreadHelper;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
public class TerminalMultiplexerManager {
private static UUID pendingMultiplexerLaunch;
private static Instant lastCheck = Instant.now();
private static final Map<UUID, TerminalMultiplexer> connectionHubRequests = new HashMap<>();
public static void registerMultiplexerLaunch(UUID uuid) {
@ -29,7 +32,12 @@ public class TerminalMultiplexerManager {
return Optional.ofNullable(multiplexer);
}
public static boolean requiresNewTerminalSession(UUID requestUuid) {
public static void synchronizeMultiplexerLaunchTiming() {
var mult = getEffectiveMultiplexer();
if (mult.isEmpty()) {
return;
}
// Wait if we are currently opening a new multiplexer
if (pendingMultiplexerLaunch != null) {
// Wait for max 10s
@ -44,6 +52,16 @@ public class TerminalMultiplexerManager {
ThreadHelper.sleep(1000);
}
// Synchronize between multiple existing tab launches as well as some multiplexers might break there
var elapsed = Duration.between(lastCheck, Instant.now()).toMillis();
if (elapsed < 1000) {
ThreadHelper.sleep(1000 - elapsed);
}
lastCheck = Instant.now();
}
public static boolean requiresNewTerminalSession(UUID requestUuid) {
var mult = getEffectiveMultiplexer();
if (mult.isEmpty()) {
connectionHubRequests.put(requestUuid, null);

View file

@ -36,6 +36,7 @@ Various improvements were made to the SSH implementation:
- There is now built-in support to refresh an SSO openpubkey with the opkssh tool when needed
- When the SSH password is set to none, XPipe will no longer prompt for it anyway if the preferred auth failed
- The VSCode SSH remote integration has been reworked to allow more connections it to be opened in vscode. It now supports essentially all simple SSH connections, custom SSH connections, SSH config connections, and VM SSH connections. This support includes gateways
- There is now the option to enable verbose ssh output to diagnose connection issues better
## Mini mode
@ -47,6 +48,7 @@ The application window will now hide any unnecessary sidebars when being resized
- Various speed improvements for shell operations
- Various startup speed improvements
- Many dialog windows have now been merged with the main window
- The settings menu has been updated to support continuous scrolling
- The scripts context menu now shows the respective scripts icons instead of generic ones
- Key files synced via git are now synced as pairs if a public key is available
- You can now launch custom scripts within XPipe with a command output dialog window without having to open a terminal
@ -54,6 +56,10 @@ The application window will now hide any unnecessary sidebars when being resized
- The k8s integration will now automatically add all namespaces for the current context when searching for connections
- Various error messages now contain a help link to the documentation
- The Windows application will now block the shutdown until save/sync has finished, preventing vault corruption caused by a sudden shutdown
- Various installation types like the linux apt/rpm repository and homebrew installations now support automatic updates as well
- RDP tunnel connections can now be configured with custom authentication and additional RDP options
- Improve sudo file write to support more permission cases
- The predefined sample script selection has been updated
- Add setting to disable HTTPs TLS verification for license activation API calls for cases where TLS traffic is decrypted in your organization
- Upgrade to GraalVM 24
@ -69,3 +75,5 @@ The application window will now hide any unnecessary sidebars when being resized
- Fix some file browser terminal dock window ordering issues
- Fix repeated file browser errors when remote system did not have the stat command
- Fix Windows terminal launch failing if default profile was set to launch as admin
- Fix tailscale login check not opening website on Linux
- Fix terminal connections failing to launch for some systems with a read-only file system

View file

@ -4,14 +4,18 @@ import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ScriptHelper;
import io.xpipe.app.util.ShellTemp;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.process.ShellDialect;
import io.xpipe.core.process.ShellTerminalInitCommand;
import io.xpipe.core.process.*;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.util.FailableFunction;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Supplier;
public class ScriptStoreSetup {
@ -26,6 +30,34 @@ public class ScriptStoreSetup {
return;
}
// If we don't have write permissions / it is a read-only file system, don't create scripts
if (pc.getOsType() == OsType.LINUX) {
var test = pc.command(CommandBuilder.of().add("test", "-w").addFile(pc.getSystemTemporaryDirectory())).executeAndCheck();
if (!test) {
return;
}
}
var checkedPermissions = new AtomicReference<Boolean>();
FailableFunction<ShellControl, Boolean, Exception> permissionCheck = (sc) -> {
if (checkedPermissions.get() != null) {
return checkedPermissions.get();
}
// If we don't have write permissions / it is a read-only file system, don't create scripts
if (sc.getOsType() == OsType.LINUX) {
var file = sc.getSystemTemporaryDirectory().join("xpipe-test");
var test = sc.command(CommandBuilder.of().add("touch").addFile(file).add("&&", "rm").addFile(file)).executeAndCheck();
if (!test) {
checkedPermissions.set(false);
return false;
}
}
checkedPermissions.set(true);
return true;
};
var initFlattened = flatten(enabledScripts).stream()
.filter(store -> store.getStore().isInitScript())
.toList();
@ -41,7 +73,11 @@ public class ScriptStoreSetup {
initFlattened.forEach(s -> {
pc.withInitSnippet(new ShellTerminalInitCommand() {
@Override
public Optional<String> terminalContent(ShellControl shellControl) {
public Optional<String> terminalContent(ShellControl shellControl) throws Exception {
if (!permissionCheck.apply(shellControl)) {
return Optional.empty();
}
return Optional.ofNullable(s.getStore().assembleScriptChain(shellControl));
}
@ -58,6 +94,10 @@ public class ScriptStoreSetup {
@Override
public Optional<String> terminalContent(ShellControl shellControl) throws Exception {
if (!permissionCheck.apply(shellControl)) {
return Optional.empty();
}
if (dir == null) {
dir = initScriptsDirectory(shellControl, bringFlattened);
}

View file

@ -12,6 +12,7 @@ import io.xpipe.ext.base.store.StartableStore;
import io.xpipe.ext.base.store.StoppableStore;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.ext.system.incus.IncusContainerStore;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.experimental.SuperBuilder;
@ -76,39 +77,31 @@ public class LxdContainerStore
@Override
public ShellControl control(ShellControl parent) {
var user = identity != null ? identity.unwrap().getUsername() : null;
var base = new LxdCommandView(parent).exec(containerName, user, () -> {
var sc = new LxdCommandView(parent).exec(containerName, user, () -> {
var state = getState();
var alpine = state.getOsName() != null
&& state.getOsName().toLowerCase().contains("alpine");
return alpine;
});
if (identity != null && identity.unwrap().getPassword() != null) {
base.setElevationHandler(new BaseElevationHandler(
sc.setElevationHandler(new BaseElevationHandler(
LxdContainerStore.this, identity.unwrap().getPassword())
.orElse(base.getElevationHandler()));
.orElse(sc.getElevationHandler()));
}
return base.withSourceStore(LxdContainerStore.this)
.onInit(shellControl -> {
var s = getState().toBuilder()
.osType(shellControl.getOsType())
.shellDialect(shellControl.getShellDialect())
.ttyState(shellControl.getTtyState())
.running(true)
.osName(shellControl.getOsName())
.build();
setState(s);
})
.onStartupFail(throwable -> {
if (throwable instanceof LicenseRequiredException) {
return;
}
sc.withSourceStore(LxdContainerStore.this);
sc.withShellStateInit(LxdContainerStore.this);
sc.onStartupFail(throwable -> {
if (throwable instanceof LicenseRequiredException) {
return;
}
var s = getState().toBuilder()
.running(false)
.containerState("Connection failed")
.build();
setState(s);
});
var s = getState().toBuilder()
.running(false)
.containerState("Connection failed")
.build();
setState(s);
});
return sc;
}
};
}

View file

@ -140,16 +140,6 @@ public class PodmanContainerStore
var pc = new PodmanCommandView(parent).container().exec(containerName);
pc.withSourceStore(PodmanContainerStore.this);
pc.withShellStateInit(PodmanContainerStore.this);
pc.onInit(shellControl -> {
var s = getState().toBuilder()
.osType(shellControl.getOsType())
.shellDialect(shellControl.getShellDialect())
.ttyState(shellControl.getTtyState())
.running(true)
.osName(shellControl.getOsName())
.build();
setState(s);
});
pc.onStartupFail(throwable -> {
if (throwable instanceof LicenseRequiredException) {
return;

View file

@ -504,8 +504,8 @@ blue=Blue
red=Red
asktextAlertTitle=Prompt
fileWriteSudoTitle=Sudo file write
fileWriteSudoHeader=The file you are trying to write requires root privileges. Do you want to write this file with sudo?
fileWriteSudoContent=This will automatically elevate to root with either the provided credentials or via a prompt.
#force
fileWriteSudoContent=The file you are trying to write requires root privileges. Do you want to write this file with sudo? This will automatically elevate to root with either the provided credentials or via a prompt.
dontAllowTerminalRestart=Don't allow terminal restart
dontAllowTerminalRestartDescription=By default, terminal sessions can be restarted after they ended from within the terminal. To allow this, XPipe will accept these external requests from the terminal to launch the session again\n\nXPipe doesn't have any control over the terminal and where this call comes from, so malicious local applications can use this functionality as well to launch connections through XPipe. Disabling this functionality prevents this scenario.
openDocumentation=Open documentation