Rework RDP

This commit is contained in:
crschnick
2026-06-25 13:57:45 +00:00
parent 476d7668c6
commit d4b0347ac4
17 changed files with 694 additions and 97 deletions
@@ -9,6 +9,7 @@ import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.Validator;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.LocalShell;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.Validators;
@@ -129,7 +130,7 @@ public class CustomAgentStrategy implements SshIdentityAgentStrategy {
agent = AppPrefs.get().defaultSshAgentSocket().getValue();
}
if (agent != null) {
return agent.resolveTildeHome(sc.view().userHome());
return FilePath.of(LocalShell.getShell().getShellDialect().evaluateExpression(LocalShell.getShell(), agent.toString()).readStdoutOrThrow());
}
}
@@ -16,6 +16,7 @@ import javafx.beans.property.Property;
import io.modelcontextprotocol.spec.McpSchema;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
@@ -91,4 +92,6 @@ public abstract class ProcessControlProvider {
public abstract void refreshWsl();
public abstract void showWebtopDeploymentDialog();
public abstract void importRdpFile(Path file) throws Exception;
}
@@ -1,11 +1,16 @@
package io.xpipe.app.hub.comp;
import io.xpipe.app.action.AbstractAction;
import io.xpipe.app.browser.BrowserFileChooserSessionComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.*;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.platform.MenuHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.ScanDialog;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.collections.ObservableList;
@@ -169,6 +174,21 @@ public class StoreCreationMenu {
item.setDisable(!dataStoreProvider.allowCreation());
menu.getItems().add(item);
}
if (categories[0].equals(DataStoreCreationCategory.DESKTOP)) {
var rdpFile = MenuHelper.createMenuItem(new LabelGraphic.ImageGraphic("rdpFile_icon.svg", 16), "rdpFile");
rdpFile.setOnAction(event -> {
BrowserFileChooserSessionComp.open(() -> DataStorage.get().local().ref(), () -> null, fileReference -> {
var file = fileReference.getPath().asLocalPath();
ThreadHelper.runFailableAsync(() -> {
ProcessControlProvider.get().importRdpFile(file);
});
}, false, false, entry -> entry.equals(DataStorage.get().local()), null);
event.consume();
});
menu.getItems().add(menu.getItems().size() - 2, rdpFile);
}
return menu;
}
@@ -883,6 +883,7 @@ public final class AppPrefs {
var env = System.getenv("XPIPE_API_KEY");
if (env != null && !env.isEmpty()) {
enableHttpApi.set(true);
allowExternalApiRequests.set(true);
apiKey.setValue(env);
}
}
@@ -54,6 +54,18 @@ public class FreeRdpClient implements ExternalRdpClient {
.add("-themes")
.add("/size:100%");
var gateway = configuration.getGateway();
if (gateway != null) {
b.add("/g").addQuoted(gateway.getHost());
if (gateway.getUsername() != null) {
b.add("/gu").addQuoted(gateway.getUsername());
}
if (gateway.getPassword() != null) {
var escapedPw = gateway.getPassword().getSecretValue().replaceAll("'", "\\\\'");
b.add("/gp:'" + escapedPw + "'");
}
}
if (configuration.getPassword() != null) {
var escapedPw = configuration.getPassword().getSecretValue().replaceAll("'", "\\\\'");
b.add("/p:'" + escapedPw + "'");
@@ -1,24 +1,22 @@
package io.xpipe.app.rdp;
import com.sun.jna.LastErrorException;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import io.xpipe.app.comp.base.ModalButton;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppDisplayScale;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.platform.ClipboardHelper;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.prefs.ExternalApplicationType;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.LocalShell;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.GlobalTimer;
import io.xpipe.app.util.LocalExec;
import io.xpipe.app.util.RdpConfig;
import io.xpipe.app.util.RemoteDesktopWindow;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.WindowsRegistry;
import io.xpipe.app.util.OsType;
import io.xpipe.app.util.SecretValue;
import io.xpipe.app.util.*;
import javafx.application.Platform;
import javafx.beans.property.Property;
@@ -31,12 +29,16 @@ import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import org.apache.commons.io.FileUtils;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import static io.xpipe.app.util.CredAdvapi32.*;
@JsonTypeName("mstsc")
@Value
@Jacksonized
@@ -186,9 +188,17 @@ public class MstscRdpClient implements ExternalApplicationType.PathApplication,
}
var setCache = prepareLocalhostRegistryCache(configuration);
var fullAddress = configuration.getConfig().get("full address");
if (fullAddress.isPresent()) {
disableSignatureWarning(fullAddress.get().getValue());
disableSignatureWarning(configuration);
if (configuration.getPassword() != null) {
WinCred.setCredential("TERMSRV/" + configuration.getHost(), CRED_TYPE_DOMAIN_PASSWORD, CRED_PERSIST_SESSION,
configuration.getUsername(), configuration.getPassword());
}
var gateway = configuration.getGateway();
if (gateway != null && gateway.getPassword() != null) {
WinCred.setCredential(gateway.getHost(), CRED_TYPE_DOMAIN_PASSWORD, CRED_PERSIST_SESSION,
gateway.getUsername(), gateway.getPassword());
}
var file = writeRdpConfigFile(configuration.getTitle(), adaptedRdpConfig);
@@ -204,26 +214,15 @@ public class MstscRdpClient implements ExternalApplicationType.PathApplication,
window.getDockBounds().getW(),
window.getDockBounds().getH(),
process,
Duration.ofSeconds(60),
Duration.ofSeconds(120),
p -> {
return !p.isDialog();
});
}
GlobalTimer.delay(
() -> {
FileUtils.deleteQuietly(file.toFile());
},
Duration.ofSeconds(1));
if (!setCache && configuration
.getConfig()
.get("full address").isPresent()) {
if (!setCache) {
var localhost = configuration
.getConfig()
.get("full address")
.orElseThrow()
.getValue()
.getHost()
.startsWith("localhost");
if (localhost) {
saveLocalhostRegistryCache(configuration.getStoreId());
@@ -233,7 +232,7 @@ public class MstscRdpClient implements ExternalApplicationType.PathApplication,
@Override
public boolean supportsPasswordPassing(RdpLaunchConfig config) {
return LocalShell.getLocalPowershell().isPresent();
return true;
}
@Override
@@ -267,31 +266,32 @@ public class MstscRdpClient implements ExternalApplicationType.PathApplication,
return input;
}
private RdpConfig getAdaptedConfig(RdpLaunchConfig configuration) throws Exception {
private RdpConfig getAdaptedConfig(RdpLaunchConfig configuration) {
var input = configuration.getConfig();
var pass = configuration.getPassword();
if (input.get("password 51").isPresent() || !supportsPasswordPassing(configuration) || pass == null) {
return input.overlay(Map.of("smart sizing", new RdpConfig.TypedValue("i", smartSizing ? "1" : "0")));
}
var adapted = input.overlay(Map.of(
"password 51",
new RdpConfig.TypedValue("b", encrypt(pass)),
"prompt for credentials",
new RdpConfig.TypedValue("i", "0"),
new RdpConfig.TypedValue("i", pass != null ? "0" : "1"),
"smart sizing",
new RdpConfig.TypedValue("i", smartSizing ? "1" : "0")));
return adapted;
}
private void disableSignatureWarning(String fullAddress) {
var hostname = fullAddress.split(":")[0];
private void disableSignatureWarning(RdpLaunchConfig config) {
WindowsRegistry.local()
.setIntegerValue(
WindowsRegistry.HKEY_CURRENT_USER,
"Software\\Microsoft\\Terminal Server Client\\LocalDevices",
hostname,
config.getHost(),
0x4c);
if (config.getGateway() != null) {
WindowsRegistry.local()
.setIntegerValue(
WindowsRegistry.HKEY_CURRENT_USER,
"Software\\Microsoft\\Terminal Server Client\\LocalDevices",
config.getHost() + ";" + config.getGateway().getHost(),
0x4c);
}
}
private void saveLocalhostRegistryCache(UUID entry) {
@@ -344,12 +344,8 @@ public class MstscRdpClient implements ExternalApplicationType.PathApplication,
WindowsRegistry.HKEY_CURRENT_USER,
"Software\\Microsoft\\Terminal Server Client\\Servers\\localhost");
var fullAddress = configuration.getConfig().get("full address");
if (fullAddress.isEmpty()) {
return false;
}
var localhost = fullAddress.get().getValue().startsWith("localhost");
var localhost = configuration.getHost().startsWith("localhost");
if (localhost) {
var found = getLocalhostRegistryCache(configuration.getStoreId());
if (found.isPresent()) {
@@ -380,15 +376,6 @@ public class MstscRdpClient implements ExternalApplicationType.PathApplication,
return false;
}
private String encrypt(SecretValue password) throws Exception {
var ps = LocalShell.getLocalPowershell().orElseThrow();
var cmd = ps.command(CommandBuilder.of()
.add(sc -> "(" + sc.getShellDialect().literalArgument(password.getSecretValue())
+ " | ConvertTo-SecureString -AsPlainText -Force) | ConvertFrom-SecureString"));
cmd.sensitive();
return cmd.readStdoutOrThrow();
}
@Override
public String getExecutable() {
return "mstsc.exe";
@@ -6,15 +6,33 @@ import io.xpipe.app.util.SecretValue;
import lombok.Value;
import java.util.Optional;
import java.util.UUID;
@Value
public class RdpLaunchConfig {
String title;
String host;
int port;
DataStoreEntry entry;
RdpConfig config;
UUID storeId;
String username;
SecretValue password;
RdpLaunchConfigGateway gateway;
public Optional<String> getDomain() {
var domain = username.contains("\\") ? username.split("\\\\")[0] : null;
return Optional.ofNullable(domain);
}
public String getUserWithoutDomain() {
if (username.contains("\\")) {
return username.split("\\\\")[1];
} else {
return username;
}
}
public boolean isRemoteApp() {
return config.get("remoteapplicationprogram").isPresent();
@@ -0,0 +1,18 @@
package io.xpipe.app.rdp;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.RdpConfig;
import io.xpipe.app.util.SecretValue;
import lombok.Value;
import java.util.UUID;
@Value
public class RdpLaunchConfigGateway {
String host;
String username;
SecretValue password;
boolean reused;
}
@@ -17,16 +17,6 @@ import java.util.*;
@Builder
public class RemminaRdpClient implements ExternalApplicationType.LinuxApplication, ExternalRdpClient {
private List<String> toStrip() {
return List.of("auto connect", "password 51", "prompt for credentials", "smart sizing");
}
private boolean containsOnlyRecognizedOptions(RdpLaunchConfig configuration) {
var l = new HashSet<>(configuration.getConfig().getContent().keySet());
toStrip().forEach(l::remove);
return l.size() == 2 && l.contains("username") && l.contains("full address");
}
@Override
public void launch(RdpLaunchConfig configuration) throws Exception {
// Remmina does not support RemoteApps
@@ -38,24 +28,17 @@ public class RemminaRdpClient implements ExternalApplicationType.LinuxApplicatio
}
}
if (containsOnlyRecognizedOptions(configuration)) {
var encrypted = RemminaHelper.encryptPassword(configuration.getPassword());
if (encrypted.isPresent()) {
var file = RemminaHelper.writeRemminaRdpConfigFile(configuration, encrypted.get());
launch(CommandBuilder.of().add("-c").addFile(file.toString()));
return;
}
var encrypted = RemminaHelper.encryptPassword(configuration.getPassword());
if (encrypted.isPresent()) {
var file = RemminaHelper.writeRemminaRdpConfigFile(configuration);
launch(CommandBuilder.of().add("-c").addFile(file.toString()));
return;
}
RdpConfig c = configuration.getConfig();
var file = writeRdpConfigFile(configuration.getTitle(), c);
launch(CommandBuilder.of().add("-c").addFile(file.toString()));
LocalFileTracker.deleteOnExit(file);
}
@Override
public boolean supportsPasswordPassing(RdpLaunchConfig config) {
return config.isRemoteApp() || containsOnlyRecognizedOptions(config);
return config.isRemoteApp();
}
@Override
@@ -213,9 +213,9 @@ public class OhMyPoshTerminalPrompt extends ConfigFileTerminalPrompt {
lines.add("& ([ScriptBlock]::Create((oh-my-posh init $(oh-my-posh get shell) --print" + configArg
+ ") -join \"`n\"))");
} else if (dialect == ShellDialects.FISH) {
lines.add("oh-my-posh init fish" + configArg + " | source");
lines.add("sh -c \"oh-my-posh init fish" + configArg + "\" | source");
} else {
lines.add("eval \"$(oh-my-posh init " + dialect.getId() + configArg + ")\"");
lines.add("eval \"$(sh -c \"oh-my-posh init " + dialect.getId() + configArg + "\")\"");
}
}
return ShellScript.lines(lines);
@@ -77,9 +77,9 @@ public class StarshipTerminalPrompt extends ConfigFileTerminalPrompt {
if (ShellDialects.isPowershell(shellControl)) {
lines.add("Invoke-Expression (&starship init powershell)");
} else if (dialect == ShellDialects.FISH) {
lines.add("starship init fish | source");
lines.add("sh -c \"starship init fish\" | source");
} else {
lines.add("eval \"$(starship init " + dialect.getId() + ")\"");
lines.add("eval \"$(sh -c \"starship init " + dialect.getId() + "\")\"");
}
}
return ShellScript.lines(lines);
@@ -0,0 +1,407 @@
package io.xpipe.app.util;
import com.sun.jna.LastErrorException;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.platform.win32.WinBase;
import com.sun.jna.win32.StdCallLibrary;
import com.sun.jna.win32.W32APIOptions;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* This class exposes functions from credential manager on Windows platform
* via JNA.
*
* Please refer to MSDN documentations for each method usage pattern
*/
public interface CredAdvapi32 extends StdCallLibrary {
CredAdvapi32 INSTANCE = Native.load("Advapi32", CredAdvapi32.class, W32APIOptions.UNICODE_OPTIONS);
/**
* CredRead flag
*/
int CRED_FLAGS_PROMPT_NOW = 0x0002;
int CRED_FLAGS_USERNAME_TARGET = 0x0004;
/**
* Type of Credential
*/
int CRED_TYPE_GENERIC = 1;
int CRED_TYPE_DOMAIN_PASSWORD = 2;
int CRED_TYPE_DOMAIN_CERTIFICATE = 3;
int CRED_TYPE_DOMAIN_VISIBLE_PASSWORD = 4;
int CRED_TYPE_GENERIC_CERTIFICATE = 5;
int CRED_TYPE_DOMAIN_EXTENDED = 6;
int CRED_TYPE_MAXIMUM = 7; // Maximum supported cred type
int CRED_TYPE_MAXIMUM_EX = CRED_TYPE_MAXIMUM + 1000;
/**
* CredWrite flag
*/
int CRED_PRESERVE_CREDENTIAL_BLOB = 0x1;
/**
* Values of the Credential Persist field
*/
int CRED_PERSIST_NONE = 0;
int CRED_PERSIST_SESSION = 1;
int CRED_PERSIST_LOCAL_MACHINE = 2;
int CRED_PERSIST_ENTERPRISE = 3;
/**
* Credential attributes
*
* https://msdn.microsoft.com/en-us/library/windows/desktop/aa374790(v=vs.85).aspx
*
* typedef struct _CREDENTIAL_ATTRIBUTE {
* LPTSTR Keyword;
* DWORD Flags;
* DWORD ValueSize;
* LPBYTE Value;
* } CREDENTIAL_ATTRIBUTE, *PCREDENTIAL_ATTRIBUTE;
*
*/
class CREDENTIAL_ATTRIBUTE extends Structure {
public static class ByReference extends CREDENTIAL_ATTRIBUTE implements Structure.ByReference { }
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("Keyword",
"Flags",
"ValueSize",
"Value");
}
/**
* Name of the application-specific attribute. Names should be of the form <CompanyName>_<Name>.
* This member cannot be longer than CRED_MAX_STRING_LENGTH (256) characters.
*/
public String Keyword;
/**
* Identifies characteristics of the credential attribute. This member is reserved and should be originally
* initialized as zero and not otherwise altered to permit future enhancement.
*/
public int Flags;
/**
* Length of Value in bytes. This member cannot be larger than CRED_MAX_VALUE_SIZE (256).
*/
public int ValueSize;
/**
* Data associated with the attribute. By convention, if Value is a text string, then Value should not
* include the trailing zero character and should be in UNICODE.
*
* Credentials are expected to be portable. The application should take care to ensure that the data in
* value is portable. It is the responsibility of the application to define the byte-endian and alignment
* of the data in Value.
*/
public Pointer Value;
}
/**
* Pointer to {@See CREDENTIAL_ATTRIBUTE} struct
*/
class PCREDENTIAL_ATTRIBUTE extends Structure {
@Override
protected List<String> getFieldOrder() {
return Collections.singletonList("credential_attribute");
}
public PCREDENTIAL_ATTRIBUTE() {
super();
}
public PCREDENTIAL_ATTRIBUTE(byte[] data) {
super(new Memory(data.length));
getPointer().write(0, data, 0, data.length);
read();
}
public PCREDENTIAL_ATTRIBUTE(Pointer memory) {
super(memory);
read();
}
public Pointer credential_attribute;
}
/**
* The CREDENTIAL structure contains an individual credential
*
* https://msdn.microsoft.com/en-us/library/windows/desktop/aa374788(v=vs.85).aspx
*
* typedef struct _CREDENTIAL {
* DWORD Flags;
* DWORD Type;
* LPTSTR TargetName;
* LPTSTR Comment;
* FILETIME LastWritten;
* DWORD CredentialBlobSize;
* LPBYTE CredentialBlob;
* DWORD Persist;
* DWORD AttributeCount;
* PCREDENTIAL_ATTRIBUTE Attributes;
* LPTSTR TargetAlias;
* LPTSTR UserName;
* } CREDENTIAL, *PCREDENTIAL;
*/
class CREDENTIAL extends Structure {
@Override
protected List<String> getFieldOrder() {
return Arrays.asList("Flags",
"Type",
"TargetName",
"Comment",
"LastWritten",
"CredentialBlobSize",
"CredentialBlob",
"Persist",
"AttributeCount",
"Attributes",
"TargetAlias",
"UserName");
}
public CREDENTIAL() {
super();
}
public CREDENTIAL(final int size) {
super(new Memory(size));
}
public CREDENTIAL(Pointer memory) {
super(memory);
read();
}
/**
* A bit member that identifies characteristics of the credential. Undefined bits should be initialized
* as zero and not otherwise altered to permit future enhancement.
*
* See MSDN doc for all possible flags
*/
public int Flags;
/**
* The type of the credential. This member cannot be changed after the credential is created.
*
* See MSDN doc for all possible types
*/
public int Type;
/**
* The name of the credential. The TargetName and Type members uniquely identify the credential.
* This member cannot be changed after the credential is created. Instead, the credential with the old
* name should be deleted and the credential with the new name created.
*
* See MSDN doc for additional requirement
*/
public String TargetName;
/**
* A string comment from the user that describes this credential. This member cannot be longer than
* CRED_MAX_STRING_LENGTH (256) characters.
*/
public String Comment;
/**
* The time, in Coordinated Universal Time (Greenwich Mean Time), of the last modification of the credential.
* For write operations, the value of this member is ignored.
*/
public WinBase.FILETIME LastWritten;
/**
* The size, in bytes, of the CredentialBlob member. This member cannot be larger than
* CRED_MAX_CREDENTIAL_BLOB_SIZE (512) bytes.
*/
public int CredentialBlobSize;
/**
* Secret data for the credential. The CredentialBlob member can be both read and written.
* If the Type member is CRED_TYPE_DOMAIN_PASSWORD, this member contains the plaintext Unicode password
* for UserName. The CredentialBlob and CredentialBlobSize members do not include a trailing zero character.
* Also, for CRED_TYPE_DOMAIN_PASSWORD, this member can only be read by the authentication packages.
*
* If the Type member is CRED_TYPE_DOMAIN_CERTIFICATE, this member contains the clear test
* Unicode PIN for UserName. The CredentialBlob and CredentialBlobSize members do not include a trailing
* zero character. Also, this member can only be read by the authentication packages.
*
* If the Type member is CRED_TYPE_GENERIC, this member is defined by the application.
* Credentials are expected to be portable. Applications should ensure that the data in CredentialBlob is
* portable. The application defines the byte-endian and alignment of the data in CredentialBlob.
*/
public Pointer CredentialBlob;
/**
* Defines the persistence of this credential. This member can be read and written.
*
* See MSDN doc for all possible values
*/
public int Persist;
/**
* The number of application-defined attributes to be associated with the credential. This member can be
* read and written. Its value cannot be greater than CRED_MAX_ATTRIBUTES (64).
*/
public int AttributeCount;
/**
* Application-defined attributes that are associated with the credential. This member can be read
* and written.
*/
//notTODO: Need to make this into array
public CREDENTIAL_ATTRIBUTE.ByReference Attributes;
/**
* Alias for the TargetName member. This member can be read and written. It cannot be longer than
* CRED_MAX_STRING_LENGTH (256) characters.
*
* If the credential Type is CRED_TYPE_GENERIC, this member can be non-NULL, but the credential manager
* ignores the member.
*/
public String TargetAlias;
/**
* The user name of the account used to connect to TargetName.
* If the credential Type is CRED_TYPE_DOMAIN_PASSWORD, this member can be either a DomainName\UserName
* or a UPN.
*
* If the credential Type is CRED_TYPE_DOMAIN_CERTIFICATE, this member must be a marshaled certificate
* reference created by calling CredMarshalCredential with a CertCredential.
*
* If the credential Type is CRED_TYPE_GENERIC, this member can be non-NULL, but the credential manager
* ignores the member.
*
* This member cannot be longer than CRED_MAX_USERNAME_LENGTH (513) characters.
*/
public String UserName;
}
/**
* Pointer to {@see CREDENTIAL} struct
*/
class PCREDENTIAL extends Structure {
@Override
protected List<String> getFieldOrder() {
return Collections.singletonList("credential");
}
public PCREDENTIAL() {
super();
}
public PCREDENTIAL(byte[] data) {
super(new Memory(data.length));
getPointer().write(0, data, 0, data.length);
read();
}
public PCREDENTIAL(Pointer memory) {
super(memory);
read();
}
public Pointer credential;
}
/**
* The CredRead function reads a credential from the user's credential set.
*
* The credential set used is the one associated with the logon session of the current token.
* The token must not have the user's SID disabled.
*
* https://msdn.microsoft.com/en-us/library/windows/desktop/aa374804(v=vs.85).aspx
*
* @param targetName
* String that contains the name of the credential to read.
* @param type
* Type of the credential to read. Type must be one of the CRED_TYPE_* defined types.
* @param flags
* Currently reserved and must be zero.
* @param pcredential
* Out - Pointer to a single allocated block buffer to return the credential.
* Any pointers contained within the buffer are pointers to locations within this single allocated block.
* The single returned buffer must be freed by calling <code>CredFree</code>.
*
* @return
* True if CredRead succeeded, false otherwise
*
* @throws LastErrorException
* GetLastError
*/
boolean CredRead(String targetName, int type, int flags, PCREDENTIAL pcredential) throws LastErrorException;
/**
* The CredWrite function creates a new credential or modifies an existing credential in the user's credential set.
* The new credential is associated with the logon session of the current token. The token must not have the
* user's security identifier (SID) disabled.
*
* https://msdn.microsoft.com/en-us/library/windows/desktop/aa375187(v=vs.85).aspx
*
* @param credential
* A CREDENTIAL structure to be written.
* @param flags
* Flags that control the function's operation. The following flag is defined.
* CRED_PRESERVE_CREDENTIAL_BLOB:
* The credential BLOB from an existing credential is preserved with the same
* credential name and credential type. The CredentialBlobSize of the passed
* in Credential structure must be zero.
*
* @return
* True if CredWrite succeeded, false otherwise
*
* @throws LastErrorException
* GetLastError
*/
boolean CredWrite(CREDENTIAL credential, int flags) throws LastErrorException;
/**
* The CredDelete function deletes a credential from the user's credential set. The credential set used is the one
* associated with the logon session of the current token. The token must not have the user's SID disabled.
*
* https://msdn.microsoft.com/en-us/library/windows/desktop/aa374787(v=vs.85).aspx
*
* @param targetName
* String that contains the name of the credential to read.
* @param type
* Type of the credential to delete. Must be one of the CRED_TYPE_* defined types. For a list of the
* defined types, see the Type member of the CREDENTIAL structure.
* If the value of this parameter is CRED_TYPE_DOMAIN_EXTENDED, this function can delete a credential that
* specifies a user name when there are multiple credentials for the same target. The value of the TargetName
* parameter must specify the user name as Target|UserName.
* @param flags
* Reserved and must be zero.
*
* @return
* True if CredDelete succeeded, false otherwise
*
* @throws LastErrorException
* GetLastError
*/
boolean CredDelete(String targetName, int type, int flags) throws LastErrorException;
/**
* The CredFree function frees a buffer returned by any of the credentials management functions.
*
* https://msdn.microsoft.com/en-us/library/windows/desktop/aa374796(v=vs.85).aspx
*
* @param credential
* Pointer to CREDENTIAL to be freed
*
* @throws LastErrorException
* GetLastError
*/
void CredFree(Pointer credential) throws LastErrorException;
}
@@ -17,6 +17,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import static com.sun.jna.win32.W32APIOptions.DEFAULT_OPTIONS;
@Getter
@EqualsAndHashCode
public class NativeWinWindowControl {
@@ -268,7 +270,7 @@ public class NativeWinWindowControl {
public interface Dwm extends Library {
Dwm INSTANCE = Native.load("dwmapi", Dwm.class);
Dwm INSTANCE = Native.load("dwmapi", Dwm.class, DEFAULT_OPTIONS);
WinNT.HRESULT DwmSetWindowAttribute(
WinDef.HWND hwnd, int dwAttribute, PointerType pvAttribute, int cbAttribute);
@@ -50,13 +50,7 @@ public class RemminaHelper {
}
}
public static Path writeRemminaRdpConfigFile(RdpLaunchConfig configuration, String password) throws Exception {
var user = configuration.getConfig().get("username").orElseThrow().getValue();
var domain = user.contains("\\") ? user.split("\\\\")[0] : null;
if (domain != null) {
user = user.split("\\\\")[1];
}
public static Path writeRemminaRdpConfigFile(RdpLaunchConfig configuration) throws Exception {
var w = Math.round(AppMainWindow.get().getStage().getWidth());
// Remmina's height calculation does not take the titlebar into account
var h = Math.round(AppMainWindow.get().getStage().getHeight()) - 38;
@@ -65,6 +59,27 @@ public class RemminaHelper {
var name = OsFileSystem.ofLocal().makeFileSystemCompatible(configuration.getTitle());
var file = AppLocalTemp.getLocalTempDataDirectory("remmina").resolve("xpipe-" + name + ".remmina");
var gateway = "";
if (configuration.getGateway() != null) {
gateway += "gateway_server=" + configuration.getGateway().getHost();
if (configuration.getGateway().getUsername() != null) {
var gatewayUser = configuration.getGateway().getUsername();
var gatewayDomain = gatewayUser.contains("\\") ? gatewayUser.split("\\\\")[0] : null;
if (gatewayDomain != null) {
gatewayUser = gatewayUser.split("\\\\")[1];
}
gateway += "gateway_username=" + gatewayUser;
if (gatewayDomain != null) {
gateway += "gateway_domain=" + gatewayDomain;
}
}
if (configuration.getGateway().getPassword() != null) {
gateway += "gateway_password=" + RemminaHelper.encryptPassword(configuration.getGateway().getPassword());
}
}
var string = """
[remmina]
protocol=RDP
@@ -77,16 +92,17 @@ public class RemminaHelper {
scale=2
window_width=%s
window_height=%s
window_maximize=%s
window_maximize=%s%s
""".formatted(
configuration.getTitle(),
user,
domain != null ? domain : "",
configuration.getConfig().get("full address").orElseThrow().getValue(),
password != null ? password : "",
configuration.getUsername(),
configuration.getDomain().orElse(""),
configuration.getHost(),
configuration.getPassword() != null ? encryptPassword(configuration.getPassword()) : "",
w,
h,
maximize);
maximize,
!gateway.isEmpty() ? "\n" + gateway : "");
Files.createDirectories(file.getParent());
Files.writeString(file, string);
return file;
@@ -5,6 +5,7 @@ import io.xpipe.app.ext.DataStore;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.storage.DataStoreEntryRef;
import java.util.Arrays;
import java.util.List;
public class Validators {
@@ -18,6 +19,15 @@ public class Validators {
}
}
public static <T extends DataStore> void isAnyType(DataStoreEntryRef<? extends T> ref, Class<?>... cs)
throws ValidationException {
if (ref == null
|| ref.getStore() == null
|| Arrays.stream(cs).noneMatch(c -> c.isAssignableFrom(ref.getStore().getClass()))) {
throw new ValidationException("Value must be an instance of " + Arrays.stream(cs).map(Class::getSimpleName).toList());
}
}
public static void nonNull(Object object) throws ValidationException {
if (object == null) {
throw new ValidationException(AppI18n.get("valueMustNotBeEmpty"));
@@ -0,0 +1,107 @@
package io.xpipe.app.util;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import com.sun.jna.LastErrorException;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.Advapi32;
import com.sun.jna.win32.W32APIOptions;
import io.xpipe.app.issue.ErrorEventFactory;
import lombok.Value;
public class WinCred {
@Value
public static class Credential {
String target;
String username;
String password;
}
private static Boolean libraryLoaded;
private static CredAdvapi32 INSTANCE;
private static synchronized boolean isLibrarySupported() {
if (libraryLoaded != null) {
return libraryLoaded;
}
try {
INSTANCE = Native.load("Advapi32", CredAdvapi32.class, W32APIOptions.UNICODE_OPTIONS);
return (libraryLoaded = true);
} catch (Throwable t) {
libraryLoaded = false;
ErrorEventFactory.fromThrowable(t)
.description("Unable to load native library Advapi32.dll for Windows credentials queries."
+ " Credential operations will fail and some functionality will be unavailable")
.handle();
return false;
}
}
public static synchronized Optional<Credential> getCredential(String target, int type) {
if (!isLibrarySupported()) {
return Optional.empty();
}
CredAdvapi32.PCREDENTIAL pcredMem = new CredAdvapi32.PCREDENTIAL();
try {
if (INSTANCE.CredRead(target, type, 0, pcredMem)) {
CredAdvapi32.CREDENTIAL credMem = new CredAdvapi32.CREDENTIAL(pcredMem.credential);
if (credMem.CredentialBlob == null) {
return Optional.empty();
}
byte[] passwordBytes = credMem.CredentialBlob.getByteArray(0, credMem.CredentialBlobSize);
String password = new String(passwordBytes, StandardCharsets.UTF_16LE);
Credential cred = new Credential(credMem.TargetName, credMem.UserName, password);
return Optional.of(cred);
} else {
return Optional.empty();
}
} finally {
INSTANCE.CredFree(pcredMem.credential);
}
}
public static void setCredential(String target, int type, int persist, String userName, SecretValue password) {
if (!isLibrarySupported()) {
return;
}
CredAdvapi32.CREDENTIAL credMem = new CredAdvapi32.CREDENTIAL();
credMem.Flags = 0;
credMem.TargetName = target;
credMem.Type = type;
credMem.UserName = userName;
credMem.AttributeCount = 0;
credMem.Persist = persist;
byte[] bpassword = password.getSecretValue().getBytes(StandardCharsets.UTF_16LE);
credMem.CredentialBlobSize = bpassword.length;
credMem.CredentialBlob = getPointer(bpassword);
if (!INSTANCE.CredWrite(credMem, 0)) {
int err = Native.getLastError();
throw new LastErrorException(err);
}
}
public static void deleteCredential(String target, int type) {
if (!INSTANCE.CredDelete(target, type, 0)) {
int err = Native.getLastError();
throw new LastErrorException(err);
}
}
private static Pointer getPointer(byte[] array) {
Pointer p = new Memory(array.length);
p.write(0, array, 0, array.length);
return p;
}
}
+16 -4
View File
@@ -1116,7 +1116,8 @@ configLocationDescription=The file path of the config file
gateway=Gateway
gatewayDescription=The optional gateway to use when connecting
rdpGateway=Connection gateway
rdpGatewayDescription=The optional gateway through which to tunnel the connection to the RDP port
#force
rdpGatewayDescription=The optional gateway. Supports both SSH hosts for tunneling or RD gateway servers
connectionInformation=Connection target
#context: title
connectionInformationDescription=Which system to connect to
@@ -1783,7 +1784,9 @@ hasHost=$COUNT$ available hosts
largeFileWarningTitle=Large file edit
largeFileWarningContent=The file you want to edit is quite large with $SIZE$. Do you really want to open this file in your text editor?
rdpAskpassUser=RDP username for host $HOST$
rdpAskpassPassword=Password for user $USER$
#force
rdpAskpassPassword=RDP password for user $USER$
rdpGatewayAskpassPassword=RDP gateway password for user $USER$
inPlaceKey=Key
inPlaceKeyText=Private key content
inPlaceKeyTextDescription=The private key contents
@@ -2229,7 +2232,6 @@ enpassUnlock=Enter Enpass vault master password
keeperSmsUnlock=Enter Keeper Commander SMS Code
keeper2faUnlock=Enter Keeper 2FA Code
keeperUnlock=Enter your Keeper master password to unlock
wakeOnLanHost=Host
wakeOnLanHost=Target host
wakeOnLanHostDescription=The system to wake
wakeOnLanGateway=Broadcast host
@@ -2313,5 +2315,15 @@ webtopAccessVpnTypeDescription=If you already use one of the supported VPNs, aut
webtopDeploymentTarget=Target
webtopDeploymentAccess=Access control
webtopDeploymentConfiguration=Container configuration
rdpGatewayHost=Gateway host
rdpGatewayHostDescription=The address of the RDS gateway server
rdpGatewayPassword=Use password
rdpGatewayPasswordDescription=Whether the gateway requires a password to authenticate
rdpGatewayReusePassword=Reuse credentials
rdpGatewayReusePasswordDescription=Reuse credentials of the target RDP server for gateway
rdpGatewayIdentity=Gateway credentials
rdpGatewayIdentityDescription=The credentials to authenticate with the gateway
rdpGateway.displayName=RD gateway
rdpGateway.displayDescription=Create a Remote Desktop Gateway to use with RDP connections
rdpFile=RDP file