143 lines
6.8 KiB
JavaScript
143 lines
6.8 KiB
JavaScript
// Parser für Exporte aus Devolutions Remote Desktop Manager.
|
|
// Unterstützt: .rdm / .xml (Export "Remote Desktop Manager-Datei"), .json und .csv.
|
|
// Liefert [{ folder: ['A','B'], host?: {...} }] für vault:bulkImport.
|
|
'use strict';
|
|
|
|
(function () {
|
|
// Numerische ConnectionType-Werte aus RDM-JSON (die gängigsten)
|
|
const NUMERIC_TYPES = { 1: 'RDPConfigured', 25: 'Group', 77: 'SSHShell', 24: 'Putty', 108: 'SSHShell' };
|
|
|
|
function classify(type) {
|
|
const t = String(NUMERIC_TYPES[type] || type || '').toLowerCase();
|
|
if (!t) return null;
|
|
if (t === 'group' || t === 'folder' || t === 'repository') return 'folder';
|
|
if (t.startsWith('rdp') || t === 'remotedesktop' || t === 'microsoftremotedesktop') return 'rdp';
|
|
if (t.includes('ssh') || t === 'putty' || t === 'terminal' || t === 'sshshell') return 'ssh';
|
|
return null; // VNC, Web, Credentials, Dokumente … werden übersprungen
|
|
}
|
|
|
|
const splitPath = (p) => String(p || '').split(/[\\/]/).map((s) => s.trim()).filter(Boolean);
|
|
|
|
function splitHostPort(url, defPort) {
|
|
let address = String(url || '').trim().replace(/^(rdp|ssh|https?):\/\//i, '').replace(/\/.*$/, '');
|
|
let port;
|
|
const m = address.match(/^\[(.+)\](?::(\d+))?$/) || address.match(/^([^:]+):(\d+)$/);
|
|
if (m) { address = m[1]; port = m[2] ? Number(m[2]) : undefined; }
|
|
return { address, port: port && port !== defPort ? port : undefined };
|
|
}
|
|
|
|
function makeEntry(r) {
|
|
const kind = classify(r.type);
|
|
if (!kind) return null;
|
|
let folder = splitPath(r.group);
|
|
if (kind === 'folder') {
|
|
// RDM speichert bei Ordnern meist den vollständigen Pfad inkl. eigenem Namen im Feld "Group"
|
|
if (r.name && folder[folder.length - 1] !== r.name) folder.push(r.name);
|
|
return folder.length ? { folder } : null;
|
|
}
|
|
const defPort = kind === 'rdp' ? 3389 : 22;
|
|
const hp = splitHostPort(r.host || r.url, defPort);
|
|
if (!hp.address) return null;
|
|
const port = Number(r.port) && Number(r.port) !== defPort ? Number(r.port) : hp.port;
|
|
const host = {
|
|
protocol: kind,
|
|
label: r.name || hp.address,
|
|
address: hp.address,
|
|
port,
|
|
username: r.username || undefined,
|
|
domain: kind === 'rdp' ? r.domain || undefined : undefined,
|
|
tags: ['rdm'],
|
|
};
|
|
if (r.password) host.password = r.password;
|
|
if (r.description) host.notes = r.description;
|
|
return { folder, host };
|
|
}
|
|
|
|
// ---------- XML ----------
|
|
function child(el, path) {
|
|
let cur = el;
|
|
for (const name of path.split('/')) {
|
|
cur = [...(cur?.children || [])].find((c) => c.localName.toLowerCase() === name.toLowerCase());
|
|
if (!cur) return '';
|
|
}
|
|
return cur.textContent.trim();
|
|
}
|
|
const firstOf = (el, paths) => { for (const p of paths) { const v = child(el, p); if (v) return v; } return ''; };
|
|
|
|
function parseXml(text) {
|
|
const doc = new DOMParser().parseFromString(text, 'application/xml');
|
|
if (doc.querySelector('parsererror')) throw new Error('Datei ist kein gültiges XML.');
|
|
const conns = [...doc.getElementsByTagName('Connection')].filter((c) => child(c, 'ConnectionType'));
|
|
return conns.map((c) => makeEntry({
|
|
type: child(c, 'ConnectionType'),
|
|
name: child(c, 'Name'),
|
|
group: firstOf(c, ['Group', 'GroupName', 'Folder']),
|
|
url: firstOf(c, ['Url', 'Host', 'HostName']),
|
|
host: firstOf(c, ['Terminal/Host', 'Putty/Host', 'SSHTunnel/Host', 'RDP/Host']),
|
|
port: firstOf(c, ['Terminal/HostPort', 'Putty/Port', 'RDP/Port', 'Port', 'HostPort']),
|
|
username: firstOf(c, ['RDP/UserName', 'Terminal/Username', 'Terminal/UserName', 'Putty/UserName', 'Putty/Username', 'UserName', 'Username']),
|
|
domain: firstOf(c, ['RDP/Domain', 'Domain']),
|
|
description: child(c, 'Description'),
|
|
})).filter(Boolean);
|
|
}
|
|
|
|
// ---------- JSON ----------
|
|
function parseJson(text) {
|
|
const data = JSON.parse(text);
|
|
const list = Array.isArray(data) ? data : data.Connections || data.connections || data.Entries || [];
|
|
const g = (o, ...keys) => { for (const k of keys) { const v = k.split('.').reduce((a, p) => a?.[p], o); if (v != null && v !== '') return v; } return ''; };
|
|
return list.map((c) => makeEntry({
|
|
type: g(c, 'ConnectionType', 'Type'),
|
|
name: g(c, 'Name'),
|
|
group: g(c, 'Group', 'GroupName', 'Folder'),
|
|
url: g(c, 'Url', 'Host'),
|
|
host: g(c, 'Terminal.Host', 'Putty.Host', 'RDP.Host'),
|
|
port: g(c, 'Terminal.HostPort', 'Putty.Port', 'RDP.Port', 'Port'),
|
|
username: g(c, 'RDP.UserName', 'Terminal.Username', 'Putty.UserName', 'UserName', 'Username'),
|
|
domain: g(c, 'RDP.Domain', 'Domain'),
|
|
description: g(c, 'Description'),
|
|
})).filter(Boolean);
|
|
}
|
|
|
|
// ---------- CSV ----------
|
|
function csvRows(text) {
|
|
const delim = (text.split('\n')[0].match(/;/g) || []).length > (text.split('\n')[0].match(/,/g) || []).length ? ';' : ',';
|
|
const rows = []; let row = [], cur = '', q = false;
|
|
for (let i = 0; i < text.length; i++) {
|
|
const ch = text[i];
|
|
if (q) {
|
|
if (ch === '"' && text[i + 1] === '"') { cur += '"'; i++; }
|
|
else if (ch === '"') q = false;
|
|
else cur += ch;
|
|
} else if (ch === '"') q = true;
|
|
else if (ch === delim) { row.push(cur); cur = ''; }
|
|
else if (ch === '\n' || ch === '\r') { if (ch === '\r' && text[i + 1] === '\n') i++; row.push(cur); rows.push(row); row = []; cur = ''; }
|
|
else cur += ch;
|
|
}
|
|
if (cur || row.length) { row.push(cur); rows.push(row); }
|
|
return rows.filter((r) => r.some((c) => c.trim()));
|
|
}
|
|
function parseCsv(text) {
|
|
const [head, ...rows] = csvRows(text);
|
|
const idx = (...names) => head.findIndex((h) => names.includes(h.trim().toLowerCase().replace(/[\s_]/g, '')));
|
|
const col = {
|
|
type: idx('connectiontype', 'type', 'protocol'), name: idx('name', 'displayname'), group: idx('group', 'folder', 'groupname'),
|
|
host: idx('host', 'hostname', 'url', 'computername', 'address'), port: idx('port', 'hostport'),
|
|
username: idx('username', 'user', 'login'), domain: idx('domain'), password: idx('password'), description: idx('description'),
|
|
};
|
|
if (col.host < 0 && col.type < 0) throw new Error('CSV-Spalten nicht erkannt (erwartet u. a. Name, Host, ConnectionType, Group).');
|
|
return rows.map((r) => {
|
|
const v = (k) => (col[k] >= 0 ? (r[col[k]] || '').trim() : '');
|
|
return makeEntry({ type: v('type') || 'RDPConfigured', name: v('name'), group: v('group'), url: v('host'), port: v('port'), username: v('username'), domain: v('domain'), password: v('password'), description: v('description') });
|
|
}).filter(Boolean);
|
|
}
|
|
|
|
window.parseRdmExport = function (name, content) {
|
|
const t = content.trimStart();
|
|
if (/\.json$/i.test(name) || t.startsWith('{') || t.startsWith('[')) return parseJson(content);
|
|
if (/\.csv$/i.test(name)) return parseCsv(content);
|
|
if (t.startsWith('<')) return parseXml(content);
|
|
return parseCsv(content);
|
|
};
|
|
})();
|