mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
feat: store rdp, vnc and telnet logins in a generic per-protocol table
This commit is contained in:
1 parent
e0c30a4e6f
commit
bba06f7ff6
107 files changed
+18750
-1224
No files matched your search
@@ -351,8 +351,48 @@ export interface PluginContributions {
|
||||
* naming a sync entity over sync.
|
||||
*/
|
||||
keybindingActions?: PluginKeybindingActionContribution[];
|
||||
/**
|
||||
* Connection protocols whose per-host login core stores, encrypts,
|
||||
* shares, exports and syncs for this plugin, read back with
|
||||
* ctx.credentials.resolveHostProtocol.
|
||||
*/
|
||||
protocols?: PluginProtocolContribution[];
|
||||
}
|
||||
|
||||
/** A field a protocol login carries besides username and password. */
|
||||
export interface PluginProtocolCredentialField {
|
||||
key: string;
|
||||
/** Encrypted with the owner's data key and never sent to a browser. */
|
||||
secret?: boolean;
|
||||
}
|
||||
|
||||
export interface PluginProtocolContribution {
|
||||
/** Stored with each login. Never change it once hosts use it. */
|
||||
id: string;
|
||||
credentialFields?: PluginProtocolCredentialField[];
|
||||
/**
|
||||
* The port an import row without one gets, for a host whose main
|
||||
* protocol this is.
|
||||
*/
|
||||
defaultPort?: number;
|
||||
/**
|
||||
* What the owner's login falls back to from the host's own SSH login
|
||||
* when the protocol login leaves it empty.
|
||||
*/
|
||||
hostLoginFallback?: Array<"username" | "password">;
|
||||
}
|
||||
|
||||
/** Protocol ids core keeps for itself. */
|
||||
export const RESERVED_PROTOCOL_IDS: readonly string[] = ["ssh"];
|
||||
|
||||
/** Login keys a declared credential field may not reuse. */
|
||||
export const RESERVED_PROTOCOL_FIELD_KEYS: readonly string[] = [
|
||||
"username",
|
||||
"password",
|
||||
"authType",
|
||||
"credentialId",
|
||||
];
|
||||
|
||||
/** One parameter a saved keybinding action carries next to its type. */
|
||||
export interface PluginKeybindingParam {
|
||||
type: "string" | "boolean";
|
||||
@@ -523,6 +563,7 @@ const ALLOWED_CONTRIBUTES = new Set([
|
||||
"auth",
|
||||
"syncEntities",
|
||||
"keybindingActions",
|
||||
"protocols",
|
||||
]);
|
||||
|
||||
const ALLOWED_SETTINGS_FIELD = [
|
||||
@@ -993,6 +1034,94 @@ function validateContributes(
|
||||
validateHttpContribution(contributes.http, pluginId, errors);
|
||||
validateUiPresets(contributes.uiPresets, errors);
|
||||
validateKeybindingActions(contributes.keybindingActions, errors);
|
||||
validateProtocols(contributes.protocols, errors);
|
||||
}
|
||||
|
||||
export const PROTOCOL_ID_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
||||
export const PROTOCOL_FIELD_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9]{0,31}$/;
|
||||
|
||||
function validateProtocols(value: unknown, errors: string[]): void {
|
||||
if (value === undefined) return;
|
||||
const where = "contributes.protocols";
|
||||
if (!Array.isArray(value)) {
|
||||
errors.push(`Field "${where}" must be an array`);
|
||||
return;
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
value.forEach((raw, index) => {
|
||||
const at = `${where}[${index}]`;
|
||||
if (!isPlainObject(raw)) {
|
||||
errors.push(`${at} must be an object`);
|
||||
return;
|
||||
}
|
||||
rejectUnknown(
|
||||
raw,
|
||||
["id", "credentialFields", "defaultPort", "hostLoginFallback"],
|
||||
at,
|
||||
errors,
|
||||
);
|
||||
if (typeof raw.id !== "string" || !PROTOCOL_ID_PATTERN.test(raw.id)) {
|
||||
errors.push(
|
||||
`${at}.id must be lowercase letters, digits and dashes, starting with a letter`,
|
||||
);
|
||||
} else if (RESERVED_PROTOCOL_IDS.includes(raw.id)) {
|
||||
errors.push(`${at}.id "${raw.id}" belongs to core`);
|
||||
} else {
|
||||
if (seen.has(raw.id)) errors.push(`${at}.id duplicates "${raw.id}"`);
|
||||
seen.add(raw.id);
|
||||
}
|
||||
if (
|
||||
raw.defaultPort !== undefined &&
|
||||
(typeof raw.defaultPort !== "number" ||
|
||||
!Number.isInteger(raw.defaultPort) ||
|
||||
raw.defaultPort < 1 ||
|
||||
raw.defaultPort > 65535)
|
||||
) {
|
||||
errors.push(`${at}.defaultPort must be a port number`);
|
||||
}
|
||||
if (raw.hostLoginFallback !== undefined) {
|
||||
if (
|
||||
!Array.isArray(raw.hostLoginFallback) ||
|
||||
raw.hostLoginFallback.some(
|
||||
(entry) => entry !== "username" && entry !== "password",
|
||||
)
|
||||
) {
|
||||
errors.push(
|
||||
`${at}.hostLoginFallback must list "username" and/or "password"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (raw.credentialFields === undefined) return;
|
||||
if (!Array.isArray(raw.credentialFields)) {
|
||||
errors.push(`${at}.credentialFields must be an array`);
|
||||
return;
|
||||
}
|
||||
const keys = new Set<string>();
|
||||
raw.credentialFields.forEach((field, fieldIndex) => {
|
||||
const fieldAt = `${at}.credentialFields[${fieldIndex}]`;
|
||||
if (!isPlainObject(field)) {
|
||||
errors.push(`${fieldAt} must be an object`);
|
||||
return;
|
||||
}
|
||||
rejectUnknown(field, ["key", "secret"], fieldAt, errors);
|
||||
if (
|
||||
typeof field.key !== "string" ||
|
||||
!PROTOCOL_FIELD_KEY_PATTERN.test(field.key)
|
||||
) {
|
||||
errors.push(`${fieldAt}.key must be letters and digits`);
|
||||
} else if (RESERVED_PROTOCOL_FIELD_KEYS.includes(field.key)) {
|
||||
errors.push(`${fieldAt}.key "${field.key}" is part of every login`);
|
||||
} else {
|
||||
if (keys.has(field.key)) {
|
||||
errors.push(`${fieldAt}.key duplicates "${field.key}"`);
|
||||
}
|
||||
keys.add(field.key);
|
||||
}
|
||||
if (field.secret !== undefined && typeof field.secret !== "boolean") {
|
||||
errors.push(`${fieldAt}.secret must be a boolean`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const KEYBINDING_ACTION_PATTERN = /^[a-zA-Z][a-zA-Z0-9.-]{0,63}$/;
|
||||
|
||||
Reference in new issue
Block a user