mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
feat: finish the remote-desktop plugin (B14)
This commit is contained in:
1 parent
0af2c94983
commit
14eb558b85
215 files changed
+30029
-3921
No files matched your search
@@ -161,7 +161,7 @@ http {
|
|||||||
root /app/html;
|
root /app/html;
|
||||||
index index.html index.htm;
|
index index.html index.htm;
|
||||||
expires off;
|
expires off;
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-IaCkn7qYpeosdQvIY2TXZn11ghTwUsfkSNLXPkjBcSY='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-don5VKNqA8VQplPO1j5zyOVDdbTQrgGSSeVPwJ7b0Lo='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
|
||||||
add_header X-Frame-Options "DENY" always;
|
add_header X-Frame-Options "DENY" always;
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||||
|
|||||||
+1
-1
@@ -142,7 +142,7 @@ http {
|
|||||||
root /app/html;
|
root /app/html;
|
||||||
index index.html index.htm;
|
index index.html index.htm;
|
||||||
expires off;
|
expires off;
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-IaCkn7qYpeosdQvIY2TXZn11ghTwUsfkSNLXPkjBcSY='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'sha256-don5VKNqA8VQplPO1j5zyOVDdbTQrgGSSeVPwJ7b0Lo='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; media-src 'self' data: blob: http: https:; worker-src 'self' blob:; frame-src http: https:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
|
||||||
add_header X-Frame-Options "DENY" always;
|
add_header X-Frame-Options "DENY" always;
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- The remote desktop host options (enable_rdp, enable_vnc, enable_telnet,
|
||||||
|
-- the three ports, rdp_security, rdp_ignore_cert, security, ignore_cert,
|
||||||
|
-- guacamole_config) and user_preferences.rdp_defaults moved to the
|
||||||
|
-- remote-desktop plugin's settings. The copy runs after plugins load, which
|
||||||
|
-- is after this migration, so a real DROP COLUMN here would delete the data
|
||||||
|
-- first. The columns stay in place, unused, until 3.0.0.
|
||||||
|
SELECT 1;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -295,6 +295,13 @@
|
|||||||
"when": 1790225600497,
|
"when": 1790225600497,
|
||||||
"tag": "0041_wonderful_cargill",
|
"tag": "0041_wonderful_cargill",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 42,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1790227893950,
|
||||||
|
"tag": "0042_colorful_bill_hollister",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- The remote desktop host options (enable_rdp, enable_vnc, enable_telnet,
|
||||||
|
-- the three ports, rdp_security, rdp_ignore_cert, security, ignore_cert,
|
||||||
|
-- guacamole_config) and user_preferences.rdp_defaults moved to the
|
||||||
|
-- remote-desktop plugin's settings. The copy runs after plugins load, which
|
||||||
|
-- is after this migration, so a real DROP COLUMN here would delete the data
|
||||||
|
-- first. The columns stay in place, unused, until 3.0.0.
|
||||||
|
SELECT 1;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -302,6 +302,13 @@
|
|||||||
"when": 1790225599928,
|
"when": 1790225599928,
|
||||||
"tag": "0042_strange_shocker",
|
"tag": "0042_strange_shocker",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 43,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1790227893380,
|
||||||
|
"tag": "0043_melodic_terror",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- The remote desktop host options (enable_rdp, enable_vnc, enable_telnet,
|
||||||
|
-- the three ports, rdp_security, rdp_ignore_cert, security, ignore_cert,
|
||||||
|
-- guacamole_config) and user_preferences.rdp_defaults moved to the
|
||||||
|
-- remote-desktop plugin's settings. The copy runs after plugins load, which
|
||||||
|
-- is after this migration, so a real DROP COLUMN here would delete the data
|
||||||
|
-- first. The columns stay in place, unused, until 3.0.0.
|
||||||
|
SELECT 1;
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -274,6 +274,13 @@
|
|||||||
"when": 1790225599344,
|
"when": 1790225599344,
|
||||||
"tag": "0038_broad_avengers",
|
"tag": "0038_broad_avengers",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 39,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1790227892787,
|
||||||
|
"tag": "0039_lively_impossible_man",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+1
-4
@@ -645,6 +645,7 @@ const isolatedWindows = createIsolatedWindows({
|
|||||||
// ctx.desktop.openIsolatedWindow is the one caller today.
|
// ctx.desktop.openIsolatedWindow is the one caller today.
|
||||||
const BACKEND_REQUEST_HANDLERS = {
|
const BACKEND_REQUEST_HANDLERS = {
|
||||||
"open-isolated-window": (payload) => isolatedWindows.open(payload),
|
"open-isolated-window": (payload) => isolatedWindows.open(payload),
|
||||||
|
"launch-native-rdp": (payload) => launchNativeRdp(payload),
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handleBackendRequest(msg) {
|
async function handleBackendRequest(msg) {
|
||||||
@@ -1672,10 +1673,6 @@ ipcMain.handle("get-platform", () => {
|
|||||||
return process.platform;
|
return process.platform;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("open-native-rdp", (_event, options) =>
|
|
||||||
launchNativeRdp(options),
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle("get-embedded-server-status", () => {
|
ipcMain.handle("get-embedded-server-status", () => {
|
||||||
return {
|
return {
|
||||||
running:
|
running:
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
getPlatform: () => ipcRenderer.invoke("get-platform"),
|
getPlatform: () => ipcRenderer.invoke("get-platform"),
|
||||||
getEmbeddedServerStatus: () =>
|
getEmbeddedServerStatus: () =>
|
||||||
ipcRenderer.invoke("get-embedded-server-status"),
|
ipcRenderer.invoke("get-embedded-server-status"),
|
||||||
openNativeRdp: (options) => ipcRenderer.invoke("open-native-rdp", options),
|
|
||||||
|
|
||||||
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel),
|
removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel),
|
||||||
isElectron: true,
|
isElectron: true,
|
||||||
|
|||||||
@@ -279,6 +279,12 @@ it, `group` is a section heading, and `permission` is who may write it: admin
|
|||||||
fields fall back to `admin.plugins.manage`, and a short name resolves against
|
fields fall back to `admin.plugins.manage`, and a short name resolves against
|
||||||
the plugin's own permissions exactly as `ctx.rbac` does.
|
the plugin's own permissions exactly as `ctx.rbac` does.
|
||||||
|
|
||||||
|
`hidden: true` (**B14**) keeps a field out of the generic form: it is stored,
|
||||||
|
validated and encrypted like any other, but the plugin edits it in its own
|
||||||
|
UI. Remote desktop's host options are hidden fields, edited in its RDP, VNC
|
||||||
|
and Telnet host editor tabs, so they do not appear a second time in the
|
||||||
|
Plugins tab.
|
||||||
|
|
||||||
`type: "custom"` names a component the frontend registered, for the few things
|
`type: "custom"` names a component the frontend registered, for the few things
|
||||||
a schema cannot express (a device browser, a provider list). It is deliberately
|
a schema cannot express (a device browser, a provider list). It is deliberately
|
||||||
narrow: a plugin supplies data everywhere else, so it cannot ship its own form
|
narrow: a plugin supplies data everywhere else, so it cannot ship its own form
|
||||||
@@ -339,8 +345,10 @@ A plugin frontend is a built ESM bundle, `dist/frontend.js`, exporting
|
|||||||
no imports from `plugins/`; everything a plugin shows goes through the `app`
|
no imports from `plugins/`; everything a plugin shows goes through the `app`
|
||||||
object. `scripts/check-shell-plugin-ids.cjs` (run by lint) fails on a quoted
|
object. `scripts/check-shell-plugin-ids.cjs` (run by lint) fails on a quoted
|
||||||
plugin id in `src/ui` outside tests, apart from the entries in
|
plugin id in `src/ui` outside tests, apart from the entries in
|
||||||
`scripts/shell-plugin-id-allowlist.json`, each with a reason (host protocol
|
`scripts/shell-plugin-id-allowlist.json`, each with a reason (host data like
|
||||||
data like `rdp`, which Phase B moves).
|
the `docker` runtime name, which Phase B moves). **B14** removed the `rdp`,
|
||||||
|
`vnc` and `telnet` entries: the shell learns those protocols from
|
||||||
|
`app.registerHostProtocol`.
|
||||||
|
|
||||||
#### The loader
|
#### The loader
|
||||||
|
|
||||||
@@ -427,6 +435,7 @@ boundary and Suspense.
|
|||||||
| `registerTab` | A tab type, id in `contributes.tabs`. Options cover persistence, layouts, singletons and host needs |
|
| `registerTab` | A tab type, id in `contributes.tabs`. Options cover persistence, layouts, singletons and host needs |
|
||||||
| `registerHostEditorSection` | A host editor tab in the top strip or the SSH group, with `form`, `setField` and `updateForm` |
|
| `registerHostEditorSection` | A host editor tab in the top strip or the SSH group, with `form`, `setField` and `updateForm` |
|
||||||
| `registerHostAction` | A connect or open action on a host: sidebar row, palette, dashboard, default connect |
|
| `registerHostAction` | A connect or open action on a host: sidebar row, palette, dashboard, default connect |
|
||||||
|
| `registerHostProtocol` | A connection protocol next to SSH (**B14**): General tab switch, Hosts panel filter and grouping, Quick Connect, the host's primary type and port |
|
||||||
| `registerHostBadge`, `registerHostContextMenuItem` | Host row badges and context menu entries |
|
| `registerHostBadge`, `registerHostContextMenuItem` | Host row badges and context menu entries |
|
||||||
| `registerPaletteEntry` | A global or per-host command palette entry |
|
| `registerPaletteEntry` | A global or per-host command palette entry |
|
||||||
| `registerDashboardCard` | A dashboard card, id in `contributes.dashboardCards` |
|
| `registerDashboardCard` | A dashboard card, id in `contributes.dashboardCards` |
|
||||||
@@ -437,6 +446,7 @@ boundary and Suspense.
|
|||||||
| `registerSshAuthEditor` | An SSH auth method's editor in the host editor |
|
| `registerSshAuthEditor` | An SSH auth method's editor in the host editor |
|
||||||
| `registerLoginMethod`, `registerSecondFactorUI` | Login screen UI for a method and a second factor challenge (plus an optional `enrollment` section) |
|
| `registerLoginMethod`, `registerSecondFactorUI` | Login screen UI for a method and a second factor challenge (plus an optional `enrollment` section) |
|
||||||
| `api`, `wsUrl(path)` | axios on `/plugin-api/<id>/` and the plugin's WebSocket URL |
|
| `api`, `wsUrl(path)` | axios on `/plugin-api/<id>/` and the plugin's WebSocket URL |
|
||||||
|
| `apiFor(origin)` | The same client for a resolved connection origin: `"remote"` reaches the desktop app's connected server (**B14**) |
|
||||||
| `t`, `hasPermission` | The plugin's strings and a permission check, for code outside a component (a toast from `activate`) |
|
| `t`, `hasPermission` | The plugin's strings and a permission check, for code outside a component (a toast from `activate`) |
|
||||||
| `tabs.open`, `getLayout`, `applyLayout`, `onChange` | Tab control, used by workspaces; `tabs.openRailView` (**B12**) opens a rail view |
|
| `tabs.open`, `getLayout`, `applyLayout`, `onChange` | Tab control, used by workspaces; `tabs.openRailView` (**B12**) opens a rail view |
|
||||||
| `guest`, `info`, `onDispose` | Guest mode flag, plugin info, extra cleanup |
|
| `guest`, `info`, `onDispose` | Guest mode flag, plugin info, extra cleanup |
|
||||||
@@ -504,6 +514,19 @@ their host info), `terminal.sendToActive(text, { run })` and
|
|||||||
its terminal tab wrapper populates from the core terminal ref it already
|
its terminal tab wrapper populates from the core terminal ref it already
|
||||||
holds. The snippets plugin's run/paste flow is the first caller.
|
holds. The snippets plugin's run/paste flow is the first caller.
|
||||||
|
|
||||||
|
**Host protocols (B14).** `app.registerHostProtocol({ id, settingKey,
|
||||||
|
portKey, defaultPort, titleKey, descriptionKey, icon, quickConnect })` adds a
|
||||||
|
protocol next to SSH. Its switch and port are the plugin's own host settings,
|
||||||
|
so core reads them from `host.pluginSettings[<plugin>]` and never knows the
|
||||||
|
protocol by name: the General tab draws the switch and saves it into the
|
||||||
|
plugin's settings, the Hosts panel filters and groups by it, HostItem counts
|
||||||
|
it, Quick Connect offers it (with a domain field when asked), and a host with
|
||||||
|
SSH off takes the first protocol switched on as its `connectionType` and its
|
||||||
|
port. `hostProtocols(host)` from `@termix/plugin-sdk/frontend` lists what a
|
||||||
|
host has on (`"ssh"` included), for a plugin such as session sharing that
|
||||||
|
offers every protocol without knowing who owns it. Remote desktop registers
|
||||||
|
`rdp`, `vnc` and `telnet`.
|
||||||
|
|
||||||
#### View ownership
|
#### View ownership
|
||||||
|
|
||||||
Who owns a tab, panel or card comes from manifests, so it is known even for a
|
Who owns a tab, panel or card comes from manifests, so it is known even for a
|
||||||
@@ -547,7 +570,11 @@ connection helpers (`isElectron`, `resolveConnectionOrigin`, `pluginWsUrl`,
|
|||||||
`hydrateLocalSharedHostAuth`, `useConnectionDefaults`); and a few core APIs
|
`hydrateLocalSharedHostAuth`, `useConnectionDefaults`); and a few core APIs
|
||||||
(`logActivity`, `getHostPassword`, `patchOpenTab`, `getUserPreferences`,
|
(`logActivity`, `getHostPassword`, `patchOpenTab`, `getUserPreferences`,
|
||||||
`parseCustomKeybindings`, `setHostAutoTmux`, `getCookie`). The APIs are a
|
`parseCustomKeybindings`, `setHostAutoTmux`, `getCookie`). The APIs are a
|
||||||
stopgap in the right place: D1 turns them into typed bridge members. Publishing its
|
stopgap in the right place: D1 turns them into typed bridge members.
|
||||||
|
**B14** added `buildOriginWsUrl`, `getBasePath`, `resolveRemoteHostId` and the
|
||||||
|
`ConnectionStage` type, for remote desktop's display socket and its remote
|
||||||
|
host lookup. `useConnectionDefaults` now carries terminal defaults only; the
|
||||||
|
RDP half became remote desktop's user settings. Publishing its
|
||||||
`.d.ts` for plugins outside this repo is a follow-up for the repo split.
|
`.d.ts` for plugins outside this repo is a follow-up for the repo split.
|
||||||
|
|
||||||
#### Strings
|
#### Strings
|
||||||
@@ -596,10 +623,12 @@ every enabled plugin that declares `contributes.settings.host`, it looks up
|
|||||||
`"<id>.hostImportNormalizer"` and, if the plugin registered one, calls it with
|
`"<id>.hostImportNormalizer"` and, if the plugin registered one, calls it with
|
||||||
the raw imported row and writes back whatever it returns (or nothing, for
|
the raw imported row and writes back whatever it returns (or nothing, for
|
||||||
`null`). This is not part of the SDK's typed `ctx` surface - it is a plain
|
`null`). This is not part of the SDK's typed `ctx` surface - it is a plain
|
||||||
`ctx.registry` convention, the same mechanism `remote-desktop.sessions`
|
`ctx.registry` convention, just with a name core's own code knows to look
|
||||||
already uses, just with a name core's own code knows
|
for. web-endpoint (`src/backend/host-import.ts`) is the first plugin to
|
||||||
to look for. web-endpoint (`src/backend/host-import.ts`) is the first plugin
|
register one; remote desktop's (**B14**) also reads a Termix export's
|
||||||
to register one.
|
`pluginSettings` and the flat fields hosts had before 2.9.0. Host exports
|
||||||
|
carry every plugin's host settings (secrets redacted) under `pluginSettings`,
|
||||||
|
which is what makes the round trip work without core naming a plugin.
|
||||||
|
|
||||||
**B9**'s terminal provides `sessions.live` (live SSH sessions: look up, end,
|
**B9**'s terminal provides `sessions.live` (live SSH sessions: look up, end,
|
||||||
remove a share's participants, hand a room share's control over, subscribe to
|
remove a share's participants, hand a room share's control over, subscribe to
|
||||||
@@ -623,8 +652,11 @@ per-chunk itself), `persist(summary)` and `discard()` for a session that
|
|||||||
recorded nothing. `createFinished(input)` is the second half of the
|
recorded nothing. `createFinished(input)` is the second half of the
|
||||||
contract, for a caller that already wrote its own recording to disk and only
|
contract, for a caller that already wrote its own recording to disk and only
|
||||||
needs the database row: remote-desktop's guacd recordings use it instead of
|
needs the database row: remote-desktop's guacd recordings use it instead of
|
||||||
importing the old repository directly, and without the plugin enabled a
|
importing the old repository directly. **B14** added `enabledFor(hostId)`
|
||||||
guacd recording still lands on disk but gets no row. Recording files live
|
(version 1.1.0), because guacd has to be told to record before the session
|
||||||
|
exists: remote desktop records an RDP or Telnet session only when the plugin
|
||||||
|
is running and the host's switch is on, and writes the row as the session's
|
||||||
|
user. Recording files live
|
||||||
under `ctx.files.dataDir()/session_logs/<user>/<session>.cast`. Retention
|
under `ctx.files.dataDir()/session_logs/<user>/<session>.cast`. Retention
|
||||||
(`retentionDays`, an admin setting) runs a sweep at boot and every 24 hours.
|
(`retentionDays`, an admin setting) runs a sweep at boot and every 24 hours.
|
||||||
|
|
||||||
@@ -647,8 +679,10 @@ any other, `ctx.services.get(service, { provider })` reaches one, and
|
|||||||
`ctx.services.providers(service)` lists the running ones. A requirement is
|
`ctx.services.providers(service)` lists the running ones. A requirement is
|
||||||
satisfied by any compatible provider. `sessions.live` is the first: keyed by
|
satisfied by any compatible provider. `sessions.live` is the first: keyed by
|
||||||
session type, ssh-terminal provides `ssh` and remote desktop provides `rdp`,
|
session type, ssh-terminal provides `ssh` and remote desktop provides `rdp`,
|
||||||
`vnc` and `telnet` (B14), with `createViewerToken(sessionId, readOnly)` for
|
`vnc` and `telnet` (**B14** delivered them), with
|
||||||
a viewer. A consumer with no actor, such as a public guest route, calls it
|
`createViewerToken(sessionId, readOnly)` for a viewer. A remote desktop
|
||||||
|
session id is guacd's own connection id, and `ownerEndSession` closes it and
|
||||||
|
every viewer joined to it. A consumer with no actor, such as a public guest route, calls it
|
||||||
through `ctx.asUser(<share owner>)`.
|
through `ctx.asUser(<share owner>)`.
|
||||||
|
|
||||||
**Session sharing (B12)** is the session-sharing plugin. It provides
|
**Session sharing (B12)** is the session-sharing plugin. It provides
|
||||||
@@ -954,6 +988,19 @@ dropping a column before its data has moved loses it:
|
|||||||
A column that is still read anywhere in core is not ready to be dropped. Ship
|
A column that is still read anywhere in core is not ready to be dropped. Ship
|
||||||
the copy and the switch first, and drop it in a later step if that is safer.
|
the copy and the switch first, and drop it in a later step if that is safer.
|
||||||
|
|
||||||
|
**Order at boot (B14).** On Postgres and MySQL, core's drizzle migrations run
|
||||||
|
while the database opens, and the copies into `plugin_settings` run later,
|
||||||
|
after the plugins table exists. A real `DROP COLUMN` in step 4 therefore
|
||||||
|
deletes the data before it is copied. So step 4 removes the column from
|
||||||
|
`schema.ts`, the pg/mysql variants and the `db/index.ts` bootstrap, and the
|
||||||
|
generated drizzle migration's `DROP COLUMN` statements are replaced with a
|
||||||
|
comment and `SELECT 1;`, exactly as for an adopted table. The column stays in
|
||||||
|
the database, unused, until 3.0.0 removes it. A copy reads it with raw SQL
|
||||||
|
through `utils/crypto-migration/raw-rows.ts`, whose `selectRows` and
|
||||||
|
`runStatement` work on all three engines: `getDb().all()` exists only on
|
||||||
|
SQLite. Remote desktop's host options and `user_preferences.rdp_defaults` are
|
||||||
|
the first columns handled this way.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Manifest v2 reference
|
## Manifest v2 reference
|
||||||
@@ -1230,6 +1277,8 @@ Built per plugin in `src/backend/plugins/ctx.ts` and passed to `activate`.
|
|||||||
| `ctx.notify.*` | `notify:send` | A6 |
|
| `ctx.notify.*` | `notify:send` | A6 |
|
||||||
| `ctx.auth.*` | `auth:provide` | **A8** |
|
| `ctx.auth.*` | `auth:provide` | **A8** |
|
||||||
| `ctx.desktop.openIsolatedWindow` | `desktop:window` | **B8** |
|
| `ctx.desktop.openIsolatedWindow` | `desktop:window` | **B8** |
|
||||||
|
| `ctx.desktop.launchNativeRdp` / `.available` | `desktop:window` (launch only) | **B14** |
|
||||||
|
| `ctx.credentials.resolveHostProtocol` | `credentials:read` | **B14** |
|
||||||
| `ctx.audit.record` | none, the actor is the runtime's | **B9** |
|
| `ctx.audit.record` | none, the actor is the runtime's | **B9** |
|
||||||
| `ctx.fetch` | `network:outbound` | B |
|
| `ctx.fetch` | `network:outbound` | B |
|
||||||
|
|
||||||
@@ -1364,6 +1413,29 @@ share). Rejects outside the desktop app (`ELECTRON_EMBEDDED` unset, or no
|
|||||||
shares cookies or storage with the main window or with another isolated
|
shares cookies or storage with the main window or with another isolated
|
||||||
window.
|
window.
|
||||||
|
|
||||||
|
**B14** added `ctx.desktop.launchNativeRdp({ host, port, username, domain })`
|
||||||
|
over the same bridge (a second entry in `BACKEND_REQUEST_HANDLERS`,
|
||||||
|
`launch-native-rdp`), which opens mstsc on Windows with a generated `.rdp`
|
||||||
|
file that never carries the password, and `ctx.desktop.available()`, which
|
||||||
|
says whether the server runs embedded in the desktop app. The renderer's own
|
||||||
|
`openNativeRdp` IPC call is gone; the desktop app's frontend always asks its
|
||||||
|
embedded backend, which is the one that can reach Electron.
|
||||||
|
|
||||||
|
**B14** also added `ctx.credentials.resolveHostProtocol(hostId, protocol)`,
|
||||||
|
the first member behind `credentials:read`. It returns the host's address,
|
||||||
|
jump hosts and the plaintext RDP, VNC or Telnet login the acting user may use,
|
||||||
|
for a plugin that has to hand them to another program (guacd). The logins
|
||||||
|
stay in core next to the host: an owner gets the stored login or the stored
|
||||||
|
credential it points at; a shared recipient gets only core's sharing
|
||||||
|
resolution (the owner's shared snapshot or their own override), never the
|
||||||
|
owner's raw secret; no connect access, or no host, is `null`. Every call is
|
||||||
|
audited as `plugin_credentials_read`, allowed or refused.
|
||||||
|
|
||||||
|
`ctx.hosts.create` (**B14**) hands the created row to every plugin's
|
||||||
|
`hostImportNormalizer`, the same as a bulk import row, so a plugin creating a
|
||||||
|
host can set another plugin's host settings (proxmox marks an imported
|
||||||
|
Windows guest as RDP) without core naming either.
|
||||||
|
|
||||||
### The actor
|
### The actor
|
||||||
|
|
||||||
Never comes from plugin code. It is held in `AsyncLocalStorage` and set two
|
Never comes from plugin code. It is held in `AsyncLocalStorage` and set two
|
||||||
@@ -1888,8 +1960,8 @@ Then, with the app running:
|
|||||||
|
|
||||||
## Legacy core imports: the debt D1 removes
|
## Legacy core imports: the debt D1 removes
|
||||||
|
|
||||||
The bundled plugins predate the SDK, apart from workspaces (A9) and snippets
|
The bundled plugins predate the SDK, apart from workspaces (A9), snippets
|
||||||
(B2), which import nothing from core. The others still reach core by relative
|
(B2) and remote-desktop (B14), which import nothing from core. The others still reach core by relative
|
||||||
path (`../../../../src/backend/...`), which an esbuild plugin,
|
path (`../../../../src/backend/...`), which an esbuild plugin,
|
||||||
`packages/plugin-sdk/cli/lib/legacy-core-imports.mjs`, keeps out of the bundle
|
`packages/plugin-sdk/cli/lib/legacy-core-imports.mjs`, keeps out of the bundle
|
||||||
and rewrites to the compiled output path (`../../../backend/backend/...`, or
|
and rewrites to the compiled output path (`../../../backend/backend/...`, or
|
||||||
@@ -1916,8 +1988,8 @@ What the lint fence enforces today, in `eslint.config.mjs`:
|
|||||||
| Core importing a plugin backend | **Error** | 0 | - |
|
| Core importing a plugin backend | **Error** | 0 | - |
|
||||||
| A plugin backend importing frontend code or `@/` | **Error** | 0 | - |
|
| A plugin backend importing frontend code or `@/` | **Error** | 0 | - |
|
||||||
| The shell importing plugin code | **Error** | 0 | - |
|
| The shell importing plugin code | **Error** | 0 | - |
|
||||||
| A plugin frontend importing core through `@/` | Warning | 130 files | D1 |
|
| A plugin frontend importing core through `@/` | Warning | 120 files | D1 |
|
||||||
| A plugin importing core by relative path | Warning | 66 files | D1 |
|
| A plugin importing core by relative path | Warning | 63 files | D1 |
|
||||||
| A plugin importing another plugin's source | Warning | 3 files | B18 |
|
| A plugin importing another plugin's source | Warning | 3 files | B18 |
|
||||||
|
|
||||||
A warning does not fail a build, so the counts are held by
|
A warning does not fail a build, so the counts are held by
|
||||||
@@ -1957,6 +2029,13 @@ Known specifics:
|
|||||||
command history panel into ssh-terminal, which imports nothing from core.
|
command history panel into ssh-terminal, which imports nothing from core.
|
||||||
The file manager's terminal window renders the `terminal.view` component
|
The file manager's terminal window renders the `terminal.view` component
|
||||||
instead of importing the core terminal.
|
instead of importing the core terminal.
|
||||||
- Host data columns (`enableDocker`, `enableRdp`, `guacamoleConfig` and so on)
|
- Host data columns (`enableDocker` and so on) stay in core types until
|
||||||
stay in core types until Phase B moves them. Only UI branches on them left
|
Phase B moves them. Only UI branches on them left the shell. **B14** moved
|
||||||
the shell.
|
remote desktop's (`enableRdp`, `enableVnc`, `enableTelnet`, the three ports,
|
||||||
|
`rdpSecurity`, `rdpIgnoreCert`, `guacamoleConfig`) into its host settings;
|
||||||
|
the protocol logins stay core host fields.
|
||||||
|
- **B14** moved remote desktop onto the SDK: it imports nothing from core,
|
||||||
|
owns no module state, and its guacamole-lite server is built in `activate`
|
||||||
|
with its own signal handlers removed (guacamole-lite installs SIGTERM and
|
||||||
|
SIGINT handlers in its constructor). guacd is always an external service,
|
||||||
|
so there is no `ctx.process`; its address is an admin setting.
|
||||||
@@ -161,11 +161,9 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
|||||||
ssh-terminal host settings shipped (`ssh-terminal-settings-migration.ts`)
|
ssh-terminal host settings shipped (`ssh-terminal-settings-migration.ts`)
|
||||||
and the terminal reads its settings, but core host routes, types, the
|
and the terminal reads its settings, but core host routes, types, the
|
||||||
export/import sample and `HostEditorData` still carry the columns.
|
export/import sample and `HostEditorData` still carry the columns.
|
||||||
`enable_terminal_toolbar` also drives the RDP/VNC/Telnet toolbar
|
**B14** gave remote desktop its own `enableToolbar` host setting, copied
|
||||||
(`remote-desktop/GuacamoleApp.tsx`), whose host editor switch stays in the
|
from `enable_terminal_toolbar`, so nothing else holds the column up. Drop
|
||||||
General tab, so remote-desktop needs its own host setting and copy first.
|
all three the B14 way (see "Order at boot" in ARCHITECTURE.md). Owner: D0.
|
||||||
Then drop all three in lockstep. Owner: remote-desktop's step for its copy,
|
|
||||||
then D0.
|
|
||||||
- **B9 (ssh-terminal):** the legacy `settings` rows for the terminal
|
- **B9 (ssh-terminal):** the legacy `settings` rows for the terminal
|
||||||
(`terminal_session_timeout_minutes`, `terminal_session_persistence_enabled`,
|
(`terminal_session_timeout_minutes`, `terminal_session_persistence_enabled`,
|
||||||
`command_history_enabled`, `touch_input_settings`, `terminal_image_*`) are
|
`command_history_enabled`, `touch_input_settings`, `terminal_image_*`) are
|
||||||
@@ -255,17 +253,6 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
|||||||
section. Owner: whoever designs the 3.0.0 install flow (prebuild-per-platform
|
section. Owner: whoever designs the 3.0.0 install flow (prebuild-per-platform
|
||||||
packaging, or a documented "avoid native code" rule for community plugins).
|
packaging, or a documented "avoid native code" rule for community plugins).
|
||||||
|
|
||||||
- **B12 (session-sharing):** the remote desktop side of sharing has no
|
|
||||||
provider yet. Session sharing reaches rdp, vnc and telnet sessions through
|
|
||||||
`sessions.live` named providers `rdp`, `vnc` and `telnet`, each with
|
|
||||||
`getSession(sessionId)` (id, userId as the session's owner, hostId,
|
|
||||||
hostName, isConnected, createdAt, tabInstanceId) and
|
|
||||||
`createViewerToken(sessionId, readOnly)`, typed in
|
|
||||||
`plugins/session-sharing/src/backend/live.ts`. Until remote-desktop provides
|
|
||||||
them, sharing or presenting an RDP/VNC/Telnet session is refused and those
|
|
||||||
links resolve 404. B14 also drops `createJoinToken`/`getSessionInfo` from
|
|
||||||
its `remote-desktop.sessions` registry entry, which core no longer reads.
|
|
||||||
Owner: B14.
|
|
||||||
- **B12 (session-sharing):** `allow_session_sharing` is still a live `ssh_data`
|
- **B12 (session-sharing):** `allow_session_sharing` is still a live `ssh_data`
|
||||||
column. The copy into the plugin's host setting shipped
|
column. The copy into the plugin's host setting shipped
|
||||||
(`session-sharing-settings-migration.ts`) and nothing in core reads or
|
(`session-sharing-settings-migration.ts`) and nothing in core reads or
|
||||||
@@ -291,9 +278,8 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
|||||||
a real behaviour change from the old column default, made once here rather
|
a real behaviour change from the old column default, made once here rather
|
||||||
than silently. `ssh-terminal/src/backend/terminal-socket.ts` still reads
|
than silently. `ssh-terminal/src/backend/terminal-socket.ts` still reads
|
||||||
`resolvedHostData?.enableSessionLogging ?? hostConfig.enableSessionLogging`
|
`resolvedHostData?.enableSessionLogging ?? hostConfig.enableSessionLogging`
|
||||||
off the resolved SSH host instead of `ctx.settings.getHost`, and
|
off the resolved SSH host instead of `ctx.settings.getHost` (remote desktop
|
||||||
`plugins/remote-desktop/src/backend/routes.ts`'s
|
asks `recordings.writer.enabledFor` since B14), and `HostEditorData.ts`,
|
||||||
`host.enableSessionLogging !== false` check, `HostEditorData.ts`,
|
|
||||||
`HostManagerData.ts`, `quick-connect-host.ts` and the export/import sample
|
`HostManagerData.ts`, `quick-connect-host.ts` and the export/import sample
|
||||||
all still read the column directly. The General tab's checkbox was removed
|
all still read the column directly. The General tab's checkbox was removed
|
||||||
from `HostEditor.tsx` (the schema-driven Plugins group replaces it); the
|
from `HostEditor.tsx` (the schema-driven Plugins group replaces it); the
|
||||||
@@ -301,14 +287,6 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
|||||||
(`schema.ts`, `db/index.ts`, a drizzle migration per dialect,
|
(`schema.ts`, `db/index.ts`, a drizzle migration per dialect,
|
||||||
`schema:generate`) needs every one of those switched first. Owner: a
|
`schema:generate`) needs every one of those switched first. Owner: a
|
||||||
dedicated follow-up step, or D0.
|
dedicated follow-up step, or D0.
|
||||||
- **B13 (session-recording):** `remote-desktop`'s guacd recordings now insert
|
|
||||||
through the optional `recordings.writer` service's `createFinished(input)`
|
|
||||||
(added alongside `open()`/`RecordingSink` for ssh-terminal's incremental
|
|
||||||
writer) instead of importing `createCurrentSessionRecordingRepository`
|
|
||||||
directly. Without the session-recording plugin enabled, a guacd recording
|
|
||||||
is still written to disk but gets no `session_recordings` row and so never
|
|
||||||
appears in the list - a warning is logged. Owner: none needed unless a
|
|
||||||
second consumer of `recordings.writer` appears.
|
|
||||||
- **B13 (session-recording):** `accessId` (the legacy `access_id` column,
|
- **B13 (session-recording):** `accessId` (the legacy `access_id` column,
|
||||||
referencing the now-core-only `host_access` table) is carried in the
|
referencing the now-core-only `host_access` table) is carried in the
|
||||||
adopted table's shape as a plain unreferenced integer, since the SDK's
|
adopted table's shape as a plain unreferenced integer, since the SDK's
|
||||||
@@ -317,6 +295,56 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
|||||||
in core before this step too). Owner: none needed unless a future step
|
in core before this step too). Owner: none needed unless a future step
|
||||||
wants to actually use it.
|
wants to actually use it.
|
||||||
|
|
||||||
|
- **B14 (remote-desktop):** the moved columns stay in the database, unused:
|
||||||
|
`ssh_data.enable_rdp`, `enable_vnc`, `enable_telnet`, `rdp_port`,
|
||||||
|
`vnc_port`, `telnet_port`, `rdp_security`, `rdp_ignore_cert`, `security`,
|
||||||
|
`ignore_cert`, `guacamole_config`, and `user_preferences.rdp_defaults`.
|
||||||
|
Their drizzle migrations are `SELECT 1;` on purpose (the copy runs after
|
||||||
|
drizzle on Postgres and MySQL). Drop them physically in 3.0.0, once every
|
||||||
|
install has booted 2.9.0. The legacy `guac_enabled` and `guac_url` rows in
|
||||||
|
`settings` are left too. Owner: 3.0.0.
|
||||||
|
- **B14, found in B5 (proxmox):** `drizzle/postgres/0036_redundant_ender_wiggin.sql`
|
||||||
|
and `drizzle/mysql/0035_clumsy_leech.sql` really `DROP COLUMN` the four
|
||||||
|
proxmox host columns. On Postgres and MySQL that runs at boot before
|
||||||
|
`runProxmoxSettingsMigration` copies them, so an upgrade there loses every
|
||||||
|
host's Proxmox settings. Since nothing has shipped, rewrite those two files
|
||||||
|
to the `SELECT 1;` pattern (keeping their journal entries). Owner: D0.
|
||||||
|
- **B14, found in B5 to B13:** every earlier host-column copy
|
||||||
|
(`proxmox`, `file-manager`, `tunnels`, `web-endpoint`, `ssh-terminal`,
|
||||||
|
`tmux-monitor`, `session-sharing`, `session-recording` settings migrations)
|
||||||
|
reads the old columns with `getDb().all(sql...)`, which only exists on
|
||||||
|
SQLite. On Postgres and MySQL the call throws, the migration's catch logs a
|
||||||
|
warning and nothing is copied. Switch them to `selectRows` from
|
||||||
|
`utils/crypto-migration/raw-rows.ts`, which B14 added and uses. Owner: D0.
|
||||||
|
- **B14, found in B13:** `session-recording-settings-migration.ts` writes
|
||||||
|
`row.enable_session_logging !== false`, but SQLite returns `0`, so a host
|
||||||
|
that had recording off migrates as on. Its test only uses real booleans.
|
||||||
|
Owner: D0.
|
||||||
|
- **B14 (remote-desktop), for D2:** host payloads carry every plugin's host
|
||||||
|
settings after the sharing sanitizers, so a connect-level recipient now
|
||||||
|
receives remote desktop's `guacamoleConfig`, which can hold an RDP gateway
|
||||||
|
password in plain text. A view-level recipient saw it before B14 too.
|
||||||
|
Decide whether plugin host settings need a per-field sharing level, or make
|
||||||
|
the gateway password its own secret field. Owner: D2.
|
||||||
|
- **B14 (remote-desktop):** core still holds some locale keys that look dead
|
||||||
|
after this step but may be built dynamically: `hosts.guac.noCredential`,
|
||||||
|
`authType*`, `recording*`, `saveHostFirst`, `sharingOptionsAfterSave`,
|
||||||
|
`permissionLevel`, `typeHeader`, `targetHeader`, `permissionHeader`,
|
||||||
|
`hosts.connectRdp/Vnc/Telnet`, `hosts.copy*UrlAction`, `hosts.telnet` and
|
||||||
|
`homepage.connType_*`. Owner: whoever next cleans up `src/ui/locales/en.json`,
|
||||||
|
or D0.
|
||||||
|
- **Pre-existing, found in B14:** `npm run test -w plugins/web-endpoint`
|
||||||
|
fails "answers 503 while the tunnels plugin is not available": the route
|
||||||
|
expects `ctx.services.get` to throw for a missing service, but since B12
|
||||||
|
the runtime and the test doubles hand back an empty handle, so the route
|
||||||
|
calls an undefined `forward` and answers 500. Check `typeof forward` the
|
||||||
|
way remote desktop does. Owner: D0.
|
||||||
|
- **Pre-existing, found in B14:** each plugin's own `typecheck` script
|
||||||
|
(`tsc -p tsconfig.json`) fails with the SDK's `tsconfig.plugin.json`
|
||||||
|
(node16 resolution wants `.js` on relative imports and cannot resolve
|
||||||
|
`@termix/plugin-sdk/ui`), workspaces included. `npm run type-check` covers
|
||||||
|
plugins through `tsconfig.plugins*.json` and passes. Owner: D0.
|
||||||
|
|
||||||
## Manual checks after 2.9.0
|
## Manual checks after 2.9.0
|
||||||
|
|
||||||
- SSH terminal: password auth, key auth (and an encrypted key's passphrase
|
- SSH terminal: password auth, key auth (and an encrypted key's passphrase
|
||||||
@@ -334,3 +362,15 @@ build`'s esbuild step has no static-asset-copy pipeline the way core's Vite
|
|||||||
off records nothing, retention prunes an old recording on schedule, a guacd
|
off records nothing, retention prunes an old recording on schedule, a guacd
|
||||||
RDP/VNC/Telnet recording still gets a row and plays back, and deleting a
|
RDP/VNC/Telnet recording still gets a row and plays back, and deleting a
|
||||||
user anonymizes their recordings instead of removing them.
|
user anonymizes their recordings instead of removing them.
|
||||||
|
- Remote desktop: an RDP, a VNC and a Telnet host; a host behind one jump
|
||||||
|
host (tunnels plugin on and off) and behind a chain of two; macOS Screen
|
||||||
|
Sharing over VNC with no username; RDP recording (and none with
|
||||||
|
session-recording off or the host's switch off); file transfer through the
|
||||||
|
RDP drive, drag and drop included; clipboard both ways; sharing an RDP
|
||||||
|
session by link (read-only and read-write) and presenting it in a collab
|
||||||
|
room; opening Windows Remote Desktop from the desktop app on Windows;
|
||||||
|
changing the guacd URL in admin settings without a restart; turning Remote
|
||||||
|
Desktop off in admin (actions disappear, connects answer 403); a 2.8 host
|
||||||
|
that only had `connection_type = rdp` comes up as RDP with SSH off; RDP user
|
||||||
|
defaults apply to a saved host; Quick Connect over RDP and VNC; the desktop
|
||||||
|
app with a host on the remote server and one on This device.
|
||||||
@@ -510,6 +510,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"description": "Registered component id. Required when type is \"custom\"."
|
"description": "Registered component id. Required when type is \"custom\"."
|
||||||
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Stored and validated, but not drawn by the generic settings form."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -602,6 +606,10 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"description": "Registered component id. Required when type is \"custom\"."
|
"description": "Registered component id. Required when type is \"custom\"."
|
||||||
|
},
|
||||||
|
"hidden": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Stored and validated, but not drawn by the generic settings form."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -477,7 +477,6 @@ export interface PluginHostRecord {
|
|||||||
folder: string | null;
|
folder: string | null;
|
||||||
jumpHosts?: unknown;
|
jumpHosts?: unknown;
|
||||||
enableSsh?: boolean | null;
|
enableSsh?: boolean | null;
|
||||||
enableRdp?: boolean | null;
|
|
||||||
enableTerminal?: boolean | null;
|
enableTerminal?: boolean | null;
|
||||||
enableFileManager?: boolean | null;
|
enableFileManager?: boolean | null;
|
||||||
enableTunnel?: boolean | null;
|
enableTunnel?: boolean | null;
|
||||||
@@ -487,7 +486,11 @@ export interface PluginHostRecord {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fields a plugin may set when creating a host it will own or manage. */
|
/**
|
||||||
|
* Fields a plugin may set when creating a host it will own or manage. Fields
|
||||||
|
* that belong to another plugin's host settings (enableRdp, rdpPort) are
|
||||||
|
* handed to that plugin's host import normalizer, as on a bulk import.
|
||||||
|
*/
|
||||||
export type PluginHostCreateInput = Partial<
|
export type PluginHostCreateInput = Partial<
|
||||||
Omit<PluginHostRecord, "id" | "userId">
|
Omit<PluginHostRecord, "id" | "userId">
|
||||||
> & {
|
> & {
|
||||||
@@ -1116,10 +1119,69 @@ export interface PluginOpenIsolatedWindowRequest {
|
|||||||
* Electron only: rejects when the server is not running embedded in the
|
* Electron only: rejects when the server is not running embedded in the
|
||||||
* desktop app. Needs desktop:window.
|
* desktop app. Needs desktop:window.
|
||||||
*/
|
*/
|
||||||
|
export interface PluginNativeRdpRequest {
|
||||||
|
host: string;
|
||||||
|
port?: number;
|
||||||
|
username?: string;
|
||||||
|
domain?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PluginDesktop {
|
export interface PluginDesktop {
|
||||||
openIsolatedWindow: (
|
openIsolatedWindow: (
|
||||||
request: PluginOpenIsolatedWindowRequest,
|
request: PluginOpenIsolatedWindowRequest,
|
||||||
) => Promise<{ success: true }>;
|
) => Promise<{ success: true }>;
|
||||||
|
/**
|
||||||
|
* Opens the operating system's own RDP client (mstsc on Windows) for a
|
||||||
|
* host. The password is never passed; the client asks for it. Windows
|
||||||
|
* desktop app only.
|
||||||
|
*/
|
||||||
|
launchNativeRdp: (
|
||||||
|
request: PluginNativeRdpRequest,
|
||||||
|
) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
/** Whether the server runs embedded in the desktop app, so the calls above can work. */
|
||||||
|
available: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A host protocol whose credentials core keeps next to the host. */
|
||||||
|
export type PluginHostProtocol = "rdp" | "vnc" | "telnet";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What a plugin needs to hand a host's protocol login to something core
|
||||||
|
* does not run (guacd): the address and the plaintext credentials the acting
|
||||||
|
* user is allowed to use. A shared recipient gets the owner's shared
|
||||||
|
* snapshot or their own override, never the owner's raw secret.
|
||||||
|
*/
|
||||||
|
export interface PluginProtocolTarget {
|
||||||
|
host: {
|
||||||
|
id: number;
|
||||||
|
name: string | null;
|
||||||
|
ip: string;
|
||||||
|
/** The host's SSH port, the old fallback when no protocol port is set. */
|
||||||
|
port: number;
|
||||||
|
ownerUserId: string;
|
||||||
|
jumpHosts: Array<{ hostId: number }>;
|
||||||
|
};
|
||||||
|
/** True when the acting user is not the owner. */
|
||||||
|
shared: boolean;
|
||||||
|
auth: {
|
||||||
|
/** "direct", "credential", or "none" when the user is asked at connect time. */
|
||||||
|
authType: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
domain: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plaintext host credentials, for a plugin that has to hand them to another
|
||||||
|
* program. Needs credentials:read, and every call is audited.
|
||||||
|
*/
|
||||||
|
export interface PluginCredentials {
|
||||||
|
/** Null when the host does not exist or the acting user cannot connect to it. */
|
||||||
|
resolveHostProtocol: (
|
||||||
|
hostId: number,
|
||||||
|
protocol: PluginHostProtocol,
|
||||||
|
) => Promise<PluginProtocolTarget | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1145,7 +1207,7 @@ export interface PluginAudit {
|
|||||||
* hardware (a serial port, a USB device) is the first caller. Core cannot
|
* hardware (a serial port, a USB device) is the first caller. Core cannot
|
||||||
* mediate that access the way it mediates ctx.ssh or ctx.db, so the
|
* mediate that access the way it mediates ctx.ssh or ctx.db, so the
|
||||||
* capability here is a declared, reviewable, audited statement of intent
|
* capability here is a declared, reviewable, audited statement of intent
|
||||||
* rather than a technical gate, matching credentials:read and process:spawn.
|
* rather than a technical gate, matching process:spawn.
|
||||||
*/
|
*/
|
||||||
export interface PluginCapabilities {
|
export interface PluginCapabilities {
|
||||||
/** Whether this plugin currently holds `capability`. Not audited: for deciding whether to offer something, not for gating an action. */
|
/** Whether this plugin currently holds `capability`. Not audited: for deciding whether to offer something, not for gating an action. */
|
||||||
@@ -1181,6 +1243,8 @@ export interface PluginContext {
|
|||||||
readonly auth: PluginAuth;
|
readonly auth: PluginAuth;
|
||||||
/** Opens Electron windows outside the main renderer. Needs desktop:window. */
|
/** Opens Electron windows outside the main renderer. Needs desktop:window. */
|
||||||
readonly desktop: PluginDesktop;
|
readonly desktop: PluginDesktop;
|
||||||
|
/** Plaintext host protocol credentials. Needs credentials:read. */
|
||||||
|
readonly credentials: PluginCredentials;
|
||||||
/** Audit lines under the plugin's own action names. */
|
/** Audit lines under the plugin's own action names. */
|
||||||
readonly audit: PluginAudit;
|
readonly audit: PluginAudit;
|
||||||
|
|
||||||
|
|||||||
@@ -317,6 +317,26 @@ export interface HostActionContribution {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A connection protocol next to SSH, shown as a switch in the host editor's
|
||||||
|
* General tab and in host filters and Quick Connect. Its switch and port are
|
||||||
|
* this plugin's own host settings (declare both in contributes.settings.host).
|
||||||
|
*/
|
||||||
|
export interface HostProtocolContribution {
|
||||||
|
id: string;
|
||||||
|
/** Boolean host setting that turns the protocol on. */
|
||||||
|
settingKey: string;
|
||||||
|
/** Number host setting holding the port. */
|
||||||
|
portKey?: string;
|
||||||
|
defaultPort: number;
|
||||||
|
titleKey: string;
|
||||||
|
descriptionKey?: string;
|
||||||
|
icon: IconComponent;
|
||||||
|
order?: number;
|
||||||
|
/** Offer it in Quick Connect, optionally with a domain field. */
|
||||||
|
quickConnect?: { showDomain?: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
export interface HostBadgeContribution {
|
export interface HostBadgeContribution {
|
||||||
id: string;
|
id: string;
|
||||||
when: (host: PluginHostRecord) => boolean;
|
when: (host: PluginHostRecord) => boolean;
|
||||||
@@ -524,6 +544,7 @@ export interface TermixApp extends TermixAppInfo {
|
|||||||
section: HostEditorSectionContribution,
|
section: HostEditorSectionContribution,
|
||||||
) => Disposer;
|
) => Disposer;
|
||||||
registerHostAction: (action: HostActionContribution) => Disposer;
|
registerHostAction: (action: HostActionContribution) => Disposer;
|
||||||
|
registerHostProtocol: (protocol: HostProtocolContribution) => Disposer;
|
||||||
registerHostBadge: (badge: HostBadgeContribution) => Disposer;
|
registerHostBadge: (badge: HostBadgeContribution) => Disposer;
|
||||||
registerHostContextMenuItem: (
|
registerHostContextMenuItem: (
|
||||||
item: HostContextMenuItemContribution,
|
item: HostContextMenuItemContribution,
|
||||||
@@ -574,6 +595,11 @@ export interface TermixApp extends TermixAppInfo {
|
|||||||
|
|
||||||
/** HTTP client for this plugin's /plugin-api/<id>/ routes. */
|
/** HTTP client for this plugin's /plugin-api/<id>/ routes. */
|
||||||
api: PluginApiClient;
|
api: PluginApiClient;
|
||||||
|
/**
|
||||||
|
* The same client for a resolved connection origin: "remote" reaches the
|
||||||
|
* desktop app's connected remote server, anything else is `api`.
|
||||||
|
*/
|
||||||
|
apiFor: (origin?: unknown) => PluginApiClient;
|
||||||
/** WebSocket URL and auth subprotocols for /plugin-ws/<id>/<path>. */
|
/** WebSocket URL and auth subprotocols for /plugin-ws/<id>/<path>. */
|
||||||
wsUrl: (
|
wsUrl: (
|
||||||
path: string,
|
path: string,
|
||||||
@@ -668,6 +694,7 @@ export interface PluginHostBridge {
|
|||||||
usePluginComponent: (
|
usePluginComponent: (
|
||||||
id: string,
|
id: string,
|
||||||
) => ComponentType<Record<string, unknown>> | undefined;
|
) => ComponentType<Record<string, unknown>> | undefined;
|
||||||
|
hostProtocols: (host: PluginHostRecord) => string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
let host: PluginHostBridge | null = null;
|
let host: PluginHostBridge | null = null;
|
||||||
@@ -778,6 +805,15 @@ export function useSshAuthTypes(): {
|
|||||||
* next to its own. Same permission and `when` filtering as ComponentSlot;
|
* next to its own. Same permission and `when` filtering as ComponentSlot;
|
||||||
* `context` is what each contribution's `when` sees.
|
* `context` is what each contribution's `when` sees.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* The connection protocols a host has switched on: "ssh" plus every protocol
|
||||||
|
* a running plugin registered with registerHostProtocol (RDP, VNC). Lets a
|
||||||
|
* plugin offer a protocol without knowing which plugin owns its settings.
|
||||||
|
*/
|
||||||
|
export function hostProtocols(record: PluginHostRecord): string[] {
|
||||||
|
return requireHost().hostProtocols(record);
|
||||||
|
}
|
||||||
|
|
||||||
export function useSlotContributions(
|
export function useSlotContributions(
|
||||||
slotId: string,
|
slotId: string,
|
||||||
context?: Record<string, unknown>,
|
context?: Record<string, unknown>,
|
||||||
|
|||||||
@@ -224,6 +224,12 @@ export interface PluginSettingsField {
|
|||||||
group?: string;
|
group?: string;
|
||||||
/** Registered component id. Required when type is "custom". */
|
/** Registered component id. Required when type is "custom". */
|
||||||
component?: string;
|
component?: string;
|
||||||
|
/**
|
||||||
|
* Stored and validated like any field, but not drawn by the generic
|
||||||
|
* settings form, because the plugin edits it in its own UI (a host editor
|
||||||
|
* section).
|
||||||
|
*/
|
||||||
|
hidden?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginHostSettingsContribution {
|
export interface PluginHostSettingsContribution {
|
||||||
@@ -382,6 +388,7 @@ const ALLOWED_SETTINGS_FIELD = [
|
|||||||
"permission",
|
"permission",
|
||||||
"group",
|
"group",
|
||||||
"component",
|
"component",
|
||||||
|
"hidden",
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Settings keys are stored as-is, so they stay short and index-safe. */
|
/** Settings keys are stored as-is, so they stay short and index-safe. */
|
||||||
@@ -933,6 +940,9 @@ function validateSettingsFields(
|
|||||||
if ("permission" in raw) {
|
if ("permission" in raw) {
|
||||||
requireString(raw.permission, `${at}.permission`, errors);
|
requireString(raw.permission, `${at}.permission`, errors);
|
||||||
}
|
}
|
||||||
|
if ("hidden" in raw && typeof raw.hidden !== "boolean") {
|
||||||
|
errors.push(`${at}.hidden must be a boolean`);
|
||||||
|
}
|
||||||
|
|
||||||
if (raw.type === "select" || raw.type === "multiselect") {
|
if (raw.type === "select" || raw.type === "multiselect") {
|
||||||
validateSettingsOptions(raw.options, at, errors);
|
validateSettingsOptions(raw.options, at, errors);
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ import type {
|
|||||||
} from "./backend.js";
|
} from "./backend.js";
|
||||||
import type { SyncEntityRegistration } from "./backend.js";
|
import type { SyncEntityRegistration } from "./backend.js";
|
||||||
import type { PluginDatabase } from "./backend.js";
|
import type { PluginDatabase } from "./backend.js";
|
||||||
|
import type {
|
||||||
|
PluginNativeRdpRequest,
|
||||||
|
PluginProtocolTarget,
|
||||||
|
} from "./backend.js";
|
||||||
|
|
||||||
export interface FakeContextOptions {
|
export interface FakeContextOptions {
|
||||||
pluginId?: string;
|
pluginId?: string;
|
||||||
@@ -69,6 +73,13 @@ export interface FakeContextOptions {
|
|||||||
shareableRoles?: PluginShareableRole[];
|
shareableRoles?: PluginShareableRole[];
|
||||||
/** Hosts ctx.ssh.resolveHost answers with, secrets included. */
|
/** Hosts ctx.ssh.resolveHost answers with, secrets included. */
|
||||||
sshHosts?: PluginSshHost[];
|
sshHosts?: PluginSshHost[];
|
||||||
|
/**
|
||||||
|
* What ctx.credentials.resolveHostProtocol answers, keyed
|
||||||
|
* "<hostId>:<protocol>". A missing key answers null.
|
||||||
|
*/
|
||||||
|
protocolTargets?: Record<string, PluginProtocolTarget>;
|
||||||
|
/** What ctx.desktop.available() answers. Defaults to false. */
|
||||||
|
desktopAvailable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FakeAuthRegistrations {
|
export interface FakeAuthRegistrations {
|
||||||
@@ -123,6 +134,10 @@ export interface FakePluginContext {
|
|||||||
title?: string;
|
title?: string;
|
||||||
ignoreCert?: boolean;
|
ignoreCert?: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
/** Every ctx.desktop.launchNativeRdp call, in order. */
|
||||||
|
nativeRdpLaunches: PluginNativeRdpRequest[];
|
||||||
|
/** Every ctx.credentials.resolveHostProtocol call, in order. */
|
||||||
|
credentialReads: Array<{ hostId: number; protocol: string }>;
|
||||||
/** Every ctx.audit.record entry, in order. */
|
/** Every ctx.audit.record entry, in order. */
|
||||||
audits: Array<{ action: string; success: boolean; [key: string]: unknown }>;
|
audits: Array<{ action: string; success: boolean; [key: string]: unknown }>;
|
||||||
/** Every ctx.hosts.recordActivity call, in order. */
|
/** Every ctx.hosts.recordActivity call, in order. */
|
||||||
@@ -206,6 +221,8 @@ export function createFakeContext(
|
|||||||
const sshConnections: FakePluginContext["sshConnections"] = [];
|
const sshConnections: FakePluginContext["sshConnections"] = [];
|
||||||
const hostShares: FakePluginContext["hostShares"] = [];
|
const hostShares: FakePluginContext["hostShares"] = [];
|
||||||
const desktopWindows: FakePluginContext["desktopWindows"] = [];
|
const desktopWindows: FakePluginContext["desktopWindows"] = [];
|
||||||
|
const nativeRdpLaunches: FakePluginContext["nativeRdpLaunches"] = [];
|
||||||
|
const credentialReads: FakePluginContext["credentialReads"] = [];
|
||||||
const audits: FakePluginContext["audits"] = [];
|
const audits: FakePluginContext["audits"] = [];
|
||||||
const activities: FakePluginContext["activities"] = [];
|
const activities: FakePluginContext["activities"] = [];
|
||||||
const trackedSessions: number[] = [];
|
const trackedSessions: number[] = [];
|
||||||
@@ -635,6 +652,18 @@ export function createFakeContext(
|
|||||||
desktopWindows.push(request);
|
desktopWindows.push(request);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
launchNativeRdp: async (request) => {
|
||||||
|
nativeRdpLaunches.push(request);
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
available: () => options.desktopAvailable ?? false,
|
||||||
|
},
|
||||||
|
|
||||||
|
credentials: {
|
||||||
|
resolveHostProtocol: async (hostId, protocol) => {
|
||||||
|
credentialReads.push({ hostId, protocol });
|
||||||
|
return options.protocolTargets?.[`${hostId}:${protocol}`] ?? null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
asUser: async (userId, fn) => {
|
asUser: async (userId, fn) => {
|
||||||
@@ -666,6 +695,8 @@ export function createFakeContext(
|
|||||||
hostShares,
|
hostShares,
|
||||||
auth,
|
auth,
|
||||||
desktopWindows,
|
desktopWindows,
|
||||||
|
nativeRdpLaunches,
|
||||||
|
credentialReads,
|
||||||
audits,
|
audits,
|
||||||
activities,
|
activities,
|
||||||
trackedSessions,
|
trackedSessions,
|
||||||
@@ -704,6 +735,10 @@ export interface MockContextOptions {
|
|||||||
services?: Record<string, object>;
|
services?: Record<string, object>;
|
||||||
/** Hosts ctx.ssh.resolveHost serves. See FakeContextOptions. */
|
/** Hosts ctx.ssh.resolveHost serves. See FakeContextOptions. */
|
||||||
sshHosts?: PluginSshHost[];
|
sshHosts?: PluginSshHost[];
|
||||||
|
/** What ctx.credentials serves. See FakeContextOptions. */
|
||||||
|
protocolTargets?: Record<string, PluginProtocolTarget>;
|
||||||
|
/** What ctx.desktop.available() answers. */
|
||||||
|
desktopAvailable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MockPluginContext extends FakePluginContext {
|
export interface MockPluginContext extends FakePluginContext {
|
||||||
@@ -740,6 +775,8 @@ export function createMockCtx(
|
|||||||
hosts: options.hosts,
|
hosts: options.hosts,
|
||||||
sshHosts: options.sshHosts,
|
sshHosts: options.sshHosts,
|
||||||
services: options.services,
|
services: options.services,
|
||||||
|
protocolTargets: options.protocolTargets,
|
||||||
|
desktopAvailable: options.desktopAvailable,
|
||||||
manifest: {
|
manifest: {
|
||||||
capabilities: options.capabilities ?? [],
|
capabilities: options.capabilities ?? [],
|
||||||
...options.manifest,
|
...options.manifest,
|
||||||
@@ -972,6 +1009,18 @@ export function createMockCtx(
|
|||||||
require("desktop:window");
|
require("desktop:window");
|
||||||
return ctx.desktop.openIsolatedWindow(request);
|
return ctx.desktop.openIsolatedWindow(request);
|
||||||
},
|
},
|
||||||
|
launchNativeRdp: async (request) => {
|
||||||
|
require("desktop:window");
|
||||||
|
return ctx.desktop.launchNativeRdp(request);
|
||||||
|
},
|
||||||
|
available: () => ctx.desktop.available(),
|
||||||
|
},
|
||||||
|
|
||||||
|
credentials: {
|
||||||
|
resolveHostProtocol: async (hostId, protocol) => {
|
||||||
|
require("credentials:read");
|
||||||
|
return ctx.credentials.resolveHostProtocol(hostId, protocol);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
capabilities: {
|
capabilities: {
|
||||||
@@ -1180,6 +1229,7 @@ export interface RenderedPluginApp {
|
|||||||
tabs: () => string[];
|
tabs: () => string[];
|
||||||
panels: () => string[];
|
panels: () => string[];
|
||||||
hostActions: () => Array<{ id: string; tabType?: string }>;
|
hostActions: () => Array<{ id: string; tabType?: string }>;
|
||||||
|
hostProtocols: () => string[];
|
||||||
hostEditorSections: () => string[];
|
hostEditorSections: () => string[];
|
||||||
dashboardCards: () => string[];
|
dashboardCards: () => string[];
|
||||||
settingsComponents: () => string[];
|
settingsComponents: () => string[];
|
||||||
|
|||||||
@@ -300,7 +300,6 @@ function ConsoleTerminalInner({
|
|||||||
|
|
||||||
const origin = isElectronApp
|
const origin = isElectronApp
|
||||||
? await resolveConnectionOrigin({
|
? await resolveConnectionOrigin({
|
||||||
connectionType: "ssh",
|
|
||||||
connectionOrigin: hostConfig.connectionOrigin,
|
connectionOrigin: hostConfig.connectionOrigin,
|
||||||
})
|
})
|
||||||
: "local";
|
: "local";
|
||||||
|
|||||||
@@ -982,7 +982,6 @@ export async function ensureSSHSessionForHost(
|
|||||||
): Promise<EnsureSSHSessionResult> {
|
): Promise<EnsureSSHSessionResult> {
|
||||||
const sessionId = host.id.toString();
|
const sessionId = host.id.toString();
|
||||||
const origin = await resolveConnectionOrigin({
|
const origin = await resolveConnectionOrigin({
|
||||||
connectionType: host.connectionType,
|
|
||||||
connectionOrigin: host.connectionOrigin,
|
connectionOrigin: host.connectionOrigin,
|
||||||
});
|
});
|
||||||
setSessionOrigin(sessionId, origin);
|
setSessionOrigin(sessionId, origin);
|
||||||
|
|||||||
@@ -652,15 +652,11 @@ async function syncProxmoxHost(
|
|||||||
rdpUser: null,
|
rdpUser: null,
|
||||||
rdpPassword: null,
|
rdpPassword: null,
|
||||||
rdpDomain: null,
|
rdpDomain: null,
|
||||||
rdpSecurity: null,
|
|
||||||
rdpIgnoreCert: 0,
|
|
||||||
rdpPort: connectionType === "rdp" ? 3389 : null,
|
rdpPort: connectionType === "rdp" ? 3389 : null,
|
||||||
vncUser: null,
|
vncUser: null,
|
||||||
vncPassword: null,
|
vncPassword: null,
|
||||||
vncPort: null,
|
|
||||||
telnetUser: null,
|
telnetUser: null,
|
||||||
telnetPassword: null,
|
telnetPassword: null,
|
||||||
telnetPort: null,
|
|
||||||
defaultPath: "/",
|
defaultPath: "/",
|
||||||
tunnelConnections: "[]",
|
tunnelConnections: "[]",
|
||||||
jumpHosts: serializeProxmoxJumpHosts(discovery.jumpHosts),
|
jumpHosts: serializeProxmoxJumpHosts(discovery.jumpHosts),
|
||||||
|
|||||||
@@ -3,3 +3,15 @@
|
|||||||
## 1.0.0
|
## 1.0.0
|
||||||
|
|
||||||
Bundled with Termix 2.9.0.
|
Bundled with Termix 2.9.0.
|
||||||
|
|
||||||
|
- Runs entirely through the plugin SDK: no imports from Termix core.
|
||||||
|
- Remote Desktop's admin switch and guacd URL, each user's RDP defaults and
|
||||||
|
every host's RDP, VNC and Telnet options are this plugin's settings now,
|
||||||
|
moved over on upgrade.
|
||||||
|
- The user's RDP defaults now apply to saved hosts too, not only Quick Connect.
|
||||||
|
- Provides `sessions.live` for RDP, VNC and Telnet, so these sessions can be
|
||||||
|
shared and presented in collab rooms.
|
||||||
|
- Jump host tunnels and the macOS VNC proxy close with their session instead
|
||||||
|
of after an hour.
|
||||||
|
- Opening the Windows Remote Desktop client goes through the desktop app's
|
||||||
|
backend.
|
||||||
@@ -1,68 +1,60 @@
|
|||||||
# Remote Desktop
|
# Remote Desktop
|
||||||
|
|
||||||
RDP, VNC and Telnet remote desktop sessions, packaged as a first-party plugin.
|
RDP, VNC and Telnet sessions over guacd, with clipboard, file transfer through
|
||||||
|
RDP drive redirection, session recording and session sharing.
|
||||||
|
|
||||||
## Why this plugin runs in-process
|
## guacd
|
||||||
|
|
||||||
Every other plugin runs in a `worker_threads` worker and reaches the server
|
guacd (the Apache Guacamole proxy daemon that speaks RDP, VNC and Telnet) is an
|
||||||
through a structured-clone `postMessage` boundary. That boundary is what makes
|
external service this plugin talks to over TCP. The stock Docker compose runs
|
||||||
the capability gate and the audit trail mean anything: a worker plugin cannot
|
it as its own `guacamole/guacd` container. This plugin never starts it.
|
||||||
hold a db handle, an ssh2 `Client` or a live socket, so every privileged action
|
|
||||||
has to go through the broker.
|
|
||||||
|
|
||||||
This plugin cannot work that way:
|
Where to find it is the admin setting **guacd URL** (`host:port`). The
|
||||||
|
`GUACD_URL`, or `GUACD_HOST` and `GUACD_PORT`, environment variables override
|
||||||
|
the setting, so a compose file stays the source of truth where it sets them.
|
||||||
|
Changing the setting rebuilds the connection server without a restart.
|
||||||
|
|
||||||
- it owns a `guacamole-lite` WebSocket server on port 30008,
|
Other environment variables the plugin reads:
|
||||||
- its `/connect-host/:hostId` route does multi-step credential and tunnel
|
|
||||||
resolution (SSH jump-host tunnels, a macOS VNC compatibility proxy) using
|
|
||||||
raw `net.Socket` plumbing,
|
|
||||||
- it needs plaintext host credentials to connect at all.
|
|
||||||
|
|
||||||
None of that can cross a structured-clone boundary, so this plugin runs on the
|
| Variable | What it sets |
|
||||||
main thread instead, under the `process:transport-owner` tier, the same as
|
| ------------------------------ | --------------------------------------------------------------------------- |
|
||||||
`ssh-terminal`, `docker` and `host-metrics`.
|
| `GUACD_TUNNEL_HOST` | The name guacd uses to reach Termix for a jump host tunnel (default termix) |
|
||||||
|
| `GUACD_RECORDING_PATH` | Where guacd writes recordings, as guacd sees it |
|
||||||
|
| `GUACD_RECORDING_BACKEND_PATH` | The same folder as Termix sees it |
|
||||||
|
| `GUACD_DRIVE_PATH` | The root of each user's RDP drive folder on the guacd host |
|
||||||
|
| `GUACAMOLE_ENCRYPTION_KEY` | The connection token key; derived from `JWT_SECRET` when unset |
|
||||||
|
|
||||||
## guacd is not spawned by this plugin
|
## Settings
|
||||||
|
|
||||||
guacd (the Apache Guacamole proxy daemon that actually speaks RDP/VNC/Telnet)
|
- **Admin**: turn Remote Desktop on or off, and the guacd URL.
|
||||||
is a separate, external service reached over plain TCP -- by default a
|
- **User**: RDP defaults (colour depth, resize method, wallpaper, clipboard
|
||||||
sibling Docker container, or any network-reachable host configured through the
|
and the rest). A host's own values win; anything a host leaves on its
|
||||||
admin "guacd URL" setting. This plugin does not download, bundle or spawn
|
default takes the user's.
|
||||||
guacd as a child process.
|
- **Host**: the RDP, VNC and Telnet switches, ports, security mode,
|
||||||
|
certificate handling and every guacd parameter, edited in the host
|
||||||
|
editor's RDP, VNC and Telnet tabs. The toolbar switch is in the Plugins tab.
|
||||||
|
|
||||||
That is a deliberate limitation, not an oversight: Apache Guacamole does not
|
The logins (users, passwords, stored credentials, domain) stay on the host in
|
||||||
publish prebuilt guacd binaries for any platform (only source tarballs and
|
core, because sharing decides which of them a recipient may use.
|
||||||
their own Docker image), and there is no viable native guacd for Windows at
|
|
||||||
all. Bundling guacd for Linux and macOS is possible in principle but would
|
|
||||||
make Termix the ongoing upstream builder and security patcher for guacd plus
|
|
||||||
FreeRDP/libvncclient/libssh2/libtelnet -- out of scope for this conversion.
|
|
||||||
guacd's availability is handled as an external dependency with graceful
|
|
||||||
degradation, exactly as it was before this plugin existed.
|
|
||||||
|
|
||||||
## What lives where
|
## Capabilities
|
||||||
|
|
||||||
Unlike `ssh-terminal` (whose implementation stays in core because
|
- `credentials:read`: guacd needs the host's RDP, VNC or Telnet password in
|
||||||
`session-manager.ts` has consumers well beyond the terminal), this plugin's
|
plain text, so the plugin reads it through `ctx.credentials`. A shared
|
||||||
backend and frontend code is fully relocated here from
|
recipient only ever gets the owner's shared snapshot or their own override.
|
||||||
`src/backend/hosts/guacamole/` and `src/ui/features/guacamole/`, the same
|
- `ssh:connect`, `credentials:use`: jump host tunnels, through
|
||||||
shape `docker` and `host-metrics` use.
|
`ctx.ssh.jumpChain`, or the tunnels plugin's `tunnels.access` for a single
|
||||||
|
hop when it is running.
|
||||||
|
- `network:serve`: the routes and the `/display` socket.
|
||||||
|
- `desktop:window`: opening the Windows Remote Desktop client from the desktop
|
||||||
|
app.
|
||||||
|
- `hosts:read`: the online indicator while a session is open.
|
||||||
|
- `ui:surface`: everything the frontend registers.
|
||||||
|
|
||||||
Two pieces of shared infrastructure still reach across the plugin boundary
|
## Services
|
||||||
into this plugin rather than living in core:
|
|
||||||
|
|
||||||
- `src/backend/hosts/collab/routes.ts` and
|
Provides `sessions.live` under the names `rdp`, `vnc` and `telnet`, which is
|
||||||
`src/backend/hosts/session-sharing/` import `token-service.ts` and
|
how session sharing and collab rooms find a live session and mint a viewer
|
||||||
`guacamole-server.ts` directly to resolve and validate live Guacamole
|
token for it. Uses `recordings.writer` (session-recording) to decide whether
|
||||||
sessions for collaborative/shared viewing.
|
to record and to list finished recordings, and `tunnels.access` (tunnels)
|
||||||
- `src/ui/features/collab/` and `src/ui/features/session-sharing/` import
|
for single-hop jump tunnels. Both are optional.
|
||||||
`GuacamoleDisplay.tsx` directly for the same reason.
|
|
||||||
|
|
||||||
This is an accepted, temporary coupling: collab and session-sharing are
|
|
||||||
expected to become plugins of their own later, at which point these become
|
|
||||||
real inter-plugin references instead of raw relative imports.
|
|
||||||
|
|
||||||
## Disabling
|
|
||||||
|
|
||||||
Disabling behaves like any other plugin: the rdp/vnc/telnet tabs stop being
|
|
||||||
offered from the host list, command palette and quick connect, the
|
|
||||||
`/guacamole` router 404s, and the WebSocket server on port 30008 is closed.
|
|
||||||
@@ -73,7 +73,8 @@
|
|||||||
"fitToScreen": "Fit to screen",
|
"fitToScreen": "Fit to screen",
|
||||||
"switchToTrackpad": "Switch to trackpad mode (drag to move cursor, tap to click)",
|
"switchToTrackpad": "Switch to trackpad mode (drag to move cursor, tap to click)",
|
||||||
"switchToTouch": "Switch to touch mode (tap directly where you want to click)"
|
"switchToTouch": "Switch to touch mode (tap directly where you want to click)"
|
||||||
}
|
},
|
||||||
|
"disabled": "Remote Desktop is turned off. An admin can turn it on in Settings."
|
||||||
},
|
},
|
||||||
"hosts": {
|
"hosts": {
|
||||||
"nativeRdpOpened": "Opened in Windows Remote Desktop",
|
"nativeRdpOpened": "Opened in Windows Remote Desktop",
|
||||||
@@ -204,11 +205,134 @@
|
|||||||
"fontName": "Font Name",
|
"fontName": "Font Name",
|
||||||
"fontSize": "Font Size",
|
"fontSize": "Font Size",
|
||||||
"colorScheme": "Color Scheme",
|
"colorScheme": "Color Scheme",
|
||||||
"backspaceKey": "Backspace Key"
|
"backspaceKey": "Backspace Key",
|
||||||
}
|
"colorDepth": "Color Depth",
|
||||||
|
"resizeMethod": "Resize Method"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Remote Desktop",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Virtual Network",
|
||||||
|
"unencryptedShell": "Unencrypted shell",
|
||||||
|
"remoteDesktopNotes": "Notes",
|
||||||
|
"userDefaultsNote": "Settings left on their default use your Remote Desktop defaults from Settings.",
|
||||||
|
"connectionOriginNote": "On the desktop app, RDP, VNC and Telnet use the remote server unless the host is set to This device, which needs a guacd this machine can reach. Set one in the Remote Desktop settings, or per host under guacd Proxy."
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Remote desktop",
|
"feature_desktop": "Remote desktop",
|
||||||
"feature_desktop_desc": "Open RDP, VNC and Telnet sessions in a tab, same as SSH."
|
"feature_desktop_desc": "Open RDP, VNC and Telnet sessions in a tab, same as SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Color Depth",
|
||||||
|
"description": "Used for RDP hosts that leave it on their default."
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Resize Method",
|
||||||
|
"description": "How the remote screen follows the window size."
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Display Update",
|
||||||
|
"resizeReconnect": "Reconnect",
|
||||||
|
"inherit": "Inherit",
|
||||||
|
"on": "On",
|
||||||
|
"off": "Off",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Force Lossless",
|
||||||
|
"description": "Force lossless image encoding (higher quality, more bandwidth)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Wallpaper",
|
||||||
|
"description": "Show desktop wallpaper (disabling improves performance)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Font Smoothing",
|
||||||
|
"description": "Enable ClearType font rendering"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Desktop Composition",
|
||||||
|
"description": "Enable Aero glass effects"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Disable Audio",
|
||||||
|
"description": "Mute all audio from the remote session"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Enable Printing",
|
||||||
|
"description": "Redirect local printers to the remote session"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Enable Drive Redirection",
|
||||||
|
"description": "Map a local folder as a drive in the remote session"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Disable Copy",
|
||||||
|
"description": "Prevent copying text from the remote session"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Disable Paste",
|
||||||
|
"description": "Prevent pasting text into the remote session"
|
||||||
|
},
|
||||||
|
"group": "Remote Desktop defaults",
|
||||||
|
"colorDepth16": "16-bit",
|
||||||
|
"colorDepth24": "24-bit",
|
||||||
|
"colorDepth32": "32-bit"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"enabled": {
|
||||||
|
"label": "Enable Remote Desktop",
|
||||||
|
"description": "Allow RDP, VNC and Telnet connections through guacd."
|
||||||
|
},
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd URL",
|
||||||
|
"description": "Where guacd listens, as host:port. The GUACD_URL, GUACD_HOST and GUACD_PORT environment variables override this.",
|
||||||
|
"placeholder": "guacd:4822"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"host": {
|
||||||
|
"enableRdp": {
|
||||||
|
"label": "RDP"
|
||||||
|
},
|
||||||
|
"enableVnc": {
|
||||||
|
"label": "VNC"
|
||||||
|
},
|
||||||
|
"enableTelnet": {
|
||||||
|
"label": "Telnet"
|
||||||
|
},
|
||||||
|
"rdpPort": {
|
||||||
|
"label": "RDP port"
|
||||||
|
},
|
||||||
|
"vncPort": {
|
||||||
|
"label": "VNC port"
|
||||||
|
},
|
||||||
|
"telnetPort": {
|
||||||
|
"label": "Telnet port"
|
||||||
|
},
|
||||||
|
"rdpSecurity": {
|
||||||
|
"label": "RDP security mode"
|
||||||
|
},
|
||||||
|
"rdpIgnoreCert": {
|
||||||
|
"label": "Ignore certificate errors"
|
||||||
|
},
|
||||||
|
"guacamoleConfig": {
|
||||||
|
"label": "guacd settings"
|
||||||
|
},
|
||||||
|
"enableToolbar": {
|
||||||
|
"label": "Remote desktop toolbar",
|
||||||
|
"description": "Show the toolbar in RDP, VNC and Telnet sessions."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"palette": {
|
||||||
|
"connectRdp": "Connect with RDP",
|
||||||
|
"connectVnc": "Connect with VNC",
|
||||||
|
"connectTelnet": "Connect with Telnet"
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"sessions": {
|
||||||
|
"title": "Share remote desktop sessions",
|
||||||
|
"description": "Let session sharing and collab rooms show this user's RDP, VNC and Telnet sessions to others."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Lettertipe Naam",
|
"fontName": "Lettertipe Naam",
|
||||||
"fontSize": "Lettergrootte",
|
"fontSize": "Lettergrootte",
|
||||||
"colorScheme": "Kleurskema",
|
"colorScheme": "Kleurskema",
|
||||||
"backspaceKey": "Terugspasie-sleutel"
|
"backspaceKey": "Terugspasie-sleutel",
|
||||||
}
|
"colorDepth": "Kleurdiepte",
|
||||||
|
"resizeMethod": "Vergrootingsmetode"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Afstandslessenaar",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Virtuele Netwerk",
|
||||||
|
"unencryptedShell": "Ongeënkripteerde dop"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Afstandlessenaar",
|
"feature_desktop": "Afstandlessenaar",
|
||||||
"feature_desktop_desc": "Maak RDP-, VNC- en Telnet-sessies in 'n oortjie oop, dieselfde as SSH."
|
"feature_desktop_desc": "Maak RDP-, VNC- en Telnet-sessies in 'n oortjie oop, dieselfde as SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Kleurdiepte"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Vergrootingsmetode"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Skermopdatering",
|
||||||
|
"resizeReconnect": "Herkoppel",
|
||||||
|
"inherit": "Erf",
|
||||||
|
"on": "Aan",
|
||||||
|
"off": "Af",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Forseer verliesloos",
|
||||||
|
"description": "Forseer verlieslose beeldkodering (hoër gehalte, meer bandwydte)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Agtergrond",
|
||||||
|
"description": "Wys lessenaar agtergrond (deaktivering verbeter werkverrigting)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Lettertipe-gladmaak",
|
||||||
|
"description": "Aktiveer ClearType-lettertipe-weergawe"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Komposisie op die lessenaar",
|
||||||
|
"description": "Aktiveer Aero-glaseffekte"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Deaktiveer oudio",
|
||||||
|
"description": "Demp alle klank van die afstandsessie"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Aktiveer drukwerk",
|
||||||
|
"description": "Herlei plaaslike drukkers na die afgeleë sessie"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Aktiveer dryfherleiding",
|
||||||
|
"description": "Karaktereer 'n plaaslike vouer as 'n skyf in die afstandsessie"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Deaktiveer Kopieer",
|
||||||
|
"description": "Voorkom die kopiëring van teks vanaf die afgeleë sessie"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Deaktiveer plak",
|
||||||
|
"description": "Voorkom die plak van teks in die afgeleë sessie"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd-URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "اسم الخط",
|
"fontName": "اسم الخط",
|
||||||
"fontSize": "حجم الخط",
|
"fontSize": "حجم الخط",
|
||||||
"colorScheme": "نظام الألوان",
|
"colorScheme": "نظام الألوان",
|
||||||
"backspaceKey": "مفتاح Backspace"
|
"backspaceKey": "مفتاح Backspace",
|
||||||
}
|
"colorDepth": "عمق اللون",
|
||||||
|
"resizeMethod": "طريقة تغيير الحجم"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "سطح المكتب البعيد",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "الشبكة الافتراضية",
|
||||||
|
"unencryptedShell": "shell غير مشفر"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "سطح المكتب البعيد",
|
"feature_desktop": "سطح المكتب البعيد",
|
||||||
"feature_desktop_desc": "افتح جلسات RDP وVNC وTelnet في علامة تبويب، تمامًا مثل SSH."
|
"feature_desktop_desc": "افتح جلسات RDP وVNC وTelnet في علامة تبويب، تمامًا مثل SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "عمق اللون"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "طريقة تغيير الحجم"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "تحديث الشاشة",
|
||||||
|
"resizeReconnect": "إعادة الاتصال",
|
||||||
|
"inherit": "يرث",
|
||||||
|
"on": "على",
|
||||||
|
"off": "عن",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "فرض بدون فقدان",
|
||||||
|
"description": "فرض ترميز الصور بدون فقدان (جودة أعلى، استهلاك أكبر للنطاق الترددي)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "خلفية سطح المكتب",
|
||||||
|
"description": "إظهار خلفية سطح المكتب (التعطيل يحسن الأداء)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "تنعيم الخطوط",
|
||||||
|
"description": "تمكين عرض خطوط ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "تركيب سطح المكتب",
|
||||||
|
"description": "تمكين تأثيرات Aero الزجاجية"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "تعطيل الصوت",
|
||||||
|
"description": "كتم كل الصوت من الجلسة البعيدة"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "تمكين الطباعة",
|
||||||
|
"description": "إعادة توجيه الطابعات المحلية إلى الجلسة البعيدة"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "تمكين إعادة توجيه القرص",
|
||||||
|
"description": "تعيين مجلد محلي كقرص في الجلسة البعيدة"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "تعطيل النسخ",
|
||||||
|
"description": "منع نسخ النص من الجلسة البعيدة"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "تعطيل اللصق",
|
||||||
|
"description": "منع لصق النص في الجلسة البعيدة"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "رابط guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Име на шрифта",
|
"fontName": "Име на шрифта",
|
||||||
"fontSize": "Размер на шрифта",
|
"fontSize": "Размер на шрифта",
|
||||||
"colorScheme": "Цветова схема",
|
"colorScheme": "Цветова схема",
|
||||||
"backspaceKey": "Клавиш Backspace"
|
"backspaceKey": "Клавиш Backspace",
|
||||||
}
|
"colorDepth": "Дълбочина на цвета",
|
||||||
|
"resizeMethod": "Метод за преоразмеряване"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Отдалечен работен плот",
|
||||||
|
"tabRdp": "ПРСР",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Телнет",
|
||||||
|
"virtualNetwork": "Виртуална мрежа",
|
||||||
|
"unencryptedShell": "Некриптирана обвивка"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Отдалечен работен плот",
|
"feature_desktop": "Отдалечен работен плот",
|
||||||
"feature_desktop_desc": "Отваряне на RDP, VNC и Telnet сесии в раздел, както при SSH."
|
"feature_desktop_desc": "Отваряне на RDP, VNC и Telnet сесии в раздел, както при SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Дълбочина на цвета"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Метод за преоразмеряване"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Актуализация на дисплея",
|
||||||
|
"resizeReconnect": "Свържете се отново",
|
||||||
|
"inherit": "Наследяване",
|
||||||
|
"on": "Включено",
|
||||||
|
"off": "Изключено",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Принудително беззагубно",
|
||||||
|
"description": "Принудително кодиране на изображения без загуби (по-високо качество, по-голяма честотна лента)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Тапет",
|
||||||
|
"description": "Показване на тапет на работния плот (деактивирането му подобрява производителността)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Изглаждане на шрифта",
|
||||||
|
"description": "Активиране на рендирането на шрифтове ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Композиция на работния плот",
|
||||||
|
"description": "Активиране на ефектите на Aero Glass"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Деактивиране на звука",
|
||||||
|
"description": "Изключване на звука от отдалечената сесия"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Активиране на печат",
|
||||||
|
"description": "Пренасочване на локални принтери към отдалечена сесия"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Активиране на пренасочване на устройството",
|
||||||
|
"description": "Карта на локална папка като устройство в отдалечена сесия"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Деактивиране на копирането",
|
||||||
|
"description": "Предотвратяване на копирането на текст от отдалечената сесия"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Деактивиране на поставянето",
|
||||||
|
"description": "Предотвратяване на поставянето на текст в отдалечената сесия"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL адрес на GUACD"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "ফন্টের নাম",
|
"fontName": "ফন্টের নাম",
|
||||||
"fontSize": "ফন্ট সাইজ",
|
"fontSize": "ফন্ট সাইজ",
|
||||||
"colorScheme": "রঙের পরিকল্পনা",
|
"colorScheme": "রঙের পরিকল্পনা",
|
||||||
"backspaceKey": "ব্যাকস্পেস কী"
|
"backspaceKey": "ব্যাকস্পেস কী",
|
||||||
}
|
"colorDepth": "রঙের গভীরতা",
|
||||||
|
"resizeMethod": "আকার পরিবর্তন পদ্ধতি"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "রিমোট ডেস্কটপ",
|
||||||
|
"tabRdp": "আরডিপি",
|
||||||
|
"tabVnc": "ভিএনসি",
|
||||||
|
"tabTelnet": "টেলনেট",
|
||||||
|
"virtualNetwork": "ভার্চুয়াল নেটওয়ার্ক",
|
||||||
|
"unencryptedShell": "এনক্রিপ্টবিহীন শেল"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "রিমোট ডেস্কটপ",
|
"feature_desktop": "রিমোট ডেস্কটপ",
|
||||||
"feature_desktop_desc": "SSH-এর মতোই একটি ট্যাবে RDP, VNC এবং Telnet সেশন খুলুন।"
|
"feature_desktop_desc": "SSH-এর মতোই একটি ট্যাবে RDP, VNC এবং Telnet সেশন খুলুন।"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "রঙের গভীরতা"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "আকার পরিবর্তন পদ্ধতি"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "ডিসপ্লে আপডেট",
|
||||||
|
"resizeReconnect": "পুনরায় সংযোগ করুন",
|
||||||
|
"inherit": "উত্তরাধিকার সূত্রে",
|
||||||
|
"on": "চালু",
|
||||||
|
"off": "বন্ধ",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "বল ক্ষয়হীন",
|
||||||
|
"description": "লসলেস ইমেজ এনকোডিং বাধ্যতামূলক করুন (উচ্চতর গুণমান, বেশি ব্যান্ডউইথ)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "ওয়ালপেপার",
|
||||||
|
"description": "ডেস্কটপ ওয়ালপেপার দেখান (এটি বন্ধ করলে পারফরম্যান্স উন্নত হয়)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "ফন্ট মসৃণকরণ",
|
||||||
|
"description": "ক্লিয়ারটাইপ ফন্ট রেন্ডারিং সক্ষম করুন"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "ডেস্কটপ কম্পোজিশন",
|
||||||
|
"description": "এরো গ্লাস এফেক্টস সক্রিয় করুন"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "অডিও নিষ্ক্রিয় করুন",
|
||||||
|
"description": "রিমোট সেশন থেকে সমস্ত অডিও মিউট করুন"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "প্রিন্টিং সক্ষম করুন",
|
||||||
|
"description": "স্থানীয় প্রিন্টারগুলিকে রিমোট সেশনে পুনঃনির্দেশিত করুন"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "ড্রাইভ পুনঃনির্দেশনা সক্ষম করুন",
|
||||||
|
"description": "রিমোট সেশনে একটি স্থানীয় ফোল্ডারকে ড্রাইভ হিসেবে ম্যাপ করুন।"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "কপি নিষ্ক্রিয় করুন",
|
||||||
|
"description": "রিমোট সেশন থেকে টেক্সট কপি করা প্রতিরোধ করুন"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "পেস্ট নিষ্ক্রিয় করুন",
|
||||||
|
"description": "রিমোট সেশনে টেক্সট পেস্ট করা প্রতিরোধ করুন"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Nom de la font",
|
"fontName": "Nom de la font",
|
||||||
"fontSize": "Mida de la lletra",
|
"fontSize": "Mida de la lletra",
|
||||||
"colorScheme": "Esquema de colors",
|
"colorScheme": "Esquema de colors",
|
||||||
"backspaceKey": "Tecla Retrocés"
|
"backspaceKey": "Tecla Retrocés",
|
||||||
}
|
"colorDepth": "Profunditat de color",
|
||||||
|
"resizeMethod": "Mètode de redimensionament"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Escriptori remot",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Xarxa virtual",
|
||||||
|
"unencryptedShell": "Shell sense xifrar"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Escriptori remot",
|
"feature_desktop": "Escriptori remot",
|
||||||
"feature_desktop_desc": "Obriu sessions RDP, VNC i Telnet en una pestanya, igual que SSH."
|
"feature_desktop_desc": "Obriu sessions RDP, VNC i Telnet en una pestanya, igual que SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Profunditat de color"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Mètode de redimensionament"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Actualització de la pantalla",
|
||||||
|
"resizeReconnect": "Reconnecta",
|
||||||
|
"inherit": "Heretar",
|
||||||
|
"on": "Activat",
|
||||||
|
"off": "Desactivat",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Força sense pèrdues",
|
||||||
|
"description": "Força la codificació d'imatges sense pèrdues (més qualitat, més amplada de banda)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Fons de pantalla",
|
||||||
|
"description": "Mostra el fons de pantalla de l'escriptori (desactivar-lo millora el rendiment)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Suavitzat de fonts",
|
||||||
|
"description": "Activa la renderització de fonts ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Composició d'escriptori",
|
||||||
|
"description": "Activa els efectes Aero Glass"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Desactiva l'àudio",
|
||||||
|
"description": "Silencia tot l'àudio de la sessió remota"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Habilita la impressió",
|
||||||
|
"description": "Redirigir les impressores locals a la sessió remota"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Activa la redirecció de la unitat",
|
||||||
|
"description": "Assignar una carpeta local com a unitat a la sessió remota"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Desactiva la còpia",
|
||||||
|
"description": "Impedir la còpia de text des de la sessió remota"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Desactiva l'enganxament",
|
||||||
|
"description": "Impedir enganxar text a la sessió remota"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL de guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Název písma",
|
"fontName": "Název písma",
|
||||||
"fontSize": "Velikost písma",
|
"fontSize": "Velikost písma",
|
||||||
"colorScheme": "Barevné schéma",
|
"colorScheme": "Barevné schéma",
|
||||||
"backspaceKey": "Klávesa Backspace"
|
"backspaceKey": "Klávesa Backspace",
|
||||||
}
|
"colorDepth": "Barevná hloubka",
|
||||||
|
"resizeMethod": "Metoda změny velikosti"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Vzdálená plocha",
|
||||||
|
"tabRdp": "PRV",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Virtuální síť",
|
||||||
|
"unencryptedShell": "Nešifrovaný shell"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Vzdálená plocha",
|
"feature_desktop": "Vzdálená plocha",
|
||||||
"feature_desktop_desc": "Otevírejte relace RDP, VNC a Telnet na kartě, stejně jako SSH."
|
"feature_desktop_desc": "Otevírejte relace RDP, VNC a Telnet na kartě, stejně jako SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Barevná hloubka"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Metoda změny velikosti"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Aktualizace displeje",
|
||||||
|
"resizeReconnect": "Znovu se připojte",
|
||||||
|
"inherit": "Zdědit",
|
||||||
|
"on": "Na",
|
||||||
|
"off": "Vypnuto",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Vynutit bezztrátové",
|
||||||
|
"description": "Vynucení bezztrátového kódování obrazu (vyšší kvalita, větší šířka pásma)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Tapeta",
|
||||||
|
"description": "Zobrazit tapetu plochy (zakázání zlepšuje výkon)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Vyhlazování písma",
|
||||||
|
"description": "Povolit vykreslování písma ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Složení plochy",
|
||||||
|
"description": "Povolit efekty Aero Glass"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Zakázat zvuk",
|
||||||
|
"description": "Ztlumit veškerý zvuk ze vzdálené relace"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Povolit tisk",
|
||||||
|
"description": "Přesměrování lokálních tiskáren na vzdálenou relaci"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Povolit přesměrování disku",
|
||||||
|
"description": "Namapování lokální složky jako jednotky ve vzdálené relaci"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Zakázat kopírování",
|
||||||
|
"description": "Zabránit kopírování textu ze vzdálené relace"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Zakázat vkládání",
|
||||||
|
"description": "Zabránit vkládání textu do vzdálené relace"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL GUACD"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Skrifttypenavn",
|
"fontName": "Skrifttypenavn",
|
||||||
"fontSize": "Skriftstørrelse",
|
"fontSize": "Skriftstørrelse",
|
||||||
"colorScheme": "Farveskema",
|
"colorScheme": "Farveskema",
|
||||||
"backspaceKey": "Tilbagetast"
|
"backspaceKey": "Tilbagetast",
|
||||||
}
|
"colorDepth": "Farvedybde",
|
||||||
|
"resizeMethod": "Ændre størrelse-metode"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Fjernskrivebord",
|
||||||
|
"tabRdp": "Landdistriktsudviklingsplan",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Virtuelt netværk",
|
||||||
|
"unencryptedShell": "Ukrypteret skal"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Fjernskrivebord",
|
"feature_desktop": "Fjernskrivebord",
|
||||||
"feature_desktop_desc": "Åbn RDP-, VNC- og Telnet-sessioner i en fane, ligesom med SSH."
|
"feature_desktop_desc": "Åbn RDP-, VNC- og Telnet-sessioner i en fane, ligesom med SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Farvedybde"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Ændre størrelse-metode"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Opdatering af display",
|
||||||
|
"resizeReconnect": "Genopret forbindelse",
|
||||||
|
"inherit": "Arve",
|
||||||
|
"on": "På",
|
||||||
|
"off": "Slukket",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Tving tabsfri",
|
||||||
|
"description": "Tving tabsfri billedkodning (højere kvalitet, mere båndbredde)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Tapet",
|
||||||
|
"description": "Vis skrivebordsbaggrund (deaktivering forbedrer ydeevnen)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Skrifttypeudjævning",
|
||||||
|
"description": "Aktivér gengivelse af ClearType-skrifttyper"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Skrivebordskomposition",
|
||||||
|
"description": "Aktivér Aero Glass-effekter"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Deaktiver lyd",
|
||||||
|
"description": "Slå al lyd fra fra fjernsessionen"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Aktivér udskrivning",
|
||||||
|
"description": "Omdiriger lokale printere til fjernsessionen"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Aktivér drevomdirigering",
|
||||||
|
"description": "Tilknyt en lokal mappe som et drev i den eksterne session"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Deaktiver kopiering",
|
||||||
|
"description": "Forhindr kopiering af tekst fra fjernsessionen"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Deaktiver indsætning",
|
||||||
|
"description": "Forhindr indsættelse af tekst i fjernsessionen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd-URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Schriftname",
|
"fontName": "Schriftname",
|
||||||
"fontSize": "Schriftgröße",
|
"fontSize": "Schriftgröße",
|
||||||
"colorScheme": "Farbschema",
|
"colorScheme": "Farbschema",
|
||||||
"backspaceKey": "Backspace-Taste"
|
"backspaceKey": "Backspace-Taste",
|
||||||
}
|
"colorDepth": "Farbtiefe",
|
||||||
|
"resizeMethod": "Skalierungsmethode"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Remotedesktop",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Virtuelles Netzwerk",
|
||||||
|
"unencryptedShell": "Unverschlüsselte Shell"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Remote-Desktop",
|
"feature_desktop": "Remote-Desktop",
|
||||||
"feature_desktop_desc": "Öffnen Sie RDP-, VNC- und Telnet-Sitzungen in einem Tab, genau wie bei SSH."
|
"feature_desktop_desc": "Öffnen Sie RDP-, VNC- und Telnet-Sitzungen in einem Tab, genau wie bei SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Farbtiefe"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Skalierungsmethode"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Anzeigeaktualisierung",
|
||||||
|
"resizeReconnect": "Wiederverbinden",
|
||||||
|
"inherit": "Erben",
|
||||||
|
"on": "An",
|
||||||
|
"off": "Aus",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Verlustfreie Übertragung erzwingen",
|
||||||
|
"description": "Verlustfreie Bildcodierung erzwingen (höhere Qualität, mehr Bandbreite)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Hintergrundbild",
|
||||||
|
"description": "Desktop-Hintergrundbild anzeigen (Deaktivierung verbessert die Leistung)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Schriftglättung",
|
||||||
|
"description": "ClearType-Schriftdarstellung aktivieren"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Desktopkomposition",
|
||||||
|
"description": "Aero-Glaseffekte aktivieren"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Audio deaktivieren",
|
||||||
|
"description": "Gesamten Ton der Remote-Sitzung stummschalten"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Drucken aktivieren",
|
||||||
|
"description": "Lokale Drucker an die Remote-Sitzung umleiten"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Laufwerksumleitung aktivieren",
|
||||||
|
"description": "Lokalen Ordner als Laufwerk in der Remote-Sitzung einbinden"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Kopieren deaktivieren",
|
||||||
|
"description": "Kopieren von Text aus der entfernten Sitzung verhindern"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Einfügen deaktivieren",
|
||||||
|
"description": "Einfügen von Text in die entfernte Sitzung verhindern"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd-URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Όνομα γραμματοσειράς",
|
"fontName": "Όνομα γραμματοσειράς",
|
||||||
"fontSize": "Μέγεθος γραμματοσειράς",
|
"fontSize": "Μέγεθος γραμματοσειράς",
|
||||||
"colorScheme": "Χρωματικό σχήμα",
|
"colorScheme": "Χρωματικό σχήμα",
|
||||||
"backspaceKey": "Πλήκτρο Backspace"
|
"backspaceKey": "Πλήκτρο Backspace",
|
||||||
}
|
"colorDepth": "Βάθος χρώματος",
|
||||||
|
"resizeMethod": "Μέθοδος αλλαγής μεγέθους"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Απομακρυσμένη επιφάνεια εργασίας",
|
||||||
|
"tabRdp": "ΠΑΑ",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Τέλνετ",
|
||||||
|
"virtualNetwork": "Εικονικό Δίκτυο",
|
||||||
|
"unencryptedShell": "Μη κρυπτογραφημένο κέλυφος"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Απομακρυσμένη επιφάνεια εργασίας",
|
"feature_desktop": "Απομακρυσμένη επιφάνεια εργασίας",
|
||||||
"feature_desktop_desc": "Ανοίξτε τις συνεδρίες RDP, VNC και Telnet σε μια καρτέλα, όπως και το SSH."
|
"feature_desktop_desc": "Ανοίξτε τις συνεδρίες RDP, VNC και Telnet σε μια καρτέλα, όπως και το SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Βάθος χρώματος"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Μέθοδος αλλαγής μεγέθους"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Ενημέρωση οθόνης",
|
||||||
|
"resizeReconnect": "Επανασύνδεση",
|
||||||
|
"inherit": "Κληρονομώ",
|
||||||
|
"on": "Επί",
|
||||||
|
"off": "Μακριά από",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Δύναμη χωρίς απώλειες",
|
||||||
|
"description": "Επιβολή κωδικοποίησης εικόνας χωρίς απώλειες (υψηλότερη ποιότητα, μεγαλύτερο εύρος ζώνης)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Ταπετσαρία",
|
||||||
|
"description": "Εμφάνιση ταπετσαρίας επιφάνειας εργασίας (η απενεργοποίηση βελτιώνει την απόδοση)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Εξομάλυνση γραμματοσειράς",
|
||||||
|
"description": "Ενεργοποίηση απόδοσης γραμματοσειράς ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Σύνθεση επιφάνειας εργασίας",
|
||||||
|
"description": "Ενεργοποίηση εφέ Aero glass"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Απενεργοποίηση ήχου",
|
||||||
|
"description": "Σίγαση όλου του ήχου από την απομακρυσμένη συνεδρία"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Ενεργοποίηση εκτύπωσης",
|
||||||
|
"description": "Ανακατεύθυνση τοπικών εκτυπωτών στην απομακρυσμένη συνεδρία"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Ενεργοποίηση ανακατεύθυνσης μονάδας δίσκου",
|
||||||
|
"description": "Αντιστοίχιση τοπικού φακέλου ως μονάδας δίσκου στην απομακρυσμένη συνεδρία"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Απενεργοποίηση αντιγραφής",
|
||||||
|
"description": "Αποτροπή αντιγραφής κειμένου από την απομακρυσμένη συνεδρία"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Απενεργοποίηση επικόλλησης",
|
||||||
|
"description": "Αποτροπή επικόλλησης κειμένου στην απομακρυσμένη συνεδρία"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Nombre de fuente",
|
"fontName": "Nombre de fuente",
|
||||||
"fontSize": "Tamaño de fuente",
|
"fontSize": "Tamaño de fuente",
|
||||||
"colorScheme": "Esquema de color",
|
"colorScheme": "Esquema de color",
|
||||||
"backspaceKey": "Tecla de retroceso"
|
"backspaceKey": "Tecla de retroceso",
|
||||||
}
|
"colorDepth": "Profundidad de color",
|
||||||
|
"resizeMethod": "Método de redimensionamiento"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Escritorio remoto",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Red virtual",
|
||||||
|
"unencryptedShell": "Shell sin cifrar"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Escritorio remoto",
|
"feature_desktop": "Escritorio remoto",
|
||||||
"feature_desktop_desc": "Abre sesiones RDP, VNC y Telnet en una pestaña, igual que con SSH."
|
"feature_desktop_desc": "Abre sesiones RDP, VNC y Telnet en una pestaña, igual que con SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Profundidad de color"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Método de redimensionamiento"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Actualización de pantalla",
|
||||||
|
"resizeReconnect": "Reconectar",
|
||||||
|
"inherit": "Heredar",
|
||||||
|
"on": "En",
|
||||||
|
"off": "Apagado",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Forzar sin pérdida",
|
||||||
|
"description": "Forzar codificación de imagen sin pérdida (más calidad, más ancho de banda)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Fondo de pantalla",
|
||||||
|
"description": "Mostrar fondo de escritorio (desactivarlo mejora el rendimiento)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Suavizado de fuentes",
|
||||||
|
"description": "Activar renderizado de fuentes ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Composición de escritorio",
|
||||||
|
"description": "Activar efectos Aero Glass"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Desactivar audio",
|
||||||
|
"description": "Silenciar todo el audio de la sesión remota"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Activar impresión",
|
||||||
|
"description": "Redirigir las impresoras locales a la sesión remota"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Activar redirección de unidades",
|
||||||
|
"description": "Asignar una carpeta local como una unidad en la sesión remota"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Deshabilitar copia",
|
||||||
|
"description": "Impedir la copia de texto de la sesión remota"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Deshabilitar pegado",
|
||||||
|
"description": "Impedir el pegado de texto en la sesión remota"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL de guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Fontin nimi",
|
"fontName": "Fontin nimi",
|
||||||
"fontSize": "Fonttikoko",
|
"fontSize": "Fonttikoko",
|
||||||
"colorScheme": "Värimaailma",
|
"colorScheme": "Värimaailma",
|
||||||
"backspaceKey": "Askelpalautin-näppäin"
|
"backspaceKey": "Askelpalautin-näppäin",
|
||||||
}
|
"colorDepth": "Värisyvyys",
|
||||||
|
"resizeMethod": "Koon muuttamismenetelmä"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Etätyöpöytä",
|
||||||
|
"tabRdp": "Maaseudun kehittämisohjelma",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Virtuaaliverkko",
|
||||||
|
"unencryptedShell": "Salaamaton kuori"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Etätyöpöytä",
|
"feature_desktop": "Etätyöpöytä",
|
||||||
"feature_desktop_desc": "Avaa RDP-, VNC- ja Telnet-istunnot välilehdellä, samalla tavalla kuin SSH:ssa."
|
"feature_desktop_desc": "Avaa RDP-, VNC- ja Telnet-istunnot välilehdellä, samalla tavalla kuin SSH:ssa."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Värisyvyys"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Koon muuttamismenetelmä"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Näyttöpäivitys",
|
||||||
|
"resizeReconnect": "Yhdistä uudelleen",
|
||||||
|
"inherit": "Periä",
|
||||||
|
"on": "Päällä",
|
||||||
|
"off": "Pois",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Pakota häviötön",
|
||||||
|
"description": "Pakota häviötön kuvan koodaus (korkeampi laatu, enemmän kaistanleveyttä)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Taustakuva",
|
||||||
|
"description": "Näytä työpöydän taustakuva (poistaminen käytöstä parantaa suorituskykyä)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Fontin pehmennys",
|
||||||
|
"description": "Ota ClearType-fonttien renderöinti käyttöön"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Työpöydän sommittelu",
|
||||||
|
"description": "Ota Aero Glass -tehosteet käyttöön"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Poista ääni käytöstä",
|
||||||
|
"description": "Mykistä kaikki etäistunnon äänet"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Ota tulostus käyttöön",
|
||||||
|
"description": "Ohjaa paikalliset tulostimet etäistuntoon"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Ota aseman uudelleenohjaus käyttöön",
|
||||||
|
"description": "Yhdistä paikallinen kansio asemaksi etäistunnossa"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Poista kopiointi käytöstä",
|
||||||
|
"description": "Estä tekstin kopioiminen etäistunnosta"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Poista liittäminen käytöstä",
|
||||||
|
"description": "Estä tekstin liittäminen etäistuntoon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd-URL-osoite"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Nom de la police",
|
"fontName": "Nom de la police",
|
||||||
"fontSize": "Taille de la police",
|
"fontSize": "Taille de la police",
|
||||||
"colorScheme": "Schéma de couleurs",
|
"colorScheme": "Schéma de couleurs",
|
||||||
"backspaceKey": "Touche Backspace"
|
"backspaceKey": "Touche Backspace",
|
||||||
}
|
"colorDepth": "Profondeur de couleur",
|
||||||
|
"resizeMethod": "Méthode de redimensionnement"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Bureau à distance",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Réseau virtuel",
|
||||||
|
"unencryptedShell": "Shell non chiffré"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Bureau à distance",
|
"feature_desktop": "Bureau à distance",
|
||||||
"feature_desktop_desc": "Ouvrez les sessions RDP, VNC et Telnet dans un onglet, de la même manière que SSH."
|
"feature_desktop_desc": "Ouvrez les sessions RDP, VNC et Telnet dans un onglet, de la même manière que SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Profondeur de couleur"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Méthode de redimensionnement"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Mise à jour de l'affichage",
|
||||||
|
"resizeReconnect": "Reconnect",
|
||||||
|
"inherit": "Hériter",
|
||||||
|
"on": "Sur",
|
||||||
|
"off": "Désactivé",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Forcer sans perte",
|
||||||
|
"description": "Forcer l'encodage d'image sans perte (meilleure qualité, plus de bande passante)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Fond d'écran",
|
||||||
|
"description": "Afficher le fond d'écran du bureau (désactiver cette option améliore les performances)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Lissage des polices",
|
||||||
|
"description": "Activer le rendu des polices ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Composition du bureau",
|
||||||
|
"description": "Activer les effets Aero Glass"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Désactiver l'audio",
|
||||||
|
"description": "Couper tout le son de la session distante"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Activer l'impression",
|
||||||
|
"description": "Rediriger les imprimantes locales vers la session distante"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Activer la redirection de lecteur",
|
||||||
|
"description": "Monter un dossier local en tant que lecteur dans la session distante"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Désactiver la copie",
|
||||||
|
"description": "Empêcher la copie de texte depuis la session distante"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Désactiver le collage",
|
||||||
|
"description": "Empêcher le collage de texte dans la session distante"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "שם הגופן",
|
"fontName": "שם הגופן",
|
||||||
"fontSize": "גודל גופן",
|
"fontSize": "גודל גופן",
|
||||||
"colorScheme": "ערכת צבעים",
|
"colorScheme": "ערכת צבעים",
|
||||||
"backspaceKey": "מקש Backspace"
|
"backspaceKey": "מקש Backspace",
|
||||||
}
|
"colorDepth": "עומק צבע",
|
||||||
|
"resizeMethod": "שיטת שינוי גודל"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "שולחן עבודה מרוחק",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "טלנט",
|
||||||
|
"virtualNetwork": "רשת וירטואלית",
|
||||||
|
"unencryptedShell": "מעטפת לא מוצפנת"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "שולחן עבודה מרוחק",
|
"feature_desktop": "שולחן עבודה מרוחק",
|
||||||
"feature_desktop_desc": "פתחו הפעלות RDP, VNC ו-Telnet בכרטיסייה, בדומה ל-SSH."
|
"feature_desktop_desc": "פתחו הפעלות RDP, VNC ו-Telnet בכרטיסייה, בדומה ל-SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "עומק צבע"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "שיטת שינוי גודל"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "עדכון תצוגה",
|
||||||
|
"resizeReconnect": "התחבר מחדש",
|
||||||
|
"inherit": "לָרֶשֶׁת",
|
||||||
|
"on": "עַל",
|
||||||
|
"off": "כבוי",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "כפיית אובדן נתונים",
|
||||||
|
"description": "כפיית קידוד תמונה ללא אובדן נתונים (איכות גבוהה יותר, רוחב פס גדול יותר)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "טָפֵּט",
|
||||||
|
"description": "הצגת טפט שולחן עבודה (השבתה משפרת את הביצועים)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "החלקת גופנים",
|
||||||
|
"description": "הפעלת עיבוד גופני ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "קומפוזיציה לשולחן העבודה",
|
||||||
|
"description": "הפעלת אפקטים של Aero Glass"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "השבתת שמע",
|
||||||
|
"description": "השתקת כל האודיו מההפעלה המרוחקת"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "הפעל הדפסה",
|
||||||
|
"description": "ניתוב מחדש של מדפסות מקומיות להפעלה מרוחקת"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "הפעל ניתוב מחדש של כונן",
|
||||||
|
"description": "מיפוי תיקייה מקומית ככונן בהפעלה מרוחקת"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "השבתת העתקה",
|
||||||
|
"description": "מניעת העתקת טקסט מההפעלה המרוחקת"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "השבתת הדבקה",
|
||||||
|
"description": "מניעת הדבקת טקסט לתוך ההפעלה המרוחקת"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "כתובת אתר של guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "फ़ॉन्ट नाम",
|
"fontName": "फ़ॉन्ट नाम",
|
||||||
"fontSize": "फ़ॉन्ट आकार",
|
"fontSize": "फ़ॉन्ट आकार",
|
||||||
"colorScheme": "रंग योजना",
|
"colorScheme": "रंग योजना",
|
||||||
"backspaceKey": "बैकस्पेस कुंजी"
|
"backspaceKey": "बैकस्पेस कुंजी",
|
||||||
}
|
"colorDepth": "रंग गहराई",
|
||||||
|
"resizeMethod": "रीसाइज़ विधि"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "दूरस्थ डेस्कटॉप",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "वर्चुअल नेटवर्क",
|
||||||
|
"unencryptedShell": "अनएन्क्रिप्टेड शेल"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "दूरवर्ती डेस्कटॉप",
|
"feature_desktop": "दूरवर्ती डेस्कटॉप",
|
||||||
"feature_desktop_desc": "RDP, VNC और Telnet सेशन को SSH की तरह ही एक टैब में खोलें।"
|
"feature_desktop_desc": "RDP, VNC और Telnet सेशन को SSH की तरह ही एक टैब में खोलें।"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "रंग गहराई"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "रीसाइज़ विधि"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "डिस्प्ले अपडेट",
|
||||||
|
"resizeReconnect": "रिकनेक्ट",
|
||||||
|
"inherit": "इनहेरिट",
|
||||||
|
"on": "पर",
|
||||||
|
"off": "बंद",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "दोषरहित बाध्य करें",
|
||||||
|
"description": "दोषरहित छवि एन्कोडिंग बाध्य करें (उच्च गुणवत्ता, अधिक बैंडविड्थ)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "वॉलपेपर",
|
||||||
|
"description": "डेस्कटॉप वॉलपेपर दिखाएं (अक्षम करने से प्रदर्शन बेहतर होता है)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "फ़ॉन्ट स्मूथिंग",
|
||||||
|
"description": "ClearType फ़ॉन्ट रेंडरिंग सक्षम करें"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "डेस्कटॉप कंपोज़िशन",
|
||||||
|
"description": "Aero ग्लास प्रभाव सक्षम करें"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "ऑडियो अक्षम करें",
|
||||||
|
"description": "दूरस्थ सत्र से सभी ऑडियो म्यूट करें"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "प्रिंटिंग सक्षम करें",
|
||||||
|
"description": "स्थानीय प्रिंटर को दूरस्थ सत्र में पुनर्निर्देशित करें"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "ड्राइव पुनर्निर्देशन सक्षम करें",
|
||||||
|
"description": "स्थानीय फ़ोल्डर को दूरस्थ सत्र में ड्राइव के रूप में मैप करें"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "कॉपी अक्षम करें",
|
||||||
|
"description": "रिमोट सत्र से टेक्स्ट कॉपी करना रोकें"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "पेस्ट अक्षम करें",
|
||||||
|
"description": "रिमोट सत्र में टेक्स्ट पेस्ट करना रोकें"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Betűtípus neve",
|
"fontName": "Betűtípus neve",
|
||||||
"fontSize": "Betűméret",
|
"fontSize": "Betűméret",
|
||||||
"colorScheme": "Színséma",
|
"colorScheme": "Színséma",
|
||||||
"backspaceKey": "Visszatörlés billentyű"
|
"backspaceKey": "Visszatörlés billentyű",
|
||||||
}
|
"colorDepth": "Színmélység",
|
||||||
|
"resizeMethod": "Átméretezési módszer"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Távoli asztal",
|
||||||
|
"tabRdp": "Vidékfejlesztési Program",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Virtuális hálózat",
|
||||||
|
"unencryptedShell": "Titkosítatlan shell"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Távoli asztal",
|
"feature_desktop": "Távoli asztal",
|
||||||
"feature_desktop_desc": "Nyisson meg RDP, VNC és Telnet munkameneteket egy lapon, ugyanúgy, mint az SSH-n."
|
"feature_desktop_desc": "Nyisson meg RDP, VNC és Telnet munkameneteket egy lapon, ugyanúgy, mint az SSH-n."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Színmélység"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Átméretezési módszer"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Kijelzőfrissítés",
|
||||||
|
"resizeReconnect": "Újracsatlakozás",
|
||||||
|
"inherit": "Örököl",
|
||||||
|
"on": "On",
|
||||||
|
"off": "Le",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Veszteségmentes erő",
|
||||||
|
"description": "Veszteségmentes képkódolás kényszerítése (jobb minőség, nagyobb sávszélesség)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Tapéta",
|
||||||
|
"description": "Asztali háttérkép megjelenítése (a letiltás javítja a teljesítményt)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Betűsimítás",
|
||||||
|
"description": "ClearType betűtípus-megjelenítés engedélyezése"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Asztali kompozíció",
|
||||||
|
"description": "Aero Glass effektek engedélyezése"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Hang letiltása",
|
||||||
|
"description": "Némítsa el az összes hangot a távoli munkamenetből"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Nyomtatás engedélyezése",
|
||||||
|
"description": "Helyi nyomtatók átirányítása a távoli munkamenetre"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Meghajtó átirányításának engedélyezése",
|
||||||
|
"description": "Helyi mappa meghajtóként való leképezése a távoli munkamenetben"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Másolás letiltása",
|
||||||
|
"description": "Szöveg másolásának megakadályozása a távoli munkamenetből"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Beillesztés letiltása",
|
||||||
|
"description": "Szöveg beillesztésének megakadályozása a távoli munkamenetbe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Nama Font",
|
"fontName": "Nama Font",
|
||||||
"fontSize": "Ukuran Huruf",
|
"fontSize": "Ukuran Huruf",
|
||||||
"colorScheme": "Skema Warna",
|
"colorScheme": "Skema Warna",
|
||||||
"backspaceKey": "Tombol Backspace"
|
"backspaceKey": "Tombol Backspace",
|
||||||
}
|
"colorDepth": "Kedalaman Warna",
|
||||||
|
"resizeMethod": "Metode Pengubahan Ukuran"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Desktop Jarak Jauh",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Jaringan Virtual",
|
||||||
|
"unencryptedShell": "Shell yang tidak terenkripsi"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Desktop jarak jauh",
|
"feature_desktop": "Desktop jarak jauh",
|
||||||
"feature_desktop_desc": "Buka sesi RDP, VNC, dan Telnet dalam satu tab, sama seperti SSH."
|
"feature_desktop_desc": "Buka sesi RDP, VNC, dan Telnet dalam satu tab, sama seperti SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Kedalaman Warna"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Metode Pengubahan Ukuran"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Pembaruan Tampilan",
|
||||||
|
"resizeReconnect": "Terhubung kembali",
|
||||||
|
"inherit": "Mewarisi",
|
||||||
|
"on": "Pada",
|
||||||
|
"off": "Mati",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Paksa Tanpa Kehilangan",
|
||||||
|
"description": "Paksa pengkodean gambar tanpa kehilangan data (kualitas lebih tinggi, bandwidth lebih besar)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Wallpaper",
|
||||||
|
"description": "Tampilkan wallpaper desktop (menonaktifkannya akan meningkatkan kinerja)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Penghalusan Font",
|
||||||
|
"description": "Aktifkan rendering font ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Komposisi Desktop",
|
||||||
|
"description": "Aktifkan efek kaca Aero"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Nonaktifkan Audio",
|
||||||
|
"description": "Nonaktifkan semua audio dari sesi jarak jauh."
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Aktifkan Pencetakan",
|
||||||
|
"description": "Alihkan printer lokal ke sesi jarak jauh."
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Aktifkan Pengalihan Drive",
|
||||||
|
"description": "Petakan folder lokal sebagai drive di sesi jarak jauh."
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Nonaktifkan Salin",
|
||||||
|
"description": "Mencegah penyalinan teks dari sesi jarak jauh"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Nonaktifkan Tempel",
|
||||||
|
"description": "Mencegah penyisipan teks ke dalam sesi jarak jauh"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Nome font",
|
"fontName": "Nome font",
|
||||||
"fontSize": "Dimensione font",
|
"fontSize": "Dimensione font",
|
||||||
"colorScheme": "Schema colori",
|
"colorScheme": "Schema colori",
|
||||||
"backspaceKey": "Tasto Backspace"
|
"backspaceKey": "Tasto Backspace",
|
||||||
}
|
"colorDepth": "Profondità colore",
|
||||||
|
"resizeMethod": "Metodo di ridimensionamento"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Desktop remoto",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Rete virtuale",
|
||||||
|
"unencryptedShell": "Shell non crittografata"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Desktop remoto",
|
"feature_desktop": "Desktop remoto",
|
||||||
"feature_desktop_desc": "Apri le sessioni RDP, VNC e Telnet in una scheda, proprio come per SSH."
|
"feature_desktop_desc": "Apri le sessioni RDP, VNC e Telnet in una scheda, proprio come per SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Profondità colore"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Metodo di ridimensionamento"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Aggiornamento della visualizzazione",
|
||||||
|
"resizeReconnect": "Ricollega",
|
||||||
|
"inherit": "Ereditare",
|
||||||
|
"on": "SU",
|
||||||
|
"off": "Spento",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Forza lossless",
|
||||||
|
"description": "Forza la codifica lossless delle immagini (qualità più alta, maggiore utilizzo di banda)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Sfondo",
|
||||||
|
"description": "Mostra lo sfondo del desktop (disabilitarlo migliora le prestazioni)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Smussamento caratteri",
|
||||||
|
"description": "Abilita il rendering dei caratteri ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Composizione desktop",
|
||||||
|
"description": "Abilita gli effetti Aero Glass"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Disabilita audio",
|
||||||
|
"description": "Disattiva tutto l'audio dalla sessione remota"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Abilita stampa",
|
||||||
|
"description": "Reindirizza le stampanti locali alla sessione remota"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Abilita reindirizzamento unità",
|
||||||
|
"description": "Mappa una cartella locale come unità nella sessione remota"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Disabilita copia",
|
||||||
|
"description": "Impedisce la copia di testo dalla sessione remota"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Disabilita incolla",
|
||||||
|
"description": "Impedisce di incollare testo nella sessione remota"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "フォント名",
|
"fontName": "フォント名",
|
||||||
"fontSize": "フォントサイズ",
|
"fontSize": "フォントサイズ",
|
||||||
"colorScheme": "カラースキーム",
|
"colorScheme": "カラースキーム",
|
||||||
"backspaceKey": "バックスペースキー"
|
"backspaceKey": "バックスペースキー",
|
||||||
}
|
"colorDepth": "色深度",
|
||||||
|
"resizeMethod": "リサイズ方法"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "リモートデスクトップ",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "仮想ネットワーク",
|
||||||
|
"unencryptedShell": "暗号化されていないシェル"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "リモートデスクトップ",
|
"feature_desktop": "リモートデスクトップ",
|
||||||
"feature_desktop_desc": "RDP、VNC、Telnetのセッションは、SSHと同様にタブで開きます。"
|
"feature_desktop_desc": "RDP、VNC、Telnetのセッションは、SSHと同様にタブで開きます。"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "色深度"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "リサイズ方法"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "ディスプレイの更新",
|
||||||
|
"resizeReconnect": "再接続",
|
||||||
|
"inherit": "継承する",
|
||||||
|
"on": "の上",
|
||||||
|
"off": "オフ",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "ロスレスを強制",
|
||||||
|
"description": "ロスレス画像エンコーディングを強制する(高品質だが帯域幅を多く消費)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "壁紙",
|
||||||
|
"description": "デスクトップ壁紙を表示する(無効にするとパフォーマンスが向上)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "フォントスムージング",
|
||||||
|
"description": "ClearTypeフォントレンダリングを有効にする"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "デスクトップコンポジション",
|
||||||
|
"description": "Aero Glass効果を有効にする"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "オーディオを無効化",
|
||||||
|
"description": "リモートセッションの音声をすべてミュートする"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "印刷を有効化",
|
||||||
|
"description": "ローカルプリンターをリモートセッションにリダイレクトする"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "ドライブリダイレクションを有効化",
|
||||||
|
"description": "リモートセッションでローカルフォルダーをドライブとしてマッピングする"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "コピーを無効にする",
|
||||||
|
"description": "リモートセッションからのテキストコピーを禁止します"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "貼り付けを無効にする",
|
||||||
|
"description": "リモートセッションへのテキスト貼り付けを禁止します"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "글꼴 이름",
|
"fontName": "글꼴 이름",
|
||||||
"fontSize": "글꼴 크기",
|
"fontSize": "글꼴 크기",
|
||||||
"colorScheme": "색 구성표",
|
"colorScheme": "색 구성표",
|
||||||
"backspaceKey": "백스페이스 키"
|
"backspaceKey": "백스페이스 키",
|
||||||
}
|
"colorDepth": "색 농도",
|
||||||
|
"resizeMethod": "크기 조정 방법"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "원격 데스크톱",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "텔넷",
|
||||||
|
"virtualNetwork": "가상 네트워크",
|
||||||
|
"unencryptedShell": "암호화되지 않은 셸"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "원격 데스크톱",
|
"feature_desktop": "원격 데스크톱",
|
||||||
"feature_desktop_desc": "SSH와 마찬가지로 RDP, VNC 및 Telnet 세션을 탭에서 열 수 있습니다."
|
"feature_desktop_desc": "SSH와 마찬가지로 RDP, VNC 및 Telnet 세션을 탭에서 열 수 있습니다."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "색 농도"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "크기 조정 방법"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "디스플레이 업데이트",
|
||||||
|
"resizeReconnect": "다시 연결",
|
||||||
|
"inherit": "상속하다",
|
||||||
|
"on": "~에",
|
||||||
|
"off": "끄다",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "무손실 강제",
|
||||||
|
"description": "무손실 이미지 인코딩 강제 (더 높은 품질, 더 많은 대역폭)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "배경 화면",
|
||||||
|
"description": "바탕 화면 배경 표시 (비활성화 시 성능 향상)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "글꼴 다듬기",
|
||||||
|
"description": "ClearType 글꼴 렌더링 사용"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "데스크톱 구성",
|
||||||
|
"description": "Aero 글래스 효과 활성화"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "오디오 비활성화",
|
||||||
|
"description": "원격 세션의 모든 오디오 음소거"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "인쇄 활성화",
|
||||||
|
"description": "로컬 프린터를 원격 세션으로 리디렉션"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "드라이브 리디렉션 활성화",
|
||||||
|
"description": "로컬 폴더를 원격 세션에서 드라이브로 매핑"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "복사 비활성화",
|
||||||
|
"description": "원격 세션에서 텍스트 복사 방지"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "붙여넣기 비활성화",
|
||||||
|
"description": "원격 세션으로 텍스트 붙여넣기 방지"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Lettertypenaam",
|
"fontName": "Lettertypenaam",
|
||||||
"fontSize": "Lettergrootte",
|
"fontSize": "Lettergrootte",
|
||||||
"colorScheme": "Kleurenschema",
|
"colorScheme": "Kleurenschema",
|
||||||
"backspaceKey": "Backspace-toets"
|
"backspaceKey": "Backspace-toets",
|
||||||
}
|
"colorDepth": "Kleurdiepte",
|
||||||
|
"resizeMethod": "Formaatwijzigingsmethode"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Extern bureaublad",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Virtueel netwerk",
|
||||||
|
"unencryptedShell": "Niet-versleutelde shell"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Extern bureaublad",
|
"feature_desktop": "Extern bureaublad",
|
||||||
"feature_desktop_desc": "Open RDP-, VNC- en Telnet-sessies in een tabblad, net als bij SSH."
|
"feature_desktop_desc": "Open RDP-, VNC- en Telnet-sessies in een tabblad, net als bij SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Kleurdiepte"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Formaatwijzigingsmethode"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Weergave bijwerken",
|
||||||
|
"resizeReconnect": "Opnieuw verbinding maken",
|
||||||
|
"inherit": "Erven",
|
||||||
|
"on": "Op",
|
||||||
|
"off": "Uit",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Krachtverliesloos",
|
||||||
|
"description": "Forceer verliesvrije beeldcodering (hogere kwaliteit, meer bandbreedte)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Behang",
|
||||||
|
"description": "Toon bureaubladachtergrond (uitschakelen verbetert de prestaties)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Lettertype-vloeiendheid",
|
||||||
|
"description": "Schakel ClearType-lettertypeweergave in"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Bureaubladcompositie",
|
||||||
|
"description": "Schakel Aero-glaseffecten in"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Geluid uitschakelen",
|
||||||
|
"description": "Demp alle audio van de externe sessie."
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Afdrukken inschakelen",
|
||||||
|
"description": "Lokale printers doorverwijzen naar de externe sessie"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Schakel schijfomleiding in",
|
||||||
|
"description": "Een lokale map toewijzen als schijfstation in de externe sessie."
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Kopiëren uitschakelen",
|
||||||
|
"description": "Voorkom het kopiëren van tekst vanuit de externe sessie."
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Plakken uitschakelen",
|
||||||
|
"description": "Voorkom dat tekst in de externe sessie wordt geplakt."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Skriftnavn",
|
"fontName": "Skriftnavn",
|
||||||
"fontSize": "Skriftstørrelse",
|
"fontSize": "Skriftstørrelse",
|
||||||
"colorScheme": "Fargeskjema",
|
"colorScheme": "Fargeskjema",
|
||||||
"backspaceKey": "Tilbaketast"
|
"backspaceKey": "Tilbaketast",
|
||||||
}
|
"colorDepth": "Fargedybde",
|
||||||
|
"resizeMethod": "Endre størrelse-metoden"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Eksternt skrivebord",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Virtuelt nettverk",
|
||||||
|
"unencryptedShell": "Ukryptert skall"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Eksternt skrivebord",
|
"feature_desktop": "Eksternt skrivebord",
|
||||||
"feature_desktop_desc": "Åpne RDP-, VNC- og Telnet-økter i en fane, på samme måte som SSH."
|
"feature_desktop_desc": "Åpne RDP-, VNC- og Telnet-økter i en fane, på samme måte som SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Fargedybde"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Endre størrelse-metoden"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Oppdatering av skjerm",
|
||||||
|
"resizeReconnect": "Koble til igjen",
|
||||||
|
"inherit": "Arve",
|
||||||
|
"on": "På",
|
||||||
|
"off": "Av",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Tving tapsfri",
|
||||||
|
"description": "Tving frem tapsfri bildekoding (høyere kvalitet, mer båndbredde)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Bakgrunn",
|
||||||
|
"description": "Vis skrivebordsbakgrunn (deaktivering forbedrer ytelsen)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Skriftutjevning",
|
||||||
|
"description": "Aktiver gjengivelse av ClearType-skrifttyper"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Skrivebordskomposisjon",
|
||||||
|
"description": "Aktiver Aero Glass-effekter"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Deaktiver lyd",
|
||||||
|
"description": "Demp all lyd fra den eksterne økten"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Aktiver utskrift",
|
||||||
|
"description": "Omdiriger lokale skrivere til den eksterne økten"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Aktiver omdirigering av stasjoner",
|
||||||
|
"description": "Tilordne en lokal mappe som en stasjon i den eksterne økten"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Deaktiver kopiering",
|
||||||
|
"description": "Forhindre kopiering av tekst fra den eksterne økten"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Deaktiver liming",
|
||||||
|
"description": "Forhindre innliming av tekst i den eksterne økten"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd-URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Nazwa czcionki",
|
"fontName": "Nazwa czcionki",
|
||||||
"fontSize": "Rozmiar czcionki",
|
"fontSize": "Rozmiar czcionki",
|
||||||
"colorScheme": "Schemat kolorów",
|
"colorScheme": "Schemat kolorów",
|
||||||
"backspaceKey": "Klawisz Backspace"
|
"backspaceKey": "Klawisz Backspace",
|
||||||
}
|
"colorDepth": "Głębia koloru",
|
||||||
|
"resizeMethod": "Metoda zmiany rozmiaru"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Pulpit zdalny",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Sieć wirtualna",
|
||||||
|
"unencryptedShell": "Niezaszyfrowana powłoka"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Zdalny pulpit",
|
"feature_desktop": "Zdalny pulpit",
|
||||||
"feature_desktop_desc": "Otwieraj sesje RDP, VNC i Telnet w karcie, tak samo jak SSH."
|
"feature_desktop_desc": "Otwieraj sesje RDP, VNC i Telnet w karcie, tak samo jak SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Głębia koloru"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Metoda zmiany rozmiaru"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Aktualizacja ekranu",
|
||||||
|
"resizeReconnect": "Połącz ponownie",
|
||||||
|
"inherit": "Dziedzicz",
|
||||||
|
"on": "Wł.",
|
||||||
|
"off": "Wył.",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Wymuś bezstratność",
|
||||||
|
"description": "Wymuś bezstratne kodowanie obrazu (wyższa jakość, większa przepustowość)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Tapeta",
|
||||||
|
"description": "Pokaż tapetę pulpitu (wyłączenie poprawia wydajność)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Wygładzanie czcionek",
|
||||||
|
"description": "Włącz renderowanie czcionek ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Kompozycja pulpitu",
|
||||||
|
"description": "Włącz efekty Aero Glass"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Wyłącz dźwięk",
|
||||||
|
"description": "Wycisz cały dźwięk z sesji zdalnej"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Włącz drukowanie",
|
||||||
|
"description": "Przekieruj drukarki lokalne do sesji zdalnej"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Włącz przekierowanie dysku",
|
||||||
|
"description": "Mapuj folder lokalny jako dysk w sesji zdalnej"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Wyłącz kopiowanie",
|
||||||
|
"description": "Zapobiegaj kopiowaniu tekstu z sesji zdalnej"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Wyłącz wklejanie",
|
||||||
|
"description": "Zapobiegaj wklejaniu tekstu do sesji zdalnej"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "Adres URL guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Nome da fonte",
|
"fontName": "Nome da fonte",
|
||||||
"fontSize": "Tamanho da fonte",
|
"fontSize": "Tamanho da fonte",
|
||||||
"colorScheme": "Esquema de cores",
|
"colorScheme": "Esquema de cores",
|
||||||
"backspaceKey": "Tecla Backspace"
|
"backspaceKey": "Tecla Backspace",
|
||||||
}
|
"colorDepth": "Profundidade de cor",
|
||||||
|
"resizeMethod": "Método de redimensionamento"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Área de Trabalho Remota",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Rede Virtual",
|
||||||
|
"unencryptedShell": "Shell não criptografado"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Área de trabalho remota",
|
"feature_desktop": "Área de trabalho remota",
|
||||||
"feature_desktop_desc": "Abra sessões RDP, VNC e Telnet em uma aba, da mesma forma que o SSH."
|
"feature_desktop_desc": "Abra sessões RDP, VNC e Telnet em uma aba, da mesma forma que o SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Profundidade de cor"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Método de redimensionamento"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Atualização de exibição",
|
||||||
|
"resizeReconnect": "Reconectar",
|
||||||
|
"inherit": "Herdar",
|
||||||
|
"on": "Sobre",
|
||||||
|
"off": "Desligado",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Força sem perdas",
|
||||||
|
"description": "Forçar codificação de imagem sem perdas (maior qualidade, mais largura de banda)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Papel de parede",
|
||||||
|
"description": "Exibir papel de parede da área de trabalho (desativar melhora o desempenho)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Suavização de fontes",
|
||||||
|
"description": "Ativar renderização de fontes ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Composição de mesa",
|
||||||
|
"description": "Ative os efeitos de vidro Aero"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Desativar áudio",
|
||||||
|
"description": "Silenciar todo o áudio da sessão remota."
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Habilitar impressão",
|
||||||
|
"description": "Redirecionar impressoras locais para a sessão remota."
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Ativar o redirecionamento de unidades",
|
||||||
|
"description": "Mapear uma pasta local como uma unidade na sessão remota."
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Desativar cópia",
|
||||||
|
"description": "Impedir a cópia de texto da sessão remota"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Desativar colagem",
|
||||||
|
"description": "Impedir a colagem de texto na sessão remota."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL do guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Nome da Fonte",
|
"fontName": "Nome da Fonte",
|
||||||
"fontSize": "Tamanho da Fonte",
|
"fontSize": "Tamanho da Fonte",
|
||||||
"colorScheme": "Esquema de Cores",
|
"colorScheme": "Esquema de Cores",
|
||||||
"backspaceKey": "Tecla Backspace"
|
"backspaceKey": "Tecla Backspace",
|
||||||
}
|
"colorDepth": "Profundidade de cor",
|
||||||
|
"resizeMethod": "Método de redimensionamento"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Ambiente de trabalho remoto",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Rede Virtual",
|
||||||
|
"unencryptedShell": "Shell não encriptado"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Área de trabalho remota",
|
"feature_desktop": "Área de trabalho remota",
|
||||||
"feature_desktop_desc": "Abra as sessões RDP, VNC e Telnet num separador, da mesma forma que o SSH."
|
"feature_desktop_desc": "Abra as sessões RDP, VNC e Telnet num separador, da mesma forma que o SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Profundidade de cor"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Método de redimensionamento"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Atualização de visualização",
|
||||||
|
"resizeReconnect": "Reconectar",
|
||||||
|
"inherit": "Herdar",
|
||||||
|
"on": "Em",
|
||||||
|
"off": "Desligado",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Forçar sem perdas",
|
||||||
|
"description": "Forçar codificação de imagem sem perdas (maior qualidade, mais largura de banda)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Fundo de ecrã",
|
||||||
|
"description": "Mostrar fundo de ecrã (desativar melhora o desempenho)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Suavização de Fontes",
|
||||||
|
"description": "Ativar renderização de fontes ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Composição do ambiente de trabalho",
|
||||||
|
"description": "Ativar efeitos de transparência Aero"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Desativar Áudio",
|
||||||
|
"description": "Silenciar todo o áudio da sessão remota"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Ativar Impressão",
|
||||||
|
"description": "Redirecionar impressoras locais para a sessão remota"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Ativar Redirecionamento de Unidades",
|
||||||
|
"description": "Mapear uma pasta local como unidade na sessão remota"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Desativar Cópia",
|
||||||
|
"description": "Impedir a cópia de texto da sessão remota"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Desativar Colagem",
|
||||||
|
"description": "Impedir a colagem de texto na sessão remota"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL do guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Nume font",
|
"fontName": "Nume font",
|
||||||
"fontSize": "Dimensiunea fontului",
|
"fontSize": "Dimensiunea fontului",
|
||||||
"colorScheme": "Schemă de culori",
|
"colorScheme": "Schemă de culori",
|
||||||
"backspaceKey": "Tasta Backspace"
|
"backspaceKey": "Tasta Backspace",
|
||||||
}
|
"colorDepth": "Adâncimea culorii",
|
||||||
|
"resizeMethod": "Metodă de redimensionare"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Desktop la distanță",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Rețea virtuală",
|
||||||
|
"unencryptedShell": "Shell necriptat"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Desktop la distanță",
|
"feature_desktop": "Desktop la distanță",
|
||||||
"feature_desktop_desc": "Deschideți sesiuni RDP, VNC și Telnet într-o filă, la fel ca în cazul SSH."
|
"feature_desktop_desc": "Deschideți sesiuni RDP, VNC și Telnet într-o filă, la fel ca în cazul SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Adâncimea culorii"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Metodă de redimensionare"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Actualizare afișare",
|
||||||
|
"resizeReconnect": "Reconectați-vă",
|
||||||
|
"inherit": "Moşteni",
|
||||||
|
"on": "Pe",
|
||||||
|
"off": "Dezactivat",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Forța fără pierderi",
|
||||||
|
"description": "Forțează codificarea imaginilor fără pierderi (calitate mai mare, lățime de bandă mai mare)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Tapet",
|
||||||
|
"description": "Afișează imaginea de fundal a desktopului (dezactivarea îmbunătățește performanța)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Netezirea fonturilor",
|
||||||
|
"description": "Activează randarea fonturilor ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Compoziție desktop",
|
||||||
|
"description": "Activează efectele Aero Glass"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Dezactivați sunetul",
|
||||||
|
"description": "Dezactivați tot sunetul din sesiunea la distanță"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Activează imprimarea",
|
||||||
|
"description": "Redirecționează imprimantele locale către sesiunea la distanță"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Activați redirecționarea unității",
|
||||||
|
"description": "Maparea unui folder local ca unitate în sesiunea la distanță"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Dezactivați copierea",
|
||||||
|
"description": "Împiedicați copierea textului din sesiunea la distanță"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Dezactivați lipirea",
|
||||||
|
"description": "Împiedicați lipirea textului în sesiunea la distanță"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL-ul guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Название шрифта",
|
"fontName": "Название шрифта",
|
||||||
"fontSize": "Размер шрифта",
|
"fontSize": "Размер шрифта",
|
||||||
"colorScheme": "Цветовая схема",
|
"colorScheme": "Цветовая схема",
|
||||||
"backspaceKey": "Клавиша Backspace"
|
"backspaceKey": "Клавиша Backspace",
|
||||||
}
|
"colorDepth": "Глубина цвета",
|
||||||
|
"resizeMethod": "Метод изменения размера"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Удаленный рабочий стол",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Виртуальная сеть",
|
||||||
|
"unencryptedShell": "Незашифрованная оболочка"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Удаленный рабочий стол",
|
"feature_desktop": "Удаленный рабочий стол",
|
||||||
"feature_desktop_desc": "Открывайте сеансы RDP, VNC и Telnet во вкладке, как и SSH."
|
"feature_desktop_desc": "Открывайте сеансы RDP, VNC и Telnet во вкладке, как и SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Глубина цвета"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Метод изменения размера"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Обновление дисплея",
|
||||||
|
"resizeReconnect": "Переподключитесь",
|
||||||
|
"inherit": "Наследовать",
|
||||||
|
"on": "На",
|
||||||
|
"off": "Выключенный",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Принудительно без потерь",
|
||||||
|
"description": "Принудительное кодирование изображений без потерь (более высокое качество, больший расход трафика)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Обои",
|
||||||
|
"description": "Показывать обои рабочего стола (отключение повышает производительность)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Сглаживание шрифтов",
|
||||||
|
"description": "Включить рендеринг шрифтов ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Композиция рабочего стола",
|
||||||
|
"description": "Включить эффекты Aero Glass"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Отключить звук",
|
||||||
|
"description": "Отключить весь звук в удаленном сеансе"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Включить печать",
|
||||||
|
"description": "Перенаправлять локальные принтеры в удаленный сеанс"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Включить перенаправление дисков",
|
||||||
|
"description": "Подключать локальную папку как диск в удаленном сеансе"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Отключить копирование",
|
||||||
|
"description": "Запретить копирование текста из удаленного сеанса"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Отключить вставку",
|
||||||
|
"description": "Запретить вставку текста в удаленный сеанс"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Назив фонта",
|
"fontName": "Назив фонта",
|
||||||
"fontSize": "Величина фонта",
|
"fontSize": "Величина фонта",
|
||||||
"colorScheme": "Шема боја",
|
"colorScheme": "Шема боја",
|
||||||
"backspaceKey": "Тастер Backspace"
|
"backspaceKey": "Тастер Backspace",
|
||||||
}
|
"colorDepth": "Дубина боје",
|
||||||
|
"resizeMethod": "Метод промене величине"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Удаљена радна површина",
|
||||||
|
"tabRdp": "РДП",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Телнет",
|
||||||
|
"virtualNetwork": "Виртуелна мрежа",
|
||||||
|
"unencryptedShell": "Нешифрована љуска"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Удаљена радна површина",
|
"feature_desktop": "Удаљена радна површина",
|
||||||
"feature_desktop_desc": "Отворите RDP, VNC и Telnet сесије у картици, исто као и SSH."
|
"feature_desktop_desc": "Отворите RDP, VNC и Telnet сесије у картици, исто као и SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Дубина боје"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Метод промене величине"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Ажурирање екрана",
|
||||||
|
"resizeReconnect": "Поново се повежите",
|
||||||
|
"inherit": "Наследи",
|
||||||
|
"on": "Укључено",
|
||||||
|
"off": "Искључено",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Присилно без губитака",
|
||||||
|
"description": "Присилно кодирање слике без губитака (виши квалитет, већи пропусни опсег)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Позадина",
|
||||||
|
"description": "Прикажи позадину радне површине (онемогућавање побољшава перформансе)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Изглађивање фонта",
|
||||||
|
"description": "Омогући приказивање ClearType фонта"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Композиција на радној површини",
|
||||||
|
"description": "Омогући ефекте Aero Glass-а"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Онемогући звук",
|
||||||
|
"description": "Искључите сав звук из удаљене сесије"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Омогући штампање",
|
||||||
|
"description": "Преусмерите локалне штампаче на удаљену сесију"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Омогући преусмеравање диска",
|
||||||
|
"description": "Мапирајте локалну фасциклу као диск у удаљеној сесији"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Онемогући копирање",
|
||||||
|
"description": "Спречите копирање текста из удаљене сесије"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Онемогући лепљење",
|
||||||
|
"description": "Спречите лепљење текста у удаљену сесију"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL адреса guacd-а"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Typsnittsnamn",
|
"fontName": "Typsnittsnamn",
|
||||||
"fontSize": "Fontstorlek",
|
"fontSize": "Fontstorlek",
|
||||||
"colorScheme": "Färgschema",
|
"colorScheme": "Färgschema",
|
||||||
"backspaceKey": "Backstegstangenten"
|
"backspaceKey": "Backstegstangenten",
|
||||||
}
|
"colorDepth": "Färgdjup",
|
||||||
|
"resizeMethod": "Ändra storleksmetod"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Fjärrskrivbord",
|
||||||
|
"tabRdp": "Landsbygdsutveckling",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Virtuellt nätverk",
|
||||||
|
"unencryptedShell": "Okrypterat skal"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Fjärrskrivbord",
|
"feature_desktop": "Fjärrskrivbord",
|
||||||
"feature_desktop_desc": "Öppna RDP-, VNC- och Telnet-sessioner i en flik, samma som med SSH."
|
"feature_desktop_desc": "Öppna RDP-, VNC- och Telnet-sessioner i en flik, samma som med SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Färgdjup"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Ändra storleksmetod"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Uppdatering av displayen",
|
||||||
|
"resizeReconnect": "Återanslut",
|
||||||
|
"inherit": "Ärva",
|
||||||
|
"on": "På",
|
||||||
|
"off": "Av",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Tvinga förlustfri",
|
||||||
|
"description": "Tvinga fram förlustfri bildkodning (högre kvalitet, mer bandbredd)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Tapet",
|
||||||
|
"description": "Visa skrivbordsbakgrund (inaktivering förbättrar prestandan)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Utjämning av teckensnitt",
|
||||||
|
"description": "Aktivera rendering av ClearType-teckensnitt"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Skrivbordskomposition",
|
||||||
|
"description": "Aktivera Aero Glass-effekter"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Inaktivera ljud",
|
||||||
|
"description": "Stäng av allt ljud från fjärrsessionen"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Aktivera utskrift",
|
||||||
|
"description": "Omdirigera lokala skrivare till fjärrsessionen"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Aktivera omdirigering av kördata",
|
||||||
|
"description": "Mappa en lokal mapp som en enhet i fjärrsessionen"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Inaktivera kopiering",
|
||||||
|
"description": "Förhindra kopiering av text från fjärrsessionen"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Inaktivera inklistring",
|
||||||
|
"description": "Förhindra att text klistras in i fjärrsessionen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd-URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "ชื่อแบบอักษร",
|
"fontName": "ชื่อแบบอักษร",
|
||||||
"fontSize": "ขนาดตัวอักษร",
|
"fontSize": "ขนาดตัวอักษร",
|
||||||
"colorScheme": "โทนสี",
|
"colorScheme": "โทนสี",
|
||||||
"backspaceKey": "ปุ่มลบ"
|
"backspaceKey": "ปุ่มลบ",
|
||||||
}
|
"colorDepth": "ความลึกของสี",
|
||||||
|
"resizeMethod": "วิธีการปรับขนาด"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "รีโมทเดสก์ท็อป",
|
||||||
|
"tabRdp": "อาร์ดีพี",
|
||||||
|
"tabVnc": "วีเอ็นซี",
|
||||||
|
"tabTelnet": "เทลเน็ต",
|
||||||
|
"virtualNetwork": "เครือข่ายเสมือน",
|
||||||
|
"unencryptedShell": "เชลล์ที่ไม่ได้เข้ารหัส"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "การเข้าถึงเดสก์ท็อประยะไกล",
|
"feature_desktop": "การเข้าถึงเดสก์ท็อประยะไกล",
|
||||||
"feature_desktop_desc": "เปิดเซสชัน RDP, VNC และ Telnet ในแท็บเดียวกัน เหมือนกับการเปิด SSH"
|
"feature_desktop_desc": "เปิดเซสชัน RDP, VNC และ Telnet ในแท็บเดียวกัน เหมือนกับการเปิด SSH"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "ความลึกของสี"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "วิธีการปรับขนาด"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "อัปเดตการแสดงผล",
|
||||||
|
"resizeReconnect": "เชื่อมต่อใหม่",
|
||||||
|
"inherit": "สืบทอด",
|
||||||
|
"on": "บน",
|
||||||
|
"off": "ปิด",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "บังคับการสูญเสีย",
|
||||||
|
"description": "บังคับใช้การเข้ารหัสภาพแบบไม่สูญเสียคุณภาพ (คุณภาพสูงกว่า ใช้แบนด์วิดท์มากกว่า)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "วอลเปเปอร์",
|
||||||
|
"description": "แสดงภาพพื้นหลังเดสก์ท็อป (การปิดใช้งานจะช่วยเพิ่มประสิทธิภาพ)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "การปรับความเรียบของแบบอักษร",
|
||||||
|
"description": "เปิดใช้งานการแสดงผลฟอนต์ ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "การจัดองค์ประกอบเดสก์ท็อป",
|
||||||
|
"description": "เปิดใช้งานเอฟเฟกต์กระจก Aero"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "ปิดใช้งานเสียง",
|
||||||
|
"description": "ปิดเสียงทั้งหมดจากการเชื่อมต่อระยะไกล"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "เปิดใช้งานการพิมพ์",
|
||||||
|
"description": "โอนย้ายเครื่องพิมพ์ในเครื่องไปยังเซสชันระยะไกล"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "เปิดใช้งานการเปลี่ยนเส้นทางไดรฟ์",
|
||||||
|
"description": "แมปโฟลเดอร์ในเครื่องเป็นไดรฟ์ในเซสชันระยะไกล"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "ปิดใช้งานการคัดลอก",
|
||||||
|
"description": "ป้องกันการคัดลอกข้อความจากเซสชันระยะไกล"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "ปิดใช้งานการวาง",
|
||||||
|
"description": "ป้องกันการวางข้อความลงในเซสชันระยะไกล"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Yazı Tipi Adı",
|
"fontName": "Yazı Tipi Adı",
|
||||||
"fontSize": "Yazı Tipi Boyutu",
|
"fontSize": "Yazı Tipi Boyutu",
|
||||||
"colorScheme": "Renk Şeması",
|
"colorScheme": "Renk Şeması",
|
||||||
"backspaceKey": "Geri Silme Tuşu"
|
"backspaceKey": "Geri Silme Tuşu",
|
||||||
}
|
"colorDepth": "Renk Derinliği",
|
||||||
|
"resizeMethod": "Yeniden Boyutlandırma Yöntemi"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Uzak Masaüstü",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Sanal Ağ",
|
||||||
|
"unencryptedShell": "Şifrelenmemiş Shell"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Uzaktan masaüstü",
|
"feature_desktop": "Uzaktan masaüstü",
|
||||||
"feature_desktop_desc": "SSH'de olduğu gibi, RDP, VNC ve Telnet oturumlarını da bir sekmede açın."
|
"feature_desktop_desc": "SSH'de olduğu gibi, RDP, VNC ve Telnet oturumlarını da bir sekmede açın."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Renk Derinliği"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Yeniden Boyutlandırma Yöntemi"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Ekran Güncellemesi",
|
||||||
|
"resizeReconnect": "Yeniden bağlan",
|
||||||
|
"inherit": "Miras almak",
|
||||||
|
"on": "Açık",
|
||||||
|
"off": "Kapalı",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Kayıpsız Zorla",
|
||||||
|
"description": "Kayıpsız görüntü kodlamasını zorla (daha yüksek kalite, daha fazla bant genişliği)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Duvar Kağıdı",
|
||||||
|
"description": "Masaüstü duvar kağıdını göster (devre dışı bırakmak performansı artırır)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Yazı Tipi Yumuşatma",
|
||||||
|
"description": "ClearType yazı tipi işlemeyi etkinleştir"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Masaüstü Birleştirme",
|
||||||
|
"description": "Aero cam efektlerini etkinleştir"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Sesi Devre Dışı Bırak",
|
||||||
|
"description": "Uzak oturumdaki tüm sesleri kapat"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Yazdırmayı Etkinleştir",
|
||||||
|
"description": "Yerel yazıcıları uzak oturuma yönlendir"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Sürücü Yönlendirmesini Etkinleştir",
|
||||||
|
"description": "Yerel bir klasörü uzak oturumda sürücü olarak eşleştir"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Kopyalamayı Devre Dışı Bırak",
|
||||||
|
"description": "Uzak oturumdan metin kopyalamayı engelle"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Yapıştırmayı Devre Dışı Bırak",
|
||||||
|
"description": "Uzak oturuma metin yapıştırmayı engelle"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd URL'si"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Назва шрифту",
|
"fontName": "Назва шрифту",
|
||||||
"fontSize": "Розмір шрифту",
|
"fontSize": "Розмір шрифту",
|
||||||
"colorScheme": "Колірна схема",
|
"colorScheme": "Колірна схема",
|
||||||
"backspaceKey": "Клавіша Backspace"
|
"backspaceKey": "Клавіша Backspace",
|
||||||
}
|
"colorDepth": "Глибина кольору",
|
||||||
|
"resizeMethod": "Метод зміни розміру"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Віддалений робочий стіл",
|
||||||
|
"tabRdp": "ПРП",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Телнет",
|
||||||
|
"virtualNetwork": "Віртуальна мережа",
|
||||||
|
"unencryptedShell": "Незашифрована оболонка"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Віддалений робочий стіл",
|
"feature_desktop": "Віддалений робочий стіл",
|
||||||
"feature_desktop_desc": "Відкривати сеанси RDP, VNC та Telnet у вкладці, так само, як SSH."
|
"feature_desktop_desc": "Відкривати сеанси RDP, VNC та Telnet у вкладці, так само, як SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Глибина кольору"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Метод зміни розміру"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Оновлення дисплея",
|
||||||
|
"resizeReconnect": "Знову підключитися",
|
||||||
|
"inherit": "Успадкувати",
|
||||||
|
"on": "Увімкнено",
|
||||||
|
"off": "Вимкнено",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Примусово без втрат",
|
||||||
|
"description": "Примусове кодування зображень без втрат (вища якість, більша пропускна здатність)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Шпалери",
|
||||||
|
"description": "Показувати шпалери робочого столу (вимикання покращує продуктивність)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Згладжування шрифтів",
|
||||||
|
"description": "Увімкнути рендеринг шрифтів ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Композиція робочого столу",
|
||||||
|
"description": "Увімкнути ефекти Aero Glass"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Вимкнути аудіо",
|
||||||
|
"description": "Вимкнути весь звук під час віддаленого сеансу"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Увімкнути друк",
|
||||||
|
"description": "Перенаправити локальні принтери на віддалений сеанс"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Увімкнути перенаправлення диска",
|
||||||
|
"description": "Призначення локальної папки як диска у віддаленому сеансі"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Вимкнути копіювання",
|
||||||
|
"description": "Запобігти копіюванню тексту з віддаленого сеансу"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Вимкнути вставку",
|
||||||
|
"description": "Запобігти вставці тексту у віддалений сеанс"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL-адреса GUACD"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "Tên phông chữ",
|
"fontName": "Tên phông chữ",
|
||||||
"fontSize": "Cỡ chữ",
|
"fontSize": "Cỡ chữ",
|
||||||
"colorScheme": "Bảng màu",
|
"colorScheme": "Bảng màu",
|
||||||
"backspaceKey": "Phím Backspace"
|
"backspaceKey": "Phím Backspace",
|
||||||
}
|
"colorDepth": "Độ sâu màu",
|
||||||
|
"resizeMethod": "Phương thức thay đổi kích thước"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "Máy tính từ xa",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "Mạng ảo",
|
||||||
|
"unencryptedShell": "Shell không mã hóa"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "Máy tính từ xa",
|
"feature_desktop": "Máy tính từ xa",
|
||||||
"feature_desktop_desc": "Mở các phiên RDP, VNC và Telnet trong cùng một tab, tương tự như SSH."
|
"feature_desktop_desc": "Mở các phiên RDP, VNC và Telnet trong cùng một tab, tương tự như SSH."
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "Độ sâu màu"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "Phương thức thay đổi kích thước"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "Cập nhật hiển thị",
|
||||||
|
"resizeReconnect": "Kết nối lại",
|
||||||
|
"inherit": "Kế thừa",
|
||||||
|
"on": "TRÊN",
|
||||||
|
"off": "Tắt",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "Ép buộc Lossless",
|
||||||
|
"description": "Ép buộc mã hóa ảnh không mất dữ liệu (chất lượng cao hơn, tốn băng thông)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "Hình nền",
|
||||||
|
"description": "Hiển thị hình nền desktop (tắt để cải thiện hiệu suất)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "Làm mịn phông chữ",
|
||||||
|
"description": "Bật kết xuất phông chữ ClearType"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "Tổ hợp màn hình nền",
|
||||||
|
"description": "Bật hiệu ứng kính Aero"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "Tắt âm thanh",
|
||||||
|
"description": "Tắt tất cả âm thanh từ phiên từ xa"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "Bật in",
|
||||||
|
"description": "Chuyển hướng máy in cục bộ đến phiên từ xa"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "Bật chuyển hướng ổ đĩa",
|
||||||
|
"description": "Ánh xạ thư mục cục bộ thành ổ đĩa trong phiên từ xa"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "Tắt sao chép",
|
||||||
|
"description": "Ngăn sao chép văn bản từ phiên từ xa"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "Tắt dán",
|
||||||
|
"description": "Ngăn dán văn bản vào phiên từ xa"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "URL guacd"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "字体名称",
|
"fontName": "字体名称",
|
||||||
"fontSize": "字体大小",
|
"fontSize": "字体大小",
|
||||||
"colorScheme": "配色方案",
|
"colorScheme": "配色方案",
|
||||||
"backspaceKey": "退格键"
|
"backspaceKey": "退格键",
|
||||||
}
|
"colorDepth": "颜色深度",
|
||||||
|
"resizeMethod": "调整大小方法"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "远程桌面",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "虚拟网络",
|
||||||
|
"unencryptedShell": "未加密 Shell"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "远程桌面",
|
"feature_desktop": "远程桌面",
|
||||||
"feature_desktop_desc": "像 SSH 一样,在标签页中直接打开 RDP、VNC 和 Telnet 会话。"
|
"feature_desktop_desc": "像 SSH 一样,在标签页中直接打开 RDP、VNC 和 Telnet 会话。"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "颜色深度"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "调整大小方法"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "显示更新",
|
||||||
|
"resizeReconnect": "重新连接",
|
||||||
|
"inherit": "继承",
|
||||||
|
"on": "开启",
|
||||||
|
"off": "关闭",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "强制无损",
|
||||||
|
"description": "强制无损图像编码(质量更高,占用更多带宽)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "壁纸",
|
||||||
|
"description": "显示桌面壁纸(禁用可提高性能)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "字体平滑",
|
||||||
|
"description": "启用 ClearType 字体渲染"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "桌面合成",
|
||||||
|
"description": "启用 Aero 玻璃效果"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "禁用音频",
|
||||||
|
"description": "将远程会话的所有音频静音"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "启用打印",
|
||||||
|
"description": "将本地打印机重定向到远程会话"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "启用驱动器重定向",
|
||||||
|
"description": "将本地文件夹映射为远程会话中的驱动器"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "禁用复制",
|
||||||
|
"description": "禁止从远程会话复制文本"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "禁用粘贴",
|
||||||
|
"description": "禁止粘贴文本到远程会话"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +204,75 @@
|
|||||||
"fontName": "字型名稱",
|
"fontName": "字型名稱",
|
||||||
"fontSize": "字型大小",
|
"fontSize": "字型大小",
|
||||||
"colorScheme": "色彩配置",
|
"colorScheme": "色彩配置",
|
||||||
"backspaceKey": "退格鍵"
|
"backspaceKey": "退格鍵",
|
||||||
}
|
"colorDepth": "色彩深度",
|
||||||
|
"resizeMethod": "調整尺寸方法"
|
||||||
|
},
|
||||||
|
"remoteDesktop": "遠端桌面",
|
||||||
|
"tabRdp": "RDP",
|
||||||
|
"tabVnc": "VNC",
|
||||||
|
"tabTelnet": "Telnet",
|
||||||
|
"virtualNetwork": "虛擬網路",
|
||||||
|
"unencryptedShell": "未加密殼層"
|
||||||
},
|
},
|
||||||
"onboarding": {
|
"onboarding": {
|
||||||
"feature_desktop": "遠端桌面",
|
"feature_desktop": "遠端桌面",
|
||||||
"feature_desktop_desc": "像 SSH 一樣,在同一個標籤頁中開啟 RDP、VNC 和 Telnet 會話。"
|
"feature_desktop_desc": "像 SSH 一樣,在同一個標籤頁中開啟 RDP、VNC 和 Telnet 會話。"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"user": {
|
||||||
|
"colorDepth": {
|
||||||
|
"label": "色彩深度"
|
||||||
|
},
|
||||||
|
"resizeMethod": {
|
||||||
|
"label": "調整尺寸方法"
|
||||||
|
},
|
||||||
|
"resizeDisplayUpdate": "顯示更新",
|
||||||
|
"resizeReconnect": "重新連接",
|
||||||
|
"inherit": "繼承",
|
||||||
|
"on": "在",
|
||||||
|
"off": "離開",
|
||||||
|
"forceLossless": {
|
||||||
|
"label": "強制無損",
|
||||||
|
"description": "強制使用無損影像編碼(品質較高,頻寬需求較大)"
|
||||||
|
},
|
||||||
|
"enableWallpaper": {
|
||||||
|
"label": "桌面背景",
|
||||||
|
"description": "顯示桌面背景(停用可提升效能)"
|
||||||
|
},
|
||||||
|
"enableFontSmoothing": {
|
||||||
|
"label": "字型平滑處理",
|
||||||
|
"description": "啟用 ClearType 字型轉譯"
|
||||||
|
},
|
||||||
|
"enableDesktopComposition": {
|
||||||
|
"label": "桌面合成",
|
||||||
|
"description": "啟用 Aero 玻璃效果"
|
||||||
|
},
|
||||||
|
"disableAudio": {
|
||||||
|
"label": "停用音訊",
|
||||||
|
"description": "將遠端工作階段的音訊全部靜音"
|
||||||
|
},
|
||||||
|
"enablePrinting": {
|
||||||
|
"label": "啟用列印功能",
|
||||||
|
"description": "將本機印表機重新導向至遠端工作階段"
|
||||||
|
},
|
||||||
|
"enableDrive": {
|
||||||
|
"label": "啟用磁碟機重新導向",
|
||||||
|
"description": "在遠端工作階段中將本機資料夾對應為磁碟機"
|
||||||
|
},
|
||||||
|
"disableCopy": {
|
||||||
|
"label": "停用複製",
|
||||||
|
"description": "防止從遠端工作階段複製文字"
|
||||||
|
},
|
||||||
|
"disablePaste": {
|
||||||
|
"label": "停用貼上",
|
||||||
|
"description": "防止將文字貼上至遠端工作階段"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"guacdUrl": {
|
||||||
|
"label": "guacd URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,15 +16,33 @@
|
|||||||
},
|
},
|
||||||
"capabilities": [
|
"capabilities": [
|
||||||
"hosts:read",
|
"hosts:read",
|
||||||
|
"credentials:read",
|
||||||
"credentials:use",
|
"credentials:use",
|
||||||
"ssh:connect",
|
"ssh:connect",
|
||||||
"network:serve",
|
"network:serve",
|
||||||
"audit:read",
|
"desktop:window",
|
||||||
"ui:surface"
|
"ui:surface"
|
||||||
],
|
],
|
||||||
|
"optionalDependencies": {
|
||||||
|
"session-recording": "^1.0.0",
|
||||||
|
"tunnels": "^1.0.0"
|
||||||
|
},
|
||||||
|
"provides": [
|
||||||
|
{
|
||||||
|
"service": "sessions.live",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"permission": "remote-desktop.sessions",
|
||||||
|
"names": ["rdp", "vnc", "telnet"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"requires": [
|
"requires": [
|
||||||
{
|
{
|
||||||
"service": "recordings.writer",
|
"service": "recordings.writer",
|
||||||
|
"versionRange": "^1.1.0",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "tunnels.access",
|
||||||
"versionRange": "^1.0.0",
|
"versionRange": "^1.0.0",
|
||||||
"optional": true
|
"optional": true
|
||||||
}
|
}
|
||||||
@@ -50,30 +68,369 @@
|
|||||||
"openFrom": ["host-context-menu", "palette"]
|
"openFrom": ["host-context-menu", "palette"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hostCapability": [
|
|
||||||
{
|
|
||||||
"key": "enableRdp",
|
|
||||||
"labelKey": "hosts.tabRdp",
|
|
||||||
"editorTab": "rdp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "enableVnc",
|
|
||||||
"labelKey": "hosts.tabVnc",
|
|
||||||
"editorTab": "vnc"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "enableTelnet",
|
|
||||||
"labelKey": "hosts.tabTelnet",
|
|
||||||
"editorTab": "telnet"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"actionSlots": [
|
"actionSlots": [
|
||||||
{
|
{
|
||||||
"id": "remote-desktop.toolbar",
|
"id": "remote-desktop.toolbar",
|
||||||
"accepts": ["button"]
|
"accepts": ["button"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"guest": true
|
"guest": true,
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"name": "sessions",
|
||||||
|
"titleKey": "permissions.sessions.title",
|
||||||
|
"descriptionKey": "permissions.sessions.description",
|
||||||
|
"defaultRoles": ["admin", "user"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"admin": [
|
||||||
|
{
|
||||||
|
"key": "enabled",
|
||||||
|
"type": "boolean",
|
||||||
|
"labelKey": "settings.admin.enabled.label",
|
||||||
|
"descriptionKey": "settings.admin.enabled.description",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "guacdUrl",
|
||||||
|
"type": "string",
|
||||||
|
"labelKey": "settings.admin.guacdUrl.label",
|
||||||
|
"descriptionKey": "settings.admin.guacdUrl.description",
|
||||||
|
"placeholderKey": "settings.admin.guacdUrl.placeholder",
|
||||||
|
"requires": "enabled",
|
||||||
|
"default": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"user": [
|
||||||
|
{
|
||||||
|
"key": "colorDepth",
|
||||||
|
"type": "select",
|
||||||
|
"labelKey": "settings.user.colorDepth.label",
|
||||||
|
"descriptionKey": "settings.user.colorDepth.description",
|
||||||
|
"group": "settings.user.group",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "inherit",
|
||||||
|
"labelKey": "settings.user.inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "16",
|
||||||
|
"labelKey": "settings.user.colorDepth16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "24",
|
||||||
|
"labelKey": "settings.user.colorDepth24"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "32",
|
||||||
|
"labelKey": "settings.user.colorDepth32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": "inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "resizeMethod",
|
||||||
|
"type": "select",
|
||||||
|
"labelKey": "settings.user.resizeMethod.label",
|
||||||
|
"descriptionKey": "settings.user.resizeMethod.description",
|
||||||
|
"group": "settings.user.group",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "inherit",
|
||||||
|
"labelKey": "settings.user.inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "display-update",
|
||||||
|
"labelKey": "settings.user.resizeDisplayUpdate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "reconnect",
|
||||||
|
"labelKey": "settings.user.resizeReconnect"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": "inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "forceLossless",
|
||||||
|
"type": "select",
|
||||||
|
"labelKey": "settings.user.forceLossless.label",
|
||||||
|
"descriptionKey": "settings.user.forceLossless.description",
|
||||||
|
"group": "settings.user.group",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "inherit",
|
||||||
|
"labelKey": "settings.user.inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "on",
|
||||||
|
"labelKey": "settings.user.on"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "off",
|
||||||
|
"labelKey": "settings.user.off"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": "inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "enableWallpaper",
|
||||||
|
"type": "select",
|
||||||
|
"labelKey": "settings.user.enableWallpaper.label",
|
||||||
|
"descriptionKey": "settings.user.enableWallpaper.description",
|
||||||
|
"group": "settings.user.group",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "inherit",
|
||||||
|
"labelKey": "settings.user.inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "on",
|
||||||
|
"labelKey": "settings.user.on"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "off",
|
||||||
|
"labelKey": "settings.user.off"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": "inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "enableFontSmoothing",
|
||||||
|
"type": "select",
|
||||||
|
"labelKey": "settings.user.enableFontSmoothing.label",
|
||||||
|
"descriptionKey": "settings.user.enableFontSmoothing.description",
|
||||||
|
"group": "settings.user.group",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "inherit",
|
||||||
|
"labelKey": "settings.user.inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "on",
|
||||||
|
"labelKey": "settings.user.on"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "off",
|
||||||
|
"labelKey": "settings.user.off"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": "inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "enableDesktopComposition",
|
||||||
|
"type": "select",
|
||||||
|
"labelKey": "settings.user.enableDesktopComposition.label",
|
||||||
|
"descriptionKey": "settings.user.enableDesktopComposition.description",
|
||||||
|
"group": "settings.user.group",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "inherit",
|
||||||
|
"labelKey": "settings.user.inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "on",
|
||||||
|
"labelKey": "settings.user.on"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "off",
|
||||||
|
"labelKey": "settings.user.off"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": "inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "disableAudio",
|
||||||
|
"type": "select",
|
||||||
|
"labelKey": "settings.user.disableAudio.label",
|
||||||
|
"descriptionKey": "settings.user.disableAudio.description",
|
||||||
|
"group": "settings.user.group",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "inherit",
|
||||||
|
"labelKey": "settings.user.inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "on",
|
||||||
|
"labelKey": "settings.user.on"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "off",
|
||||||
|
"labelKey": "settings.user.off"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": "inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "enablePrinting",
|
||||||
|
"type": "select",
|
||||||
|
"labelKey": "settings.user.enablePrinting.label",
|
||||||
|
"descriptionKey": "settings.user.enablePrinting.description",
|
||||||
|
"group": "settings.user.group",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "inherit",
|
||||||
|
"labelKey": "settings.user.inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "on",
|
||||||
|
"labelKey": "settings.user.on"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "off",
|
||||||
|
"labelKey": "settings.user.off"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": "inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "enableDrive",
|
||||||
|
"type": "select",
|
||||||
|
"labelKey": "settings.user.enableDrive.label",
|
||||||
|
"descriptionKey": "settings.user.enableDrive.description",
|
||||||
|
"group": "settings.user.group",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "inherit",
|
||||||
|
"labelKey": "settings.user.inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "on",
|
||||||
|
"labelKey": "settings.user.on"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "off",
|
||||||
|
"labelKey": "settings.user.off"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": "inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "disableCopy",
|
||||||
|
"type": "select",
|
||||||
|
"labelKey": "settings.user.disableCopy.label",
|
||||||
|
"descriptionKey": "settings.user.disableCopy.description",
|
||||||
|
"group": "settings.user.group",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "inherit",
|
||||||
|
"labelKey": "settings.user.inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "on",
|
||||||
|
"labelKey": "settings.user.on"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "off",
|
||||||
|
"labelKey": "settings.user.off"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": "inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "disablePaste",
|
||||||
|
"type": "select",
|
||||||
|
"labelKey": "settings.user.disablePaste.label",
|
||||||
|
"descriptionKey": "settings.user.disablePaste.description",
|
||||||
|
"group": "settings.user.group",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "inherit",
|
||||||
|
"labelKey": "settings.user.inherit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "on",
|
||||||
|
"labelKey": "settings.user.on"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "off",
|
||||||
|
"labelKey": "settings.user.off"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": "inherit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"host": {
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"key": "enableRdp",
|
||||||
|
"type": "boolean",
|
||||||
|
"labelKey": "settings.host.enableRdp.label",
|
||||||
|
"default": false,
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "enableVnc",
|
||||||
|
"type": "boolean",
|
||||||
|
"labelKey": "settings.host.enableVnc.label",
|
||||||
|
"default": false,
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "enableTelnet",
|
||||||
|
"type": "boolean",
|
||||||
|
"labelKey": "settings.host.enableTelnet.label",
|
||||||
|
"default": false,
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "rdpPort",
|
||||||
|
"type": "number",
|
||||||
|
"labelKey": "settings.host.rdpPort.label",
|
||||||
|
"min": 1,
|
||||||
|
"max": 65535,
|
||||||
|
"default": 3389,
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "vncPort",
|
||||||
|
"type": "number",
|
||||||
|
"labelKey": "settings.host.vncPort.label",
|
||||||
|
"min": 1,
|
||||||
|
"max": 65535,
|
||||||
|
"default": 5900,
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "telnetPort",
|
||||||
|
"type": "number",
|
||||||
|
"labelKey": "settings.host.telnetPort.label",
|
||||||
|
"min": 1,
|
||||||
|
"max": 65535,
|
||||||
|
"default": 23,
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "rdpSecurity",
|
||||||
|
"type": "string",
|
||||||
|
"labelKey": "settings.host.rdpSecurity.label",
|
||||||
|
"default": "",
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "rdpIgnoreCert",
|
||||||
|
"type": "boolean",
|
||||||
|
"labelKey": "settings.host.rdpIgnoreCert.label",
|
||||||
|
"default": false,
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "guacamoleConfig",
|
||||||
|
"type": "json",
|
||||||
|
"labelKey": "settings.host.guacamoleConfig.label",
|
||||||
|
"default": {},
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "enableToolbar",
|
||||||
|
"type": "boolean",
|
||||||
|
"labelKey": "settings.host.enableToolbar.label",
|
||||||
|
"descriptionKey": "settings.host.enableToolbar.description",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"locales": "locales",
|
"locales": "locales",
|
||||||
"platforms": ["linux", "win32", "darwin"]
|
"platforms": ["linux", "win32", "darwin"]
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import GuacamoleLite from "guacamole-lite";
|
import GuacamoleLite from "guacamole-lite";
|
||||||
import { guacLogger } from "../../../../src/backend/utils/logger.js";
|
|
||||||
import {
|
|
||||||
GuacamoleTokenService,
|
|
||||||
type GuacamoleRecordingMetadata,
|
|
||||||
} from "./token-service.js";
|
|
||||||
import { getCurrentSettingValue } from "../../../../src/backend/database/repositories/factory.js";
|
|
||||||
import { resolveGuacdOptions } from "../../../../src/backend/utils/guacd-config.js";
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import type { IncomingMessage } from "http";
|
||||||
const tokenService = GuacamoleTokenService.getInstance();
|
import type { Duplex } from "stream";
|
||||||
|
import type {
|
||||||
|
GuacamoleRecordingMetadata,
|
||||||
|
GuacamoleTokenService,
|
||||||
|
TermixGuacMeta,
|
||||||
|
} from "./token-service.js";
|
||||||
|
import type { GuacdOptions } from "./guacd-config.js";
|
||||||
|
import type { RemoteSessions } from "./sessions.js";
|
||||||
|
import { errorMessage, type RemoteDesktopLogger } from "./log.js";
|
||||||
|
|
||||||
/** The recordings.writer service, when the session-recording plugin provides it. */
|
/** The recordings.writer service, when the session-recording plugin provides it. */
|
||||||
export interface RecordingsWriter {
|
export interface RecordingsWriter {
|
||||||
|
enabledFor: (hostId: number) => Promise<boolean>;
|
||||||
createFinished: (input: {
|
createFinished: (input: {
|
||||||
hostId: number;
|
hostId: number;
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -25,147 +27,38 @@ export interface RecordingsWriter {
|
|||||||
}) => Promise<{ id: number }>;
|
}) => Promise<{ id: number }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pluginRecordings: RecordingsWriter | null = null;
|
|
||||||
|
|
||||||
export function setPluginRecordings(recordings: RecordingsWriter | null): void {
|
|
||||||
pluginRecordings = recordings;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readGuacdOptions(): { host: string; port: number } {
|
|
||||||
let dbUrl: string | undefined;
|
|
||||||
try {
|
|
||||||
dbUrl = getCurrentSettingValue("guac_url") ?? undefined;
|
|
||||||
} catch {
|
|
||||||
// DB not available yet, use env var defaults
|
|
||||||
}
|
|
||||||
return resolveGuacdOptions(dbUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
const DATA_DIR = process.env.DATA_DIR || "./db/data";
|
const DATA_DIR = process.env.DATA_DIR || "./db/data";
|
||||||
const GUACAMOLE_RECORDINGS_DIR =
|
|
||||||
process.env.GUACD_RECORDING_BACKEND_PATH ||
|
|
||||||
path.join(DATA_DIR, "session_recordings", "guacamole");
|
|
||||||
|
|
||||||
type GuacamoleClientConnection = {
|
/** Where the backend reads guacd's recordings from. */
|
||||||
|
export function recordingsDir(env: NodeJS.ProcessEnv = process.env): string {
|
||||||
|
return (
|
||||||
|
env.GUACD_RECORDING_BACKEND_PATH ||
|
||||||
|
path.join(DATA_DIR, "session_recordings", "guacamole")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientConnection = {
|
||||||
guacamoleConnectionId?: string;
|
guacamoleConnectionId?: string;
|
||||||
connectionSettings?: {
|
connectionSettings?: {
|
||||||
connection?: { type?: string; join?: string; readOnly?: boolean };
|
connection?: { type?: string; join?: string; readOnly?: boolean };
|
||||||
recording?: GuacamoleRecordingMetadata;
|
recording?: GuacamoleRecordingMetadata;
|
||||||
termixMeta?: {
|
termixMeta?: TermixGuacMeta;
|
||||||
termixConnectId: string;
|
|
||||||
hostId: number;
|
|
||||||
ownerUserId: string;
|
|
||||||
protocol: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
close?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface GuacSessionInfo {
|
const clientOptions = (
|
||||||
guacamoleConnectionId: string;
|
tokens: GuacamoleTokenService,
|
||||||
hostId: number;
|
log: RemoteDesktopLogger,
|
||||||
ownerUserId: string;
|
) => ({
|
||||||
protocol: string;
|
|
||||||
openedAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keyed by termixConnectId (routes.ts's correlation id), populated once the
|
|
||||||
// primary connection's guacd handshake completes.
|
|
||||||
const guacSessionByConnectId = new Map<string, GuacSessionInfo>();
|
|
||||||
// Keyed by guacd's own guacamoleConnectionId, for join-time lookups.
|
|
||||||
const guacSessionByGuacamoleId = new Map<string, GuacSessionInfo>();
|
|
||||||
export function getGuacSessionByConnectId(
|
|
||||||
connectId: string,
|
|
||||||
userId: string,
|
|
||||||
): GuacSessionInfo | null {
|
|
||||||
const info = guacSessionByConnectId.get(connectId);
|
|
||||||
return info?.ownerUserId === userId ? info : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getGuacSessionInfo(
|
|
||||||
guacamoleConnectionId: string,
|
|
||||||
): GuacSessionInfo | null {
|
|
||||||
return guacSessionByGuacamoleId.get(guacamoleConnectionId) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function persistGuacamoleRecording(
|
|
||||||
clientConnection: GuacamoleClientConnection,
|
|
||||||
): Promise<void> {
|
|
||||||
const recording = clientConnection.connectionSettings?.recording;
|
|
||||||
if (!recording) return;
|
|
||||||
|
|
||||||
const resolvedPath = path.resolve(GUACAMOLE_RECORDINGS_DIR, recording.path);
|
|
||||||
const allowedBase = `${path.resolve(GUACAMOLE_RECORDINGS_DIR)}${path.sep}`;
|
|
||||||
if (!resolvedPath.startsWith(allowedBase)) return;
|
|
||||||
|
|
||||||
// guacd may flush/rename the recording just after the websocket closes.
|
|
||||||
for (
|
|
||||||
let attempt = 0;
|
|
||||||
attempt < 10 && !fs.existsSync(resolvedPath);
|
|
||||||
attempt++
|
|
||||||
) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
if (!fs.existsSync(resolvedPath)) {
|
|
||||||
const guacdPath = recording.guacdPath ?? GUACAMOLE_RECORDINGS_DIR;
|
|
||||||
guacLogger.warn("Guacamole recording file was not found", {
|
|
||||||
operation: "guac_recording_missing",
|
|
||||||
hostId: recording.hostId,
|
|
||||||
path: resolvedPath,
|
|
||||||
guacdPath,
|
|
||||||
hint:
|
|
||||||
"guacd writes the recording to guacdPath, the backend reads it from path. " +
|
|
||||||
"When guacd runs in its own container these must be the same volume — set " +
|
|
||||||
"GUACD_RECORDING_PATH to guacd's mount point and GUACD_RECORDING_BACKEND_PATH to this one.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pluginRecordings) {
|
|
||||||
guacLogger.warn(
|
|
||||||
"Session Recording plugin is off; guacamole recording not saved to the log",
|
|
||||||
{ operation: "guac_recording_no_writer", hostId: recording.hostId },
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const endedAt = new Date();
|
|
||||||
const startedAt = new Date(recording.startedAt);
|
|
||||||
await pluginRecordings.createFinished({
|
|
||||||
hostId: recording.hostId,
|
|
||||||
userId: recording.userId,
|
|
||||||
startedAt: startedAt.toISOString(),
|
|
||||||
endedAt: endedAt.toISOString(),
|
|
||||||
duration: Math.max(
|
|
||||||
0,
|
|
||||||
Math.floor((endedAt.getTime() - startedAt.getTime()) / 1000),
|
|
||||||
),
|
|
||||||
recordingPath: resolvedPath,
|
|
||||||
protocol: recording.protocol,
|
|
||||||
format: "guacamole",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// noServer: core owns the listening socket. guacamole-lite passes this object
|
|
||||||
// straight to ws, and exposes the WebSocketServer it builds, so the plugin can
|
|
||||||
// hand it an upgrade from /plugin-ws/remote-desktop/display. There is no
|
|
||||||
// guacamole port any more.
|
|
||||||
const websocketOptions = {
|
|
||||||
noServer: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const clientOptions = {
|
|
||||||
crypt: {
|
crypt: {
|
||||||
cypher: "AES-256-CBC",
|
cypher: "AES-256-CBC",
|
||||||
key: tokenService.getEncryptionKey(),
|
key: tokens.getEncryptionKey(),
|
||||||
},
|
},
|
||||||
log: {
|
log: {
|
||||||
level: "ERRORS",
|
level: "ERRORS",
|
||||||
stdLog: (...args: unknown[]) => {
|
stdLog: (...args: unknown[]) => log.info(args.join(" ")),
|
||||||
guacLogger.info(args.join(" "));
|
errorLog: (...args: unknown[]) => log.error(args.join(" ")),
|
||||||
},
|
|
||||||
errorLog: (...args: unknown[]) => {
|
|
||||||
guacLogger.error(args.join(" "));
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
allowedUnencryptedConnectionSettings: {
|
allowedUnencryptedConnectionSettings: {
|
||||||
rdp: ["width", "height", "dpi"],
|
rdp: ["width", "height", "dpi"],
|
||||||
@@ -202,150 +95,224 @@ const clientOptions = {
|
|||||||
"terminal-type": "xterm-256color",
|
"terminal-type": "xterm-256color",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const _origConsoleLog = console.log;
|
export interface GuacamoleServerDeps {
|
||||||
console.log = (...args: unknown[]) => {
|
log: RemoteDesktopLogger;
|
||||||
const msg = args[0];
|
tokens: GuacamoleTokenService;
|
||||||
if (typeof msg === "string" && msg.startsWith("New client connection"))
|
sessions: RemoteSessions;
|
||||||
return;
|
guacd: () => GuacdOptions;
|
||||||
_origConsoleLog(...args);
|
recordings: () => RecordingsWriter | null;
|
||||||
};
|
/** A socket event has no request behind it, so the row is written as the session's user. */
|
||||||
|
asUser: <T>(userId: string, fn: () => Promise<T>) => Promise<T>;
|
||||||
|
/** Called when a primary session opens; returns what to release on close. */
|
||||||
|
onOpen?: (hostId: number) => () => void;
|
||||||
|
}
|
||||||
|
|
||||||
function createGuacServer(): GuacamoleLite {
|
export interface GuacamoleServer {
|
||||||
const guacdOptions = readGuacdOptions();
|
handleUpgrade: (
|
||||||
const server = new GuacamoleLite(
|
request: IncomingMessage,
|
||||||
websocketOptions,
|
socket: Duplex,
|
||||||
guacdOptions,
|
head: Buffer,
|
||||||
clientOptions,
|
) => void;
|
||||||
);
|
/** Rebuilds the server so a changed guacd address takes effect. */
|
||||||
|
restart: () => void;
|
||||||
|
/** Closes a session and everyone watching it. */
|
||||||
|
endSession: (guacamoleConnectionId: string) => boolean;
|
||||||
|
close: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
server.on("open", (clientConnection: GuacamoleClientConnection) => {
|
/**
|
||||||
guacLogger.info("Guacamole connection opened", {
|
* guacamole-lite in noServer mode: core owns the listening socket and hands
|
||||||
operation: "guac_connection_open",
|
* upgrades from /plugin-ws/remote-desktop/display to its WebSocketServer.
|
||||||
type: clientConnection.connectionSettings?.connection?.type,
|
*/
|
||||||
});
|
export function createGuacamoleServer(
|
||||||
|
deps: GuacamoleServerDeps,
|
||||||
|
): GuacamoleServer {
|
||||||
|
const { log, sessions } = deps;
|
||||||
|
let server: GuacamoleLite | null = null;
|
||||||
|
|
||||||
const termixMeta = clientConnection.connectionSettings?.termixMeta;
|
const persistRecording = async (connection: ClientConnection) => {
|
||||||
const guacamoleConnectionId = clientConnection.guacamoleConnectionId;
|
const recording = connection.connectionSettings?.recording;
|
||||||
const isJoin = !!clientConnection.connectionSettings?.connection?.join;
|
if (!recording) return;
|
||||||
|
|
||||||
if (!isJoin && termixMeta && guacamoleConnectionId) {
|
const dir = recordingsDir();
|
||||||
const info: GuacSessionInfo = {
|
const resolvedPath = path.resolve(dir, recording.path);
|
||||||
guacamoleConnectionId,
|
if (!resolvedPath.startsWith(`${path.resolve(dir)}${path.sep}`)) return;
|
||||||
hostId: termixMeta.hostId,
|
|
||||||
ownerUserId: termixMeta.ownerUserId,
|
// guacd may flush or rename the recording just after the socket closes.
|
||||||
protocol: termixMeta.protocol,
|
for (
|
||||||
openedAt: Date.now(),
|
let attempt = 0;
|
||||||
};
|
attempt < 10 && !fs.existsSync(resolvedPath);
|
||||||
guacSessionByConnectId.set(termixMeta.termixConnectId, info);
|
attempt++
|
||||||
guacSessionByGuacamoleId.set(guacamoleConnectionId, info);
|
) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
}
|
}
|
||||||
});
|
if (!fs.existsSync(resolvedPath)) {
|
||||||
|
log.warn("Guacamole recording file was not found", {
|
||||||
server.on("close", (clientConnection: GuacamoleClientConnection) => {
|
operation: "guac_recording_missing",
|
||||||
guacLogger.info("Guacamole connection closed", {
|
hostId: recording.hostId,
|
||||||
operation: "guac_connection_close",
|
path: resolvedPath,
|
||||||
type: clientConnection.connectionSettings?.connection?.type,
|
guacdPath: recording.guacdPath ?? dir,
|
||||||
});
|
hint:
|
||||||
|
"guacd writes the recording to guacdPath, the backend reads it from path. " +
|
||||||
const isJoin = !!clientConnection.connectionSettings?.connection?.join;
|
"When guacd runs in its own container these must be the same volume: set " +
|
||||||
const termixMeta = clientConnection.connectionSettings?.termixMeta;
|
"GUACD_RECORDING_PATH to guacd's mount point and GUACD_RECORDING_BACKEND_PATH to this one.",
|
||||||
const guacamoleConnectionId = clientConnection.guacamoleConnectionId;
|
});
|
||||||
if (!isJoin && termixMeta && guacamoleConnectionId) {
|
return;
|
||||||
guacSessionByConnectId.delete(termixMeta.termixConnectId);
|
|
||||||
guacSessionByGuacamoleId.delete(guacamoleConnectionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
persistGuacamoleRecording(clientConnection).catch((error) => {
|
const writer = await deps.asUser(recording.userId, async () =>
|
||||||
guacLogger.error("Failed to persist Guacamole recording", error, {
|
deps.recordings(),
|
||||||
operation: "guac_recording_persist_error",
|
);
|
||||||
|
if (!writer) {
|
||||||
|
log.warn(
|
||||||
|
"Session Recording is off; the recording was kept on disk only",
|
||||||
|
{
|
||||||
|
operation: "guac_recording_no_writer",
|
||||||
|
hostId: recording.hostId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endedAt = new Date();
|
||||||
|
const startedAt = new Date(recording.startedAt);
|
||||||
|
await deps.asUser(recording.userId, () =>
|
||||||
|
writer.createFinished({
|
||||||
|
hostId: recording.hostId,
|
||||||
|
userId: recording.userId,
|
||||||
|
startedAt: startedAt.toISOString(),
|
||||||
|
endedAt: endedAt.toISOString(),
|
||||||
|
duration: Math.max(
|
||||||
|
0,
|
||||||
|
Math.floor((endedAt.getTime() - startedAt.getTime()) / 1000),
|
||||||
|
),
|
||||||
|
recordingPath: resolvedPath,
|
||||||
|
protocol: recording.protocol,
|
||||||
|
format: "guacamole",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const build = (): GuacamoleLite => {
|
||||||
|
// guacamole-lite fills in port 8080 unless it sees a server key, and ws
|
||||||
|
// refuses a port next to noServer, so the port is cleared explicitly.
|
||||||
|
const guacamole = new GuacamoleLite(
|
||||||
|
{ noServer: true, port: undefined },
|
||||||
|
deps.guacd(),
|
||||||
|
clientOptions(deps.tokens, log),
|
||||||
|
);
|
||||||
|
// It also installs its own SIGTERM and SIGINT handlers. Core decides
|
||||||
|
// when the process stops and closes this through deactivate instead.
|
||||||
|
const handlers = guacamole as unknown as {
|
||||||
|
sigTermHandler?: () => void;
|
||||||
|
sigIntHandler?: () => void;
|
||||||
|
};
|
||||||
|
if (handlers.sigTermHandler) {
|
||||||
|
process.off("SIGTERM", handlers.sigTermHandler);
|
||||||
|
}
|
||||||
|
if (handlers.sigIntHandler) {
|
||||||
|
process.off("SIGINT", handlers.sigIntHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
guacamole.on("open", (connection: ClientConnection) => {
|
||||||
|
const meta = connection.connectionSettings?.termixMeta;
|
||||||
|
const guacamoleConnectionId = connection.guacamoleConnectionId;
|
||||||
|
const isJoin = !!connection.connectionSettings?.connection?.join;
|
||||||
|
if (isJoin || !meta || !guacamoleConnectionId) return;
|
||||||
|
const release = deps.onOpen?.(meta.hostId);
|
||||||
|
sessions.opened(meta, guacamoleConnectionId, release ? [release] : []);
|
||||||
|
});
|
||||||
|
|
||||||
|
guacamole.on("close", (connection: ClientConnection) => {
|
||||||
|
const meta = connection.connectionSettings?.termixMeta;
|
||||||
|
const isJoin = !!connection.connectionSettings?.connection?.join;
|
||||||
|
if (!isJoin && meta) sessions.closed(meta.termixConnectId);
|
||||||
|
persistRecording(connection).catch((error) => {
|
||||||
|
log.error("Failed to persist Guacamole recording", {
|
||||||
|
operation: "guac_recording_persist_error",
|
||||||
|
error: errorMessage(error),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
server.on(
|
guacamole.on("error", (connection: ClientConnection, error: Error) => {
|
||||||
"error",
|
log.error("Guacamole connection error", {
|
||||||
(clientConnection: GuacamoleClientConnection, error: Error) => {
|
|
||||||
guacLogger.error("Guacamole connection error", error, {
|
|
||||||
operation: "guac_connection_error",
|
operation: "guac_connection_error",
|
||||||
type: clientConnection.connectionSettings?.connection?.type,
|
type: connection.connectionSettings?.connection?.type,
|
||||||
|
error: errorMessage(error),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return guacamole;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeServer = () => {
|
||||||
|
if (!server) return;
|
||||||
|
try {
|
||||||
|
server.close();
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Error closing the Guacamole server", {
|
||||||
|
error: errorMessage(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
server = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
server = build();
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleUpgrade(request, socket, head) {
|
||||||
|
const wss = (
|
||||||
|
server as unknown as {
|
||||||
|
webSocketServer?: {
|
||||||
|
handleUpgrade: (
|
||||||
|
request: IncomingMessage,
|
||||||
|
socket: Duplex,
|
||||||
|
head: Buffer,
|
||||||
|
done: (ws: unknown) => void,
|
||||||
|
) => void;
|
||||||
|
emit: (event: string, ...args: unknown[]) => void;
|
||||||
|
};
|
||||||
|
} | null
|
||||||
|
)?.webSocketServer;
|
||||||
|
if (!wss) {
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||||
|
wss.emit("connection", ws, request);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
|
||||||
|
|
||||||
return server;
|
restart() {
|
||||||
|
if (!server) return;
|
||||||
|
closeServer();
|
||||||
|
server = build();
|
||||||
|
},
|
||||||
|
|
||||||
|
endSession(guacamoleConnectionId) {
|
||||||
|
const active = (
|
||||||
|
server as unknown as {
|
||||||
|
activeConnections?: Map<unknown, ClientConnection>;
|
||||||
|
} | null
|
||||||
|
)?.activeConnections;
|
||||||
|
if (!active) return false;
|
||||||
|
let closed = false;
|
||||||
|
for (const connection of [...active.values()]) {
|
||||||
|
const join = connection.connectionSettings?.connection?.join;
|
||||||
|
if (
|
||||||
|
connection.guacamoleConnectionId === guacamoleConnectionId ||
|
||||||
|
join === guacamoleConnectionId
|
||||||
|
) {
|
||||||
|
connection.close?.();
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closed;
|
||||||
|
},
|
||||||
|
|
||||||
|
close: closeServer,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let guacServer: GuacamoleLite | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the guacamole-lite server. Called from this plugin's activate().
|
|
||||||
* Nothing binds a port: handleGuacamoleUpgrade below feeds it connections.
|
|
||||||
*/
|
|
||||||
export async function startGuacamoleService(): Promise<void> {
|
|
||||||
guacServer = createGuacServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hands one upgrade to guacamole-lite's own WebSocketServer.
|
|
||||||
*
|
|
||||||
* guacamole-lite constructs that server internally and has no "attach to this
|
|
||||||
* http server" option, so giving it the upgrade is the only way in. Core has
|
|
||||||
* already authenticated the request by the time this runs.
|
|
||||||
*/
|
|
||||||
export function handleGuacamoleUpgrade(
|
|
||||||
request: import("http").IncomingMessage,
|
|
||||||
socket: import("stream").Duplex,
|
|
||||||
head: Buffer,
|
|
||||||
): void {
|
|
||||||
const server = guacServer as unknown as {
|
|
||||||
webSocketServer?: {
|
|
||||||
handleUpgrade: (
|
|
||||||
request: import("http").IncomingMessage,
|
|
||||||
socket: import("stream").Duplex,
|
|
||||||
head: Buffer,
|
|
||||||
done: (ws: unknown) => void,
|
|
||||||
) => void;
|
|
||||||
emit: (event: string, ...args: unknown[]) => void;
|
|
||||||
};
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
const wss = server?.webSocketServer;
|
|
||||||
if (!wss) {
|
|
||||||
socket.destroy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
||||||
wss.emit("connection", ws, request);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stops the guacamole-lite WebSocket server. Called from this plugin's
|
|
||||||
* deactivate().
|
|
||||||
*/
|
|
||||||
export async function stopGuacamoleService(): Promise<void> {
|
|
||||||
if (!guacServer) return;
|
|
||||||
try {
|
|
||||||
guacServer.close();
|
|
||||||
} catch (err) {
|
|
||||||
guacLogger.error("Error closing guac server during shutdown", err as Error);
|
|
||||||
}
|
|
||||||
guacServer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function restartGuacServer(): Promise<void> {
|
|
||||||
// A no-op while the plugin is disabled: creating a new server here would
|
|
||||||
// silently resurrect the WS server behind deactivate()'s back.
|
|
||||||
if (!guacServer) return;
|
|
||||||
try {
|
|
||||||
guacServer.close();
|
|
||||||
} catch (err) {
|
|
||||||
guacLogger.error("Error closing guac server during restart", err as Error);
|
|
||||||
}
|
|
||||||
guacServer = createGuacServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
export { guacServer, tokenService };
|
|
||||||
File renamed without changes.
@@ -0,0 +1,187 @@
|
|||||||
|
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||||
|
|
||||||
|
export type RemoteProtocol = "rdp" | "vnc" | "telnet";
|
||||||
|
|
||||||
|
export const PLUGIN_ID = "remote-desktop";
|
||||||
|
|
||||||
|
export interface RemoteDesktopHostSettings {
|
||||||
|
enableRdp: boolean;
|
||||||
|
enableVnc: boolean;
|
||||||
|
enableTelnet: boolean;
|
||||||
|
rdpPort: number;
|
||||||
|
vncPort: number;
|
||||||
|
telnetPort: number;
|
||||||
|
rdpSecurity: string;
|
||||||
|
rdpIgnoreCert: boolean;
|
||||||
|
guacamoleConfig: Record<string, unknown>;
|
||||||
|
enableToolbar: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ENABLE_KEY: Record<
|
||||||
|
RemoteProtocol,
|
||||||
|
keyof RemoteDesktopHostSettings
|
||||||
|
> = { rdp: "enableRdp", vnc: "enableVnc", telnet: "enableTelnet" };
|
||||||
|
|
||||||
|
export const PORT_KEY: Record<RemoteProtocol, keyof RemoteDesktopHostSettings> =
|
||||||
|
{ rdp: "rdpPort", vnc: "vncPort", telnet: "telnetPort" };
|
||||||
|
|
||||||
|
export const DEFAULT_PORT: Record<RemoteProtocol, number> = {
|
||||||
|
rdp: 3389,
|
||||||
|
vnc: 5900,
|
||||||
|
telnet: 23,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isRemoteProtocol(value: unknown): value is RemoteProtocol {
|
||||||
|
return value === "rdp" || value === "vnc" || value === "telnet";
|
||||||
|
}
|
||||||
|
|
||||||
|
function asPort(value: unknown): number | undefined {
|
||||||
|
const port = typeof value === "string" ? Number(value) : value;
|
||||||
|
return typeof port === "number" &&
|
||||||
|
Number.isInteger(port) &&
|
||||||
|
port > 0 &&
|
||||||
|
port <= 65535
|
||||||
|
? port
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function asObject(value: unknown): Record<string, unknown> | undefined {
|
||||||
|
let parsed = value;
|
||||||
|
if (typeof value === "string") {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
||||||
|
? (parsed as Record<string, unknown>)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function asBool(value: unknown): boolean | undefined {
|
||||||
|
if (typeof value === "boolean") return value;
|
||||||
|
if (value === 1 || value === "1" || value === "true") return true;
|
||||||
|
if (value === 0 || value === "0" || value === "false") return false;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readHostSettings(
|
||||||
|
ctx: PluginContext,
|
||||||
|
hostId: number,
|
||||||
|
): Promise<RemoteDesktopHostSettings> {
|
||||||
|
const values = await ctx.settings.getAll("host", hostId);
|
||||||
|
return {
|
||||||
|
enableRdp: values.enableRdp === true,
|
||||||
|
enableVnc: values.enableVnc === true,
|
||||||
|
enableTelnet: values.enableTelnet === true,
|
||||||
|
rdpPort: asPort(values.rdpPort) ?? DEFAULT_PORT.rdp,
|
||||||
|
vncPort: asPort(values.vncPort) ?? DEFAULT_PORT.vnc,
|
||||||
|
telnetPort: asPort(values.telnetPort) ?? DEFAULT_PORT.telnet,
|
||||||
|
rdpSecurity:
|
||||||
|
typeof values.rdpSecurity === "string" ? values.rdpSecurity : "",
|
||||||
|
rdpIgnoreCert: values.rdpIgnoreCert === true,
|
||||||
|
guacamoleConfig: asObject(values.guacamoleConfig) ?? {},
|
||||||
|
enableToolbar: values.enableToolbar !== false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns an imported host row into this plugin's host settings.
|
||||||
|
*
|
||||||
|
* A Termix export carries them under pluginSettings["remote-desktop"]. Older
|
||||||
|
* exports, other tools and a plugin creating a host (proxmox) use the flat
|
||||||
|
* fields the host row had before 2.9.0, including the connectionType-only
|
||||||
|
* shape of hosts from before the per-protocol switches existed.
|
||||||
|
*/
|
||||||
|
export function normalizeImportedHost(
|
||||||
|
raw: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> | null {
|
||||||
|
const nested =
|
||||||
|
asObject(asObject(raw.pluginSettings)?.[PLUGIN_ID]) ??
|
||||||
|
({} as Record<string, unknown>);
|
||||||
|
const pick = (key: string, legacy?: string) =>
|
||||||
|
nested[key] !== undefined
|
||||||
|
? nested[key]
|
||||||
|
: raw[key] !== undefined
|
||||||
|
? raw[key]
|
||||||
|
: legacy !== undefined
|
||||||
|
? raw[legacy]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const protocol of ["rdp", "vnc", "telnet"] as const) {
|
||||||
|
const key = ENABLE_KEY[protocol];
|
||||||
|
const flag = asBool(pick(key));
|
||||||
|
if (flag !== undefined) out[key] = flag;
|
||||||
|
const port = asPort(pick(PORT_KEY[protocol]));
|
||||||
|
if (port !== undefined) out[PORT_KEY[protocol]] = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anyFlag = ["enableRdp", "enableVnc", "enableTelnet"].some(
|
||||||
|
(key) => out[key] === true,
|
||||||
|
);
|
||||||
|
if (!anyFlag && isRemoteProtocol(raw.connectionType)) {
|
||||||
|
out[ENABLE_KEY[raw.connectionType]] = true;
|
||||||
|
const port = asPort(raw.port);
|
||||||
|
if (port !== undefined && out[PORT_KEY[raw.connectionType]] === undefined) {
|
||||||
|
out[PORT_KEY[raw.connectionType]] = port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const security = pick("rdpSecurity", "security");
|
||||||
|
if (typeof security === "string" && security) out.rdpSecurity = security;
|
||||||
|
const ignoreCert = asBool(pick("rdpIgnoreCert", "ignoreCert"));
|
||||||
|
if (ignoreCert !== undefined) out.rdpIgnoreCert = ignoreCert;
|
||||||
|
const config = asObject(pick("guacamoleConfig"));
|
||||||
|
if (config && Object.keys(config).length > 0) out.guacamoleConfig = config;
|
||||||
|
const toolbar = asBool(pick("enableToolbar", "enableTerminalToolbar"));
|
||||||
|
if (toolbar !== undefined) out.enableToolbar = toolbar;
|
||||||
|
|
||||||
|
return Object.keys(out).length > 0 ? out : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** User setting key to the guacd parameter it sets. */
|
||||||
|
const USER_DEFAULT_PARAMS: Record<string, string> = {
|
||||||
|
colorDepth: "color-depth",
|
||||||
|
resizeMethod: "resize-method",
|
||||||
|
forceLossless: "force-lossless",
|
||||||
|
enableWallpaper: "enable-wallpaper",
|
||||||
|
enableFontSmoothing: "enable-font-smoothing",
|
||||||
|
enableDesktopComposition: "enable-desktop-composition",
|
||||||
|
disableAudio: "disable-audio",
|
||||||
|
enablePrinting: "enable-printing",
|
||||||
|
enableDrive: "enable-drive",
|
||||||
|
disableCopy: "disable-copy",
|
||||||
|
disablePaste: "disable-paste",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user's RDP defaults as guacd parameters. "inherit" leaves a parameter
|
||||||
|
* to the host and guacd; on and off become booleans.
|
||||||
|
*/
|
||||||
|
export function userDefaultParams(
|
||||||
|
values: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const params: Record<string, unknown> = {};
|
||||||
|
for (const [key, param] of Object.entries(USER_DEFAULT_PARAMS)) {
|
||||||
|
const value = values[key];
|
||||||
|
if (value === undefined || value === null || value === "inherit") continue;
|
||||||
|
if (key === "colorDepth") {
|
||||||
|
const depth = Number(value);
|
||||||
|
if (Number.isInteger(depth) && depth > 0) params[param] = depth;
|
||||||
|
} else if (key === "resizeMethod") {
|
||||||
|
if (typeof value === "string" && value) params[param] = value;
|
||||||
|
} else if (value === "on" || value === "off") {
|
||||||
|
params[param] = value === "on";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readUserDefaults(
|
||||||
|
ctx: PluginContext,
|
||||||
|
userId: string,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
return userDefaultParams(await ctx.settings.getAll("user", userId));
|
||||||
|
}
|
||||||
@@ -1,63 +1,108 @@
|
|||||||
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||||
import { setPluginSsh } from "./ssh.js";
|
import { GuacamoleTokenService } from "./token-service.js";
|
||||||
|
import { RemoteSessions } from "./sessions.js";
|
||||||
import {
|
import {
|
||||||
getGuacSessionInfo,
|
createGuacamoleServer,
|
||||||
handleGuacamoleUpgrade,
|
|
||||||
restartGuacServer,
|
|
||||||
setPluginRecordings,
|
|
||||||
startGuacamoleService,
|
|
||||||
stopGuacamoleService,
|
|
||||||
tokenService,
|
|
||||||
type RecordingsWriter,
|
type RecordingsWriter,
|
||||||
} from "./guacamole-server.js";
|
} from "./guacamole-server.js";
|
||||||
import { startRemoteDesktopService } from "./routes.js";
|
import { resolveGuacdOptions } from "./guacd-config.js";
|
||||||
|
import { registerRoutes } from "./routes.js";
|
||||||
|
import { createLiveSessions } from "./live-sessions.js";
|
||||||
|
import { normalizeImportedHost } from "./host-settings.js";
|
||||||
|
import { createLogger } from "./log.js";
|
||||||
|
|
||||||
export async function activate(ctx: PluginContext) {
|
export async function activate(ctx: PluginContext) {
|
||||||
setPluginSsh(ctx.ssh);
|
const log = createLogger(ctx.log);
|
||||||
ctx.disposables.add(() => setPluginSsh(null));
|
const tokens = new GuacamoleTokenService(log);
|
||||||
|
const sessions = new RemoteSessions();
|
||||||
|
ctx.disposables.add(() => sessions.clear());
|
||||||
|
|
||||||
// Optional: without the session-recording plugin, a guacd recording is
|
// The admin setting, unless GUACD_URL / GUACD_HOST / GUACD_PORT override it.
|
||||||
// still made on disk but never gets a row, so it never shows in the list.
|
let guacdUrl = (await ctx.settings.get<string>("guacdUrl")) ?? "";
|
||||||
const recordings = ctx.services.get<RecordingsWriter>("recordings.writer");
|
const guacd = () => resolveGuacdOptions(guacdUrl || undefined);
|
||||||
setPluginRecordings("createFinished" in recordings ? recordings : null);
|
const enabled = async () =>
|
||||||
ctx.disposables.add(() => setPluginRecordings(null));
|
(await ctx.settings.get<boolean>("enabled")) !== false;
|
||||||
|
|
||||||
await startGuacamoleService();
|
// Optional: without session-recording nothing is recorded.
|
||||||
|
const recordings = (): RecordingsWriter | null => {
|
||||||
|
const service =
|
||||||
|
ctx.services.get<Partial<RecordingsWriter>>("recordings.writer");
|
||||||
|
return typeof service.createFinished === "function" &&
|
||||||
|
typeof service.enabledFor === "function"
|
||||||
|
? (service as RecordingsWriter)
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
startRemoteDesktopService(ctx.http.router());
|
// guacamole-lite prints every new connection to stdout.
|
||||||
|
const consoleLog = console.log;
|
||||||
|
console.log = (...args: unknown[]) => {
|
||||||
|
if (
|
||||||
|
typeof args[0] === "string" &&
|
||||||
|
args[0].startsWith("New client connection")
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
consoleLog(...args);
|
||||||
|
};
|
||||||
|
ctx.disposables.add(() => {
|
||||||
|
console.log = consoleLog;
|
||||||
|
});
|
||||||
|
|
||||||
|
const server = createGuacamoleServer({
|
||||||
|
log,
|
||||||
|
tokens,
|
||||||
|
sessions,
|
||||||
|
guacd,
|
||||||
|
recordings,
|
||||||
|
asUser: (userId, fn) => ctx.asUser(userId, fn),
|
||||||
|
onOpen: (hostId) => ctx.hosts.trackSession(hostId),
|
||||||
|
});
|
||||||
|
ctx.disposables.add(() => server.close());
|
||||||
|
|
||||||
|
ctx.settings.onChange("guacdUrl", (value) => {
|
||||||
|
guacdUrl = typeof value === "string" ? value : "";
|
||||||
|
server.restart();
|
||||||
|
});
|
||||||
|
|
||||||
|
registerRoutes(ctx.http.router(), {
|
||||||
|
ctx,
|
||||||
|
log,
|
||||||
|
tokens,
|
||||||
|
sessions,
|
||||||
|
guacd,
|
||||||
|
enabled,
|
||||||
|
recordings,
|
||||||
|
});
|
||||||
|
|
||||||
// guacamole-lite owns its own WebSocketServer, so it takes the raw upgrade.
|
// guacamole-lite owns its own WebSocketServer, so it takes the raw upgrade.
|
||||||
//
|
// Public because a display authenticates with the encrypted, single-use
|
||||||
// Public because a Guacamole display authenticates with the encrypted,
|
// token minted by an authenticated route, not with a session JWT.
|
||||||
// single-use connection token minted by POST /token, not with a session
|
|
||||||
// JWT. guacamole-lite decrypts and validates that token itself, and the
|
|
||||||
// route that mints one is authenticated normally.
|
|
||||||
ctx.ws.upgrade(
|
ctx.ws.upgrade(
|
||||||
"/display",
|
"/display",
|
||||||
(request, socket, head) =>
|
(request, socket, head) =>
|
||||||
handleGuacamoleUpgrade(
|
server.handleUpgrade(
|
||||||
request as Parameters<typeof handleGuacamoleUpgrade>[0],
|
request as Parameters<typeof server.handleUpgrade>[0],
|
||||||
socket as Parameters<typeof handleGuacamoleUpgrade>[1],
|
socket as Parameters<typeof server.handleUpgrade>[1],
|
||||||
head as Buffer,
|
head as Buffer,
|
||||||
),
|
),
|
||||||
{ public: true },
|
{ public: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
// What collab, session-sharing and the admin guacd setting need from a
|
for (const protocol of ["rdp", "vnc", "telnet"] as const) {
|
||||||
// running Guacamole server. Revoked automatically on deactivate, and
|
ctx.services.provide(
|
||||||
// hosts/guacamole-sessions.ts degrades safely while it is gone.
|
"sessions.live",
|
||||||
ctx.registry.provide("remote-desktop.sessions", {
|
createLiveSessions(protocol, {
|
||||||
restart: restartGuacServer,
|
sessions,
|
||||||
createJoinToken: (guacamoleConnectionId: string, readOnly: boolean) =>
|
tokens,
|
||||||
tokenService.createJoinToken(guacamoleConnectionId, readOnly),
|
endSession: server.endSession,
|
||||||
getSessionInfo: getGuacSessionInfo,
|
}),
|
||||||
});
|
{ name: protocol },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ctx.log.info(
|
ctx.registry.provide(
|
||||||
"Remote Desktop mounted at /plugin-api/remote-desktop and /plugin-ws/remote-desktop/display",
|
"remote-desktop.hostImportNormalizer",
|
||||||
|
normalizeImportedHost,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deactivate() {
|
export async function deactivate() {}
|
||||||
await stopGuacamoleService();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import net from "net";
|
||||||
|
import type { Client } from "ssh2";
|
||||||
|
import type { PluginContext } from "@termix/plugin-sdk/backend";
|
||||||
|
import type { RemoteDesktopLogger } from "./log.js";
|
||||||
|
|
||||||
|
/** The tunnels plugin's service, as ctx.services.get("tunnels.access") returns it. */
|
||||||
|
interface TunnelsAccess {
|
||||||
|
forward(
|
||||||
|
sourceHostId: number,
|
||||||
|
target: { targetHost: string; targetPort: number; bindHost?: string },
|
||||||
|
options?: { name?: string; idleTimeoutMs?: number },
|
||||||
|
): Promise<{
|
||||||
|
bindHost: string;
|
||||||
|
bindPort: number;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The tunnels plugin keeps these names for on-demand forwards. */
|
||||||
|
const TUNNEL_NAME_PREFIX = "web:";
|
||||||
|
/** A forward nothing is using closes itself after this long. */
|
||||||
|
const FORWARD_IDLE_TIMEOUT_MS = 10 * 60 * 1000;
|
||||||
|
|
||||||
|
export interface JumpTunnel {
|
||||||
|
port: number;
|
||||||
|
close: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface JumpTunnelRequest {
|
||||||
|
jumpHosts: Array<{ hostId: number }>;
|
||||||
|
targetHost: string;
|
||||||
|
targetPort: number;
|
||||||
|
bindHost: string;
|
||||||
|
/** Names the forward, so each session gets one of its own. */
|
||||||
|
connectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A local port guacd can dial that reaches the target through the host's
|
||||||
|
* jump hosts.
|
||||||
|
*
|
||||||
|
* One hop is exactly what the tunnels plugin's forward does, so that is used
|
||||||
|
* when the plugin is running and the user may use it. A chain, or no tunnels
|
||||||
|
* plugin, goes through ctx.ssh.jumpChain with a listener of our own. Either
|
||||||
|
* way the caller closes it when the session ends.
|
||||||
|
*/
|
||||||
|
export async function openJumpTunnel(
|
||||||
|
ctx: PluginContext,
|
||||||
|
log: RemoteDesktopLogger,
|
||||||
|
request: JumpTunnelRequest,
|
||||||
|
): Promise<JumpTunnel> {
|
||||||
|
if (request.jumpHosts.length === 1) {
|
||||||
|
const viaTunnels = await forwardThroughTunnels(ctx, log, request);
|
||||||
|
if (viaTunnels) return viaTunnels;
|
||||||
|
}
|
||||||
|
return forwardThroughChain(ctx, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function forwardThroughTunnels(
|
||||||
|
ctx: PluginContext,
|
||||||
|
log: RemoteDesktopLogger,
|
||||||
|
request: JumpTunnelRequest,
|
||||||
|
): Promise<JumpTunnel | null> {
|
||||||
|
const tunnels = ctx.services.get<Partial<TunnelsAccess>>("tunnels.access");
|
||||||
|
if (typeof tunnels.forward !== "function") return null;
|
||||||
|
const hopId = request.jumpHosts[0].hostId;
|
||||||
|
try {
|
||||||
|
const handle = await tunnels.forward(
|
||||||
|
hopId,
|
||||||
|
{
|
||||||
|
targetHost: request.targetHost,
|
||||||
|
targetPort: request.targetPort,
|
||||||
|
bindHost: request.bindHost,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `${TUNNEL_NAME_PREFIX}${hopId}:rd-${request.connectId}`,
|
||||||
|
idleTimeoutMs: FORWARD_IDLE_TIMEOUT_MS,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
port: handle.bindPort,
|
||||||
|
close: () => void handle.close().catch(() => undefined),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// A user without the tunnels permission still reaches their host.
|
||||||
|
log.warn("Tunnels forward failed, using a jump chain instead", {
|
||||||
|
operation: "guac_tunnels_forward",
|
||||||
|
hopId,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function forwardThroughChain(
|
||||||
|
ctx: PluginContext,
|
||||||
|
request: JumpTunnelRequest,
|
||||||
|
): Promise<JumpTunnel> {
|
||||||
|
// The chain dials the first hop through that hop's own SOCKS5 settings;
|
||||||
|
// the target host's proxy config does not apply to it.
|
||||||
|
const { client, dispose } = await ctx.ssh.jumpChain<Client>(
|
||||||
|
request.jumpHosts,
|
||||||
|
);
|
||||||
|
|
||||||
|
const sockets = new Set<net.Socket>();
|
||||||
|
const server = net.createServer((socket) => {
|
||||||
|
sockets.add(socket);
|
||||||
|
socket.once("close", () => sockets.delete(socket));
|
||||||
|
client.forwardOut(
|
||||||
|
"127.0.0.1",
|
||||||
|
0,
|
||||||
|
request.targetHost,
|
||||||
|
request.targetPort,
|
||||||
|
(error, stream) => {
|
||||||
|
if (error) {
|
||||||
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socket.pipe(stream).pipe(socket);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let port: number;
|
||||||
|
try {
|
||||||
|
port = await new Promise<number>((resolve, reject) => {
|
||||||
|
server.once("error", reject);
|
||||||
|
server.listen(0, request.bindHost, () =>
|
||||||
|
resolve((server.address() as net.AddressInfo).port),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await dispose();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
port,
|
||||||
|
close: () => {
|
||||||
|
for (const socket of sockets) socket.destroy();
|
||||||
|
server.close();
|
||||||
|
void dispose();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import type { GuacamoleTokenService } from "./token-service.js";
|
||||||
|
import type { RemoteSessions } from "./sessions.js";
|
||||||
|
import type { RemoteProtocol } from "./host-settings.js";
|
||||||
|
|
||||||
|
/** sessions.live v1, as session-sharing reads it (plugins/session-sharing/src/backend/live.ts). */
|
||||||
|
export interface LiveSessionInfo {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
hostId: number;
|
||||||
|
hostName: string;
|
||||||
|
isConnected: boolean;
|
||||||
|
createdAt: number;
|
||||||
|
tabInstanceId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoteLiveSessions {
|
||||||
|
getSession: (sessionId: string) => LiveSessionInfo | null;
|
||||||
|
ownerEndSession: (sessionId: string, reason: string) => void;
|
||||||
|
createViewerToken: (sessionId: string, readOnly: boolean) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One provider per protocol. A session id is guacd's own connection id,
|
||||||
|
* which is what a viewer's join token names.
|
||||||
|
*/
|
||||||
|
export function createLiveSessions(
|
||||||
|
protocol: RemoteProtocol,
|
||||||
|
deps: {
|
||||||
|
sessions: RemoteSessions;
|
||||||
|
tokens: GuacamoleTokenService;
|
||||||
|
endSession: (guacamoleConnectionId: string) => boolean;
|
||||||
|
},
|
||||||
|
): RemoteLiveSessions {
|
||||||
|
const find = (sessionId: string) => {
|
||||||
|
const session = deps.sessions.byGuacamole(sessionId);
|
||||||
|
return session && session.protocol === protocol ? session : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getSession(sessionId) {
|
||||||
|
const session = find(sessionId);
|
||||||
|
if (!session) return null;
|
||||||
|
return {
|
||||||
|
id: session.guacamoleConnectionId,
|
||||||
|
userId: session.ownerUserId,
|
||||||
|
hostId: session.hostId,
|
||||||
|
hostName: session.hostName,
|
||||||
|
isConnected: true,
|
||||||
|
createdAt: session.openedAt,
|
||||||
|
tabInstanceId: session.tabInstanceId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
ownerEndSession(sessionId) {
|
||||||
|
if (find(sessionId)) deps.endSession(sessionId);
|
||||||
|
},
|
||||||
|
|
||||||
|
createViewerToken(sessionId, readOnly) {
|
||||||
|
if (!find(sessionId)) {
|
||||||
|
throw new Error(`No live ${protocol} session ${sessionId}`);
|
||||||
|
}
|
||||||
|
return deps.tokens.createJoinToken(sessionId, readOnly);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type { PluginLogger } from "@termix/plugin-sdk/backend";
|
||||||
|
|
||||||
|
export type Meta = Record<string, unknown>;
|
||||||
|
|
||||||
|
export interface RemoteDesktopLogger {
|
||||||
|
info: (message: string, meta?: Meta) => void;
|
||||||
|
warn: (message: string, meta?: Meta) => void;
|
||||||
|
error: (message: string, meta?: Meta) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function withMeta(message: string, meta?: Meta): string {
|
||||||
|
return meta && Object.keys(meta).length > 0
|
||||||
|
? `${message} ${JSON.stringify(meta)}`
|
||||||
|
: message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ctx.log with the structured context the old guac logger carried. */
|
||||||
|
export function createLogger(log: PluginLogger): RemoteDesktopLogger {
|
||||||
|
return {
|
||||||
|
info: (message, meta) => log.info(withMeta(message, meta)),
|
||||||
|
warn: (message, meta) => log.warn(withMeta(message, meta)),
|
||||||
|
error: (message, meta) => log.error(withMeta(message, meta)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function errorMessage(error: unknown, fallback = "Unknown error") {
|
||||||
|
if (error instanceof Error && error.message) return error.message;
|
||||||
|
if (typeof error === "string" && error) return error;
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
@@ -34,31 +34,3 @@ export function resolveRdpDomain(
|
|||||||
? promptedDomain
|
? promptedDomain
|
||||||
: storedDomain;
|
: storedDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SharedRdpAuthResolution =
|
|
||||||
| { source: "personal-override" }
|
|
||||||
| { source: "owner-shared"; authType: string }
|
|
||||||
| { source: "secretless" }
|
|
||||||
| { source: "required" }
|
|
||||||
| null;
|
|
||||||
|
|
||||||
export function resolveRdpAuthTypeForConnect({
|
|
||||||
storedAuthType,
|
|
||||||
credentialId,
|
|
||||||
sharedResolution,
|
|
||||||
}: {
|
|
||||||
storedAuthType?: string | null;
|
|
||||||
credentialId?: number | null;
|
|
||||||
sharedResolution?: SharedRdpAuthResolution;
|
|
||||||
}): string {
|
|
||||||
if (sharedResolution === undefined) {
|
|
||||||
return storedAuthType || (credentialId ? "credential" : "direct");
|
|
||||||
}
|
|
||||||
if (sharedResolution?.source === "personal-override") return "credential";
|
|
||||||
if (sharedResolution?.source === "owner-shared") {
|
|
||||||
return sharedResolution.authType;
|
|
||||||
}
|
|
||||||
return sharedResolution?.source === "secretless" && storedAuthType === "none"
|
|
||||||
? "none"
|
|
||||||
: "direct";
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,116 @@
|
|||||||
|
import type { TermixGuacMeta } from "./token-service.js";
|
||||||
|
|
||||||
|
export interface RemoteSession {
|
||||||
|
guacamoleConnectionId: string;
|
||||||
|
termixConnectId: string;
|
||||||
|
hostId: number;
|
||||||
|
hostName: string;
|
||||||
|
ownerUserId: string;
|
||||||
|
protocol: TermixGuacMeta["protocol"];
|
||||||
|
tabInstanceId: string | null;
|
||||||
|
openedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Cleanup = () => void;
|
||||||
|
|
||||||
|
/** How long a minted token's tunnel waits for guacd to connect through it. */
|
||||||
|
export const PENDING_TIMEOUT_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live guacd sessions and what each one holds open.
|
||||||
|
*
|
||||||
|
* A connect request builds its jump tunnel or VNC proxy before guacd dials
|
||||||
|
* in, so those resources are parked under the request's connect id and move
|
||||||
|
* onto the session when it opens. They are released when the session
|
||||||
|
* closes, when guacd never shows up, or when the plugin stops.
|
||||||
|
*/
|
||||||
|
export class RemoteSessions {
|
||||||
|
private readonly pending = new Map<
|
||||||
|
string,
|
||||||
|
{ cleanups: Cleanup[]; timer: ReturnType<typeof setTimeout> }
|
||||||
|
>();
|
||||||
|
private readonly byConnectId = new Map<string, RemoteSession>();
|
||||||
|
private readonly byGuacamoleId = new Map<string, RemoteSession>();
|
||||||
|
private readonly cleanupsByConnectId = new Map<string, Cleanup[]>();
|
||||||
|
|
||||||
|
constructor(private readonly pendingTimeoutMs = PENDING_TIMEOUT_MS) {}
|
||||||
|
|
||||||
|
/** Resources for a connection that guacd has not opened yet. */
|
||||||
|
park(termixConnectId: string, cleanups: Cleanup[]): void {
|
||||||
|
if (cleanups.length === 0) return;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
this.pending.delete(termixConnectId);
|
||||||
|
run(cleanups);
|
||||||
|
}, this.pendingTimeoutMs);
|
||||||
|
timer.unref?.();
|
||||||
|
this.pending.set(termixConnectId, { cleanups, timer });
|
||||||
|
}
|
||||||
|
|
||||||
|
opened(
|
||||||
|
meta: TermixGuacMeta,
|
||||||
|
guacamoleConnectionId: string,
|
||||||
|
extra: Cleanup[] = [],
|
||||||
|
): RemoteSession {
|
||||||
|
const session: RemoteSession = {
|
||||||
|
guacamoleConnectionId,
|
||||||
|
termixConnectId: meta.termixConnectId,
|
||||||
|
hostId: meta.hostId,
|
||||||
|
hostName: meta.hostName,
|
||||||
|
ownerUserId: meta.ownerUserId,
|
||||||
|
protocol: meta.protocol,
|
||||||
|
tabInstanceId: meta.tabInstanceId ?? null,
|
||||||
|
openedAt: Date.now(),
|
||||||
|
};
|
||||||
|
const parked = this.pending.get(meta.termixConnectId);
|
||||||
|
if (parked) {
|
||||||
|
clearTimeout(parked.timer);
|
||||||
|
this.pending.delete(meta.termixConnectId);
|
||||||
|
}
|
||||||
|
this.cleanupsByConnectId.set(meta.termixConnectId, [
|
||||||
|
...(parked?.cleanups ?? []),
|
||||||
|
...extra,
|
||||||
|
]);
|
||||||
|
this.byConnectId.set(meta.termixConnectId, session);
|
||||||
|
this.byGuacamoleId.set(guacamoleConnectionId, session);
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
closed(termixConnectId: string): void {
|
||||||
|
const session = this.byConnectId.get(termixConnectId);
|
||||||
|
if (session) this.byGuacamoleId.delete(session.guacamoleConnectionId);
|
||||||
|
this.byConnectId.delete(termixConnectId);
|
||||||
|
run(this.cleanupsByConnectId.get(termixConnectId) ?? []);
|
||||||
|
this.cleanupsByConnectId.delete(termixConnectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
byConnect(termixConnectId: string): RemoteSession | null {
|
||||||
|
return this.byConnectId.get(termixConnectId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byGuacamole(guacamoleConnectionId: string): RemoteSession | null {
|
||||||
|
return this.byGuacamoleId.get(guacamoleConnectionId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Releases everything, for deactivate. */
|
||||||
|
clear(): void {
|
||||||
|
for (const { cleanups, timer } of this.pending.values()) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
run(cleanups);
|
||||||
|
}
|
||||||
|
this.pending.clear();
|
||||||
|
for (const cleanups of this.cleanupsByConnectId.values()) run(cleanups);
|
||||||
|
this.cleanupsByConnectId.clear();
|
||||||
|
this.byConnectId.clear();
|
||||||
|
this.byGuacamoleId.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(cleanups: Cleanup[]): void {
|
||||||
|
for (const cleanup of cleanups) {
|
||||||
|
try {
|
||||||
|
cleanup();
|
||||||
|
} catch {
|
||||||
|
// One failed close must not keep the rest open.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import type { Client } from "ssh2";
|
|
||||||
import type {
|
|
||||||
PluginSsh,
|
|
||||||
PluginSshConnection,
|
|
||||||
PluginSshConnectOptions,
|
|
||||||
PluginSshHost,
|
|
||||||
} from "@termix/plugin-sdk/backend";
|
|
||||||
|
|
||||||
let current: PluginSsh | null = null;
|
|
||||||
|
|
||||||
/** Set in activate, cleared on deactivate. */
|
|
||||||
export function setPluginSsh(ssh: PluginSsh | null): void {
|
|
||||||
current = ssh;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pluginSsh(): PluginSsh {
|
|
||||||
if (!current) throw new Error("The plugin is not active");
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** withConnection with ssh2's Client type filled in. */
|
|
||||||
export function withSshConnection<T>(
|
|
||||||
host: number | PluginSshHost,
|
|
||||||
options: PluginSshConnectOptions & { pool: string },
|
|
||||||
fn: (client: Client) => Promise<T>,
|
|
||||||
): Promise<T> {
|
|
||||||
return pluginSsh().withConnection<T, Client>(host, options, fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** connect with ssh2's Client type filled in. */
|
|
||||||
export function connectSsh(
|
|
||||||
host: number | PluginSshHost,
|
|
||||||
options?: PluginSshConnectOptions,
|
|
||||||
): Promise<PluginSshConnection<Client>> {
|
|
||||||
return pluginSsh().connect<Client>(host, options);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { guacLogger } from "../../../../src/backend/utils/logger.js";
|
|
||||||
|
interface TokenLogger {
|
||||||
|
warn: (message: string, meta?: Record<string, unknown>) => void;
|
||||||
|
error: (message: string, meta?: Record<string, unknown>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GuacamoleConnectionSettings {
|
export interface GuacamoleConnectionSettings {
|
||||||
type?: "rdp" | "vnc" | "telnet";
|
type?: "rdp" | "vnc" | "telnet";
|
||||||
@@ -33,8 +37,10 @@ export interface GuacamoleConnectionSettings {
|
|||||||
export interface TermixGuacMeta {
|
export interface TermixGuacMeta {
|
||||||
termixConnectId: string;
|
termixConnectId: string;
|
||||||
hostId: number;
|
hostId: number;
|
||||||
|
hostName: string;
|
||||||
ownerUserId: string;
|
ownerUserId: string;
|
||||||
protocol: "rdp" | "vnc" | "telnet";
|
protocol: "rdp" | "vnc" | "telnet";
|
||||||
|
tabInstanceId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GuacamoleToken {
|
export interface GuacamoleToken {
|
||||||
@@ -57,21 +63,19 @@ export interface GuacamoleRecordingMetadata {
|
|||||||
const CIPHER = "aes-256-cbc";
|
const CIPHER = "aes-256-cbc";
|
||||||
const KEY_LENGTH = 32;
|
const KEY_LENGTH = 32;
|
||||||
|
|
||||||
|
const silentLogger: TokenLogger = { warn: () => {}, error: () => {} };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypts the connection tokens guacamole-lite decrypts. Built in activate;
|
||||||
|
* the key is derived from the environment, so a restart keeps it.
|
||||||
|
*/
|
||||||
export class GuacamoleTokenService {
|
export class GuacamoleTokenService {
|
||||||
private static instance: GuacamoleTokenService;
|
|
||||||
private encryptionKey: Buffer;
|
private encryptionKey: Buffer;
|
||||||
|
|
||||||
private constructor() {
|
constructor(private readonly log: TokenLogger = silentLogger) {
|
||||||
this.encryptionKey = this.initializeKey();
|
this.encryptionKey = this.initializeKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
static getInstance(): GuacamoleTokenService {
|
|
||||||
if (!GuacamoleTokenService.instance) {
|
|
||||||
GuacamoleTokenService.instance = new GuacamoleTokenService();
|
|
||||||
}
|
|
||||||
return GuacamoleTokenService.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private initializeKey(): Buffer {
|
private initializeKey(): Buffer {
|
||||||
const existingKey = process.env.GUACAMOLE_ENCRYPTION_KEY;
|
const existingKey = process.env.GUACAMOLE_ENCRYPTION_KEY;
|
||||||
if (existingKey) {
|
if (existingKey) {
|
||||||
@@ -91,12 +95,9 @@ export class GuacamoleTokenService {
|
|||||||
.digest();
|
.digest();
|
||||||
}
|
}
|
||||||
|
|
||||||
guacLogger.warn(
|
this.log.warn("No persistent encryption key found, generating random key", {
|
||||||
"No persistent encryption key found, generating random key",
|
operation: "guac_key_generation",
|
||||||
{
|
});
|
||||||
operation: "guac_key_generation",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return crypto.randomBytes(KEY_LENGTH);
|
return crypto.randomBytes(KEY_LENGTH);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,8 +135,9 @@ export class GuacamoleTokenService {
|
|||||||
|
|
||||||
return JSON.parse(decrypted) as GuacamoleToken;
|
return JSON.parse(decrypted) as GuacamoleToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
guacLogger.error("Failed to decrypt guacamole token", error, {
|
this.log.error("Failed to decrypt guacamole token", {
|
||||||
operation: "guac_token_decrypt_error",
|
operation: "guac_token_decrypt_error",
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { watchGuacamoleConnectionId } from "./guacamole-session-id";
|
import { watchGuacamoleConnectionId } from "./guacamole-session-id";
|
||||||
import { getErrorMessage } from "@/lib/error-message.js";
|
|
||||||
import React, {
|
import React, {
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -14,40 +13,42 @@ import {
|
|||||||
type GuacamoleDisplayHandle,
|
type GuacamoleDisplayHandle,
|
||||||
type GuacamoleTouchMode,
|
type GuacamoleTouchMode,
|
||||||
} from "./GuacamoleDisplay.tsx";
|
} from "./GuacamoleDisplay.tsx";
|
||||||
import { getSSHHosts, logActivity, isElectron } from "@/main-axios";
|
|
||||||
import { getGuacamoleTokenFromHost, getGuacdStatus } from "./guacamole-api";
|
import { getGuacamoleTokenFromHost, getGuacdStatus } from "./guacamole-api";
|
||||||
import { readConfiguredDimension } from "./guacamole-display-size.ts";
|
import { readConfiguredDimension } from "./guacamole-display-size.ts";
|
||||||
import { getGuacamoleToken, parseGuacamoleConfig } from "./guacamole-api";
|
import { getGuacamoleToken } from "./guacamole-api";
|
||||||
import {
|
|
||||||
resolveConnectionOrigin,
|
|
||||||
type ConnectionOrigin,
|
|
||||||
} from "@/lib/connection-origin.ts";
|
|
||||||
import {
|
import {
|
||||||
|
useHost,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
useConnectionRetry,
|
useConnectionRetry,
|
||||||
} from "@termix/plugin-sdk/frontend";
|
} from "@termix/plugin-sdk/frontend";
|
||||||
import { GuacamoleToolbar } from "./GuacamoleToolbar.tsx";
|
|
||||||
import { GuacamoleFileBrowser } from "./GuacamoleFileBrowser.tsx";
|
|
||||||
import { describeUploadError } from "./guacamole-filesystem.ts";
|
|
||||||
import { canUploadToRdpDrive } from "./guacamole-file-drop.ts";
|
|
||||||
import { Button } from "@/components/button.tsx";
|
|
||||||
import { Input } from "@/components/input.tsx";
|
|
||||||
import { PasswordInput } from "@/components/password-input.tsx";
|
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
|
ConnectionLogProvider,
|
||||||
|
ConnectionScreen,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/dialog.tsx";
|
Input,
|
||||||
import { ConnectionScreen } from "@/components/connection/ConnectionScreen.tsx";
|
PasswordInput,
|
||||||
import {
|
isElectron,
|
||||||
ConnectionLogProvider,
|
logActivity,
|
||||||
|
resolveConnectionOrigin,
|
||||||
useConnectionLog,
|
useConnectionLog,
|
||||||
} from "@/ssh/connection-log/ConnectionLogContext.tsx";
|
type ConnectionOrigin,
|
||||||
import type { SSHHost } from "@/types";
|
} from "@termix/plugin-sdk/ui";
|
||||||
import { useConnectionDefaults } from "@/contexts/ConnectionDefaultsContext";
|
import type { GuacamoleConfig } from "./guacamole-config";
|
||||||
import { resolveConnectionDefaults } from "@/lib/connection-defaults";
|
import {
|
||||||
|
errorMessage,
|
||||||
|
hostRemoteOptions,
|
||||||
|
type Protocol,
|
||||||
|
type RemoteHostLogin,
|
||||||
|
} from "./host-remote";
|
||||||
|
import { GuacamoleToolbar } from "./GuacamoleToolbar.tsx";
|
||||||
|
import { GuacamoleFileBrowser } from "./GuacamoleFileBrowser.tsx";
|
||||||
|
import { describeUploadError } from "./guacamole-filesystem.ts";
|
||||||
|
import { canUploadToRdpDrive } from "./guacamole-file-drop.ts";
|
||||||
import { needsRdpCredentialPrompt } from "./rdp-credential-prompt";
|
import { needsRdpCredentialPrompt } from "./rdp-credential-prompt";
|
||||||
|
|
||||||
interface GuacamoleAppProps {
|
interface GuacamoleAppProps {
|
||||||
@@ -59,10 +60,48 @@ interface GuacamoleAppProps {
|
|||||||
quickConnectHost?: GuacamoleQuickHost;
|
quickConnectHost?: GuacamoleQuickHost;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** What GuacamoleApp needs from a host that has no database row. */
|
/**
|
||||||
export type GuacamoleQuickHost = GuacamoleAppInnerProps["hostConfig"] & {
|
* What a session needs from its host. A saved host brings its options; a
|
||||||
|
* quick-connect host has no row, so it also carries its address and login.
|
||||||
|
*/
|
||||||
|
export interface GuacamoleHostConfig {
|
||||||
name?: string;
|
name?: string;
|
||||||
};
|
ip: string;
|
||||||
|
connectionType?: Protocol;
|
||||||
|
domain?: string;
|
||||||
|
guacamoleConfig?: GuacamoleConfig;
|
||||||
|
rdpAuthType?: string;
|
||||||
|
authOverrides?: RemoteHostLogin["authOverrides"];
|
||||||
|
syncId?: string | null;
|
||||||
|
connectionOrigin?: "local" | "remote" | null;
|
||||||
|
showToolbar?: boolean;
|
||||||
|
port?: number;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GuacamoleQuickHost = GuacamoleHostConfig;
|
||||||
|
|
||||||
|
function savedHostConfig(
|
||||||
|
host: RemoteHostLogin,
|
||||||
|
protocol: Protocol | undefined,
|
||||||
|
): GuacamoleHostConfig {
|
||||||
|
const options = hostRemoteOptions(host);
|
||||||
|
return {
|
||||||
|
name: host.name ?? undefined,
|
||||||
|
ip: host.ip ?? "",
|
||||||
|
connectionType:
|
||||||
|
protocol ??
|
||||||
|
(options.enableRdp ? "rdp" : options.enableVnc ? "vnc" : "telnet"),
|
||||||
|
domain: host.domain,
|
||||||
|
guacamoleConfig: options.guacamoleConfig,
|
||||||
|
rdpAuthType: host.rdpAuthType,
|
||||||
|
authOverrides: host.authOverrides,
|
||||||
|
syncId: host.syncId,
|
||||||
|
connectionOrigin: host.connectionOrigin,
|
||||||
|
showToolbar: options.enableToolbar,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface GuacamoleAppHandle {
|
export interface GuacamoleAppHandle {
|
||||||
disconnect: () => void;
|
disconnect: () => void;
|
||||||
@@ -77,50 +116,20 @@ const GuacamoleApp = React.forwardRef<GuacamoleAppHandle, GuacamoleAppProps>(
|
|||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const defaults = useConnectionDefaults();
|
const savedHost = useHost(quickConnectHost ? undefined : hostId);
|
||||||
const [hostConfig, setHostConfig] = useState<GuacamoleQuickHost | null>(
|
const hostConfig: GuacamoleHostConfig | null = quickConnectHost
|
||||||
null,
|
? quickConnectHost
|
||||||
);
|
: savedHost
|
||||||
const [loading, setLoading] = useState(true);
|
? savedHostConfig(savedHost as RemoteHostLogin, protocol)
|
||||||
|
: null;
|
||||||
|
// The host list loads with the shell; a tab restored before it arrives
|
||||||
|
// waits for it instead of reporting the host missing.
|
||||||
|
const [waited, setWaited] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!defaults.ready) return;
|
const timer = setTimeout(() => setWaited(true), 3000);
|
||||||
if (quickConnectHost) {
|
return () => clearTimeout(timer);
|
||||||
const connectionType = protocol ?? quickConnectHost.connectionType;
|
}, []);
|
||||||
setHostConfig({
|
const loading = !hostConfig && !!hostId && !waited;
|
||||||
...quickConnectHost,
|
|
||||||
guacamoleConfig: resolveConnectionDefaults(
|
|
||||||
connectionType === "rdp" ? defaults.rdp : {},
|
|
||||||
{},
|
|
||||||
),
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!hostId) {
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
getSSHHosts()
|
|
||||||
.then((hosts) => {
|
|
||||||
const host = hosts.find((h) => h.id === parseInt(hostId, 10));
|
|
||||||
if (!host) {
|
|
||||||
setHostConfig(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const connectionType = protocol ?? host.connectionType;
|
|
||||||
const protocolDefaults = connectionType === "rdp" ? defaults.rdp : {};
|
|
||||||
setHostConfig({
|
|
||||||
...host,
|
|
||||||
guacamoleConfig: resolveConnectionDefaults(
|
|
||||||
protocolDefaults,
|
|
||||||
parseGuacamoleConfig(host.guacamoleConfig),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => setHostConfig(null))
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, [hostId, protocol, defaults.ready, defaults.rdp, quickConnectHost]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -159,23 +168,7 @@ const GuacamoleApp = React.forwardRef<GuacamoleAppHandle, GuacamoleAppProps>(
|
|||||||
|
|
||||||
interface GuacamoleAppInnerProps {
|
interface GuacamoleAppInnerProps {
|
||||||
hostId: number;
|
hostId: number;
|
||||||
hostConfig: Pick<
|
hostConfig: GuacamoleHostConfig;
|
||||||
SSHHost,
|
|
||||||
| "connectionType"
|
|
||||||
| "domain"
|
|
||||||
| "guacamoleConfig"
|
|
||||||
| "rdpAuthType"
|
|
||||||
| "authOverrides"
|
|
||||||
| "syncId"
|
|
||||||
| "connectionOrigin"
|
|
||||||
| "ip"
|
|
||||||
| "rdpPort"
|
|
||||||
| "vncPort"
|
|
||||||
| "rdpUser"
|
|
||||||
| "rdpPassword"
|
|
||||||
| "vncUser"
|
|
||||||
| "vncPassword"
|
|
||||||
> & { enableTerminalToolbar?: boolean };
|
|
||||||
hostName: string;
|
hostName: string;
|
||||||
tabId?: string;
|
tabId?: string;
|
||||||
protocol?: "rdp" | "vnc" | "telnet";
|
protocol?: "rdp" | "vnc" | "telnet";
|
||||||
@@ -223,7 +216,7 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
const [toolbarHidden, setToolbarHidden] = useState(false);
|
const [toolbarHidden, setToolbarHidden] = useState(false);
|
||||||
const [pendingUploads, setPendingUploads] = useState<File[]>([]);
|
const [pendingUploads, setPendingUploads] = useState<File[]>([]);
|
||||||
|
|
||||||
const guacConfig = parseGuacamoleConfig(hostConfig.guacamoleConfig);
|
const guacConfig = hostConfig.guacamoleConfig ?? {};
|
||||||
const allowUpload = guacConfig.disableUpload !== true;
|
const allowUpload = guacConfig.disableUpload !== true;
|
||||||
const allowDownload = guacConfig.disableDownload !== true;
|
const allowDownload = guacConfig.disableDownload !== true;
|
||||||
|
|
||||||
@@ -311,10 +304,10 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
let resolvedOrigin: ConnectionOrigin = "local";
|
let resolvedOrigin: ConnectionOrigin = "local";
|
||||||
|
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
resolvedOrigin = await resolveConnectionOrigin({
|
resolvedOrigin = await resolveConnectionOrigin(
|
||||||
connectionType: resolvedProtocolForConnect,
|
{ connectionOrigin: hostConfig.connectionOrigin },
|
||||||
connectionOrigin: hostConfig.connectionOrigin,
|
{ defaultRemote: true },
|
||||||
});
|
);
|
||||||
if (resolvedOrigin === "remote") {
|
if (resolvedOrigin === "remote") {
|
||||||
const remoteConfig = (await window.electronAPI?.invoke?.(
|
const remoteConfig = (await window.electronAPI?.invoke?.(
|
||||||
"get-remote-sync-config",
|
"get-remote-sync-config",
|
||||||
@@ -331,6 +324,9 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
message: t("remoteDesktop.checkingGuacd"),
|
message: t("remoteDesktop.checkingGuacd"),
|
||||||
});
|
});
|
||||||
const status = await getGuacdStatus(resolvedOrigin);
|
const status = await getGuacdStatus(resolvedOrigin);
|
||||||
|
if (status.enabled === false) {
|
||||||
|
throw new Error(t("remoteDesktop.disabled"));
|
||||||
|
}
|
||||||
if (status.guacd.status !== "connected") {
|
if (status.guacd.status !== "connected") {
|
||||||
throw new Error(t("remoteDesktop.guacdUnavailable"));
|
throw new Error(t("remoteDesktop.guacdUnavailable"));
|
||||||
}
|
}
|
||||||
@@ -351,21 +347,12 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
{
|
{
|
||||||
protocol: resolvedProtocolForConnect,
|
protocol: resolvedProtocolForConnect,
|
||||||
hostname: hostConfig.ip,
|
hostname: hostConfig.ip,
|
||||||
port:
|
port: hostConfig.port,
|
||||||
resolvedProtocolForConnect === "vnc"
|
username: hostConfig.username,
|
||||||
? hostConfig.vncPort
|
password: hostConfig.password,
|
||||||
: hostConfig.rdpPort,
|
|
||||||
username:
|
|
||||||
resolvedProtocolForConnect === "vnc"
|
|
||||||
? hostConfig.vncUser
|
|
||||||
: hostConfig.rdpUser,
|
|
||||||
password:
|
|
||||||
resolvedProtocolForConnect === "vnc"
|
|
||||||
? hostConfig.vncPassword
|
|
||||||
: hostConfig.rdpPassword,
|
|
||||||
domain: hostConfig.domain,
|
domain: hostConfig.domain,
|
||||||
ignoreCert: true,
|
ignoreCert: true,
|
||||||
guacamoleConfig: parseGuacamoleConfig(hostConfig.guacamoleConfig),
|
guacamoleConfig: hostConfig.guacamoleConfig,
|
||||||
},
|
},
|
||||||
resolvedOrigin,
|
resolvedOrigin,
|
||||||
)
|
)
|
||||||
@@ -375,6 +362,7 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
protocol,
|
protocol,
|
||||||
promptedCredentials ?? undefined,
|
promptedCredentials ?? undefined,
|
||||||
hostConfig.syncId,
|
hostConfig.syncId,
|
||||||
|
tabId,
|
||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
setSessionLookup(
|
setSessionLookup(
|
||||||
@@ -397,6 +385,7 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
promptedCredentials,
|
promptedCredentials,
|
||||||
resolvedProtocolForConnect,
|
resolvedProtocolForConnect,
|
||||||
hostConfig,
|
hostConfig,
|
||||||
|
tabId,
|
||||||
addLog,
|
addLog,
|
||||||
t,
|
t,
|
||||||
]);
|
]);
|
||||||
@@ -407,10 +396,7 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
await fetchToken();
|
await fetchToken();
|
||||||
tokenRetryRef.current.markConnected();
|
tokenRetryRef.current.markConnected();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const message = getErrorMessage(
|
const message = errorMessage(err, t("remoteDesktop.failedToConnect"));
|
||||||
err,
|
|
||||||
t("remoteDesktop.failedToConnect"),
|
|
||||||
);
|
|
||||||
setError(message || t("remoteDesktop.failedToConnect"));
|
setError(message || t("remoteDesktop.failedToConnect"));
|
||||||
addLog({ type: "error", stage: "error", message });
|
addLog({ type: "error", stage: "error", message });
|
||||||
tokenRetryRef.current.markFailed();
|
tokenRetryRef.current.markFailed();
|
||||||
@@ -632,7 +618,7 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
onClose={() => setFileBrowserOpen(false)}
|
onClose={() => setFileBrowserOpen(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hostConfig.enableTerminalToolbar !== false && !toolbarHidden && (
|
{hostConfig.showToolbar !== false && !toolbarHidden && (
|
||||||
<GuacamoleToolbar
|
<GuacamoleToolbar
|
||||||
displayRef={displayRef}
|
displayRef={displayRef}
|
||||||
protocol={resolvedProtocol}
|
protocol={resolvedProtocol}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { getErrorMessage } from "@/lib/error-message.js";
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import {
|
import {
|
||||||
useEffect,
|
useEffect,
|
||||||
@@ -11,15 +10,18 @@ import {
|
|||||||
import Guacamole from "guacamole-common-js";
|
import Guacamole from "guacamole-common-js";
|
||||||
import { Upload } from "lucide-react";
|
import { Upload } from "lucide-react";
|
||||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||||
import { isElectron } from "@/main-axios";
|
|
||||||
import { getGuacamoleToken } from "./guacamole-api";
|
import { getGuacamoleToken } from "./guacamole-api";
|
||||||
import { getBasePath } from "@/lib/base-path.ts";
|
|
||||||
import { buildGuacamoleWebSocketBaseUrl } from "./guacamole-websocket-url.ts";
|
import { buildGuacamoleWebSocketBaseUrl } from "./guacamole-websocket-url.ts";
|
||||||
import {
|
import {
|
||||||
resolveConnectionOrigin,
|
|
||||||
buildOriginWsUrl,
|
buildOriginWsUrl,
|
||||||
|
getBasePath,
|
||||||
|
guacStateToStage,
|
||||||
|
isElectron,
|
||||||
|
resolveConnectionOrigin,
|
||||||
type ConnectionOrigin,
|
type ConnectionOrigin,
|
||||||
} from "@/lib/connection-origin.ts";
|
type ConnectionStage,
|
||||||
|
} from "@termix/plugin-sdk/ui";
|
||||||
|
import { errorMessage } from "./host-remote";
|
||||||
import { isPasteShortcut, pasteTextToRemote } from "./guacamole-clipboard.ts";
|
import { isPasteShortcut, pasteTextToRemote } from "./guacamole-clipboard.ts";
|
||||||
import { getGuacamoleDisplaySize } from "./guacamole-display-size.ts";
|
import { getGuacamoleDisplaySize } from "./guacamole-display-size.ts";
|
||||||
import { bindPointerInput } from "./guacamole-pointer.ts";
|
import { bindPointerInput } from "./guacamole-pointer.ts";
|
||||||
@@ -31,8 +33,6 @@ import {
|
|||||||
uploadFileToClient,
|
uploadFileToClient,
|
||||||
type GuacamoleFileStreamClient,
|
type GuacamoleFileStreamClient,
|
||||||
} from "./guacamole-filesystem.ts";
|
} from "./guacamole-filesystem.ts";
|
||||||
import { guacStateToStage } from "@/components/connection/connection-status.ts";
|
|
||||||
import type { ConnectionStage } from "@/types/connection-log.ts";
|
|
||||||
import { clampGuacamoleZoom, stepGuacamoleZoom } from "./guacamole-zoom.ts";
|
import { clampGuacamoleZoom, stepGuacamoleZoom } from "./guacamole-zoom.ts";
|
||||||
|
|
||||||
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
|
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
|
||||||
@@ -243,10 +243,10 @@ export const GuacamoleDisplay = forwardRef<
|
|||||||
// Resolved before the token is minted, not just before the socket is
|
// Resolved before the token is minted, not just before the socket is
|
||||||
// opened: the token has to come from whichever backend will serve the
|
// opened: the token has to come from whichever backend will serve the
|
||||||
// session, so both steps have to agree on the origin.
|
// session, so both steps have to agree on the origin.
|
||||||
const origin = await resolveConnectionOrigin({
|
const origin = await resolveConnectionOrigin(
|
||||||
connectionType: connectionProtocol,
|
{ connectionOrigin: connectionConfig.connectionOrigin },
|
||||||
connectionOrigin: connectionConfig.connectionOrigin,
|
{ defaultRemote: true },
|
||||||
});
|
);
|
||||||
|
|
||||||
if (connectionConfig.token) {
|
if (connectionConfig.token) {
|
||||||
token = connectionConfig.token;
|
token = connectionConfig.token;
|
||||||
@@ -317,8 +317,8 @@ export const GuacamoleDisplay = forwardRef<
|
|||||||
if (displaySize.dpi) params.set("dpi", String(displaySize.dpi));
|
if (displaySize.dpi) params.set("dpi", String(displaySize.dpi));
|
||||||
return { url: wsBase, query: params.toString() };
|
return { url: wsBase, query: params.toString() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = getErrorMessage(error);
|
const message = errorMessage(error);
|
||||||
onError?.(errorMessage);
|
onError?.(message);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -698,7 +698,7 @@ export const GuacamoleDisplay = forwardRef<
|
|||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
setIsReady(false);
|
setIsReady(false);
|
||||||
setHasError(true);
|
setHasError(true);
|
||||||
onError?.(getErrorMessage(error, t("remoteDesktop.connectionError")));
|
onError?.(errorMessage(error, t("remoteDesktop.connectionError")));
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
getWebSocketConnection,
|
getWebSocketConnection,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getErrorMessage } from "@/lib/error-message.js";
|
import { errorMessage } from "./host-remote";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import Guacamole from "guacamole-common-js";
|
import Guacamole from "guacamole-common-js";
|
||||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "@/components/button.tsx";
|
import { Button } from "@termix/plugin-sdk/ui";
|
||||||
import {
|
import {
|
||||||
downloadFile,
|
downloadFile,
|
||||||
listDirectory,
|
listDirectory,
|
||||||
@@ -56,7 +56,7 @@ export function GuacamoleFileBrowser({
|
|||||||
setEntries(await listDirectory(filesystem, target));
|
setEntries(await listDirectory(filesystem, target));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setEntries([]);
|
setEntries([]);
|
||||||
setError(getErrorMessage(err, t("remoteDesktop.files.listFailed")));
|
setError(errorMessage(err, t("remoteDesktop.files.listFailed")));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/tooltip.tsx";
|
} from "@termix/plugin-sdk/ui";
|
||||||
import type {
|
import type {
|
||||||
GuacamoleDisplayHandle,
|
GuacamoleDisplayHandle,
|
||||||
GuacamoleTouchMode,
|
GuacamoleTouchMode,
|
||||||
} from "./GuacamoleDisplay.tsx";
|
} from "./GuacamoleDisplay.tsx";
|
||||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||||
import { ActionSlot } from "@termix/plugin-sdk/ui";
|
import { ActionSlot } from "@termix/plugin-sdk/ui";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@termix/plugin-sdk/ui";
|
||||||
import {
|
import {
|
||||||
detectRuntimeMetaKeyFamily,
|
detectRuntimeMetaKeyFamily,
|
||||||
metaKeyLabels,
|
metaKeyLabels,
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
import { useTranslation } from "@termix/plugin-sdk/frontend";
|
||||||
import { Input } from "@/components/input";
|
import {
|
||||||
import { PasswordInput } from "@/components/password-input";
|
FakeSwitch,
|
||||||
import { FakeSwitch, SectionCard, SettingRow } from "@/components/section-card";
|
Input,
|
||||||
import type { Host } from "@/types/ui-types";
|
PasswordInput,
|
||||||
|
SectionCard,
|
||||||
|
Select2,
|
||||||
|
SettingRow,
|
||||||
|
} from "@termix/plugin-sdk/ui";
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
Copy,
|
Copy,
|
||||||
@@ -16,13 +20,10 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
Cpu,
|
Cpu,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { HostEditorForm } from "@/sidebar/HostEditorData";
|
import type { RemoteDesktopForm, RemoteFormSetField } from "./remote-form";
|
||||||
import { Select2 } from "@/components/select2";
|
|
||||||
|
|
||||||
type HostEditorSetField = <K extends keyof HostEditorForm>(
|
type HostEditorForm = RemoteDesktopForm;
|
||||||
key: K,
|
type HostEditorSetField = RemoteFormSetField;
|
||||||
value: HostEditorForm[K],
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
type GuacFieldSetter = (key: string, value: unknown) => void;
|
type GuacFieldSetter = (key: string, value: unknown) => void;
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ export function HostEditorRdpTab({
|
|||||||
form: HostEditorForm;
|
form: HostEditorForm;
|
||||||
setField: HostEditorSetField;
|
setField: HostEditorSetField;
|
||||||
setGuacField: GuacFieldSetter;
|
setGuacField: GuacFieldSetter;
|
||||||
host?: Host | null;
|
host?: { macAddress?: string | null } | null;
|
||||||
credentials?: { id: string; name: string; username: string }[];
|
credentials?: { id: string; name: string; username: string }[];
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -984,7 +985,7 @@ export function HostEditorVncTab({
|
|||||||
form: HostEditorForm;
|
form: HostEditorForm;
|
||||||
setField: HostEditorSetField;
|
setField: HostEditorSetField;
|
||||||
setGuacField: GuacFieldSetter;
|
setGuacField: GuacFieldSetter;
|
||||||
host?: Host | null;
|
host?: { macAddress?: string | null } | null;
|
||||||
credentials?: { id: string; name: string; username: string }[];
|
credentials?: { id: string; name: string; username: string }[];
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -1523,7 +1524,6 @@ export function HostEditorTelnetTab({
|
|||||||
form: HostEditorForm;
|
form: HostEditorForm;
|
||||||
setField: HostEditorSetField;
|
setField: HostEditorSetField;
|
||||||
setGuacField: GuacFieldSetter;
|
setGuacField: GuacFieldSetter;
|
||||||
host?: Host | null;
|
|
||||||
credentials?: { id: string; name: string; username: string }[];
|
credentials?: { id: string; name: string; username: string }[];
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
|
import type { PluginApiClient, TermixApp } from "@termix/plugin-sdk/frontend";
|
||||||
import {
|
import {
|
||||||
authApi,
|
|
||||||
getRemoteGuacamoleApi,
|
|
||||||
handleApiError,
|
|
||||||
isElectron,
|
isElectron,
|
||||||
} from "@/main-axios";
|
resolveRemoteHostId,
|
||||||
import type { AxiosInstance } from "axios";
|
type ConnectionOrigin,
|
||||||
import type { GuacamoleConfig } from "@/types/guacamole-config";
|
} from "@termix/plugin-sdk/ui";
|
||||||
import { resolveRemoteHostId } from "@/lib/remote-server-api";
|
import type { GuacamoleConfig } from "./guacamole-config";
|
||||||
import type { ConnectionOrigin } from "@/lib/connection-origin";
|
import { errorMessage } from "./host-remote";
|
||||||
|
|
||||||
|
let app: Pick<TermixApp, "apiFor"> | null = null;
|
||||||
|
|
||||||
|
/** Set in activate, cleared on deactivate. */
|
||||||
|
export function setRemoteDesktopApp(next: Pick<TermixApp, "apiFor"> | null) {
|
||||||
|
app = next;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Picks the backend that will actually serve this session, so the token and
|
* Picks the backend that will actually serve this session, so the token and
|
||||||
@@ -24,9 +29,13 @@ import type { ConnectionOrigin } from "@/lib/connection-origin";
|
|||||||
* silently sent a missed call site to the remote server, which fails with a
|
* silently sent a missed call site to the remote server, which fails with a
|
||||||
* bare network error on a desktop that has none configured.
|
* bare network error on a desktop that has none configured.
|
||||||
*/
|
*/
|
||||||
function guacamoleApi(origin: ConnectionOrigin): AxiosInstance {
|
function guacamoleApi(origin: ConnectionOrigin): PluginApiClient {
|
||||||
if (!isElectron()) return authApi;
|
if (!app) throw new Error("Remote Desktop is not active");
|
||||||
return origin === "local" ? authApi : getRemoteGuacamoleApi();
|
return app.apiFor(origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
function apiError(error: unknown, action: string): Error {
|
||||||
|
return new Error(errorMessage(error, `Failed to ${action}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GuacamoleTokenRequest {
|
export interface GuacamoleTokenRequest {
|
||||||
@@ -51,18 +60,6 @@ type GuacamoleConfigSource = {
|
|||||||
guacamoleConfig?: string | Record<string, unknown> | null;
|
guacamoleConfig?: string | Record<string, unknown> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseGuacamoleConfig(
|
|
||||||
config?: string | GuacamoleConfig | null,
|
|
||||||
): GuacamoleConfig {
|
|
||||||
if (!config) return {};
|
|
||||||
if (typeof config !== "string") return config;
|
|
||||||
try {
|
|
||||||
return JSON.parse(config) as GuacamoleConfig;
|
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getGuacamoleDpi(
|
export function getGuacamoleDpi(
|
||||||
source?: GuacamoleConfigSource,
|
source?: GuacamoleConfigSource,
|
||||||
): number | undefined {
|
): number | undefined {
|
||||||
@@ -174,8 +171,8 @@ export async function getGuacamoleToken(
|
|||||||
try {
|
try {
|
||||||
const guacParams = toGuacamoleParams(request.guacamoleConfig);
|
const guacParams = toGuacamoleParams(request.guacamoleConfig);
|
||||||
|
|
||||||
const response = await guacamoleApi(origin).post(
|
const response = await guacamoleApi(origin).post<GuacamoleTokenResponse>(
|
||||||
"/plugin-api/remote-desktop/token",
|
"/token",
|
||||||
{
|
{
|
||||||
type: request.protocol,
|
type: request.protocol,
|
||||||
hostname: request.hostname,
|
hostname: request.hostname,
|
||||||
@@ -190,7 +187,7 @@ export async function getGuacamoleToken(
|
|||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleApiError(error, "get guacamole token");
|
throw apiError(error, "get a connection token");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +201,7 @@ export async function getGuacamoleTokenFromHost(
|
|||||||
domain?: string;
|
domain?: string;
|
||||||
},
|
},
|
||||||
syncId?: string | null,
|
syncId?: string | null,
|
||||||
|
tabInstanceId?: string,
|
||||||
): Promise<GuacamoleTokenResponse> {
|
): Promise<GuacamoleTokenResponse> {
|
||||||
try {
|
try {
|
||||||
// A locally-originated session is served by the embedded backend, which
|
// A locally-originated session is served by the embedded backend, which
|
||||||
@@ -216,10 +214,11 @@ export async function getGuacamoleTokenFromHost(
|
|||||||
throw new Error("The synced host does not exist on the remote server");
|
throw new Error("The synced host does not exist on the remote server");
|
||||||
}
|
}
|
||||||
const targetHostId = remoteHostId ?? hostId;
|
const targetHostId = remoteHostId ?? hostId;
|
||||||
const response = await guacamoleApi(origin).post(
|
const response = await guacamoleApi(origin).post<GuacamoleTokenResponse>(
|
||||||
`/plugin-api/remote-desktop/connect-host/${targetHostId}`,
|
`/connect-host/${targetHostId}`,
|
||||||
{
|
{
|
||||||
...(protocol ? { protocol } : {}),
|
...(protocol ? { protocol } : {}),
|
||||||
|
...(tabInstanceId ? { tabInstanceId } : {}),
|
||||||
...(promptedCredentials?.username
|
...(promptedCredentials?.username
|
||||||
? { promptedUsername: promptedCredentials.username }
|
? { promptedUsername: promptedCredentials.username }
|
||||||
: {}),
|
: {}),
|
||||||
@@ -233,15 +232,22 @@ export async function getGuacamoleTokenFromHost(
|
|||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw handleApiError(error, "get guacamole token from host");
|
throw apiError(error, "get a connection token");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGuacdStatus(origin: ConnectionOrigin): Promise<{
|
export interface RemoteDesktopStatus {
|
||||||
|
enabled: boolean;
|
||||||
guacd: { status: string };
|
guacd: { status: string };
|
||||||
}> {
|
}
|
||||||
const response = await guacamoleApi(origin).get(
|
|
||||||
"/plugin-api/remote-desktop/status",
|
/** `probe: false` skips dialing guacd, for a caller that only needs `enabled`. */
|
||||||
|
export async function getGuacdStatus(
|
||||||
|
origin: ConnectionOrigin,
|
||||||
|
options: { probe?: boolean } = {},
|
||||||
|
): Promise<RemoteDesktopStatus> {
|
||||||
|
const response = await guacamoleApi(origin).get<RemoteDesktopStatus>(
|
||||||
|
options.probe === false ? "/status?probe=0" : "/status",
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
@@ -251,9 +257,34 @@ export async function getGuacamoleConnectionId(
|
|||||||
origin: ConnectionOrigin,
|
origin: ConnectionOrigin,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const response = await guacamoleApi(origin).get(
|
const response = await guacamoleApi(origin).get<{
|
||||||
`/plugin-api/remote-desktop/connection/${encodeURIComponent(connectId)}`,
|
guacamoleConnectionId?: string | null;
|
||||||
{ signal },
|
}>(`/connection/${encodeURIComponent(connectId)}`, { signal });
|
||||||
);
|
|
||||||
return response.data.guacamoleConnectionId ?? null;
|
return response.data.guacamoleConnectionId ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function nativeRdpAvailable(): Promise<boolean> {
|
||||||
|
if (!isElectron()) return false;
|
||||||
|
try {
|
||||||
|
const response = await guacamoleApi("local").get<{ available: boolean }>(
|
||||||
|
"/native-rdp",
|
||||||
|
);
|
||||||
|
return response.data.available === true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Always the embedded backend: it is the one that can reach Electron. */
|
||||||
|
export async function openNativeRdp(request: {
|
||||||
|
host: string;
|
||||||
|
port?: number;
|
||||||
|
username?: string;
|
||||||
|
domain?: string;
|
||||||
|
}): Promise<{ success: boolean; error?: string }> {
|
||||||
|
const response = await guacamoleApi("local").post<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}>("/native-rdp", request);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -1,5 +1,5 @@
|
|||||||
import { getGuacamoleConnectionId } from "./guacamole-api";
|
import { getGuacamoleConnectionId } from "./guacamole-api";
|
||||||
import type { ConnectionOrigin } from "@/lib/connection-origin";
|
import type { ConnectionOrigin } from "@termix/plugin-sdk/ui";
|
||||||
|
|
||||||
export function watchGuacamoleConnectionId(
|
export function watchGuacamoleConnectionId(
|
||||||
connectId: string,
|
connectId: string,
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import type { PluginHostRecord } from "@termix/plugin-sdk/frontend";
|
||||||
|
import type { GuacamoleConfig } from "./guacamole-config";
|
||||||
|
|
||||||
|
export const PLUGIN_ID = "remote-desktop";
|
||||||
|
|
||||||
|
export type Protocol = "rdp" | "vnc" | "telnet";
|
||||||
|
|
||||||
|
/** This plugin's host settings, as they arrive on host.pluginSettings. */
|
||||||
|
export interface RemoteHostOptions {
|
||||||
|
enableRdp: boolean;
|
||||||
|
enableVnc: boolean;
|
||||||
|
enableTelnet: boolean;
|
||||||
|
rdpPort: number;
|
||||||
|
vncPort: number;
|
||||||
|
telnetPort: number;
|
||||||
|
rdpSecurity: string;
|
||||||
|
rdpIgnoreCert: boolean;
|
||||||
|
guacamoleConfig: GuacamoleConfig;
|
||||||
|
enableToolbar: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ENABLE_KEY = {
|
||||||
|
rdp: "enableRdp",
|
||||||
|
vnc: "enableVnc",
|
||||||
|
telnet: "enableTelnet",
|
||||||
|
} as const satisfies Record<Protocol, keyof RemoteHostOptions>;
|
||||||
|
|
||||||
|
export const PORT_KEY = {
|
||||||
|
rdp: "rdpPort",
|
||||||
|
vnc: "vncPort",
|
||||||
|
telnet: "telnetPort",
|
||||||
|
} as const satisfies Record<Protocol, keyof RemoteHostOptions>;
|
||||||
|
|
||||||
|
export const DEFAULT_PORT: Record<Protocol, number> = {
|
||||||
|
rdp: 3389,
|
||||||
|
vnc: 5900,
|
||||||
|
telnet: 23,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The core fields holding a protocol's login, which stay on the host. */
|
||||||
|
export interface RemoteHostLogin {
|
||||||
|
id?: number | string;
|
||||||
|
name?: string | null;
|
||||||
|
ip?: string;
|
||||||
|
domain?: string;
|
||||||
|
syncId?: string | null;
|
||||||
|
connectionOrigin?: "local" | "remote" | null;
|
||||||
|
rdpAuthType?: string;
|
||||||
|
rdpUser?: string;
|
||||||
|
rdpPassword?: string;
|
||||||
|
vncUser?: string;
|
||||||
|
vncPassword?: string;
|
||||||
|
authOverrides?: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
credentialId?: number | string | null;
|
||||||
|
required?: boolean;
|
||||||
|
ownerAuthShared?: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
pluginSettings?: Record<string, Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function port(value: unknown, fallback: number): number {
|
||||||
|
const parsed = typeof value === "string" ? Number(value) : value;
|
||||||
|
return typeof parsed === "number" && Number.isInteger(parsed) && parsed > 0
|
||||||
|
? parsed
|
||||||
|
: fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseGuacamoleConfig(value: unknown): GuacamoleConfig {
|
||||||
|
if (!value) return {};
|
||||||
|
if (typeof value === "string") {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value);
|
||||||
|
return parsed && typeof parsed === "object" ? parsed : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return typeof value === "object" ? (value as GuacamoleConfig) : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function remoteOptions(
|
||||||
|
settings: Record<string, unknown> | undefined,
|
||||||
|
): RemoteHostOptions {
|
||||||
|
const values = settings ?? {};
|
||||||
|
return {
|
||||||
|
enableRdp: values.enableRdp === true,
|
||||||
|
enableVnc: values.enableVnc === true,
|
||||||
|
enableTelnet: values.enableTelnet === true,
|
||||||
|
rdpPort: port(values.rdpPort, DEFAULT_PORT.rdp),
|
||||||
|
vncPort: port(values.vncPort, DEFAULT_PORT.vnc),
|
||||||
|
telnetPort: port(values.telnetPort, DEFAULT_PORT.telnet),
|
||||||
|
rdpSecurity:
|
||||||
|
typeof values.rdpSecurity === "string" ? values.rdpSecurity : "",
|
||||||
|
rdpIgnoreCert: values.rdpIgnoreCert === true,
|
||||||
|
guacamoleConfig: parseGuacamoleConfig(values.guacamoleConfig),
|
||||||
|
enableToolbar: values.enableToolbar !== false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hostRemoteOptions(
|
||||||
|
host:
|
||||||
|
| Pick<RemoteHostLogin, "pluginSettings">
|
||||||
|
| PluginHostRecord
|
||||||
|
| null
|
||||||
|
| undefined,
|
||||||
|
): RemoteHostOptions {
|
||||||
|
const bag = (host as RemoteHostLogin | null | undefined)?.pluginSettings;
|
||||||
|
return remoteOptions(bag?.[PLUGIN_ID]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function protocolEnabled(
|
||||||
|
host:
|
||||||
|
| Pick<RemoteHostLogin, "pluginSettings">
|
||||||
|
| PluginHostRecord
|
||||||
|
| null
|
||||||
|
| undefined,
|
||||||
|
protocol: Protocol,
|
||||||
|
): boolean {
|
||||||
|
return hostRemoteOptions(host)[ENABLE_KEY[protocol]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Quick Connect hosts are never saved; their ids carry this prefix. */
|
||||||
|
const QUICK_CONNECT_ID_PREFIX = "quick-connect-";
|
||||||
|
|
||||||
|
export function isQuickConnectHost(host: { id?: unknown } | null | undefined) {
|
||||||
|
return String(host?.id ?? "").startsWith(QUICK_CONNECT_ID_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function errorMessage(error: unknown, fallback = "Unknown error") {
|
||||||
|
const data = (error as { response?: { data?: { error?: unknown } } })
|
||||||
|
?.response?.data?.error;
|
||||||
|
if (typeof data === "string" && data) return data;
|
||||||
|
if (error instanceof Error && error.message) return error.message;
|
||||||
|
if (typeof error === "string" && error) return error;
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
@@ -1,23 +1,21 @@
|
|||||||
import type { ComponentType, Ref } from "react";
|
import type { ComponentType, Ref } from "react";
|
||||||
import i18next from "i18next";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import {
|
import {
|
||||||
|
Info,
|
||||||
MessagesSquare,
|
MessagesSquare,
|
||||||
Monitor,
|
Monitor,
|
||||||
MonitorUp,
|
MonitorUp,
|
||||||
MousePointerClick,
|
MousePointerClick,
|
||||||
Settings,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
useTranslation,
|
useTranslation,
|
||||||
type HostEditorSectionProps,
|
type HostEditorSectionProps,
|
||||||
|
type PluginHostRecord,
|
||||||
type StandaloneViewProps,
|
type StandaloneViewProps,
|
||||||
type TabProps,
|
type TabProps,
|
||||||
type TermixApp,
|
type TermixApp,
|
||||||
} from "@termix/plugin-sdk/frontend";
|
} from "@termix/plugin-sdk/frontend";
|
||||||
import { FakeSwitch, SectionCard, SettingRow } from "@termix/plugin-sdk/ui";
|
import { SectionCard, isElectron } from "@termix/plugin-sdk/ui";
|
||||||
import type { Host } from "@/types/ui-types";
|
import { toast } from "sonner";
|
||||||
import { isQuickConnectHost } from "@/sidebar/quick-connect-host";
|
|
||||||
import GuacamoleApp, { type GuacamoleAppHandle } from "./GuacamoleApp";
|
import GuacamoleApp, { type GuacamoleAppHandle } from "./GuacamoleApp";
|
||||||
import { GuacamoleDisplay } from "./GuacamoleDisplay";
|
import { GuacamoleDisplay } from "./GuacamoleDisplay";
|
||||||
import {
|
import {
|
||||||
@@ -25,48 +23,70 @@ import {
|
|||||||
HostEditorTelnetTab,
|
HostEditorTelnetTab,
|
||||||
HostEditorVncTab,
|
HostEditorVncTab,
|
||||||
} from "./HostEditorGuacamoleTabs";
|
} from "./HostEditorGuacamoleTabs";
|
||||||
import { getGuacamoleTokenFromHost } from "./guacamole-api";
|
import {
|
||||||
|
getGuacamoleTokenFromHost,
|
||||||
|
getGuacdStatus,
|
||||||
|
nativeRdpAvailable,
|
||||||
|
openNativeRdp,
|
||||||
|
setRemoteDesktopApp,
|
||||||
|
} from "./guacamole-api";
|
||||||
import { quickConnectGuacHost } from "./quick-connect-guac-host";
|
import { quickConnectGuacHost } from "./quick-connect-guac-host";
|
||||||
import { REMOTE_DESKTOP_TOOLBAR_SLOT } from "./GuacamoleToolbar.tsx";
|
import { REMOTE_DESKTOP_TOOLBAR_SLOT } from "./GuacamoleToolbar.tsx";
|
||||||
|
import {
|
||||||
type Protocol = "rdp" | "vnc" | "telnet";
|
DEFAULT_PORT,
|
||||||
|
ENABLE_KEY,
|
||||||
|
PORT_KEY,
|
||||||
|
hostRemoteOptions,
|
||||||
|
isQuickConnectHost,
|
||||||
|
protocolEnabled,
|
||||||
|
type Protocol,
|
||||||
|
type RemoteHostLogin,
|
||||||
|
} from "./host-remote";
|
||||||
|
import { remoteDesktopForm } from "./remote-form";
|
||||||
|
|
||||||
const PROTOCOLS: {
|
const PROTOCOLS: {
|
||||||
id: Protocol;
|
id: Protocol;
|
||||||
titleKey: string;
|
titleKey: string;
|
||||||
|
descriptionKey: string;
|
||||||
|
paletteKey: string;
|
||||||
icon: ComponentType<{ className?: string }>;
|
icon: ComponentType<{ className?: string }>;
|
||||||
enableKey: "enableRdp" | "enableVnc" | "enableTelnet";
|
|
||||||
priority: number;
|
priority: number;
|
||||||
order: number;
|
order: number;
|
||||||
|
quickConnect?: { showDomain?: boolean };
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
id: "rdp",
|
id: "rdp",
|
||||||
titleKey: "hosts.tabRdp",
|
titleKey: "hosts.tabRdp",
|
||||||
|
descriptionKey: "hosts.remoteDesktop",
|
||||||
|
paletteKey: "palette.connectRdp",
|
||||||
icon: Monitor,
|
icon: Monitor,
|
||||||
enableKey: "enableRdp",
|
|
||||||
priority: 50,
|
priority: 50,
|
||||||
order: 100,
|
order: 100,
|
||||||
|
quickConnect: { showDomain: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "vnc",
|
id: "vnc",
|
||||||
titleKey: "hosts.tabVnc",
|
titleKey: "hosts.tabVnc",
|
||||||
|
descriptionKey: "hosts.virtualNetwork",
|
||||||
|
paletteKey: "palette.connectVnc",
|
||||||
icon: MousePointerClick,
|
icon: MousePointerClick,
|
||||||
enableKey: "enableVnc",
|
|
||||||
priority: 40,
|
priority: 40,
|
||||||
order: 110,
|
order: 110,
|
||||||
|
quickConnect: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "telnet",
|
id: "telnet",
|
||||||
titleKey: "hosts.tabTelnet",
|
titleKey: "hosts.tabTelnet",
|
||||||
|
descriptionKey: "hosts.unencryptedShell",
|
||||||
|
paletteKey: "palette.connectTelnet",
|
||||||
icon: MessagesSquare,
|
icon: MessagesSquare,
|
||||||
enableKey: "enableTelnet",
|
|
||||||
priority: 30,
|
priority: 30,
|
||||||
order: 120,
|
order: 120,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function RemoteDesktopTab({ tab, host, isVisible, handleRef }: TabProps) {
|
function RemoteDesktopTab({ tab, host, isVisible, handleRef }: TabProps) {
|
||||||
const shellHost = host as unknown as Host;
|
const record = host as unknown as RemoteHostLogin | undefined;
|
||||||
return (
|
return (
|
||||||
<GuacamoleApp
|
<GuacamoleApp
|
||||||
ref={handleRef as Ref<GuacamoleAppHandle>}
|
ref={handleRef as Ref<GuacamoleAppHandle>}
|
||||||
@@ -75,8 +95,8 @@ function RemoteDesktopTab({ tab, host, isVisible, handleRef }: TabProps) {
|
|||||||
protocol={tab.type as Protocol}
|
protocol={tab.type as Protocol}
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
quickConnectHost={
|
quickConnectHost={
|
||||||
isQuickConnectHost(shellHost)
|
record && isQuickConnectHost(record)
|
||||||
? quickConnectGuacHost(shellHost)
|
? quickConnectGuacHost(record)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -111,79 +131,65 @@ function RemoteDisplay({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Applies one remote desktop field and stops inheriting user defaults. */
|
function SectionNotes({ protocol }: { protocol: Protocol }) {
|
||||||
function useGuacFieldSetter(
|
const { t } = useTranslation();
|
||||||
updateForm: HostEditorSectionProps["updateForm"],
|
return (
|
||||||
): (key: string, value: unknown) => void {
|
<SectionCard
|
||||||
return (key, value) =>
|
title={t("hosts.remoteDesktopNotes")}
|
||||||
updateForm((current) => ({
|
icon={<Info className="size-3.5" />}
|
||||||
inheritRemoteDesktopDefaults: false,
|
>
|
||||||
guacamoleConfig: {
|
<div className="flex flex-col gap-2 py-3 text-xs text-muted-foreground">
|
||||||
...((current.guacamoleConfig as Record<string, unknown>) ?? {}),
|
{protocol === "rdp" && <p>{t("hosts.userDefaultsNote")}</p>}
|
||||||
[key]: value,
|
{isElectron() && <p>{t("hosts.connectionOriginNote")}</p>}
|
||||||
},
|
</div>
|
||||||
}));
|
</SectionCard>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RdpSection(props: HostEditorSectionProps) {
|
function RdpSection(props: HostEditorSectionProps) {
|
||||||
const { t } = useTranslation();
|
const { form, setField, setGuacField } = remoteDesktopForm(props);
|
||||||
const setGuacField = useGuacFieldSetter(props.updateForm);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SectionCard
|
|
||||||
title={t("hosts.rdpDefaults", { defaultValue: "RDP defaults" })}
|
|
||||||
icon={<Settings className="size-3.5" />}
|
|
||||||
>
|
|
||||||
<SettingRow
|
|
||||||
label={t("hosts.useUserDefaults", {
|
|
||||||
defaultValue: "Use user defaults",
|
|
||||||
})}
|
|
||||||
description={t("hosts.useUserRdpDefaultsDesc", {
|
|
||||||
defaultValue:
|
|
||||||
"Inherit performance, redirection, and clipboard settings from User Profile.",
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<FakeSwitch
|
|
||||||
checked={!!props.form.inheritRemoteDesktopDefaults}
|
|
||||||
onChange={(value) =>
|
|
||||||
props.setField("inheritRemoteDesktopDefaults", value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SettingRow>
|
|
||||||
</SectionCard>
|
|
||||||
<HostEditorRdpTab
|
<HostEditorRdpTab
|
||||||
form={props.form}
|
form={form}
|
||||||
setField={props.setField as never}
|
setField={setField}
|
||||||
setGuacField={setGuacField as never}
|
setGuacField={setGuacField}
|
||||||
host={props.host as unknown as Host}
|
host={props.host as { macAddress?: string | null } | undefined}
|
||||||
credentials={props.credentials as never}
|
credentials={props.credentials as never}
|
||||||
/>
|
/>
|
||||||
|
<SectionNotes protocol="rdp" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VncSection(props: HostEditorSectionProps) {
|
function VncSection(props: HostEditorSectionProps) {
|
||||||
const setGuacField = useGuacFieldSetter(props.updateForm);
|
const { form, setField, setGuacField } = remoteDesktopForm(props);
|
||||||
return (
|
return (
|
||||||
<HostEditorVncTab
|
<>
|
||||||
form={props.form}
|
<HostEditorVncTab
|
||||||
setField={props.setField as never}
|
form={form}
|
||||||
setGuacField={setGuacField as never}
|
setField={setField}
|
||||||
host={props.host as unknown as Host}
|
setGuacField={setGuacField}
|
||||||
credentials={props.credentials as never}
|
host={props.host as { macAddress?: string | null } | undefined}
|
||||||
/>
|
credentials={props.credentials as never}
|
||||||
|
/>
|
||||||
|
<SectionNotes protocol="vnc" />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TelnetSection(props: HostEditorSectionProps) {
|
function TelnetSection(props: HostEditorSectionProps) {
|
||||||
const setGuacField = useGuacFieldSetter(props.updateForm);
|
const { form, setField, setGuacField } = remoteDesktopForm(props);
|
||||||
return (
|
return (
|
||||||
<HostEditorTelnetTab
|
<>
|
||||||
form={props.form}
|
<HostEditorTelnetTab
|
||||||
setField={props.setField as never}
|
form={form}
|
||||||
setGuacField={setGuacField as never}
|
setField={setField}
|
||||||
credentials={props.credentials as never}
|
setGuacField={setGuacField}
|
||||||
/>
|
credentials={props.credentials as never}
|
||||||
|
/>
|
||||||
|
<SectionNotes protocol="telnet" />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,23 +199,96 @@ const SECTIONS: Record<Protocol, ComponentType<HostEditorSectionProps>> = {
|
|||||||
telnet: TelnetSection,
|
telnet: TelnetSection,
|
||||||
};
|
};
|
||||||
|
|
||||||
async function openNativeRdp(host: Record<string, unknown>): Promise<void> {
|
function registerNativeRdp(app: TermixApp): void {
|
||||||
const t = (key: string) => i18next.t(`remote-desktop:${key}`);
|
let disposed = false;
|
||||||
try {
|
app.onDispose(() => {
|
||||||
const result = await window.electronAPI.openNativeRdp({
|
disposed = true;
|
||||||
host: String(host.ip),
|
});
|
||||||
port: (host.rdpPort as number | undefined) ?? 3389,
|
void nativeRdpAvailable().then((available) => {
|
||||||
username: host.rdpUser as string | undefined,
|
if (disposed || !available) return;
|
||||||
domain: host.domain as string | undefined,
|
app.registerHostAction({
|
||||||
|
id: "rdp-native",
|
||||||
|
titleKey: "hosts.openNativeRdp",
|
||||||
|
icon: MonitorUp,
|
||||||
|
kind: "open",
|
||||||
|
order: 105,
|
||||||
|
when: (host) => protocolEnabled(host, "rdp"),
|
||||||
|
run: (host) => {
|
||||||
|
const record = host as unknown as RemoteHostLogin;
|
||||||
|
void openNativeRdp({
|
||||||
|
host: String(record.ip ?? ""),
|
||||||
|
port: hostRemoteOptions(record).rdpPort,
|
||||||
|
username: record.rdpUser,
|
||||||
|
domain: record.domain,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
if (result.success) toast.success(app.t("hosts.nativeRdpOpened"));
|
||||||
|
else toast.error(result.error || app.t("hosts.nativeRdpFailed"));
|
||||||
|
})
|
||||||
|
.catch(() => toast.error(app.t("hosts.nativeRdpFailed")));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerHostSurfaces(app: TermixApp): void {
|
||||||
|
for (const protocol of PROTOCOLS) {
|
||||||
|
const when = (host: PluginHostRecord) => protocolEnabled(host, protocol.id);
|
||||||
|
|
||||||
|
app.registerHostProtocol({
|
||||||
|
id: protocol.id,
|
||||||
|
settingKey: ENABLE_KEY[protocol.id],
|
||||||
|
portKey: PORT_KEY[protocol.id],
|
||||||
|
defaultPort: DEFAULT_PORT[protocol.id],
|
||||||
|
titleKey: protocol.titleKey,
|
||||||
|
descriptionKey: protocol.descriptionKey,
|
||||||
|
icon: protocol.icon,
|
||||||
|
order: protocol.order,
|
||||||
|
quickConnect: protocol.quickConnect,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.registerHostAction({
|
||||||
|
id: protocol.id,
|
||||||
|
titleKey: protocol.titleKey,
|
||||||
|
icon: protocol.icon,
|
||||||
|
kind: "connect",
|
||||||
|
priority: protocol.priority,
|
||||||
|
order: protocol.order,
|
||||||
|
tabType: protocol.id,
|
||||||
|
copyUrlView: protocol.id,
|
||||||
|
when,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.registerPaletteEntry({
|
||||||
|
id: `remote-desktop.${protocol.id}`,
|
||||||
|
titleKey: protocol.paletteKey,
|
||||||
|
icon: protocol.icon,
|
||||||
|
keywords: [protocol.id],
|
||||||
|
scope: "host",
|
||||||
|
when: (host) => !!host && when(host),
|
||||||
|
run: (shell, host) => {
|
||||||
|
if (host) shell.openTab(host, protocol.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.registerHostEditorSection({
|
||||||
|
id: protocol.id,
|
||||||
|
group: "top",
|
||||||
|
titleKey: protocol.titleKey,
|
||||||
|
icon: protocol.icon,
|
||||||
|
order: protocol.order / 5,
|
||||||
|
visible: (protocols) => !!protocols[ENABLE_KEY[protocol.id]],
|
||||||
|
component: SECTIONS[protocol.id],
|
||||||
});
|
});
|
||||||
if (result.success) toast.success(t("hosts.nativeRdpOpened"));
|
|
||||||
else toast.error(result.error || t("hosts.nativeRdpFailed"));
|
|
||||||
} catch {
|
|
||||||
toast.error(t("hosts.nativeRdpFailed"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isElectron()) registerNativeRdp(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function activate(app: TermixApp): void {
|
export function activate(app: TermixApp): void {
|
||||||
|
setRemoteDesktopApp(app);
|
||||||
|
app.onDispose(() => setRemoteDesktopApp(null));
|
||||||
|
|
||||||
// Guest pages only draw shared streams.
|
// Guest pages only draw shared streams.
|
||||||
app.registerSlotContribution("session.remoteDisplay", {
|
app.registerSlotContribution("session.remoteDisplay", {
|
||||||
actionId: "remote-desktop.display",
|
actionId: "remote-desktop.display",
|
||||||
@@ -253,57 +332,24 @@ export function activate(app: TermixApp): void {
|
|||||||
noHostMessageKey: "remoteDesktop.noHostSelected",
|
noHostMessageKey: "remoteDesktop.noHostSelected",
|
||||||
persistent: true,
|
persistent: true,
|
||||||
session: true,
|
session: true,
|
||||||
restore: (host) => !!host[protocol.enableKey],
|
restore: (host) => protocolEnabled(host, protocol.id),
|
||||||
activityTypes: [protocol.id],
|
activityTypes: [protocol.id],
|
||||||
standalone: RemoteDesktopStandalone,
|
standalone: RemoteDesktopStandalone,
|
||||||
preload: () => import("./GuacamoleApp"),
|
preload: () => import("./GuacamoleApp"),
|
||||||
});
|
});
|
||||||
|
|
||||||
app.registerHostAction({
|
|
||||||
id: protocol.id,
|
|
||||||
titleKey: protocol.titleKey,
|
|
||||||
icon: protocol.icon,
|
|
||||||
kind: "connect",
|
|
||||||
priority: protocol.priority,
|
|
||||||
order: protocol.order,
|
|
||||||
tabType: protocol.id,
|
|
||||||
copyUrlView: protocol.id,
|
|
||||||
when: (host) => !!host[protocol.enableKey],
|
|
||||||
});
|
|
||||||
|
|
||||||
app.registerHostEditorSection({
|
|
||||||
id: protocol.id,
|
|
||||||
group: "top",
|
|
||||||
titleKey: protocol.titleKey,
|
|
||||||
icon: protocol.icon,
|
|
||||||
order: protocol.order / 5,
|
|
||||||
visible: (protocols) => !!protocols[protocol.enableKey],
|
|
||||||
component: SECTIONS[protocol.id],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The desktop app on Windows can hand RDP to the system client instead.
|
// An admin can turn Remote Desktop off; its ways in go with it.
|
||||||
if (window.electronAPI?.isElectron) {
|
let disposed = false;
|
||||||
let disposed = false;
|
app.onDispose(() => {
|
||||||
app.onDispose(() => {
|
disposed = true;
|
||||||
disposed = true;
|
});
|
||||||
|
void getGuacdStatus("local", { probe: false })
|
||||||
|
.then((status) => status.enabled !== false)
|
||||||
|
.catch(() => true)
|
||||||
|
.then((enabled) => {
|
||||||
|
if (!disposed && enabled) registerHostSurfaces(app);
|
||||||
});
|
});
|
||||||
window.electronAPI
|
|
||||||
.getPlatform()
|
|
||||||
.then((platform: string) => {
|
|
||||||
if (disposed || platform !== "win32") return;
|
|
||||||
app.registerHostAction({
|
|
||||||
id: "rdp-native",
|
|
||||||
titleKey: "hosts.openNativeRdp",
|
|
||||||
icon: MonitorUp,
|
|
||||||
kind: "open",
|
|
||||||
order: 105,
|
|
||||||
when: (host) => !!host.enableRdp,
|
|
||||||
run: (host) => void openNativeRdp(host),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
|
|
||||||
app.registerSlotContribution("onboarding.features", {
|
app.registerSlotContribution("onboarding.features", {
|
||||||
actionId: "remote-desktop.feature",
|
actionId: "remote-desktop.feature",
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
import type { Host } from "@/types/ui-types";
|
|
||||||
import type { GuacamoleQuickHost } from "./GuacamoleApp";
|
import type { GuacamoleQuickHost } from "./GuacamoleApp";
|
||||||
|
import {
|
||||||
|
hostRemoteOptions,
|
||||||
|
type Protocol,
|
||||||
|
type RemoteHostLogin,
|
||||||
|
} from "./host-remote";
|
||||||
|
|
||||||
/** The slice of a quick-connect host that GuacamoleApp mints a token from. */
|
/** The slice of a quick-connect host that GuacamoleApp mints a token from. */
|
||||||
export function quickConnectGuacHost(host: Host): GuacamoleQuickHost {
|
export function quickConnectGuacHost(
|
||||||
|
host: RemoteHostLogin,
|
||||||
|
): GuacamoleQuickHost {
|
||||||
|
const options = hostRemoteOptions(host);
|
||||||
|
const connectionType: Protocol = options.enableVnc ? "vnc" : "rdp";
|
||||||
return {
|
return {
|
||||||
name: host.name,
|
name: host.name ?? undefined,
|
||||||
ip: host.ip,
|
ip: host.ip ?? "",
|
||||||
connectionType: host.enableVnc ? "vnc" : "rdp",
|
connectionType,
|
||||||
domain: host.domain,
|
domain: host.domain,
|
||||||
rdpPort: host.rdpPort,
|
port: connectionType === "vnc" ? options.vncPort : options.rdpPort,
|
||||||
vncPort: host.vncPort,
|
|
||||||
rdpAuthType: host.rdpAuthType,
|
rdpAuthType: host.rdpAuthType,
|
||||||
rdpUser: host.rdpUser,
|
username: connectionType === "vnc" ? host.vncUser : host.rdpUser,
|
||||||
rdpPassword: host.rdpPassword,
|
password: connectionType === "vnc" ? host.vncPassword : host.rdpPassword,
|
||||||
vncUser: host.vncUser,
|
|
||||||
vncPassword: host.vncPassword,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { HostAuthOverrides } from "@/types/auth-protocols";
|
import type { RemoteHostLogin } from "./host-remote";
|
||||||
|
|
||||||
export function needsRdpCredentialPrompt({
|
export function needsRdpCredentialPrompt({
|
||||||
protocol,
|
protocol,
|
||||||
@@ -7,7 +7,7 @@ export function needsRdpCredentialPrompt({
|
|||||||
}: {
|
}: {
|
||||||
protocol: "rdp" | "vnc" | "telnet";
|
protocol: "rdp" | "vnc" | "telnet";
|
||||||
rdpAuthType?: string;
|
rdpAuthType?: string;
|
||||||
authOverrides?: HostAuthOverrides;
|
authOverrides?: RemoteHostLogin["authOverrides"];
|
||||||
}): boolean {
|
}): boolean {
|
||||||
return (
|
return (
|
||||||
protocol === "rdp" &&
|
protocol === "rdp" &&
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import type { HostEditorSectionProps } from "@termix/plugin-sdk/frontend";
|
||||||
|
import { PLUGIN_ID, remoteOptions } from "./host-remote";
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The host editor fields the RDP, VNC and Telnet tabs edit. The logins are
|
||||||
|
* core host fields; the options (ports, security, guacd settings) are this
|
||||||
|
* plugin's host settings, presented here under the names the tabs use.
|
||||||
|
*/
|
||||||
|
export interface RemoteDesktopForm {
|
||||||
|
domain: string;
|
||||||
|
security: string;
|
||||||
|
ignoreCert: boolean;
|
||||||
|
rdpPort: number;
|
||||||
|
vncPort: number;
|
||||||
|
telnetPort: number;
|
||||||
|
guacamoleConfig: Record<string, any>;
|
||||||
|
rdpAuthType: "direct" | "credential" | "none";
|
||||||
|
rdpCredentialId: string;
|
||||||
|
rdpUser: string;
|
||||||
|
rdpPassword: string;
|
||||||
|
vncAuthType: "direct" | "credential";
|
||||||
|
vncCredentialId: string;
|
||||||
|
vncUser: string;
|
||||||
|
vncPassword: string;
|
||||||
|
telnetAuthType: "direct" | "credential";
|
||||||
|
telnetCredentialId: string;
|
||||||
|
telnetUser: string;
|
||||||
|
telnetPassword: string;
|
||||||
|
statsConfig: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RemoteFormSetField = <K extends keyof RemoteDesktopForm>(
|
||||||
|
key: K,
|
||||||
|
value: RemoteDesktopForm[K],
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
/** Tab field name to this plugin's host setting key. */
|
||||||
|
const SETTING_FOR_FIELD: Partial<Record<keyof RemoteDesktopForm, string>> = {
|
||||||
|
rdpPort: "rdpPort",
|
||||||
|
vncPort: "vncPort",
|
||||||
|
telnetPort: "telnetPort",
|
||||||
|
security: "rdpSecurity",
|
||||||
|
ignoreCert: "rdpIgnoreCert",
|
||||||
|
};
|
||||||
|
|
||||||
|
type PluginSettingsBag = Record<string, Record<string, unknown>>;
|
||||||
|
|
||||||
|
function patchSettings(
|
||||||
|
current: Record<string, unknown>,
|
||||||
|
patch: (settings: Record<string, unknown>) => Record<string, unknown>,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const bag = (current.pluginSettings as PluginSettingsBag | undefined) ?? {};
|
||||||
|
const mine = bag[PLUGIN_ID] ?? {};
|
||||||
|
return {
|
||||||
|
pluginSettings: { ...bag, [PLUGIN_ID]: { ...mine, ...patch(mine) } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function remoteDesktopForm(props: HostEditorSectionProps): {
|
||||||
|
form: RemoteDesktopForm;
|
||||||
|
setField: RemoteFormSetField;
|
||||||
|
setGuacField: (key: string, value: unknown) => void;
|
||||||
|
} {
|
||||||
|
const bag = props.form?.pluginSettings as PluginSettingsBag | undefined;
|
||||||
|
const options = remoteOptions(bag?.[PLUGIN_ID]);
|
||||||
|
const form: RemoteDesktopForm = {
|
||||||
|
...(props.form as RemoteDesktopForm),
|
||||||
|
rdpPort: options.rdpPort,
|
||||||
|
vncPort: options.vncPort,
|
||||||
|
telnetPort: options.telnetPort,
|
||||||
|
security: options.rdpSecurity,
|
||||||
|
ignoreCert: options.rdpIgnoreCert,
|
||||||
|
guacamoleConfig: options.guacamoleConfig as Record<string, any>,
|
||||||
|
};
|
||||||
|
|
||||||
|
const setField: RemoteFormSetField = (key, value) => {
|
||||||
|
const setting = SETTING_FOR_FIELD[key];
|
||||||
|
if (!setting) {
|
||||||
|
props.setField(key, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
props.updateForm((current) =>
|
||||||
|
patchSettings(current, () => ({ [setting]: value })),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setGuacField = (key: string, value: unknown) =>
|
||||||
|
props.updateForm((current) =>
|
||||||
|
patchSettings(current, (mine) => ({
|
||||||
|
guacamoleConfig: {
|
||||||
|
...remoteOptions(mine).guacamoleConfig,
|
||||||
|
[key]: value,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { form, setField, setGuacField };
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
normalizeImportedHost,
|
||||||
|
userDefaultParams,
|
||||||
|
} from "../../src/backend/host-settings.js";
|
||||||
|
|
||||||
|
describe("normalizeImportedHost", () => {
|
||||||
|
it("reads a Termix export's pluginSettings", () => {
|
||||||
|
expect(
|
||||||
|
normalizeImportedHost({
|
||||||
|
pluginSettings: {
|
||||||
|
"remote-desktop": {
|
||||||
|
enableVnc: true,
|
||||||
|
vncPort: 5901,
|
||||||
|
guacamoleConfig: { cursor: "local" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
enableVnc: true,
|
||||||
|
vncPort: 5901,
|
||||||
|
guacamoleConfig: { cursor: "local" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads the flat fields hosts had before 2.9.0", () => {
|
||||||
|
expect(
|
||||||
|
normalizeImportedHost({
|
||||||
|
enableRdp: true,
|
||||||
|
rdpPort: "3390",
|
||||||
|
security: "nla",
|
||||||
|
ignoreCert: 1,
|
||||||
|
guacamoleConfig: JSON.stringify({ dpi: 120 }),
|
||||||
|
enableTerminalToolbar: false,
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
enableRdp: true,
|
||||||
|
rdpPort: 3390,
|
||||||
|
rdpSecurity: "nla",
|
||||||
|
rdpIgnoreCert: true,
|
||||||
|
guacamoleConfig: { dpi: 120 },
|
||||||
|
enableToolbar: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("turns a connectionType-only host into its switch and port", () => {
|
||||||
|
expect(
|
||||||
|
normalizeImportedHost({ connectionType: "telnet", port: 2323 }),
|
||||||
|
).toEqual({ enableTelnet: true, telnetPort: 2323 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes nothing for a plain SSH row", () => {
|
||||||
|
expect(
|
||||||
|
normalizeImportedHost({ connectionType: "ssh", port: 22 }),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("drops a port out of range", () => {
|
||||||
|
expect(normalizeImportedHost({ enableRdp: true, rdpPort: 70000 })).toEqual({
|
||||||
|
enableRdp: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("userDefaultParams", () => {
|
||||||
|
it("maps the user settings onto guacd parameters, skipping inherit", () => {
|
||||||
|
expect(
|
||||||
|
userDefaultParams({
|
||||||
|
colorDepth: "24",
|
||||||
|
resizeMethod: "reconnect",
|
||||||
|
disableCopy: "on",
|
||||||
|
enableWallpaper: "off",
|
||||||
|
enableDrive: "inherit",
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
"color-depth": 24,
|
||||||
|
"resize-method": "reconnect",
|
||||||
|
"disable-copy": true,
|
||||||
|
"enable-wallpaper": false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
buildRdpSettings,
|
buildRdpSettings,
|
||||||
resolveRdpAuthTypeForConnect,
|
|
||||||
resolveRdpDomain,
|
resolveRdpDomain,
|
||||||
} from "../../src/backend/rdp-settings.js";
|
} from "../../src/backend/rdp-settings.js";
|
||||||
|
|
||||||
@@ -53,32 +52,3 @@ describe("buildRdpSettings", () => {
|
|||||||
expect(resolveRdpDomain("none", undefined, "SAVED")).toBe("SAVED");
|
expect(resolveRdpDomain("none", undefined, "SAVED")).toBe("SAVED");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("resolveRdpAuthTypeForConnect", () => {
|
|
||||||
it("keeps prompt-on-connect authentication for a secretless recipient", () => {
|
|
||||||
expect(
|
|
||||||
resolveRdpAuthTypeForConnect({
|
|
||||||
storedAuthType: "none",
|
|
||||||
sharedResolution: { source: "secretless" },
|
|
||||||
}),
|
|
||||||
).toBe("none");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses a recipient override instead of prompting", () => {
|
|
||||||
expect(
|
|
||||||
resolveRdpAuthTypeForConnect({
|
|
||||||
storedAuthType: "none",
|
|
||||||
sharedResolution: { source: "personal-override" },
|
|
||||||
}),
|
|
||||||
).toBe("credential");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses authentication shared by the owner", () => {
|
|
||||||
expect(
|
|
||||||
resolveRdpAuthTypeForConnect({
|
|
||||||
storedAuthType: "credential",
|
|
||||||
sharedResolution: { source: "owner-shared", authType: "credential" },
|
|
||||||
}),
|
|
||||||
).toBe("credential");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
import http from "node:http";
|
||||||
|
import type { AddressInfo } from "node:net";
|
||||||
|
import express, { type Router } from "express";
|
||||||
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
createMockCtx,
|
||||||
|
type MockPluginContext,
|
||||||
|
} from "@termix/plugin-sdk/testing";
|
||||||
|
import type {
|
||||||
|
PluginContext,
|
||||||
|
PluginProtocolTarget,
|
||||||
|
} from "@termix/plugin-sdk/backend";
|
||||||
|
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
|
||||||
|
import manifestJson from "../../manifest.json";
|
||||||
|
import { activate } from "../../src/backend/index.js";
|
||||||
|
import { GuacamoleTokenService } from "../../src/backend/token-service.js";
|
||||||
|
|
||||||
|
const manifest = manifestJson as unknown as PluginManifest;
|
||||||
|
|
||||||
|
// The token key comes from JWT_SECRET; without one each service picks its own.
|
||||||
|
process.env.JWT_SECRET = "remote-desktop-test-secret";
|
||||||
|
|
||||||
|
function target(
|
||||||
|
overrides: Partial<PluginProtocolTarget["auth"]> = {},
|
||||||
|
): PluginProtocolTarget {
|
||||||
|
return {
|
||||||
|
host: {
|
||||||
|
id: 7,
|
||||||
|
name: "Win box",
|
||||||
|
ip: "10.0.0.7",
|
||||||
|
port: 22,
|
||||||
|
ownerUserId: "user-1",
|
||||||
|
jumpHosts: [],
|
||||||
|
},
|
||||||
|
shared: false,
|
||||||
|
auth: {
|
||||||
|
authType: "direct",
|
||||||
|
username: "admin",
|
||||||
|
password: "secret",
|
||||||
|
domain: "CORP",
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Server {
|
||||||
|
mock: MockPluginContext;
|
||||||
|
request: (
|
||||||
|
method: string,
|
||||||
|
path: string,
|
||||||
|
body?: unknown,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
) => Promise<{ status: number; body: any }>;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let server: Server | null = null;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await server?.close();
|
||||||
|
server = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function start(
|
||||||
|
options: {
|
||||||
|
protocolTargets?: Record<string, PluginProtocolTarget>;
|
||||||
|
services?: Record<string, object>;
|
||||||
|
capabilities?: string[];
|
||||||
|
} = {},
|
||||||
|
): Promise<Server> {
|
||||||
|
let router: Router | null = null;
|
||||||
|
const mock = createMockCtx({
|
||||||
|
pluginId: manifest.id,
|
||||||
|
manifest,
|
||||||
|
capabilities: options.capabilities ?? manifest.capabilities,
|
||||||
|
router: () => (router = express.Router()),
|
||||||
|
protocolTargets: options.protocolTargets,
|
||||||
|
services: options.services,
|
||||||
|
});
|
||||||
|
await activate(mock.ctx as PluginContext);
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use((req, _res, next) => {
|
||||||
|
mock.setActor(req.header("x-test-user") ?? "user-1");
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
app.use((req, res, next) => router!(req, res, next));
|
||||||
|
const httpServer = http.createServer(app);
|
||||||
|
await new Promise<void>((resolve) => httpServer.listen(0, resolve));
|
||||||
|
const { port } = httpServer.address() as AddressInfo;
|
||||||
|
|
||||||
|
return {
|
||||||
|
mock,
|
||||||
|
async request(method, path, body) {
|
||||||
|
const response = await fetch(`http://127.0.0.1:${port}${path}`, {
|
||||||
|
method,
|
||||||
|
headers: body ? { "content-type": "application/json" } : {},
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
return { status: response.status, body: text ? JSON.parse(text) : null };
|
||||||
|
},
|
||||||
|
async close() {
|
||||||
|
await new Promise<void>((resolve) => httpServer.close(() => resolve()));
|
||||||
|
for (const dispose of [...mock.disposals].reverse()) await dispose();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = new GuacamoleTokenService();
|
||||||
|
|
||||||
|
describe("POST /connect-host/:hostId", () => {
|
||||||
|
it("mints a token from the resolved login and the host's own settings", async () => {
|
||||||
|
server = await start({ protocolTargets: { "7:rdp": target() } });
|
||||||
|
await server.mock.ctx.settings.setHost(7, "enableRdp", true);
|
||||||
|
await server.mock.ctx.settings.setHost(7, "rdpPort", 3390);
|
||||||
|
await server.mock.ctx.settings.setHost(7, "rdpSecurity", "nla");
|
||||||
|
await server.mock.ctx.settings.setHost(7, "guacamoleConfig", {
|
||||||
|
"color-depth": 16,
|
||||||
|
"enable-wallpaper": "auto",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await server.request("POST", "/connect-host/7", {
|
||||||
|
protocol: "rdp",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.termixConnectId).toEqual(expect.any(String));
|
||||||
|
const token = tokens.decryptToken(response.body.token);
|
||||||
|
expect(token?.connection.settings).toMatchObject({
|
||||||
|
hostname: "10.0.0.7",
|
||||||
|
port: 3390,
|
||||||
|
username: "admin",
|
||||||
|
password: "secret",
|
||||||
|
domain: "CORP",
|
||||||
|
security: "nla",
|
||||||
|
"color-depth": 16,
|
||||||
|
});
|
||||||
|
expect(token?.connection.settings).not.toHaveProperty("enable-wallpaper");
|
||||||
|
expect(token?.termixMeta).toMatchObject({
|
||||||
|
hostId: 7,
|
||||||
|
hostName: "Win box",
|
||||||
|
protocol: "rdp",
|
||||||
|
});
|
||||||
|
expect(server.mock.audits.at(-1)).toMatchObject({
|
||||||
|
action: "rdp_connect",
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("puts the user's RDP defaults under the host's own values", async () => {
|
||||||
|
server = await start({ protocolTargets: { "7:rdp": target() } });
|
||||||
|
await server.mock.ctx.settings.setHost(7, "enableRdp", true);
|
||||||
|
await server.mock.ctx.settings.setHost(7, "guacamoleConfig", {
|
||||||
|
"color-depth": 32,
|
||||||
|
});
|
||||||
|
await server.mock.ctx.settings.setUser("user-1", "colorDepth", "16");
|
||||||
|
await server.mock.ctx.settings.setUser("user-1", "disableCopy", "on");
|
||||||
|
|
||||||
|
const response = await server.request("POST", "/connect-host/7", {
|
||||||
|
protocol: "rdp",
|
||||||
|
});
|
||||||
|
const settings = tokens.decryptToken(response.body.token)?.connection
|
||||||
|
.settings;
|
||||||
|
expect(settings?.["color-depth"]).toBe(32);
|
||||||
|
expect(settings?.["disable-copy"]).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the prompted login when the host asks at connect time", async () => {
|
||||||
|
server = await start({
|
||||||
|
protocolTargets: { "7:rdp": target({ authType: "none", username: "" }) },
|
||||||
|
});
|
||||||
|
await server.mock.ctx.settings.setHost(7, "enableRdp", true);
|
||||||
|
|
||||||
|
const response = await server.request("POST", "/connect-host/7", {
|
||||||
|
protocol: "rdp",
|
||||||
|
promptedUsername: "bob",
|
||||||
|
promptedPassword: "pw",
|
||||||
|
promptedDomain: "",
|
||||||
|
});
|
||||||
|
const settings = tokens.decryptToken(response.body.token)?.connection
|
||||||
|
.settings;
|
||||||
|
expect(settings).toMatchObject({ username: "bob", password: "pw" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refuses a protocol the host has switched off", async () => {
|
||||||
|
server = await start({ protocolTargets: { "7:vnc": target() } });
|
||||||
|
const response = await server.request("POST", "/connect-host/7", {
|
||||||
|
protocol: "vnc",
|
||||||
|
});
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("answers 404 when the caller cannot reach the host", async () => {
|
||||||
|
server = await start();
|
||||||
|
await server.mock.ctx.settings.setHost(7, "enableRdp", true);
|
||||||
|
const response = await server.request("POST", "/connect-host/7", {
|
||||||
|
protocol: "rdp",
|
||||||
|
});
|
||||||
|
expect(response.status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refuses everything while an admin has turned it off", async () => {
|
||||||
|
server = await start({ protocolTargets: { "7:rdp": target() } });
|
||||||
|
await server.mock.ctx.settings.set("enabled", false);
|
||||||
|
await server.mock.ctx.settings.setHost(7, "enableRdp", true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(await server.request("POST", "/connect-host/7", { protocol: "rdp" }))
|
||||||
|
.status,
|
||||||
|
).toBe(403);
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
await server.request("POST", "/token", {
|
||||||
|
type: "rdp",
|
||||||
|
hostname: "10.0.0.9",
|
||||||
|
})
|
||||||
|
).status,
|
||||||
|
).toBe(403);
|
||||||
|
const status = await server.request("GET", "/status?probe=0");
|
||||||
|
expect(status.body.enabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("records only when session recording is on for the host", async () => {
|
||||||
|
const enabledFor = async (hostId: number) => hostId === 7;
|
||||||
|
server = await start({
|
||||||
|
protocolTargets: { "7:rdp": target() },
|
||||||
|
services: {
|
||||||
|
"recordings.writer": {
|
||||||
|
enabledFor,
|
||||||
|
createFinished: async () => ({ id: 1 }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await server.mock.ctx.settings.setHost(7, "enableRdp", true);
|
||||||
|
const response = await server.request("POST", "/connect-host/7", {
|
||||||
|
protocol: "rdp",
|
||||||
|
});
|
||||||
|
const token = tokens.decryptToken(response.body.token);
|
||||||
|
expect(token?.recording).toMatchObject({ hostId: 7, protocol: "rdp" });
|
||||||
|
expect(token?.connection.settings["recording-name"]).toEqual(
|
||||||
|
expect.any(String),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("the rest of the routes", () => {
|
||||||
|
it("mints a quick connect token with the caller's defaults", async () => {
|
||||||
|
server = await start();
|
||||||
|
await server.mock.ctx.settings.setUser("user-1", "colorDepth", "24");
|
||||||
|
const response = await server.request("POST", "/token", {
|
||||||
|
type: "rdp",
|
||||||
|
hostname: "10.0.0.9",
|
||||||
|
username: "u",
|
||||||
|
password: "p",
|
||||||
|
});
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(
|
||||||
|
tokens.decryptToken(response.body.token)?.connection.settings,
|
||||||
|
).toMatchObject({ hostname: "10.0.0.9", port: 3389, "color-depth": 24 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("offers native RDP only in the desktop app", async () => {
|
||||||
|
server = await start();
|
||||||
|
expect((await server.request("GET", "/native-rdp")).body).toEqual({
|
||||||
|
available: false,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
(await server.request("POST", "/native-rdp", { host: "10.0.0.7" }))
|
||||||
|
.status,
|
||||||
|
).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not hand out another user's session id", async () => {
|
||||||
|
server = await start();
|
||||||
|
const response = await server.request("GET", "/connection/unknown");
|
||||||
|
expect(response.body).toEqual({ guacamoleConnectionId: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("activate", () => {
|
||||||
|
it("fails closed without network:serve", async () => {
|
||||||
|
await expect(
|
||||||
|
start({
|
||||||
|
capabilities: manifest.capabilities.filter(
|
||||||
|
(capability) => capability !== "network:serve",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/network:serve/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cannot read a host's login without credentials:read", async () => {
|
||||||
|
server = await start({
|
||||||
|
protocolTargets: { "7:rdp": target() },
|
||||||
|
capabilities: manifest.capabilities.filter(
|
||||||
|
(capability) => capability !== "credentials:read",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
await server.mock.ctx.settings.setHost(7, "enableRdp", true);
|
||||||
|
const response = await server.request("POST", "/connect-host/7", {
|
||||||
|
protocol: "rdp",
|
||||||
|
});
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(server.mock.checked).toContain("credentials:read");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("provides sessions.live for rdp, vnc and telnet", async () => {
|
||||||
|
server = await start();
|
||||||
|
expect([...server.mock.services.keys()].sort()).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
"sessions.live#rdp",
|
||||||
|
"sessions.live#telnet",
|
||||||
|
"sessions.live#vnc",
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { RemoteSessions } from "../../src/backend/sessions.js";
|
||||||
|
import { createLiveSessions } from "../../src/backend/live-sessions.js";
|
||||||
|
import { GuacamoleTokenService } from "../../src/backend/token-service.js";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
termixConnectId: "c1",
|
||||||
|
hostId: 7,
|
||||||
|
hostName: "Win box",
|
||||||
|
ownerUserId: "user-1",
|
||||||
|
protocol: "rdp" as const,
|
||||||
|
tabInstanceId: "tab-1",
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("RemoteSessions", () => {
|
||||||
|
it("moves parked resources onto the session and releases them on close", () => {
|
||||||
|
const sessions = new RemoteSessions();
|
||||||
|
const tunnel = vi.fn();
|
||||||
|
const tracking = vi.fn();
|
||||||
|
sessions.park("c1", [tunnel]);
|
||||||
|
sessions.opened(meta, "g1", [tracking]);
|
||||||
|
|
||||||
|
expect(sessions.byConnect("c1")?.guacamoleConnectionId).toBe("g1");
|
||||||
|
expect(sessions.byGuacamole("g1")?.hostName).toBe("Win box");
|
||||||
|
expect(tunnel).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
sessions.closed("c1");
|
||||||
|
expect(tunnel).toHaveBeenCalledOnce();
|
||||||
|
expect(tracking).toHaveBeenCalledOnce();
|
||||||
|
expect(sessions.byGuacamole("g1")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("releases a tunnel guacd never used", () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const sessions = new RemoteSessions(1000);
|
||||||
|
const tunnel = vi.fn();
|
||||||
|
sessions.park("c1", [tunnel]);
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
expect(tunnel).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("releases everything on clear, and one failing close does not stop the rest", () => {
|
||||||
|
const sessions = new RemoteSessions();
|
||||||
|
const failing = vi.fn(() => {
|
||||||
|
throw new Error("already closed");
|
||||||
|
});
|
||||||
|
const pending = vi.fn();
|
||||||
|
const open = vi.fn();
|
||||||
|
sessions.park("c2", [failing, pending]);
|
||||||
|
sessions.park("c1", [open]);
|
||||||
|
sessions.opened(meta, "g1");
|
||||||
|
sessions.clear();
|
||||||
|
expect(pending).toHaveBeenCalledOnce();
|
||||||
|
expect(open).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sessions.live providers", () => {
|
||||||
|
const tokens = new GuacamoleTokenService();
|
||||||
|
|
||||||
|
it("describes a live session of its own protocol only", () => {
|
||||||
|
const sessions = new RemoteSessions();
|
||||||
|
sessions.opened(meta, "g1");
|
||||||
|
const deps = { sessions, tokens, endSession: vi.fn(() => true) };
|
||||||
|
const rdp = createLiveSessions("rdp", deps);
|
||||||
|
const vnc = createLiveSessions("vnc", deps);
|
||||||
|
|
||||||
|
expect(rdp.getSession("g1")).toMatchObject({
|
||||||
|
id: "g1",
|
||||||
|
userId: "user-1",
|
||||||
|
hostId: 7,
|
||||||
|
hostName: "Win box",
|
||||||
|
isConnected: true,
|
||||||
|
tabInstanceId: "tab-1",
|
||||||
|
});
|
||||||
|
expect(vnc.getSession("g1")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mints a join token for a viewer and refuses an unknown session", () => {
|
||||||
|
const sessions = new RemoteSessions();
|
||||||
|
sessions.opened(meta, "g1");
|
||||||
|
const rdp = createLiveSessions("rdp", {
|
||||||
|
sessions,
|
||||||
|
tokens,
|
||||||
|
endSession: vi.fn(() => true),
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = tokens.decryptToken(rdp.createViewerToken("g1", true));
|
||||||
|
expect(token?.connection).toMatchObject({ join: "g1", readOnly: true });
|
||||||
|
expect(token?.recording).toBeUndefined();
|
||||||
|
expect(() => rdp.createViewerToken("nope", false)).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ends the session for its owner", () => {
|
||||||
|
const sessions = new RemoteSessions();
|
||||||
|
sessions.opened(meta, "g1");
|
||||||
|
const endSession = vi.fn(() => true);
|
||||||
|
const rdp = createLiveSessions("rdp", { sessions, tokens, endSession });
|
||||||
|
rdp.ownerEndSession("g1", "owner ended");
|
||||||
|
expect(endSession).toHaveBeenCalledWith("g1");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
import { readFileSync } from "node:fs";
|
|
||||||
import { runInNewContext } from "node:vm";
|
|
||||||
import ts from "typescript";
|
|
||||||
import { expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
it("returns the minted token without waiting for a connection that needs that token", async () => {
|
|
||||||
const source = readFileSync(
|
|
||||||
new URL("../../src/backend/routes.ts", import.meta.url),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
const ast = ts.createSourceFile(
|
|
||||||
"routes.ts",
|
|
||||||
source,
|
|
||||||
ts.ScriptTarget.Latest,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
const route = ast.statements.find(
|
|
||||||
(s) =>
|
|
||||||
ts.isExpressionStatement(s) &&
|
|
||||||
ts.isCallExpression(s.expression) &&
|
|
||||||
s.expression.arguments[0]?.getText(ast) === '"/connect-host/:hostId"',
|
|
||||||
) as ts.ExpressionStatement;
|
|
||||||
const callback = (route.expression as ts.CallExpression)
|
|
||||||
.arguments[1] as ts.ArrowFunction;
|
|
||||||
const block = (callback.body as ts.Block).statements[0] as ts.TryStatement;
|
|
||||||
const index = block.tryBlock.statements
|
|
||||||
.map((s, i) => (ts.isSwitchStatement(s) ? i : -1))
|
|
||||||
.filter((i) => i >= 0)
|
|
||||||
.at(-1)!;
|
|
||||||
expect(index).toBeGreaterThan(-1);
|
|
||||||
const tail = block.tryBlock.statements
|
|
||||||
.slice(index + 1)
|
|
||||||
.map((s) => s.getText(ast))
|
|
||||||
.join("\n");
|
|
||||||
const json = vi.fn();
|
|
||||||
const wait = vi.fn(() => {
|
|
||||||
throw new Error("Client has not received the token yet");
|
|
||||||
});
|
|
||||||
await runInNewContext(
|
|
||||||
ts.transpileModule(`(async () => { ${tail} })()`, {
|
|
||||||
compilerOptions: { target: ts.ScriptTarget.ES2022 },
|
|
||||||
}).outputText,
|
|
||||||
{
|
|
||||||
waitForGuacdOpen: wait,
|
|
||||||
token: "minted-token",
|
|
||||||
termixConnectId: "correlation-id",
|
|
||||||
req: {},
|
|
||||||
getRequestMeta: () => ({}),
|
|
||||||
logAudit: async () => {},
|
|
||||||
getAuditUsername: async () => "owner",
|
|
||||||
userId: "owner",
|
|
||||||
connectionType: "rdp",
|
|
||||||
hostId: 7,
|
|
||||||
hostname: "host",
|
|
||||||
port: 3389,
|
|
||||||
res: { json },
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(wait).not.toHaveBeenCalled();
|
|
||||||
expect(json).toHaveBeenCalledWith({
|
|
||||||
token: "minted-token",
|
|
||||||
termixConnectId: "correlation-id",
|
|
||||||
guacamoleConnectionId: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("only reveals the live session ID to its owner", () => {
|
|
||||||
const source = readFileSync(
|
|
||||||
new URL("../../src/backend/guacamole-server.ts", import.meta.url),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
const ast = ts.createSourceFile(
|
|
||||||
"server.ts",
|
|
||||||
source,
|
|
||||||
ts.ScriptTarget.Latest,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
const fn = ast.statements.find(
|
|
||||||
(s) =>
|
|
||||||
ts.isFunctionDeclaration(s) &&
|
|
||||||
s.name?.text === "getGuacSessionByConnectId",
|
|
||||||
)!;
|
|
||||||
const info = { ownerUserId: "owner", guacamoleConnectionId: "guacd-id" };
|
|
||||||
const context = {
|
|
||||||
exports: {} as Record<string, (id: string, user: string) => unknown>,
|
|
||||||
guacSessionByConnectId: new Map([["connect-id", info]]),
|
|
||||||
};
|
|
||||||
runInNewContext(
|
|
||||||
ts.transpileModule(fn.getText(ast), {
|
|
||||||
compilerOptions: { module: ts.ModuleKind.CommonJS },
|
|
||||||
}).outputText,
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
const lookup = context.exports.getGuacSessionByConnectId;
|
|
||||||
expect(lookup("connect-id", "owner")).toEqual(info);
|
|
||||||
expect(lookup("connect-id", "other")).toBeNull();
|
|
||||||
expect(lookup("pending", "owner")).toBeNull();
|
|
||||||
});
|
|
||||||
@@ -1,20 +1,9 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
vi.mock("../../../../src/backend/utils/logger.js", () => ({
|
import { GuacamoleTokenService } from "../../src/backend/token-service.js";
|
||||||
guacLogger: {
|
|
||||||
debug: vi.fn(),
|
|
||||||
info: vi.fn(),
|
|
||||||
warn: vi.fn(),
|
|
||||||
error: vi.fn(),
|
|
||||||
success: vi.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { GuacamoleTokenService } =
|
|
||||||
await import("../../src/backend/token-service.js");
|
|
||||||
|
|
||||||
describe("GuacamoleTokenService", () => {
|
describe("GuacamoleTokenService", () => {
|
||||||
const tokenService = GuacamoleTokenService.getInstance();
|
const tokenService = new GuacamoleTokenService();
|
||||||
|
|
||||||
it("disables RDP pre-authentication when no credentials are configured", () => {
|
it("disables RDP pre-authentication when no credentials are configured", () => {
|
||||||
const token = tokenService.createRdpToken("windows.example.test", "", "");
|
const token = tokenService.createRdpToken("windows.example.test", "", "");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
renderWithApp,
|
renderWithApp,
|
||||||
type RenderedPluginApp,
|
type RenderedPluginApp,
|
||||||
@@ -45,9 +45,39 @@ describe(`${manifest.id} activate`, () => {
|
|||||||
expect(app.registered.panels()).toEqual([]);
|
expect(app.registered.panels()).toEqual([]);
|
||||||
expect(app.registered.railItems()).toEqual([]);
|
expect(app.registered.railItems()).toEqual([]);
|
||||||
expect(app.registered.hostActions()).toEqual([]);
|
expect(app.registered.hostActions()).toEqual([]);
|
||||||
|
expect(app.registered.hostProtocols()).toEqual([]);
|
||||||
expect(app.registered.hostEditorSections()).toEqual([]);
|
expect(app.registered.hostEditorSections()).toEqual([]);
|
||||||
expect(app.registered.dashboardCards()).toEqual([]);
|
expect(app.registered.dashboardCards()).toEqual([]);
|
||||||
expect(app.registered.settingsComponents()).toEqual([]);
|
expect(app.registered.settingsComponents()).toEqual([]);
|
||||||
expect(app.registered.actions()).toEqual([]);
|
expect(app.registered.actions()).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds RDP, VNC and Telnet as host protocols while turned on", async () => {
|
||||||
|
rendered = await renderWithApp(plugin, {
|
||||||
|
manifest,
|
||||||
|
locales,
|
||||||
|
api: { get: async () => ({ data: { enabled: true } }) } as never,
|
||||||
|
});
|
||||||
|
await vi.waitFor(() =>
|
||||||
|
expect(rendered!.registered.hostProtocols().sort()).toEqual([
|
||||||
|
"rdp",
|
||||||
|
"telnet",
|
||||||
|
"vnc",
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(rendered.registered.hostActions().map((a) => a.id)).toEqual(
|
||||||
|
expect.arrayContaining(["rdp", "vnc", "telnet"]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("offers no way in while an admin has turned it off", async () => {
|
||||||
|
rendered = await renderWithApp(plugin, {
|
||||||
|
manifest,
|
||||||
|
locales,
|
||||||
|
api: { get: async () => ({ data: { enabled: false } }) } as never,
|
||||||
|
});
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||||
|
expect(rendered.registered.hostProtocols()).toEqual([]);
|
||||||
|
expect(rendered.registered.hostActions()).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -11,22 +11,27 @@ const remoteApiMock = vi.hoisted(() => ({
|
|||||||
const isElectronMock = vi.hoisted(() => vi.fn(() => false));
|
const isElectronMock = vi.hoisted(() => vi.fn(() => false));
|
||||||
const resolveRemoteHostIdMock = vi.hoisted(() => vi.fn());
|
const resolveRemoteHostIdMock = vi.hoisted(() => vi.fn());
|
||||||
|
|
||||||
vi.mock("@/main-axios", () => ({
|
vi.mock("@termix/plugin-sdk/ui", () => ({
|
||||||
authApi: authApiMock,
|
|
||||||
getRemoteGuacamoleApi: () => remoteApiMock,
|
|
||||||
isElectron: isElectronMock,
|
isElectron: isElectronMock,
|
||||||
handleApiError: (error: unknown) => error,
|
|
||||||
}));
|
|
||||||
vi.mock("@/lib/remote-server-api", () => ({
|
|
||||||
resolveRemoteHostId: resolveRemoteHostIdMock,
|
resolveRemoteHostId: resolveRemoteHostIdMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
setRemoteDesktopApp,
|
||||||
getGuacdStatus,
|
getGuacdStatus,
|
||||||
getGuacamoleConnectionId,
|
getGuacamoleConnectionId,
|
||||||
getGuacamoleTokenFromHost,
|
getGuacamoleTokenFromHost,
|
||||||
} from "../../src/frontend/guacamole-api";
|
} from "../../src/frontend/guacamole-api";
|
||||||
|
|
||||||
|
// Mirrors app.apiFor: the remote server only for a remote origin in the
|
||||||
|
// desktop app, the ordinary client otherwise.
|
||||||
|
setRemoteDesktopApp({
|
||||||
|
apiFor: (origin) =>
|
||||||
|
(origin === "remote" && isElectronMock()
|
||||||
|
? remoteApiMock
|
||||||
|
: authApiMock) as never,
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
authApiMock.get.mockClear();
|
authApiMock.get.mockClear();
|
||||||
authApiMock.post.mockClear();
|
authApiMock.post.mockClear();
|
||||||
@@ -42,9 +47,7 @@ describe("guacamole API origin", () => {
|
|||||||
await getGuacdStatus("remote");
|
await getGuacdStatus("remote");
|
||||||
await getGuacamoleTokenFromHost(9, "remote", "vnc");
|
await getGuacamoleTokenFromHost(9, "remote", "vnc");
|
||||||
|
|
||||||
expect(authApiMock.get).toHaveBeenCalledWith(
|
expect(authApiMock.get).toHaveBeenCalledWith("/status");
|
||||||
"/plugin-api/remote-desktop/status",
|
|
||||||
);
|
|
||||||
expect(authApiMock.post).toHaveBeenCalledOnce();
|
expect(authApiMock.post).toHaveBeenCalledOnce();
|
||||||
expect(remoteApiMock.get).not.toHaveBeenCalled();
|
expect(remoteApiMock.get).not.toHaveBeenCalled();
|
||||||
expect(remoteApiMock.post).not.toHaveBeenCalled();
|
expect(remoteApiMock.post).not.toHaveBeenCalled();
|
||||||
@@ -60,9 +63,7 @@ describe("guacamole API origin", () => {
|
|||||||
|
|
||||||
expect(status.guacd.status).toBe("connected");
|
expect(status.guacd.status).toBe("connected");
|
||||||
expect(token.token).toBe("remote-token");
|
expect(token.token).toBe("remote-token");
|
||||||
expect(remoteApiMock.get).toHaveBeenCalledWith(
|
expect(remoteApiMock.get).toHaveBeenCalledWith("/status");
|
||||||
"/plugin-api/remote-desktop/status",
|
|
||||||
);
|
|
||||||
expect(remoteApiMock.post).toHaveBeenCalledOnce();
|
expect(remoteApiMock.post).toHaveBeenCalledOnce();
|
||||||
expect(authApiMock.get).not.toHaveBeenCalled();
|
expect(authApiMock.get).not.toHaveBeenCalled();
|
||||||
expect(authApiMock.post).not.toHaveBeenCalled();
|
expect(authApiMock.post).not.toHaveBeenCalled();
|
||||||
@@ -84,15 +85,12 @@ describe("guacamole API origin", () => {
|
|||||||
"host-sync-id",
|
"host-sync-id",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(remoteApiMock.post).toHaveBeenCalledWith(
|
expect(remoteApiMock.post).toHaveBeenCalledWith("/connect-host/41", {
|
||||||
"/plugin-api/remote-desktop/connect-host/41",
|
protocol: "rdp",
|
||||||
{
|
promptedUsername: "admin",
|
||||||
protocol: "rdp",
|
promptedPassword: "secret",
|
||||||
promptedUsername: "admin",
|
promptedDomain: "EXAMPLE",
|
||||||
promptedPassword: "secret",
|
});
|
||||||
promptedDomain: "EXAMPLE",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(resolveRemoteHostIdMock).toHaveBeenCalledWith("host-sync-id");
|
expect(resolveRemoteHostIdMock).toHaveBeenCalledWith("host-sync-id");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -105,15 +103,12 @@ describe("guacamole API origin", () => {
|
|||||||
domain: "",
|
domain: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(authApiMock.post).toHaveBeenCalledWith(
|
expect(authApiMock.post).toHaveBeenCalledWith("/connect-host/9", {
|
||||||
"/plugin-api/remote-desktop/connect-host/9",
|
protocol: "rdp",
|
||||||
{
|
promptedUsername: "local-admin",
|
||||||
protocol: "rdp",
|
promptedPassword: "secret",
|
||||||
promptedUsername: "local-admin",
|
promptedDomain: "",
|
||||||
promptedPassword: "secret",
|
});
|
||||||
promptedDomain: "",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Support#1240: a host set to "This Device" is served by the embedded
|
// Support#1240: a host set to "This Device" is served by the embedded
|
||||||
@@ -125,12 +120,9 @@ describe("guacamole API origin", () => {
|
|||||||
const token = await getGuacamoleTokenFromHost(9, "local", "rdp");
|
const token = await getGuacamoleTokenFromHost(9, "local", "rdp");
|
||||||
|
|
||||||
expect(token.token).toBe("local-token");
|
expect(token.token).toBe("local-token");
|
||||||
expect(authApiMock.post).toHaveBeenCalledWith(
|
expect(authApiMock.post).toHaveBeenCalledWith("/connect-host/9", {
|
||||||
"/plugin-api/remote-desktop/connect-host/9",
|
protocol: "rdp",
|
||||||
{
|
});
|
||||||
protocol: "rdp",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
expect(remoteApiMock.post).not.toHaveBeenCalled();
|
expect(remoteApiMock.post).not.toHaveBeenCalled();
|
||||||
expect(resolveRemoteHostIdMock).not.toHaveBeenCalled();
|
expect(resolveRemoteHostIdMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -141,9 +133,7 @@ describe("guacamole API origin", () => {
|
|||||||
const status = await getGuacdStatus("local");
|
const status = await getGuacdStatus("local");
|
||||||
|
|
||||||
expect(status.guacd.status).toBe("disconnected");
|
expect(status.guacd.status).toBe("disconnected");
|
||||||
expect(authApiMock.get).toHaveBeenCalledWith(
|
expect(authApiMock.get).toHaveBeenCalledWith("/status");
|
||||||
"/plugin-api/remote-desktop/status",
|
|
||||||
);
|
|
||||||
expect(remoteApiMock.get).not.toHaveBeenCalled();
|
expect(remoteApiMock.get).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -177,9 +167,8 @@ it("looks up the session on the backend that issued its token", async () => {
|
|||||||
isElectronMock.mockReturnValue(true);
|
isElectronMock.mockReturnValue(true);
|
||||||
const signal = new AbortController().signal;
|
const signal = new AbortController().signal;
|
||||||
await getGuacamoleConnectionId("connect/id", "remote", signal);
|
await getGuacamoleConnectionId("connect/id", "remote", signal);
|
||||||
expect(remoteApiMock.get).toHaveBeenCalledWith(
|
expect(remoteApiMock.get).toHaveBeenCalledWith("/connection/connect%2Fid", {
|
||||||
"/plugin-api/remote-desktop/connection/connect%2Fid",
|
signal,
|
||||||
{ signal },
|
});
|
||||||
);
|
|
||||||
expect(authApiMock.get).not.toHaveBeenCalled();
|
expect(authApiMock.get).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -1,41 +1,51 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { createQuickConnectHost } from "@/sidebar/quick-connect-host";
|
|
||||||
import { quickConnectGuacHost } from "../../src/frontend/quick-connect-guac-host";
|
import { quickConnectGuacHost } from "../../src/frontend/quick-connect-guac-host";
|
||||||
|
|
||||||
|
// The shape core's Quick Connect builds for a plugin protocol: the switch and
|
||||||
|
// port in this plugin's host settings, the login on the protocol's fields.
|
||||||
|
function quickHost(protocol: "rdp" | "vnc", extra: Record<string, unknown>) {
|
||||||
|
return {
|
||||||
|
id: "quick-connect-1",
|
||||||
|
name: "admin@10.0.0.2",
|
||||||
|
ip: "10.0.0.2",
|
||||||
|
pluginSettings: {
|
||||||
|
"remote-desktop": {
|
||||||
|
[protocol === "rdp" ? "enableRdp" : "enableVnc"]: true,
|
||||||
|
[protocol === "rdp" ? "rdpPort" : "vncPort"]:
|
||||||
|
protocol === "rdp" ? 3390 : 5901,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...extra,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe("quickConnectGuacHost", () => {
|
describe("quickConnectGuacHost", () => {
|
||||||
it("carries an RDP quick connect's address and credentials", () => {
|
it("carries an RDP quick connect's address and credentials", () => {
|
||||||
const host = createQuickConnectHost({
|
expect(
|
||||||
ip: "10.0.0.2",
|
quickConnectGuacHost(
|
||||||
port: 3390,
|
quickHost("rdp", {
|
||||||
username: "admin",
|
rdpUser: "admin",
|
||||||
authType: "password",
|
rdpPassword: "pw",
|
||||||
password: "pw",
|
domain: "CORP",
|
||||||
protocol: "rdp",
|
}),
|
||||||
domain: "CORP",
|
),
|
||||||
});
|
).toMatchObject({
|
||||||
expect(quickConnectGuacHost(host)).toMatchObject({
|
|
||||||
ip: "10.0.0.2",
|
ip: "10.0.0.2",
|
||||||
connectionType: "rdp",
|
connectionType: "rdp",
|
||||||
rdpPort: 3390,
|
port: 3390,
|
||||||
rdpUser: "admin",
|
username: "admin",
|
||||||
rdpPassword: "pw",
|
password: "pw",
|
||||||
domain: "CORP",
|
domain: "CORP",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("puts a VNC quick connect's password on the VNC fields", () => {
|
it("takes a VNC quick connect's login from the VNC fields", () => {
|
||||||
const host = createQuickConnectHost({
|
expect(
|
||||||
ip: "10.0.0.3",
|
quickConnectGuacHost(quickHost("vnc", { vncPassword: "vncpw" })),
|
||||||
port: 5901,
|
).toMatchObject({
|
||||||
username: "",
|
|
||||||
authType: "password",
|
|
||||||
password: "vncpw",
|
|
||||||
protocol: "vnc",
|
|
||||||
});
|
|
||||||
expect(quickConnectGuacHost(host)).toMatchObject({
|
|
||||||
connectionType: "vnc",
|
connectionType: "vnc",
|
||||||
vncPort: 5901,
|
port: 5901,
|
||||||
vncPassword: "vncpw",
|
password: "vncpw",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
{
|
{
|
||||||
"extends": "@termix/plugin-sdk/tsconfig.plugin.json",
|
"extends": "@termix/plugin-sdk/tsconfig.plugin.json",
|
||||||
"compilerOptions": {
|
|
||||||
"paths": {
|
|
||||||
"@/types/*": ["../../src/types/*"],
|
|
||||||
"@/*": ["../../src/ui/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"]
|
"include": ["src/**/*.ts", "src/**/*.tsx", "tests/**/*.ts", "tests/**/*.tsx"]
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"provides": [
|
"provides": [
|
||||||
{
|
{
|
||||||
"service": "recordings.writer",
|
"service": "recordings.writer",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"permission": "session-recording.view"
|
"permission": "session-recording.view"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { SessionRecordingRepository } from "./repository.js";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* recordings.writer v1, the service ssh-terminal calls to record sessions.
|
* recordings.writer v1, the service ssh-terminal calls to record sessions.
|
||||||
|
* 1.1 added enabledFor, for a caller that records outside this plugin.
|
||||||
*
|
*
|
||||||
* One call per already-batched chunk (ssh-terminal coalesces on a 300ms
|
* One call per already-batched chunk (ssh-terminal coalesces on a 300ms
|
||||||
* trailing edge, issue #1049): mkdir and writeFile on the first append,
|
* trailing edge, issue #1049): mkdir and writeFile on the first append,
|
||||||
@@ -22,6 +23,11 @@ export interface RecordingSink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RecordingsWriterV1 {
|
export interface RecordingsWriterV1 {
|
||||||
|
/**
|
||||||
|
* Whether the host's recording switch is on, for a caller that has to set
|
||||||
|
* up recording before a session exists (remote desktop tells guacd).
|
||||||
|
*/
|
||||||
|
enabledFor: (hostId: number) => Promise<boolean>;
|
||||||
/** Null when recording is off for this user or host. */
|
/** Null when recording is off for this user or host. */
|
||||||
open: (meta: {
|
open: (meta: {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -53,13 +59,15 @@ export function createRecordingsWriter(
|
|||||||
ctx: PluginContext,
|
ctx: PluginContext,
|
||||||
repository: SessionRecordingRepository,
|
repository: SessionRecordingRepository,
|
||||||
): RecordingsWriterV1 {
|
): RecordingsWriterV1 {
|
||||||
|
const enabledFor = async (hostId: number) =>
|
||||||
|
(await ctx.settings.getHost<boolean>(hostId, "enableSessionRecording")) !==
|
||||||
|
false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
enabledFor,
|
||||||
|
|
||||||
async open(meta) {
|
async open(meta) {
|
||||||
const enabled = await ctx.settings.getHost<boolean>(
|
if (!(await enabledFor(meta.hostId))) return null;
|
||||||
meta.hostId,
|
|
||||||
"enableSessionRecording",
|
|
||||||
);
|
|
||||||
if (enabled === false) return null;
|
|
||||||
|
|
||||||
const dataDir = await ctx.files.dataDir();
|
const dataDir = await ctx.files.dataDir();
|
||||||
const dir = path.join(dataDir, "session_logs", meta.userId);
|
const dir = path.join(dataDir, "session_logs", meta.userId);
|
||||||
|
|||||||
@@ -171,4 +171,12 @@ describe("recordings.writer", () => {
|
|||||||
|
|
||||||
expect(row.id).toBeGreaterThan(0);
|
expect(row.id).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("enabledFor follows the host's recording switch", async () => {
|
||||||
|
const { mock, writer } = await setup();
|
||||||
|
await mock.ctx.settings.setHost(7, "enableSessionRecording", true);
|
||||||
|
await expect(writer.enabledFor(7)).resolves.toBe(true);
|
||||||
|
await mock.ctx.settings.setHost(7, "enableSessionRecording", false);
|
||||||
|
await expect(writer.enabledFor(7)).resolves.toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
|
hostProtocols,
|
||||||
useHosts,
|
useHosts,
|
||||||
useTranslation,
|
useTranslation,
|
||||||
type PluginHostRecord,
|
type PluginHostRecord,
|
||||||
@@ -64,12 +65,17 @@ import {
|
|||||||
type DirectoryRole,
|
type DirectoryRole,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
|
|
||||||
|
type SharedProtocol = "ssh" | "rdp" | "vnc" | "telnet";
|
||||||
|
|
||||||
|
function isSharedProtocol(value: string): value is SharedProtocol {
|
||||||
|
return (
|
||||||
|
value === "ssh" || value === "rdp" || value === "vnc" || value === "telnet"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** A host from the shell's list; the connection fields ride along by name. */
|
/** A host from the shell's list; the connection fields ride along by name. */
|
||||||
type SSHHostWithStatus = PluginHostRecord & {
|
type SSHHostWithStatus = PluginHostRecord & {
|
||||||
enableSsh?: boolean;
|
enableSsh?: boolean;
|
||||||
enableRdp?: boolean;
|
|
||||||
enableVnc?: boolean;
|
|
||||||
enableTelnet?: boolean;
|
|
||||||
connectionOrigin?: unknown;
|
connectionOrigin?: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -350,11 +356,13 @@ export function CollabRoomTab({
|
|||||||
}
|
}
|
||||||
const response = await createRemoteSessionToken(
|
const response = await createRemoteSessionToken(
|
||||||
Number(host.id),
|
Number(host.id),
|
||||||
await resolveConnectionOrigin({
|
await resolveConnectionOrigin(
|
||||||
connectionType: protocol,
|
{
|
||||||
connectionOrigin: host.connectionOrigin as
|
connectionOrigin: host.connectionOrigin as
|
||||||
"local" | "remote" | undefined,
|
"local" | "remote" | undefined,
|
||||||
}),
|
},
|
||||||
|
{ defaultRemote: true },
|
||||||
|
),
|
||||||
protocol,
|
protocol,
|
||||||
);
|
);
|
||||||
if (!response?.connectionId) {
|
if (!response?.connectionId) {
|
||||||
@@ -780,11 +788,9 @@ export function CollabRoomTab({
|
|||||||
<CenteredNote text={t("collab.noHostsFound")} />
|
<CenteredNote text={t("collab.noHostsFound")} />
|
||||||
)}
|
)}
|
||||||
{filteredHosts.map((host) => {
|
{filteredHosts.map((host) => {
|
||||||
const protocols: Array<"ssh" | "rdp" | "vnc" | "telnet"> = [];
|
const protocols = hostProtocols(
|
||||||
if (host.enableSsh) protocols.push("ssh");
|
host as unknown as PluginHostRecord,
|
||||||
if (host.enableRdp) protocols.push("rdp");
|
).filter(isSharedProtocol);
|
||||||
if (host.enableVnc) protocols.push("vnc");
|
|
||||||
if (host.enableTelnet) protocols.push("telnet");
|
|
||||||
if (protocols.length === 0) return null;
|
if (protocols.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Loaded 100 of 215 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user