mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-07-08 11:30:42 +00:00
Rework RDP
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Generated
+16
-4
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user